强网杯2025线上初赛wp
本文最后更新于:几秒前
Web-CeleRace
此题解纪念我被浪费的一整天时间,第一天结束前所有解题的先决条件已经具备,唯独没有意识到去CVE库中查找celery相关的历史漏洞,一直将思路卡死在框架的分析上。
这道题审计源码后,很明显的能够发现在/tasks/fetch/<path:target>下是存在一处SSRF的,乍一看似乎是发起http请求的SSRF,但是后续测试中,以打下redis为目标,猜测可以在verb字段进行一个伪造RESP数据流的包进行攻击,url字段只是提供一个目标ip

打下redis后进一步看了眼代码发现原来已经明确进行了定义了socket数据格式了(
总而言之就是尝试进入这个接口进行一个ssrf攻击

但是显而易见的,这个接口上存在一个flask的修饰器去进行一个鉴权。
这里是通过path传入参数因此有理由怀疑出题者的意图在于发现了一个WSGI处理逻辑上的缺陷绕制能直接步入接口中
绕过require_admin的思路很快锁定在了两个方向:
- 黑盒FUZZ
- 对framework中的框架代码进行debug,发现其中的处理缺陷
权限绕过
不幸的是,我选择了最耗时的白盒,因为这道题用字典去fuzz很容易搞出来
幸运的是运气很好很快就发现了问题的所在
先讲结论:访问/tasks/fetch/../即可绕过
在wrapper处打下一个断点发起一个/tasks/fetch/a接口的请求观察观察

回退一帧调用栈,注意到这里的handler是经过方法_wrap_with_middlewares()处理后的返回值

跟进去,这里调用了self._collect_route_middlewares()返回一个回调函数并传入view获得handler,跟进去看看处理逻辑

这里根据endpoint获取scoped
并且粗略看一眼能发现如果能走进for循环那么貌似就会根据我们的路径参数去获取一个中间件(下面会详细分析)

但是非常神奇的,self.route_middlewares中居然不包含queue_fetch

于是我们寻找对route_middlewares赋值的位置
这里由于rule_obj中包含尖括号,因此不往route_middlewares中插入,而是往wildcard_middlewares中记录

回到上面的_collect_route_middlewares()函数继续分析,上面提到由于queue_fetch不在route_middlewares中,因此scoped为空且self.wildcard_middlewares也被插值了不为空,不提前retuen,而是继续往下走
然后就走到了_normalize_path(path),路径normalize处理?,直觉告诉我这里可能会存在绕过

跟进去看看:
大致上的逻辑就是对路径url解码路径后再进行标准化处理,即处理掉./和../等

步出,然后继续往下走到self._pattern_matches()函数,这里的匹配表达式是/tasks/fetch/**,如果我们请求一个/tasks/fetch/aaa路由,必然会被匹配到从而返回一个识别require_login的中间件
如果足够敏锐的话,结合上面的路径标准化,应该很快就意识到,我们如果传入/tasks/fetch/../的话,那么这里既能跳过两个提前的return,经过标准化处理后路径变为/tasks/又可以绕过下面的路径匹配,到最后return一个空的scoped,来绕过require_login

那么我们不妨尝试一下传入/tasks/fetch/../看看结果如何
和我们预期的一样成功返回了一个空的scoped!而空的scoped就如同其他不需要鉴权的接口的表现一样

步出,因为scoped为空直接跳出了当前for循环,下面的self.middlewares发现不管怎么样都是空的也走不进,一路步过的表现如同不需要鉴权的接口一样

更令人惊喜的是后面直接走进了目标接口中,传入json字段url和verb也自然能够打SSRF了

SSRF
RESP协议的流量特征这里就不赘述了,网上诸多dict协议打redis的文章都有提到,这里直接写个脚本用来方便操作
贴个比赛期间用到的
1 | |
前面SSRF探测过环境不可出网,且在redis.acl中发现对CONFIG这类指令权限控制的是挺死的,虽然可以直接用ACL指令关掉限制
1 | |
但是redis版本过高,无法直接在redis-cli中修改dir、dbfilename等保护属性,也打不了前段时间的redis UAF

恶意类
整个代码让ai帮忙分析后,不难发现一个类DiagnosticsPersistError,这个类是一个自定义的Error类,但是很明显在整个项目中都没有使用到它,而且它的功能也非常可疑:
它的_maybe_persist方法中,很明显的存在一个任意文件写的点

并且可以看到在它的构造方法中去调用了这个可疑的方法,但是存在一个条件,就是需要写/tmp/debug这个文件

写文件
拿到redis权限后,又要写文件,这里的第一反应应该是使用config set去设置redis的数据库来写文件,但是我们在上面已经提到了由于redis版本过高这是行不通的。那怎么办呢?
我们将视角切换到框架中,寻找有没有其他地方可以写文件
很快我们会发现存在这样一个FileSessionManager类,用来将session持久化保存在磁盘中

保存session文件会调用下面这个方法,最终保存路径为path

而我们去看看path的处理逻辑,非常明显的,这里存在一个路径拼接的漏洞

怎么去控制这个路径呢,实际上很容易发现就是将mini_session这个cookie的值改为../debug即可

未能实现的最后一步
到目前为止实际上最大的问题就是如何去实例化那个DiagnosticsPersistError,如果能够实例化它其实已经有很明确的思路就是写pycache去执行任意代码了,但是如何触发呢
联系到题目给出的celery,搜索引擎中搜索一下实际上只能搜到Celery<4.0时的一个漏洞,至于大于4.0的,赛时应当在CVE数据库中去寻找相关漏洞的
https://security.snyk.io/vuln/SNYK-PYTHON-CELERY-2314953
本地尝试运行给出的poc,发现提示报错,即需要实例化的类必须是一个exception
你说这不巧了么,题目中给的这个可疑的类不正好是RuntimeError么

进行一个小改

然后debug,成功实例化

全局搜一下exception_to_python()的用法,发现在包内的backend/redis.py中存在以下逻辑,当state为几种情况时就会触发反序列化逻辑

详细地跟进看一下,实际上就是status为下面这几种情况的时候报错

我们详细地分析一下一个正常的任务的序列化数据的格式:
1 | |
这里合理猜测将status改为FAILURE,result改成我们预设的恶意payload
1 | |
触发一下

成功上传testtttt

后续在复盘的时候,发现了一件事
实际上不需要去查找CVE,让ai帮忙写了个简单celery的本地demo:
1 | |
1 | |
然后终端:
1 | |
再将run_tasks.py跑起来
哐当一下报错了

然后不经意间查了下内容:

遗憾呐!