又是一年Hackergame,我这个菜鸡又来被虐了。
如果有读者想看完整且严谨的Writeup,建议移步官方GitHub,本文章仅图一乐。
签到
一开始的思路是想办法修改时间限制,使得时限内也能签出2022。但找了一圈发现超出了自己的知识范围,遂作罢。
之后随意点了一下提交,URL中出现了一个query string,value和识别结果不能说相似度很高吧,只能说完全一样。这和去年的签到题有点相像,都是通过修改URL的query string来拿到正确的token。
将value修改为2022,根本不用画,就拿到了token。
PS:2022年的网页竟然还在用Vue 2……
猫咪问答喵
灌注关注taffy喵,关注taffy谢谢喵。
一如既往是非USTC学生也能做,但出题人似乎在努力平衡校内校外人的难度。
1
直觉当然是前往中科大信安协会官网寻找信息,但即使动用了Wayback Machine也并没有什么收获(而且这两个组织竟然是分立的)。
后来发现只需Google一下,在学校新闻网上就可以找到是2017年3月。
2
虽然问的是演讲内容,但是完全不需要找USTC的资料。KDE的程序就那么多,可以直接枚举得到结果。枚举到Kdenlive发现答案正确,搜索发现在描述的场景下确实有显示问题。
3
找到 Firefox version history 页面,可以看到“Firefox 12 is the final release to support Windows 2000 and Windows XP RTM & SP1.”
此外也可以直接搜索”firefox drop support for windows 2000″,然后找到Mozilla的公告。
4
开头先Google “linux kernel argc zero”,找到对应的 LWN 新闻。通读“towards a general fix”部分,发现Ariadne提出了一个修复方案,而Torvalds本人是倾向于这个方案的。从这部分的链接进入 commit 的详情,发现修改的是 fs/exec.c,合入主线时的修改应该也修改了此文件。合入主线的日期肯定晚于2022/1/26,即这个commit的日期。
然后找到 kernel.org中 kernel/git/torvalds/linux.git(不要找GitHub),找到fs/exec.c的修改log,然后从上面的日期往后找。找到这个commit,标题中的argv is empty正好对应了argc为0,上面也附有Ariadne的说明。填进去Hash为dcd46d897adb70d63e025f175a00a89797d31a43
,正好对了。
对OS熟悉的读者或许能够直接推断出argv is empty这另一种说法,用这个直接Google能更快找到答案。
5
这道题非常有迷惑性,很容易将研究导向错误的方向。但是,遵循着猫咪问答的传统——考察搜索技能,找到答案和密码学知识其实没有半毛钱关系。更何况,懂密码学也没法在这个方向搞出答案。
很容易将MD5直接使用Google搜索,但搜索框会识别一些以冒号开头的命令(如site:cyp0633.icu),所以Google很容易将冒号分隔的词语各作为一个关键词而搜索。而将关键词使用双引号括起来能够禁止转义,或者使用Google高级搜索,使用”与以下字词完全匹配”功能。
搜索结构中很容易找到一段SSH日志(如果没有结果,删掉开头的MD5:
),从日志中可以找到一个Host IP地址205.166.94.16,使用SSH连接,提示信息如下:
The authenticity of host '205.166.94.16 (205.166.94.16)' can't be established. ED25519 key fingerprint is MD5:e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])?
正好是我们所要的MD5(如果是SHA256,使用ssh -o FingerprintHash=md5
)。使用IP访问网页,能够找到页面上一个域名sdf.org,对应的就是这个IP地址。
综上,答案为sdf.org。在WHOIS上查询,确实是1996年10月12日创建的。
6
此处百度搜索起来效率更高,Google索引不到什么东西。
这题也有比较大的迷惑性。很容易我们可以找到中科大网络信息中心网站,此处展示了中科大历年的网络方面通知文件。我们可以找到两个“关于实行新的网络费用分担办法的通知”,一个发布于2003年,另一个发布于2010年。
但是,后者并没有针对网络通服务定价作出什么修改,也就是说“20元一月”的定价并不是从此时开始的。找到另一个文件,提到“网络通自2003年3月1日起开通”,国际连接也是20元一月,所以本题答案为2003-03-01。
家目录里的秘密
拿到压缩包,直接使用VS Code全目录搜索flag
,第一个flag就在.config/Code/User/History/2f23f721/DUGV.c里。
第二个flag与Rclone有关,而这个压缩包内与Rclone有关的文件就是.config/rclone/rclone.conf。这里面保存了一份使用Rclone连接一个FTP服务器的配置文件,然而文件夹中并没有办法找到example.com具体指的是什么,加之题目上也说在压缩包里找flag,就并不应该试图努力从远程服务器拿flag。秘密应该在pass
字段里。
联想到FTP连接是需要把密码还原出来的,所以这个pass
应该是使用了对称式加密。在一篇社区文章里证实了我的想法,使用的是AES加密;而通过搜索”decrypt rclone pass”之后找到了另一篇社区文章,贴出了一段代码,能够将pass
解密。按照帖子中的指引操作即可,解出来正好是flag。
有意思的是,第二篇帖子在10月20日发布,不由得让人猜想这其中的关联性。
HeiLang
到了第一天晚上才做出来,咕咕咕了大半天,转发抽一人送一个AirPods Pro(bushi)。
手撸了一个简单的translator,将HeiLang转换为Python。非常不优雅而且低效,但能用。
f=[]
out=open("getflag.py","w")
with open('./getflag.hei.py') as file:
f=file.readlines()
for i in f:
if i[:2]=='a[':
eq=i.rfind('=') # 查找等号的位置
result=int(i[eq+2:].strip()) # 获取数字
i=i[2:eq-2]
nums=i.split('|') # 分割数字
for n in nums:
index=int(n.strip()) # 获取下标
out.write("a[{}] = {}\n".format(index, result))
else:
out.write(i) # 非赋值语句,直接写入
把它运行一遍,看看输出内容,你就知道它的工作原理了。
这段代码写得贼累,求赞求投币求转发,最重要的是点一个大大的关注,后面我忘了(逃
Xcaptcha
逻辑比较简单,请求题目就对/xcaptcha
发送GET请求,请求时需提交Cookie以便于识别个人Token。提交结果时,就对/xcaptcha
发送POST请求。要带上刚刚GET请求的Cookie,否则无法对应刚刚获取的题目。
刚开始使用浏览器JavaScript断点捕获了一套题目,做出来之后再通过Postman发送POST提交,却会提示超时,这意味着我们必须使用程序自动化提交。
考虑到得到的字符串中含有中文,而且还包含整个算式,我想了想,还是用Python比较舒服。程序如下:
import requests
import re
url="http://202.38.93.111:10047/xcaptcha"
headers={
"Cookie":r"your-cookie",
}
r=requests.get(url,headers=headers)
newCookie=r.headers["Set-Cookie"]
ans=[]
pattern=re.compile(r'<label for="captcha\d">.+<\/label>') # 匹配题目部分
captcha=pattern.findall(r.text)
for c in captcha:
c=c[22:-14] # 去掉标签
print(c)
ans.append(eval(c))
data={
"captcha1":ans[0],
"captcha2":ans[1],
"captcha3":ans[2],
}
headers["Cookie"]=newCookie
r=requests.post(url,headers=headers,data=data)
print(r.text)
然后只需要从输出的内容中将赤裸裸的flag粘进去就行了。
Headless browser and requests are all your friend
旅行照片 2.0
又到了和去年一样的社工环节,不同的是今年有100分可以直接读EXIF骗过来。太简单就不讲了。
邮政编码可不一定是中国大陆的,照片中出现了”welcome to zozomarine stadium”字样,搜索发现是在日本千叶,进一步,很容易推断出拍摄者身处“アパホテル&リゾート〈東京ベイ幕張〉”。但是体育馆和酒店的邮编并不相同,所以答案为2610021。
熟悉手机的人可以一眼看出机型是红米Note 9 4G,而对于其他人,手机分辨率的突破口其实仍然是EXIF。相机型号未给出准确值(可能用了谷歌相机之类的),但SM6115代表骁龙662,再搭配小米品牌的条件就不难找到了,这是一块2340*1080的屏幕。
然后通过地图搜索机场,附近东京都市圈内的机场有成田和羽田两座。成田机场在球馆背侧,跑道大致南北向,所以通过的飞机不应该在图中;那剩下的就是羽田机场(IATA: HND)可能性最大。
飞机像是在起飞,所以起飞机场就是HND。寻找数月之前的航班难度较大,而鉴于5月14日是周六,我们可以尝试搜索10月22日(同是周六)18点23分之前起飞的航班。
方法很简单,一个一个往前试。最终试到了NH683,由HND飞往HIJ。
猜数字
研究了一下请求,通过对/status
的POST提交结果,然后再对/status
进行一次GET来获取服务器反馈。
结合题目描述的大数定律(我概率论真的不好),那确实是收敛于某个值?于是用Python写了个暴力猜数程序,试图找找规律。
import requests
nums=[]
cookie={}
header={
"Authorization":"Bearer your-token",
}
url="http://202.38.93.111:18000/state"
for i in range(0,100):
min=0.0
max=1.0
resp=requests.get(url,headers=header,cookies=cookie)
while(1):
max=round(float(max),6)
min=round(float(min),6)
sendNum=round((max+min)/2.0,6)
resp=requests.post(url,headers=header,cookies=cookie,data="<state><guess>{}</guess></state>".format(sendNum))
print("尝试{},max={},min={}".format(sendNum,max,min),end=" ")
resp=requests.get(url,headers=header,cookies=cookie)
print(resp)
if resp.text.find("<talented>1</talented>")!=-1:
print("已经达成一次猜出数字,正在退出")
elif resp.text.find("less=\"false\" more=\"false\"")!=-1: # 猜对了
nums.append(sendNum)
print("猜对了,数字为:{}\n====================================".format(sendNum))
break
elif resp.text.find("less=\"true\" more=\"false\"")!=-1: # 猜小了
min=sendNum
print("猜小了,数字为:{}".format(sendNum))
elif resp.text.find("less=\"false\" more=\"true\"")!=-1: # 猜大了
max=sendNum
print("猜大了,数字为:{}".format(sendNum))
elif resp.text.find("less=\"true\" more=\"true\"")!=-1:
print("猜错了,数字为:{}".format(sendNum))
else:
nums.append(sendNum)
print("未知错误,应该是猜对了:"+resp.text+"\n====================================")
break
print("当前已经猜对的数字有:{}".format(nums))
print(nums)
但并没有找到什么规律。
LaTeX 机器人
纯文本
一看到能够自由输入内容,就想到了某种注入。但阅读Dockerfile,进行本地转换部分的命令似乎没有什么可以自由发挥的空间,就放弃了在Shell中注入的计划。
转念一想,LaTeX语法是可以包含其他文本文件的,语法为\input{filepath}
。于是输入\input{/flag2}
,弹出的信息就是Flag……吗?
不完全是。LaTeX自带转义,只需要在合适的地方加上花括号就行了,具体位置可以仿照之前的Flag。
特殊字符混入
此题仍然可以使用\input
解决,但是要小心两种控制字符的转义。LaTeX的控制字符可以使用\catcode
指令禁用(参考),不过此处只需要禁用#
和_
,剩下两个不必要。构建的字符串如下。
\catcode `\#=12\catcode `\_=12\input{/flag2}
那这题的难度在哪里呢?我想,应该是对LaTeX各种术语的熟悉吧,比如不知道“控制字符”(control character)就没法搜出对应的方法。
安全的在线测评
在看到题之后,就隐隐约约感觉到,当年打NOIP敢想却没条件做的事情——直接读答案输出,说不定就是这道题的正解。又想到成熟的OJ,例如青岛大学的OJ,对这种情况都有相应的防范措施,而这个判题脚本又显得过于简单,应该八九不离十。
评测机的工作目录树大概是这个样子:
. ├── data │ ├── dynamic*.in │ ├── dynamic*.out │ ├── problem.txt │ ├── static.in │ └── static.out ├── online_judge.py ├── README.md └── temp ├── code.c └── temp_bin
无法AC的题目
这里我们的目标是读取./data/static.out
的内容,然后输出。注意程序运行的目录并不是在temp
里。很容易得到以下的代码:
#include<stdio.h>
int main(int argc, char *argv[])
{
FILE *fp;
char buf[100];
fp = fopen("./data/static.out", "r");
while (fgets(buf, 100, fp) != NULL)
printf("%s", buf);
fclose(fp);
return 0;
}
当然这样只能读到静态数据的输出,后面的显然会WA。
[path,]
,这是机器上没有 runner 账户造成的。动态数据
一开始以为思路其实是差不多的,都是读答案。但为了判断用哪个答案对应的输出,还需要同时读取输入文件进行比对。但是生成的动态数据权限均为700,这意味着评测机上runner
账户运行的程序无法直接读取答案。
所以有了一份本地能过,云端却过不了的代码,仅供取笑:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
FILE *fp;
// read input from stdin to char static_in[]
char buf[1000];
memset(buf, 0, 1000 * sizeof(char));
fgets(buf, 1000, stdin);
// remove \n
if (buf[strlen(buf) - 1] == '\n')
{
buf[strlen(buf) - 1] = '\0';
}
// read from ./data/static.in
char static_in[1000];
memset(static_in, 0, 1000 * sizeof(char));
fp = fopen("./data/static.in", "r");
fgets(static_in, 1000, fp);
if(static_in[strlen(static_in) - 1] == '\n')
{
static_in[strlen(static_in) - 1] = '\0';
}
fclose(fp);
// compare input with static.in
if (strcmp(buf, static_in) == 0)
{ // static data
// read from ./data/static.out (multi line) and print
fp = fopen("./data/static.out", "r");
while (fgets(buf, 1000, fp) != NULL)
{
printf("%s", buf);
}
fclose(fp);
return 0;
}
char path[1000];
memset(path, 0, 1000 * sizeof(char));
for (int i = 0; i < 5; i++)
{
sprintf(path, "./data/dynamic%d.in", i);
fp = fopen(path, "r");
// read from fp and compare with buf
memset(static_in, 0, 1000 * sizeof(char));
fgets(static_in, 1000, fp);
if(static_in[strlen(static_in) - 1] == '\n')
{
static_in[strlen(static_in) - 1] = '\0';
}
fclose(fp);
if (strcmp(buf, static_in) == 0)
{ // dynamic data
// read from ./data/dynamic.out (multi line) and print
sprintf(path, "./data/dynamic%d.out", i);
fp = fopen(path, "r");
while (fgets(buf, 1000, fp) != NULL)
{
printf("%s", buf);
}
fclose(fp);
return 0;
}
}
}
线路板
真没想到工训过了这么长时间,还能用得上这玩意。首先需要下载一个KiCad。
打开KiCad的Gerber文件查看器,打开从网站上下载的zip文件,就可以看到Gerber文件的预览了。
一开始当然不是上面那个样子的,可以在右侧边栏的第6层(Copper L1)上右键,选择隐藏其他层。
但是Gerber又不能通过编辑把过孔去掉,此时可以使用“文件-导出到PCB编辑器”,然后再用KiCad PCB编辑器打开。运气好,打开顶部铜层就看到了flag。
微积分计算小练习
经典的微积分题和微积分没关系。为了这道题我把XSS学了一遍。
首先从下载的代码入手,可以看到bot.py的56行处,将flag放进了Selenium的Cookie。所以我们所要做的,就是在提交服务器运行代码将Cookie盗取出来。我们传入服务器的数据只有一个网址,且只能是做题网站的网址,所以需要用XSS脚本注入做题网站。
再观察提交bot代码可以看到,它在将Cookie设置之后,会打开我们提交的结果网址,然后找到用户名和分数两个字段,并显示在终端上。很令人惊喜啊,完全没有清洗过输入的数据。由于Selenium会将整个网页加载完成再执行下面的内容,而用户名部分会将我们插入的用户名作为HTML直接插入,我们有了在用户名部分执行外部JavaScript的能力。
XSS注入有存储、反射和DOM型这几种。反射型就是在URL的query string后面附上攻击脚本,这个我试了,会报错。反射型插入 <script>
,但是对于此处使用innerHTML
的情况,JavaScript并不会被执行。所以这里使用DOM型XSS(其实或许没必要分这么清楚)。
一般来说有两种途径,使用<img onerror="script">
或者<iframe>
。前者的基本原理是设置一个完全不存在的src,访问失败就会执行后面的JavaScript。一开始我的思路是将Cookie作为query string发一个GET请求,然后自己开一个服务器去记录。但这个东西似乎对外网有限制,连不上。突然想到用户名文本仍然会显示出来,那么直接替换文字,让用户名的区域直接显示Cookie不就行了?
构造注入代码如下:
</p><img src="x" onerror="document.getElementById('cookie').innerText=document.cookie;" ><p id="cookie">
直接将上面的代码输入名字输入框,提交页面就可以看到一个坏掉的图片图标,以及Cookie。正好就是flag。
很幸运的是设置Cookie的时候没有做HTTP- Only,不然JavaScript操作起来就有难度了。
企鹅拼盘
我真的没想到过我能在math部分拿到分……虽然方法跟数学基本没什么关系就是了。
进入TUI界面后,点击input可以输入,再点击其他地方可以发送命令。
第一个Level只允许输入四个bit,暴力枚举拿分。后面的不会了。