又是一年 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) # 非赋值语句,直接写入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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}
1
\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
1
2
3
4
5
6
7
8
9
10
11
12
.
├── 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
#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;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#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">
1
</ 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,暴力枚举拿分。后面的不会了。