Hackergame2023 Writeup
Lysithea
后记
刚打完GG3隔了一周就来打HG了。能感觉自己确实在变强。今年HG确实比GG难,而且一如既往的马拉松(题也太多了),不过还是很开心那么多题能做出来或者有思路的。
目前我的CTF主攻方向是Web+Pwn,misc看脸,Crypto一看就困,说实话有点杂糅。校赛里会点二进制确实比较吃香,因为考虑到大家的平均水平Pwn都不会出的很难(更别提隔壁GG还有动态分机制)
最后,感觉最近玩的太疯啦,这一波CTF狂潮直接摧毁了我的作息。该回到主业上了,明年见啦。
[TOC]
签到
HG传统拿web题当签到。打开之后是一个录音接口,需要录制一段大声喊出“Hackergame,启动”的音频来获得flag。题目给了段示例音频,我一开始的想法是我直接把这段音频放给它就可以了吧?用OBS可以实现监听到麦克风。结果发现用内录的音频也只能到66%左右。进一步观察发现相似度判断在前端,可以直接改query,改到100就拿flag。
猫咪小测
4个简单的信息收集。

-
在arxiv的astrophysics板块直接搜索“chicken upper limit observable universe”,搜出这篇文章。注意文中其实有多个基于不同假设的上限,可以都试一遍。
-
在Linux的github repo中直接搜BBR,网页ctrl+f CONFIG_,能找到CONFIG_TCP_CONG_BBR符合要求。
-
不太熟悉信息领域的会议论文有什么更好渠道,我在谷歌学术上搜的,关键词好像是"mypy halting"和"mypy turing complete"(因为停机问题等价于图灵完备性),能搜出这篇论文,读了摘要发现内容和题干说的相符,但是这只是预印本不是会议论文。最后直接把大标题带双引号拿来搜索,几个结论里面,第四个是一个会议文集,满足要求,答案是ECOOP

