2024 楚慧杯 线下awdp web | Writeup

队里有几个成员去打,回来后在群里发了附件,看了一下感觉不难,就决定复现一下 因为不知道具体题目顺序和名字,只有队员给的附件(复现也都是按照队员给的附件内容来,如果同样有参加的师傅发现缺漏,欢迎在评论区告诉我),所以就按照我的做题顺序来排序。同时为了还原真实线下做题场景,不使用在线工具 第一题 攻 首页一个登录框和一个注册,扫一下目录发现有个cache,访问一下得到一个.pyc文件,用pycdc反编译后得到下面代码 # Source Generated with Decompyle++ # File: app.cpython-38.pyc (Python 3.8) from flask import Flask, request, render_template, redirect, url_for, session, send_from_directory import os import pickle import base64 app = Flask(__name__) app.secret_key = 's3cr3t_Key_Y0u_Nev3r_GuesS' USERS_DIR = 'users' if not os.path.exists(USERS_DIR): os.makedirs(USERS_DIR) def register(): Unsupported opcode: BEGIN_FINALLY (97) username = request.form.get('username') password = request.form.get('password') user_data = { 'username': username, 'password': password } user_file = os.path.join(USERS_DIR, base64.b64encode(username.encode('utf-8')).decode('utf-8') + '.data') # WARNING: Decompyle incomplete register = app.route('/reg', [ 'POST'], **('methods',))(register) def register_page(): return render_template('register.html') register_page = app.route('/register')(register_page) def login(): Unsupported opcode: BEGIN_FINALLY (97) username = request.form.get('username') password = request.form.get('password') user_file = os.path.join(USERS_DIR, base64.b64encode(username.encode('utf-8')).decode('utf-8') + '.data') # WARNING: Decompyle incomplete login = app.route('/login', [ 'POST'], **('methods',))(login) def index(): data = session.get('data', None) if data: data = base64.b64decode(data) if b'R' in data and b'built' in data or b'setstate' in data: return 'hacker???' user_data = None.loads(data) username = user_data['username'] return render_template('index.html', username, **('username',)) return None(url_for('login_page')) index = app.route('/index')(index) def login_page(): return render_template('login.html') login_page = app.route('/')(login_page) def logout(): session.pop('data', None) return redirect(url_for('login_page')) logout = app.route('/logout', [ 'POST'], **('methods',))(logout) def download_cache_file(): cache_file_path = os.path.join('__pycache__', 'app.cpython-38.pyc') if os.path.exists(cache_file_path): return send_from_directory(os.path.dirname(cache_file_path), os.path.basename(cache_file_path), True, **('as_attachment',)) return None download_cache_file = app.route('/cache')(download_cache_file) if __name__ == '__main__': app.run('0.0.0.0', **('host',)) 核心利用逻辑在这里 ...

January 4, 2025 · 5 min · Red

2024国赛 sanic复现

