USTC Hackergame 2022 一游 & 部分题目 Writeup
本文最后更新于 147 天前,其中的信息可能已经有所发展或是发生改变。

又是一年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。

如果使用判题脚本测试时,正解也 RE,可以将44行改为 [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文件的预览了。

Gerber

一开始当然不是上面那个样子的,可以在右侧边栏的第6层(Copper L1)上右键,选择隐藏其他层。

但是Gerber又不能通过编辑把过孔去掉,此时可以使用“文件-导出到PCB编辑器”,然后再用KiCad PCB编辑器打开。运气好,打开顶部铜层就看到了flag。

PCB

微积分计算小练习

经典的微积分题和微积分没关系。为了这道题我把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,暴力枚举拿分。后面的不会了。

CC BY-SA

This content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International license. 题目版权属于LUG@USTC。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