更深更暗
一个几乎无限下拉的页面。看看前端js,发现直接搜索能在一个潜艇的像素画里找到${flag}模板。所以我们直接改js,在这里加一句console.log(flag),然后把整段js重新执行一遍就出了。
旅行照片3
OSINT好难啊!只做了第一问。奖牌上的名字一搜就发现是诺奖获得者小柴昌俊,奖牌被毕业的东京大学拿去展览(值得一提这位还在东北大学工作过,而且东北大学也有诺奖),搜索“日本 诺贝尔奖”可以得到维基百科一个页面。然后找诺奖奖牌介绍的时候也查到,诺奖物理和化学的奖牌是一样的,其他的都不一样。尽量找时间比较晚,看起来比较年轻的,然后点进去看有没有在东大的教育/工作经历。最后得出来小柴的学生:梶田隆章(1959)是符合要求的。(有个天野浩是1960的,但是是nagoya的,所以不行)。
至于是哪一天?我不知道。好在HG这边提交不限制频率,所以写了个js脚本直接爆破了。不得不说这个框架还挺有意思的,把答案base64之后做成个txt文件放在服务器上,如果答案正确就200OK返回txt里的flag,答案错误就404。
// 2021.3.18是Nintendo World的开园日,作为我爆破的一个时间上限
var ed_date = new Date(2023, 10, 28);
var st_date = new Date(2021, 3, 18);
const sleep = ms => new Promise(r => setTimeout(r, ms));
var date = ed_date;
while(date >= st_date){
let month = (date.getMonth()+1).toString()
let day = (date.getDate()).toString()
if(month < 10){
month = `0${month}`
}
if(day < 10){
day = `0${day}`
}
let date_str = `${date.getFullYear()}-${month}-${day}`;
$("input[name=Answer1]").attr("value", date_str);
console.log(date_str)
$("input[name=Answer2]").attr("value", "ICRR");
$("#form1").submit()
await sleep(200);
date.setDate(date.getDate() - 1);
}
其他信息的话,二三照片可以得到拉面店的店名和一个看起来很标志性的公园,Google Lens告诉我是上野公园。然后找上野公园的官网,上面有公园举行大型活动的列表,在archive.org里找8月的记录,能查到这个活动叫ウエノデ.ビアフェスタ2023(是个啤酒文化节?)。但是尝试查志愿者问卷的时候没找到,想过这种信息应该在活动时间附近的活动参与者/组织者的社交媒体能搜到?但是官推反正是没有,其他方面也没搜到,最接近的一个是一个学生社团去这个展上 摆摊的一个记录,当然也没有问卷的链接。这个断了的话,后面的题做了也没有意义(话说票价也可以爆破吧)
完全不旅行的旅行照片呢。
赛后补充:看了官方WP发现自己又🤡了,我好像爆破的时候从F12复制日期复制错了啊,原来是8月10日吗,活动是另外一个梅酒节,怪不得做不出来
赛博井字棋
众所周知,两边都会玩的话井字棋是赢不了的,最多只能平。受隔壁GG猜emoji的启发,首先去看的是cookie,发现有个session疑似JWT变体,第一个字段是个json,是我的token。我竟然还尝试了一段时间攻击这个cookie,现在看来这是不是属于攻击比赛平台(
然后F12里抓包看了看服务器通信,我们发送我们要在哪个位置下棋,服务器返回当前的棋盘状态。看起来无懈可击,直到我尝试改包把我的棋子下到了一个对面已经下过的位置……
这个题卡了好久,确实没想到这个题服务端竟然是如此的大聪明,这么基本的判断都没做。
奶奶的睡前flag故事
这个题明明是个图片隐写,题干却一直加粗暗示“谷歌亲儿子”“旧手机”“没有升级系统”“截图”,有猫腻啊。我确实听说过,有些手机截图会藏信息,甚至有苹果手机和安卓手机打开不一样的图片(我现在找不到来源了,我记得是苹果手机有特别设置可以读另一个源,所以会读出完 全不一样的图片),但没听说过安卓(谷歌)手机有这样的特性。
下载下来拿010 editor,pngcheck,zsteg,stegsolve都招呼一遍。很容易发现除了文件最后加了大量额外内容(符合图片文字描述“最后”),其他都很正常。可以发现如果搜索关键词“IDAT”,会发现额外内容里其实都是合法的IDAT块,甚至最后还有个额外的IEND,所以额外的图片肯定都在这里。但是额外数据的第一个IDAT块到第一个IEND块之间有大量垃圾信息不知道是什么。有过猜想是这是被切割的后半段IDAT块,但是大小并不能凑成0x2000这个整数,可行性存疑。然后我尝试拿常规的方法处理,改图片大小并更新CRC,把新的IDAT块插在后面,或者计算好偏移让最后一个块凑成0x2000,都不行。
尝试另一个思路。谷歌亲儿子搜出来是nexus手机,然后brainstorm一些可行的关键词,比如【隐藏】【泄露】之类的,最后还是【泄露】赢了,具体来说就是Nexus老机型有漏洞(后门?),对截图编辑后的截取的信息还是能被提取,还有专门工具Acropalyse能做到这一点。

(说起来题解第一段的思路我一看到题就有,但是一开始怎么也搜不出来,过了几天刷群看到群友聊到这个题“不看新闻的话还真就不知道”之后5分钟就搜出来了。这算被群友透了吗?如果算的话给群友ban一下谢谢(开个玩笑))
组委会模拟器
整活性质大于实际意义的一个题,后来刷群天天有人发【这个题的flag是hack[xxxxx]】,笑死。
其实会写javascript就一切好办了,querySelect下来,正则匹配一下,触发click,做个setInterval。刷新页面直接把脚本复制进去回车,等它跑完
setInterval(()=>{
all_msg = Array.from(document.querySelectorAll(
".fakeqq-message .fakeqq-message__bubble"
)).filter((e)=>{
var flag_ptr = /hack\[[a-z]+\]/;
return flag_ptr.test(e.innerHTML)
}).map((e)=>{
// console.log(e.innerHTML)
e.click()
})
}, 1000)
话说真的要等好久啊,这个题我把脚本挂着就去干别的去了,结果flag被alert出来的时候被我一个回车给它干掉了,控制台输出里竟然还找不到,又得再跑一遍,笑死。为什么不用console.log👿
JSON ⊂ YAML?
又是个自研不如搜索的题。这个题要给出一个合法的JSON,让它能在YAML1.1标准下得到和JSON不同的结果,或者在YAML1.2标准下会报错。一开始尝试了yaml带注释而json没有的特性,尝试拿hashtag(#)作为字段,结果毫无卵用。
我觉得生啃两个数据协议的文档没有什么意义,于是开始搜索。先是尝试到poe.com上去问Claude-Instant,给我的都是不能用的。之后再搜YAML和JSON的Wikipedia,发现两个wiki有各自引用对方的页面,说明两个协议的包含关系。在wiki的引用中,最有用的是这两篇:metacpan和9年前的stackoverflow。甚至,后者直接给出了符合题目的两个例子:
- 让YAML1.1给出不同结果:1234e999在JSON里是科学计数法的inf,在yaml里是字符串,可以构造
[1234e999] - YAML1.2会把duplicate key作为error(是stackoverflow提问者在问题里提到有这个特性),所以构造
{"1":"12","1":"23"}即可
Git? Git!
Git仓库信息泄露题。众所周知Git是个非常成功的分布式版本控制工具,以至于非专业人士很难把一个Git仓库里的历史记录删除干净,如果被push到线上那就更难了(当然这个题是本地)
具体来说,git里包含各个版本的文件内容是作为object,以特定的hash作为key保存的,具体来说,是在.git/objects/b2/b1962e7b4b6c3e1136028e20425a28a392c501下xx+yyyyyyy组成一个共40位的hash(sha1?),然后只要git cat-file -p $hash就可以把对应文件内容打印出来了。于是我写了简单脚本把git仓库里所有objects全部打印了一遍,flag就在其中。
HTTP集邮册
你知道我在11个状态码卡了多久吗?(虽然好像有群友说能爆14个,但雨我无瓜)
主要参考资料是NFC文档,MDN文档和Nginx源码。按获得顺序整理:
- 200 OK:直接点就可以一键获得,最简单的一个
- 405 not allowed:换成HEAD和GET以外 任意方法可以获得,POST是最简单的,换一个不存在的也行,比如AAAJIUMING。后续尝试中很容易出的代码
- 404 not found:把/换成任意不存在的合法路径即可
- 400 bad request:我第一遍是/../出的,其实你随便发点什么东西都会出这个代码,也是后期最不想看到的代码
- 505 http version not supported:改成HTTP/2.0,3.0都可以触发。好像9.9也可以触发
========= flag1分界线 ==============
- 414 uri too large:让URL变得特别长就可以了。或者让method变得特别长也是这个,应该就是首行太长就会返回这个
- 206, 416:两个是Range相关的,可以在header加一个Range字段,让服务器只返回固定字节偏移的内容。如果能够截取这个偏移(比如
Range: bytes=0-1),就会返回206 Partial Content;如果不能截取(比如Range: bytes=114514-1919816或者Range: bytes=1-0),就会返回416 Requested Range Not Satisfiable。 - 100 Continue:这个状态码可能本来是在发一个大请求之前,先预告服务器连接状态用的,只要加
Expect: 100-continue头就可以在200OK之前先返回一个100 Continue。值得一提这个Expect头只有100-Continue这一个可用的值,而且只在HTTP/1.1有效 - 无状态码:只要不带后面协议只发一个
GET /\r\n,NGINX就会认为这是HTTP/0.9版本,从而发一个向下兼容的不带状态码的回复(那个年代可能还没有请求头,状态码这些东西) - 412 Precondition Failed: MDN文档提到,如果包含If-Unmodified-Since条件不满足时会报这个错(一般这个请求是用本地文件较新文件覆盖远程文件的,如果服务器更新那就不用修改)
- 413 Content Too Large: 只要加一个数值够大的Content-Length字段 即可。看起来不管method是否应该携带数据,NGINX都会尝试处理Content-Length头。
- 304 Not Modified: 这个是卡我最久的,原理就是用If-Modified-Since和If-Match来请求。NGINX会对某些资源(通常是静态图片,字体等)采取默认的缓存策略,只要本地浏览器缓存的时间更新/本地资源的etag符合,服务器就不会返回内容而是返回304让浏览器直接使用自己的缓存。事实上服务器就只是用这个头来判断的,它无法检查本地浏览器真的缓存了没有。一个trick是,这两个头必须要同时加,nginx才会认为你真的缓存了正确的资源。
一些不能用/没发现怎么用的:
- 401, 403:虽然自己配服务器天天见403,但这个还真就出不来,因为默认配置没有.htaccess这种不可访问文件。401同理。
- 重定向相关:301,302这些
- WebDAV相关,太多了,MDN会说哪些是WebDAV的code
- upgrade相关:本来想能不能HTTP/1.0升级HTTP/1.1,很可惜不行,upgrade最低支持HTTP/1.1,websocket或者HTTPS那就更不可能。所以101,426不行
- PUT, POST相关:因为GET和HEAD都不能带Content,会排除大量的code。所以说413能用我反而很意外。
- 超时相关:我确实尝试过python脚本爆破服务器去打429 Too Many Requests和408 Request Timeout!很可惜前端python应该是串行的,不行。
- 服务器报错:我们nginx是很稳定哒!50x系列很难出了(505是例外)(话说如果出了什么事故导致后台停机了是不是会免费送选手一个502)
- 写在标准里但是NGINX不认的:点名431 Request Header Fields too large!写很大的header会被NGINX直接规约成400,甚至title都不一样!源码如下:
if (overwrite == -1) {
switch (err->status) {
case NGX_HTTP_TO_HTTPS:
case NGX_HTTPS_CERT_ERROR:
case NGX_HTTPS_NO_CERT:
case NGX_HTTP_REQUEST_HEADER_TOO_LARGE:
err->overwrite = NGX_HTTP_BAD_REQUEST;
}
}
/* 你是否在嘲讽我?
<html>
<head><title>400 Request Header Or Cookie Too Large</title></head>
*/