我敢说这些嗯抄gxngxngxn那篇分析sanic博客的家伙都是要么没过脑子要么懒成狗= = 包括gxngxngxn的那篇分析,其实最关键的步骤只告诉了“要怎么做”,而没有告诉“为什么要这样做” 我就直接把自己总结的python框架的原型链污染题的解题思路在这道题中体现出来吧 学框架污染前提肯定是python原型链基础,建议可以先看这篇。如果你还不理解原型链污染,那么建议先看看js原型链污染 这里我就跳过admin绕过,因为这不是我们本篇分析的重点。这一部分可以去看gxngxngxn的分析博客 第一步,找污染点 核心肯定就是利用对pollute类的pydash._set污染,从而污染到其他我们想污染的内容。目标很明确,我们想要读取flag,也就是要读取文件。就先来找找题目源码有没有可以直接污染就能读文件的地方 很明显,我们如果能控制__file__的值,就能读取任意文件。这里我放两个repeater repeater 1用来查看src的内容和全局变量状态 repeater 2用来发送污染数据 并且在src的return打个断点 首先访问一下src,看一下全局变量状态 很容易发现__file__,而我们要污染到全局变量的__file__,就要先知道污染点现在在哪。很明显在Pollute类,所以我们用__class__访问类,__init__访问初始化函数,__globals__访问全局变量,就能访问到__file__。具体原理参考上面贴的原型链污染基础文章 这里我先污染成我自己创建的文件试一下。先把刚才的断点放行后污染一下 在访问一遍src,就可以发现__file__值被污染 但如果我们上靶机上试,直接污染成/flag是读取不了的,很有可能是flag文件名是随机的,所以我们要想别的办法。所以__file__污染这条路是走不通的。一些wp并没有说明这点,容易误导新手 那还有什么地方可以污染吗?通篇阅读一下题目源码,会发现只有app.static这一个地方有涉及到文件路径,但很明显这里是写死的路径,我们没有办法修改./static这个参数的值(也就是下图的file_or_directory)。但一般这类框架内置方法都不会只有短短两个参数,所以我们可以跟进一下static方法 会发现参数确实不少,在题目源码中被赋值的只有uri和file_or_directory两个参数。我们接着往下翻,就会看到一大串注释,这些注释就是介绍每个参数的功能。如果你在国赛当天写这道题,那当然是要把每个注释都看过去一遍。所以我们看到这个地方 这两个参数在题目源码里是没有被赋值的,一个意思是开启列目录的功能,另一个意思是所列目录的路径。我们可以先污染一下directory_view成true看看会发生什么。 第二步,找在哪进行污染 这里在哪进行污染并不只是admin路由下的pydash._set,更重要的是我要污染哪个变量下的哪个属性,也就是污染链。 我们可以很清楚知道__class__.__init__.__globals__是正在执行的题目文件的全局变量环境,所以我们先来全局变量里有哪些变量可以污染。一样在src打断点访问 除了特殊变量和函数、类以外,只有两个变量。pydash是我们用来进行污染的工具,本身要污染的点肯定不在pydash里,而是在sanic。而app=Sanic(__name__),所以我们跟进app变量 此时你会看到一堆眼花缭乱的变量,但别着急,我们为了找到能过污染的点,必须要先清楚一个python小知识,也就是污染点必须是能够被我们写入的。所以反过来想,如果我们把不能写入的能先排除,那就可以进一步缩小范围了。而在python中,常用且不能写入的基础数据类型只有一种,那就是元组。所以我们就先不看元组数据,也就是像下面这样的子变量 并且我们其实也可以先将上面这种特殊变量先跳过,因为我们现在很明确自己要找的是和static下的路径,也就是和路由有关的内容,所以我们可以将目光放在app变量下和route类似的名字上。 (其实在这个地方我想过很久的思维链,为什么会想要找和route有关的东西?直到发文前一天晚上还在一位和很有开发经验的朋友一起研究时才告诉我,这其实只能当作一种常识记下来,就像在web开发时,其实大部分时间也是在做和路由有关的事情,所以就应该首先想到路由。如果你和我一开始一样,还是觉得这样看到route有点无厘头,觉得在实战中可能并不一定会出现在route,那么你也可以像我一样将别的变量全部检测过去一次,以防万一,甚至还有可能发现别的污染链) 可以发现一个router 接着我们继续按照上面说到的筛选条件,也就是不看元组,来继续找找可利用的变量。可以发现有name_index,routes_all,routes_static,static_routes等一众可疑变量 到这里如果是在比赛中,就必须要将每一个可以变量跟进看看能不能达到directory_view(这里需要注意,我们要污染的directory_view在static方法下,所以我们只用看static的路由)。可以的话例如我尝试跟进routes_all和routes_static 这是routes_all,可能存在污染 routes_static没有static路由,所以不可能存在污染 就像这样,将每一个变量跟一遍为止,直到找出directory_view。并逐个污染,看污染后的值会不会保留。 比如我污染routes_all试试 返回success,再次访问src验证是否污染成功 仍然是false 经过大量尝试后会发现只有在污染name_index下的变量时才能成功保留。层级如下(太长了截屏截不下,只能用表达式表示) app.router.routes_all[('static', '<__file_uri__:path>')].handler.keywords['directory_handler'].directory_view 这也是这道题为什么在当时只有一支队做出来的原因,也是这题真正的难点。但gxngxngxn的分析博客和其他抄他的人只在wp中草草提到这个name_index是“经过查询资料”后得到的。 也正因为查了两周的大大小小各种资料仍找不到他说的这个功能,所以才决定自己复现= =也许人家是真找到了,但可以说跟风的人都没有去验证的脑子啊…… 第三步,开始污染 找到了需要污染点和怎么污染的链子,就可以开始污染了。 再来回顾一下第一步的总结,我们需要污染这两个东西 directory_view已经在上一步污染成功,那么接下来要污染的就是显示的路径。我们跟进static找一下directory_handler的引用 ...

