本文首发先知社区,文章链接:https://xz.aliyun.com/t/4862
最近打了打DDCTF,本来是无聊打算水一波。最后竟然做high了,硬肛了几天..
以下为本次比赛web题目的WriteUp:
[100pt] 滴~
看到url疑似base64,尝试解密后发现加密规则如下。
1 | b64(b64(ascii2hex(filename))) |
于是可以自己构造,使其实现任意文件读取,首先先尝试/etc/passwd。
1 | [plain] -> ../../../../../../../etc/passwd |
发现斜杠被过滤掉了。此时尝试读一下index.php源码。来看一下规则。
1 | [plain] -> index.php |
最终获取到源码如下。
在这里,可以看到对对文件读取做了限制,想绕正则,是不存在的。此时打开预留hint看看,猜测可能是echo的问题?试了许久,还是放弃了。
过了几天,默默打开CSDN评论,还是看到一点有意思的东西的。最终发现出题人故意将hint放在practice.txt.swp
emm,贼迷的一题。然后提示flag!ddctf.php
读源码,此时用config替代!号:
1 | ~/D/D/web-di~ $ python a.py f1agconfigddctf.php |
然后直接构造就好了
1 | url: http://117.51.150.246/f1ag!ddctf.php |
[130pt] 签到题
很简单的一个代码审计题目。一开始有点脑洞,需要绕一下认证,不过也不难。
访问页面,会有一个登陆认证,此时分析流量数据,可以发现他向auth.php请求了一下,返回值刚好是没权限。也就是权限验证在这里,分析数据包,可以发现,请求头有一个username字段。尝试修改为admin,此时成功通过认证。
然后返回了一个源码页面。此时进入分析源码阶段。源码不是太多,核心逻辑也很好懂,包括利用链的构造。
首先,分析源码,可以看到危险函数unserialize,以及file_get_contents。
此时可以大概知道题目大体解题流程如下:
通过session反序列化 –>创建Application对象–> 控制path –> getfalg
此时一步一步来。
分析代码,可以发现session这个变量,是由cookie传入的。此时经过签名校验,确定cookie不可更改。代码如下:
此时可以看到,签名规则是md5(eancrykey+session),也就是说,我们要想获得cookie控制权,必须得到eancrykey。通读代码,分析eancrykey出现地点。最终发现两个可疑点
a) eancrykey存放目录为../config/key.txt。
由于不在web目录且没有读文件的漏洞,此时攻击者不可获取。
b) 某处代码存在蜜汁调用。
很明显,可以看出是主办方给的后门,但是怎么用呢?
sprintf函数,是格式化字符串用的函数。可以参考c语言的printf,只不过这里不会打印,而是返回格式化后的字符串。
此时可以分析一下逻辑。
1 | # python 伪代码 |
此时问题来了,为什么会输出两次?因为在第一次格式化的时候,已经将eval填入data中,第二次格式化前的字符串为:Welcome my friend eval。此时没有%s占位,key也就无处可去了。
所以,此时我们将eval改成%s,遍可以成功打印出key。机智!
此时成功getkey。然后就可以愉快地伪造session了。
然后继续分析Application,我们该如何伪造session。此时,建议down下来Application.php,方便调试使用。
可以发现,代码中做了两层防护,来保证path的安全性。此时sanitizepath可以通过一个最经典的绕过—“双写” 来进行绕过。
1 | payload: ../ |
此时可以看出,在经过这个函数后,第二三四个字符将会被转为空。然后成功使../逃逸出来。
再看第二个限制了字符为18。此时我们可以通过../和./来进行绕过,不过,唯一缺点是,字符不能超过18个。
此时尝试读取/etc/passwd。计算其长度,为10。此时我们可以构造如下:
1 | /etc/../etc/passwd |
此时可以看到成功读取了/etc/passwd。
最终在 ../config/flag.txt读到flag,如下:
[130pt] Upload-IMG
比较经典的一个题目了,绕过GD库,实现图片马。一般来说,搭配一个文件包含,简直是无敌的。在这里不多解释,直接上脚本了。
https://github.com/BlackFan/jpg_payload
Usage: php jpg_payload.php <jpg_name.jpg>
[140pt] homebrew event loop
这题给好评,思路超级棒!
先说一下题目:开局给你3块钱,让你买5个一元一个的钻石。从而得到flag。
上来可以拿到源码,首先分析源码。
1 | # flag获取函数 |
源码大概意思如上,可以看出大概流程。然后仔细分析,可以发现在购买逻辑中。先调用增加钻石,再调用计算价钱的。也就是先货后款。
现实生活中,肯定没毛病,但是在计算机中,会不会出现先给了货后,无法扣款,然后货被拿跑了。此时继续往下看,发现consume_point_function函数中,当钱不够时,会抛出一个RollBackException。此时,在逻辑处理函数execute_event_loop中,会捕获这个异常,并将现有状态置为上一session状态。如下:
此时,天真的我,想起了条件竞争,如果我够快的话,会不会让他加几个钻石,重置session时,重置到已经加完的。
但此时,仔细分析代码,以及flask的特性,你会发现一件事,他的状态并非是基于服务端session,而是客户端session,此时不应该叫他session了,叫cookie更合适一点。也就是,所有的状态都存在客户端。你竞争的话,他的session是单线程的。我必须操作完上一状态,才可以操作下一状态。
此时,条件竞争凉凉。
那既然状态是在客户端,那我可不可以修改?答案是可以。但是你得需要知道flask的secret_key。然后从而伪造cookie,此时我们无法伪造。思路继续断掉。继续分析代码。
仔细分析execute_event_loop,会发现里面有一个eval函数。无论在什么语言中,eval可控, 必定是一个灾难。
此时 action使我们可控的,但是由于白名单过滤的存在,我们可控的范围较小。
此时,我们可控点为eval前面对的action部分,于是后面的脏字符,我们可以通过#去注释掉。(p.s.用的时候请url编码为%23)
但是由于白名单限制,我们无法做一些操作。所以只能依靠其本身的作用 – 动态执行函数。
此时action,即需要执行的函数名,args,执行函数的参数。这两个都在我们可控范围。
尝试构造如下payload:
1 | ?action:show_flag_function%23;123 |
此时,成功返回:
果然,我是最天真的那个崽。
但此时也证明了我们思路的可行性,此时我们只需要找一个函数,可以给其传一个参数的那种。进而getflag。
(p.s. flag函数无参数,所以我们无法直接执行。)
找啊找啊找朋友,一天过去了,又一天快要过去了… 代码都快会背了….
最终功夫不负有心人,终于发现一个神奇的地方。
第144行,trigger_event函数中,他传入了两个功能。然后回想代码,可以发现前面对各个函数执行,是通过execute_event_loop来对队列里的任务进行执行的。trigger_event正是那个添加任务到队列中的函数,此时该函数我们可控。
再想到之前的条件竞争,我们可以在内部构造一个竞争?对的,可以的,但是此时不配称之为竞争了。
首先我们看一下我们购买的正常逻辑。
此时,由于其先进先出的原因,我们可以一开始就传入两个参数,如下:
1 | ?action:trigger_event%23;action:buy;111%23action:get_flag; |
此时传入一个buy和getfalg。我们再看一下逻辑。这样成功实现了,没钱买东西。
此时,问题来了,即便我们够了5个钻石,此时也获取不到flag。
因为他将打印flag的语句注释掉了。
可以看图片155行,此时return的为“天真的孩子”。那这样的话,是不是这题就没法解了?
肯定能解啊,怎么可能不能解。
此时仔细看165行,发现了什么?他是将flag作为参数传到show_flag的。别忘了,trigger_event是有log功能的,也就是此时flag会加进log里的。虽然log是在session中,但是,此时flask的特性,我们之前已经说过了。session是在本地的。虽然不能伪造,但是我们还是通过工具解开,查看内容的。
[200pt] 欢迎报名DDCTF
emm,感觉这题应该比吃鸡分高的。这道题,感觉比较偏实战渗透。不过作为ctf题目来说,的确有点脑洞了。因为大部分同学没往实战上想。
首先第一步:XSS
说实话,一开始拿到这题第一反应是注入。瞎注了半天。最后无疾而终。
知道hint出来,竟然是我最喜欢的xss,然后就做了。做到后面,已经拿到接口了,也尝试了注入,然而又没卵用。
第二步:注入
主办方不放hint是注入的话。这题我就不打算做了。实在get不到点。还是太菜了、
——以下正文——
xss读文件,很基础。发现waf了iframe和window。在es6新语法中,很好绕的。
1 | function s(e) { |
这里还有一个坑点是,渲染payload是在admin.php,此时如果读当前页面源码,返回的是你的payload。必须再次通过iframe读取admin.php,才能获取到本来的源码。
从源码中,可以得到一个接口:
1 | http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id= |
这就是传说中的注入点!!!要不是主办方肯定他是,我都不敢信…
最后终于通过宽字节注入,试出了点眉目。
p.s.注入过程真心迷,不跑5遍以上脚本,读不出来正确的东西
1 | import requests |
[210pt] 大吉大利,今晚吃鸡
很迷很尬的一道题,最后小手段才做出来。
首先,很容易可以看出来,是一个go写的。
而且买票时,票价只可以多 ,不可以少。此时可以猜到是溢出,从而实现购买。
可以看一下,go中的数字范围。然后天真的从大往小试。最终卡在了以下俩数。
1 | 9223372036854775807 // 可以输入,显示正常 |
于是自己天真的认为 ,题目对溢出做了判断,然后就凉了。蜜汁分析了半天。
最后再注册处发现一个越权漏洞。每次注册,无论成功与否,都会返回注册用户的cookie,此时可以直接登录。
于是看了一下榜单,挨个试了一下榜单师傅们的id。
最后还真找到了rmb122师傅的账号,然后发现他用的4294967296溢出。也就是uint32
心态炸了。竟然不是uint64,自己也没试uint32。哭了。
此时通过溢出,可以直接购票。然后我们进入下一关,如何删除竞争对手。一说到游戏,顿时想起了“白导”。我自己也导演一场呗。
于是,新建账号 -> 买票 -> 付款 -> 加入游戏 -> 获取id踢掉。一条龙服务。脚本如下:
1 | import requests |
[260pt] mysql弱口令
挺有意思的一道题,最后算是非预期出的吧。预期解实在是想不出来了。
首先拿到题目,其实就感觉是杭电那题了 wp链接
通过蜜罐sql客户端,来获取连接者的数据。但这题,说实话前期把我玩蒙了。
题目逻辑,首先下载扫描验证端,放到服务器上,做一下信息收集。然后再服务端填写自己服务器信息,就可以开始弱口令扫描了。
天真的我以为端口是填agent的端口。酿成一大惨祸。最后才发现,端口是填数据库端口。这样一来,心结解开。终于能拿数据了。
拿数据的心路历程更加艰难险阻。首先肯定是先读/etc/passwd。进而直接尝试读/flag,发现没有。
此时,自己意识到了事情的不简单。这是要和flag玩捉迷藏么。
这是很难受的一件事…
p.s.心酸历程就不说了。读文件肯定没这么顺利,要都说的话,1万字也收不住。
按照杭电那次学到的妙招,首先可以先读一下/proc/self/environ,如下图。
此时可以发现两个点,第9行和11行,组合起来/home/dc2-user/ctf_web_2/restart.sh。此时读取发现如下信息
从中,我们可以分析出来,该应用为flask应用,用gunicorn中间件起来的。
此时根据规则 gunicorn 文件名:项目名。可以推出:
/home/dc2-user/ctf_web_2/didi_ctf_web2.py
尝试读取,可以发现:
此时证实了我们的猜想。然后根据flask_script项目的目录结构,进而读取app。
然后一环接一环,依次读出了下面三个。
/home/dc2-user/ctf_web_2/app/init.py
/home/dc2-user/ctf_web_2/app/main/init.py
/home/dc2-user/ctf_web_2/app/main/views.py
在main/views.py中,我们可以看到一个hint。
猜测是通过curl可以从数据库中读到flag…
奈何自己比较菜,看到数据库遍想起来好像有个文件,是在手动修改数据库时,会留log。
对,没错,就是.mysql_history。此时尝试用户目录,root目录,最终在root目录读到flag
/root/.mysql_history
[390pt] 再来1杯Java
p.s.压轴题哈,说实话,这题真的学会了不少东西。毕竟自己太菜了,虽然本科专业为java开发狗。但我真的不太熟啊…
一共分为三关吧。
首先是一个PadOracle攻击,伪造cookie。这个解密Cookie可以看到hint: PadOracle:iv/cbc。
第二关,读文件,看到后端代码后,才发现,这里贼坑。
第三关,反序列化。
首先第一关好说,其实在/api/account_info这个接口,就可以拿到返回的明文信息。然后通过Padding Oracle + cbc翻转来伪造cookie即可。在这里就不多说了。网上很多资料。
最后拿到cookie,直接浏览器写入cookie就OK。然后可以获取到一个下载文件的接口。
/api/fileDownload?fileName=1.txt
虽然说是一个任意文件读取的接口,但是贼坑、
一顿操作猛如虎,最后只读出/etc/passwd…
搜到了很多字典。然后burp爆破一波,最后发现/proc/self/fd/15这里有东西,看到熟悉的pk头,情不自禁的笑了起来。(对,就是源码)
源码也不多,很容易,可以看到一个反序列化的接口。
在反序列化之前,还调用了SerialKiller,作为一个waf,对常见payload进行拦截。
首先题目给了hint:JRMP。根据这个hint,我们可以找到很多资料。在这里自己用的ysoserial,根据他的JRMP模块来进行下一步操作。
在这里,JRMP主要起了一个绕过waf的功能,因为这个waf只在反序列化userinfo时进行了调用。当通过JRMP来读取payload进行反序列化时,不会走waf。
首先,JRMP这个payload被waf掉了,我们可以采用先知上的一种绕过方式。
直接修改ysoserial源码即可,将原有的JRMPClient的payload复制一份,改名为JRMPClient2,然后保存并编译。
此时我们可以尝试使用URLDNS模块,来判断是否攻击成功。
1 | # 修改替换{{内容}} |
然后查看dnslog信息。发现存在,那就是ok了。
接下来可以尝试换payload了。此时这里还存在一个问题。服务器端无法执行命令!!
这个是hint中给的,所以我们需要找另一种方式,如:代码执行。
查阅资料,发现ysoserial预留了这块的接口,修改即可。
然后我们尝试去修改ysoserial/payloads/util/Gadgets.java中createTemplatesImpl方法如下:
1 | // createTemplatesImpl修改版,支持代码执行 |
此时,我们的payload已经可以支持代码执行了。
在这里,我是直接用本地的题目环境进行调试,尝试打印了aaa,操作如下。
1 | # 修改替换{{内容}} |
然后进而写一下获取文件,以及获取目录的代码。此时拿到文件,无法回显。我们可以用Socket来将文件发送到我们的服务器,然后nc监听端口即可。
1 | // 以下代码使用时,记得压缩到一行。 |
然后操作如下:
1 | # 修改替换{{内容}} |
p.s. /flag是个文件夹