之前看群里郁师傅推荐了这个比赛,本着试试水的心理就报了这个比赛。不过因为学校程序设计周的原因,自己大部分时间都花在跟队友爆肝Qt版贪吃蛇就没怎么特别花时间去做。(这里稍稍抱怨一下,学校程序周设计不收python程序。于是本来用python的pygame库半天就能解决的问题我们硬生生拿MFC跟Qt做。。。当然,头天自学完Qt第二天就完成完整版贪吃蛇,心里还是挺骄傲的;最后一天熬夜给贪吃蛇加上ai功能进行人机比分也是一段难忘的经历。感谢组内其他大佬,并向所有开发者致以敬意)。
回到正题,这算自己头一次参加线上型的CTF比赛,而且这一比赛更多是面向新手,自己最后拿到900分,381 / 1904 名勉强算差强人意吧。毕竟自己正式开始打ctf到现在还不到半年,大概还有上升空间吧?另一方面,自己只能做web加少量杂项题加某些入门级密码学题也显得自己的能力格外局限。还得早日把web巩固好后发展下二进制跟密码学的能力呀。
首先贴出比赛网址:https://hack.lug.ustc.edu.cn/
官方writeup:https://github.com/ustclug/hackergame2019-writeups
下面就我自己做出来的题目稍微总结下。
1.签到题 50pt
题如其名,其中明示一个提交按钮无效。chrome+f12即可解决。把button中属性-值:disabled="disabled"删掉提交token就可以了。
基本是各大平台签到题的常规模式。
白与夜 100pt
杂项签到题。点开后一张白猫图片,果断扔stegsolve,几乎每个通道都能看到图片中隐藏的flag。
不过后来听同学说还有不需要工具的方法,同样f12也能看到白猫图片的background-color:white。改为black即可。
信息安全2077 150pt
web手签到题。虽然自己基本秒反应改包,但是一开始传错变量了。总体上比较简单,以下是打开页面显示的源码
显然访问到flag.txt内容时headers中'If-Unmodified-Since':的值不应该是今年的时间,所以post传一个2077年后的时间就好了。
当时还有一个想法就是改电脑时间,毕竟源码中写的很清楚,变量now值为当前时间。只不过自己发现电脑时间改不到2077年,就此作罢。后来也在官方wp中得到证实。
宇宙终极问题 42 100pt
百度题。虽然 可以nc 202.38.93.241 10017 命令来连接题目,但也可以直接用提供的网络终端。这道42就是要求满足立方和为42的三个数。百度后发现居然是去年才刚用超级计算机算出来的三个大数,总之传就是了。
这道题剩下两个小题因为过的人太少就没看了,不过官方说第二小题也是google题???看来出题人眼中的google题跟我们眼中的不太一样。
网页读取器 150pt
看起来是ssrf。其中host白名单只有example.com与www.example.com,协议也限定成http://。大概上看应该是防住最基本的ssrf。然后点进源码看看规则:
from flask import Flask, render_template, request, send_from_directory
import requests # well, requests is designed for humans, and I like it.
app = Flask(__name__)
whitelist_hostname = ["example.com",
"www.example.com"]
whitelist_scheme = ["http://"]
def check_hostname(url):
for i in whitelist_scheme:
if url.startswith(i):
url = url[len(i):] # strip scheme
url = url[url.find("@") + 1:] # strip userinfo
if not url.find("/") == -1:
url = url[:url.find("/")] # strip parts after authority
if not url.find(":") == -1:
url = url[:url.find(":")] # strip port
if url not in whitelist_hostname:
return (False, "hostname {} not in whitelist".format(url))
return (True, "ok")
return (False, "scheme not in whitelist, only {} allowed".format(whitelist_scheme))
@app.route("/")
def index():
return render_template("index.html")
@app.route("/request")
def req_route():
url = request.args.get('url')
status, msg = check_hostname(url)
if status is False:
# print(msg)
return msg
try:
r = requests.get(url, timeout=2)
if not r.status_code == 200:
return "We tried accessing your url, but it does not return HTTP 200. Instead, it returns {}.".format(r.status_code)
return r.text
except requests.Timeout:
return "We tried our best, but it just timeout."
except requests.RequestException:
return "While accessing your url, an exception occurred. There may be a problem with your url."
@app.route("/source")
def get_source():
return send_from_directory("/static/", "app.py", as_attachment=True)
if __name__ == '__main__':
app.run("0.0.0.0", 8000, debug=False)
一看@没有过滤掉我就开心了。于是自己最后过题的url随便乱输就拿到flag了:
http://web1/@example.com/../../flag
后来看了官方wp才发现自己整了个稀奇古怪的url。实际上这里考点是url parser 和 requester 的不一致性导致的意料之外的 SSRF 问题
基于这里的轮子直接有@,就能解析出hostname为example.com。直接利用#或者?在进行request时忽略掉#/?之后的内容就好了
所以官方解是:http://web1/flag#@example.com
利用到了#fragment的性质。
达拉崩吧大冒险 150pt
这道题花了我很长时间,但有一说一,确实挺有意思的。开始先当文字冒险游戏玩,然后很快发现比较接近“成功”的流程是这样的:首先你是金钱100武力为0的玩家,先进门被收50过路费,然后答应国王从恶龙手中救出公主,然后面临选择买蛋升级/打怪升级/打龙。显然只要赢了龙应该就能拿flag,不过整个流程下来,只有买蛋时能加武力值,且最多才100。打怪打龙都打不过。。。于是自己转向源码,发现可以在f12控制台中使用ws 这一websocket变量传值。其中值对应着你每一步的选项,也就是0,1或者是卖鸡蛋时的0-10。所以自己就陷入疯狂用ws传值的过程中,或者试图跳步骤,或者试图改金钱,但是都失败了。不过在中间某个过程中,自己试图用ws传负值时,页面直接报错说我搞渗透,退出这一websocket进程。于是察觉到可能正是需要传负值来搞渗透。最后发现是买鸡蛋升武力值时,可以传负值,狂掉武力值,但是掉到某个值后就可以负溢出,成为一个极大值。这时你就可以搞定恶龙拿到flag了。
三教奇妙夜 200pt
整个比赛下来耗费我时间最长的一道。当然说到底还是我菜。一开始下下来题目文件是一个近12小时的视频。根据提示,视频中会出现白底黑字的内容,只要找全就可以拼成flag。开始我当然是想一帧帧读视频比较了,结果自己pycharm的库opencv始终下不下来。于是我就搜索其他解决方法,看到一个ffmpeg的工具可以视频转图片,于是就决定用它了。但是自己不会使用ffmpeg的详细操作,所以就用默认配置把视频输成以数字为文件名的图片。。。。。硬生生整出了30万张图片。。。这里我居然又开始整骚操作,把python用于图像识别的库也下了下来。于是有了自己第一个版本的脚本:
for i in range(1,340633):
str1 = 'D:/ffmpeg-20191019-31aafda-win64-static/ffmpeg-20191019-31aafda-win64-static/bin/' + str(i) + '.jpg'
text = pytesseract.image_to_string(Image.open(str1))
print('正在执行中:第'+str(i)+'张')
print(text, end=' ')
没错,我开始真的想遍历一遍三十万张图片。甚至在开始10分钟后,我遍历到4800张左右时就出来了两张含部分flag的图片,让我以为这方法说不定意外的可以。结果之后从4800到240000张居然什么内容都没有!?中间我去上课,去图书馆都一直开着电脑跑,结果居然毫无所获,让我有点心灰意冷。这时我尝试查看了那两张含flag的图片,发现他们大概1.5kb左右,而其他正常帧的图片大概3k左右,于是我直接在文件夹里调整为按大小排序。。。。于是出来了第25万张左右的含flag的图片!但同时我也意识到,按大小最后整理出来的含flag图片明显不全,也不知道是当初转图片时没转完还是什么别的原因。(但我觉得这个思路可行!说不定再完整转一次就能得到全部转好的图片再按大小排找得出来呢?不过这时我不想再去转几十万张图片了,打算回归opencv解法。询问同学后,得知要切换下载python库的源。这里我调整为阿里云的源后就可以在pycharm中安装opencv库了。于是新的代码如下:
import cv2
# import
def main_loop():
success, frame = video.read()
t = 0
while success:
t += 1
# cv2.imshow("Oto Video", frame) #显示
# cv2.waitKey(1000 // int(fps)) #延迟
last_frame = frame
success, frame = video.read() #获取下一帧
if ((frame - last_frame).sum()) > 100000:
print('hh')
videoWriter.write(frame) #写视频帧
if (t % 1000 == 0):
print(t)
video = cv2.VideoCapture("C:/Users/HP/Desktop/output.mp4")
fps = video.get(cv2.CAP_PROP_FPS)
frameCount = video.get(cv2.CAP_PROP_FRAME_COUNT)
size = (int(video.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)))
videoWriter = cv2.VideoWriter('oto_other.mp4', cv2.VideoWriter_fourcc(*'mp4v'),
fps, size)
print(frameCount)
main_loop()
只需十分钟,它就能把关键帧全部转到一个新视频里去。。。在此再次感慨自己之前下手方法的鲁莽以及没有技术的痛苦。。。总之最后出flag,记得分辨其字体里0与O的偏差就好了。
总之,这次比赛还是有所收获的(自己不是什么杂项都做不了的啊)。希望之后的比赛还能有所突破吧。