December 10, 2024 · 1 min · Red

2023 强网杯 thinkshop[ping]

分析完这题,时隔一年又让我想起这句话…… thinkshop 拿到手是一个.tar文件,而且用的是amd64打包的,m1折腾了好久也没搞好debug = = debug只能本地起一个服务 构建好容器后直接把html下的附件拉下来后先访问。可以发现是个thinkphp 5.0.23搭建的商店(版本号随便打个404就能发现) 进去后什么都没有,所以分析一下入口代码。发现首页html在html/application/index/view/index/index.html 在controller目录下又找到Index.php 这里的assign是在html中注册一个变量的意思。这样就能在html里用类似下面这样的方法调用php里的变量。这种方法就是php模板 一样在view目录下,发现admin里有一个login.html,所以应该是有登陆入口。看一下路径 现在的访问路径是http://127.0.0.1:36000/public/index.php/index/index/index,所以猜测是http://127.0.0.1:36000/public/index.php/index/admin/login.html。成功访问 根据代码分析可以发现,post数据交给了一个叫do_login的地方,搜索一下 跟进找到Admin.php里 想办法看能不能登陆。这里主要是要看$adminData是怎么处理的。可以看到查找了admin表,并用cache函数尝试通过缓存获取。这里因为我们有容器,所以进到容器里看一下admin表 查一下这个被md5的密码可以得到123456。但此时如果直接拿admin和123456去登陆会发现提示错误,主要问题就在于后面这个很容易被忽略的find函数 可以看到,这个find函数直接拿post进来的用户名去find,这里可以搜一thinkphp里的下find方法 可以发现,当find里是个字符串时,会将其当作主键进行查询。一般就是第一个字段,也就是id字段,而admin显然是在username字段,因此没法查到,所以才会登陆失败。所以这里要用username=1&password=123456进行登陆 登陆进来后发现有几个操作,先进入修改看看 商品信息应该是markdown格式,可以看一下代码 在goods_edit.html里发现index/admin/do_edit 根据刚刚的经验,跟进到do_edit函数 可以发现所有修改的数据都会被这个函数处理。我们先回到html看一下。发现这个地方有一个反序化的点,在进入这个页面时会显示数据库里存放的markdown内容,就是这样调出来的 或者在首页的商品详情页面也能看到类似的反序列化调用点 搜一下可以发现thinkphp5.0.24存在反序化漏洞,5.0.23可用(点这里查看,更详细分析可以看这里) 所以接下来就是想办法修改数据库的内容从而利用这个反序列化点。 不过先不着急,返回去先看一下首页还没有看的添加功能。这里我们直接看Admin.php里的操作函数就行 先来看看do_add 首先$data可控,因为反序列化的点只对$goods[‘data’]进行,所以我们只看’data’键的操作。 在第131行可以看到直接对’data’键的值进行了序列化,我们如果在这里传入payload后存入数据库是被事先序列化一遍的,并且如果我在post时试图插入一个新的键值对,他也不会写入到goods里。显然这里不可控。 回到刚刚的edit里,发现所有post进来的东西都会被丢到saveGoods函数,跟进 可以发现这里一样是对’data’键进行事先序列化,但是到目前为止整个过程还并没有对变量$data(需要注意是变量$data而不是键’data’)的其他键值进行处理,也就是说如果我在这里插入一个键值对,是可以正常传进save函数的。我们继续跟进save 一样,整个$data进入到updatedata函数,跟进 发现直接把传进来的所有$data变量里的键都存到数据库里,并且不存在过滤,所以这里我们直接构造payload如下,尝试修改数据库 data` = 111 where id = 1# 丢到cyberchef里编码一下就开打。 但是发现直接说商品更新失败了,而且sql数据库里也没有变化 说明写入失败了。看了一下网上几篇wp说是rtrim会把空格干掉 但查询后发现rtrim函数的作用是去除末尾的空格,即便指定了第二个参数也是末尾。而且根据updatedata函数的逻辑,这里的rtrim主要目的是为了删掉sql语句中最后一个多出来的逗号,这样才能写入数据库 ...

November 19, 2024 · 3 min · Red