前言
这次比赛的题目难度适中,有一些经典的题目,也有一些新颖的题目,总体来说比较有趣。选取了一些题目来写writeup,希望对大家有所帮助。
签到
直接点一下提交,发现url的最后有个?pass=false
,改成?pass=true
即可。
喜欢做签到的 CTFer 你们好呀
打开网页,发现是个仿终端的界面,输入help
,发现提供了所有的命令,逐个尝试发现env
命令可以查看环境变量,发现flag1
。
在js中搜索flag
,发现https://www.nebuu.la/_next/static/chunks/pages/index-5cb01f7ec808f452.js
中有cat
指令特判了.flag
,遂尝试cat .flag
,发现flag2。
猫咪问答(Hackergame 十周年纪念版)
- 大语言模型会把输入分解为一个一个的 token 后继续计算,请问这个网页的 HTML 源代码会被 Meta 的 Llama 3 70B 模型的 tokenizer 分解为多少个 token?(5 分)
直接从Hugging Face
上获得 meta-llama/Meta-Llama-3-70B
的 tokenizer.json
,然后分解即可。
1
2
3
4
5
6
7
8
9
10
11
| # Description: Tokenize a string using Meta-Llama-3-70B tokenizer
from tokenizers import Tokenizer
tokenizer = Tokenizer.from_file("tokenizer.json")
with open("index.html", "r", encoding="utf-8") as file:
html_content = file.read()
output = tokenizer.encode(html_content)
print(output.ids)
print(len(output.tokens))
|
打不开的盒
使用任意3D建模软件打开即可,这里因为懒得下直接用科协的电脑打开了。
(请选择你的拍屏导师)
每日论文太多了
在PDF中全文搜索flag
,发现Figure 4
中有一处空白,下载pdf之后使用编辑器打开,删除这里的白色遮罩即可看到flag。
比大小王
模拟输入显然不可行,对手一秒十题恐怖如斯,拼尽全力无法战胜。好在js没有混淆,阅读源码得知比赛题目从/game
中获得,在网页上完成比赛后还需要POST到/submit
,于是写了个脚本模拟提交。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import requests
url = "http://202.38.93.141:12122/game"
headers = {
"content-type": "application/json",
"cookie": "xxx"}
data = "{}"
resp = requests.post(url, headers=headers, data=data)
response = resp.json()
print(response)
inputs = response["values"]
print(inputs)
r = [">" if pair[0] > pair[1] else "<" for pair in inputs]
set_cookie = resp.headers.get('set-cookie')
url2 = "http://202.38.93.141:12122/submit"
print(str({"inputs": r}))
if set_cookie:
headers['cookie'] = set_cookie
response = requests.post(url2, headers=headers, data=str({"inputs": r}).replace("'", '"'))
response = response.json()
print(response)
|
不宽的宽字符
以wchar
形式读入程序然后加上L"you_cant_get_the_flag"
,然后强制类型转换成char
后读这个文件
题目的意思是让我们构造一些宽字符,使被转换成char
后变成Z:\\theflag\0
,后面的\0
是字符串结束符,会被C风格的fopen
函数识别为字符串结束,从而忽略掉程序中的you_cant_get_the_flag
。
这几个字符可以直接按ASCII码两两组合起来(要倒过来),而最后落单的\0
可以直接用0x00
后面随机填充一个字节即可。
可以写程序来验证一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
| char *c= (char*)filename.c_str();
char *c2= "Z:\\theflag";
strcpy(c,c2);
for (int i = 0; i < strlen(c); i++)
{
std::cout<<std::hex<<(int)c[i]<<" ";
}
std::cout<<std::endl;
for(int i=0;i<filename.size();i++)
{
std::cout<<std::hex<<(int)filename[i]<<" ";
}
std::cout<<std::endl;
|
输出:
1
| 3a5a 745c 6568 6c66 6761 0 61 6e 74 5f 67 65 74 5f 74 68 65 5f 66 6c 61 67
|
说明 3a5a 745c 6568 6c66 6761 0
会转换成 Z:\\theflag\0
。
1
2
3
4
5
6
7
8
9
10
| from pwn import *
p = remote('202.38.93.141', 14202)
#token
# 输入 3a5a 745c 6568 6c66 6761 0011 的十六进制数据
payload_unicode = '\u3a5a\u745c\u6568\u6c66\u6761\u1100'
p.sendline(payload_unicode)
p.interactive()
|
PowerfulShell
构造不用'\";,.%^*?!@#%^&()><\/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0
的shell命令getflag
难点在于构造字符,发现$
和 `
没有被ban,那直接执行~
然后把结果用个变量存下来,慢慢拼接最后得到shell
1
2
3
4
5
6
7
| _1=~
# _1的内容是/players,这里截取了l和s,并把ls的结果赋值给_2
_2=`${_1:2:1}${_1:7:1}
#_2 的内容是PowerfulShell.sh
${_2:14:2} ## 截取_2中的sh并执行
|
完成getshell,随后cat /flag
即可。
Node.js is Web Scale
观察source code,发现有dict合并操作,于是想到原型链污染,只需要给每个对象添加个flag
属性,值为cat /flag
,这样/execute?cmd=flag
的时候就会RCE
在网页上填入key为 __proto__.flag
,value为 cat /flag
。
随后访问 /execute?cmd=flag
就可以得到flag。
PaoluGPT
爬虫题,获得主页的 html,然后解析出所有 a 标签的 href,然后爬取,获得 flag1
conversation_id是 uuid,无法遍历,于是阅读源码,发现有sql注入点
1
2
3
4
5
6
| results = execute_query("select id, title from messages where shown = true", fetch_all=True)
···
results = execute_query(f"select title, contents from messages where id = '{conversation_id}'")
return render_template("view.html", message=Message(None, results[0], results[1]))
|
构造 payload select title, contents from messages where id ='1' OR shown = false AND '1'='1'
访问 /view?conversation_id=1' OR shown = false AND '1'='1
即可获得flag2
强大的正则表达式
好耶!是离散数学题!
Easy
flag1要求匹配十进制下被16整除的数,可以直接匹配最后四位。
1
2
3
4
5
6
7
8
9
10
11
| str1= '(16|32|64|(0|1|2|3|4|5|6|7|8|9)*(0000|0016|0032))'
strall = '('
for i in range(0, 1000, 16):
strall += str(i) + '|'
strall +='(0|1|2|3|4|5|6|7|8|9)*('
for i in range(0, 10000, 16):
# 补足四位
i4 = str(i).zfill(4)
strall += i4 + '|'
strall = strall[:-1] + '))'
print(strall)
|
Medium
flag2要求匹配二进制下被13整除的数,直接搜索发现这个问题可以转化成有限状态自动机(DFA)的问题,构造一个DFA,随后转化成正则语言。观察到题目中的刚好仅允许|()*
这些符号,因此可以构造出与、或、闭包,这个方向是正确的。
二进制下的转移线路很简单,可以构造一个DFA,状态为0-12,每个状态接受0-1的转移,最后统计从0开始的环路即可。
在Github上找到了一个repo tchajed/div-regex 提供了DFA转化成正则表达式的代码,我们仅需建图即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from dfa import Dfa
from gnfa import Gnfa
def divisible_by(n):
delta = []
for s in range(n):
s_delta = {}
for d in range(2):
x = str(d)
s_delta[x] = (s * 2 + d)%n
delta.append(s_delta)
return Dfa(delta, set([0]), 0)
dfa = divisible_by(11).minimal()
r = Gnfa.dfa_re(dfa)
ans = r.to_re()
print(ans)
|
Hard
情况变得复杂起来,需要构造一个DFA,状态为0-7,每个状态接受0-9的转移,最后统计从0开始的环路。
枚举每个Stage的出边,实现一个带初始值的crc3_gsm
函数,接受当前点的状态和此时的边,这条边会指向 crc3
计算的结果,最后即可完成DFA的构造。
值得注意的是在DFA中State
的值是中间状态,而CRC-3/GSM
的需要在运算后^0x07,只有Stage 7
在^0x07后为0,因此最后回归状态应该是7。
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
| from dfa import Dfa
from gnfa import Gnfa
# Function to calculate CRC-3/GSM checksum
def crc3_gsm(data, init=0x00):
poly = 0x03
crc = init
data = data.encode()
for byte in data:
for i in range(8):
bit = (byte >> (7 - i)) & 1
crc = ((crc << 1) & 0x07) ^ (poly if (bit != ((crc >> 2) & 1)) else 0)
return crc
# Function to build the state machine for the DFA
def build_state_machine():
delta = []
for s in range(8):
s_delta = {}
for d in range(10):
x = str(d)
s_delta[x] = crc3_gsm(x, s)
delta.append(s_delta)
return Dfa(delta, set([7]), 0)
dfa = build_state_machine().minimal()
r = Gnfa.dfa_re(dfa).to_re()
print(r)
|
优雅的不等式
注意到知乎上文章 【科普】如何优雅地“注意到”关于e、π的不等式,可以直接套用这个不等式。
然后调整参数,使其既小于400的长度,又足够紧(在[0,1]非负),即可得到flag。
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
| import sympy as sp
x, a, b = sp.symbols('x a b')
m, n = 40,80
def getAns(n,m,p,q):
exp = x**(2*m) * (1 - x**2)**n * (a + b * x**2) / (1 + x**2)
exp = sp.simplify(exp)
F = sp.integrate(exp, (x, 0, 1))
F = sp.simplify(F)
#sp.pprint(sp.simplify(F))
# Condition equations based on coeff(F, pi) and the given equations
coeff_F_pi = F.coeff(sp.pi)
eq1 = coeff_F_pi - 1
# sp.pprint(F)
eq2 = F - coeff_F_pi * sp.pi + sp.Rational(p, q)
#sp.pprint(eq1)
# Solve for a and b
solution = sp.solve([eq1, eq2], (a, b))
exp2 = exp.subs(solution)
# Output the solution
return str(sp.simplify(exp2))
from pwn import *
# Connect to the server
r = remote('202.38.93.141', 14514)
# Get the number of test cases
print(r.recvline())
token = 'I love dw'
r.sendline(token)
# Please prove that pi>=p/q
cnt = 1
while True:
st = r.recvline().decode()
print(st)
if 'Please' not in st:
continue
st=st.split('=')[1].split('/')
if len(st)==2:
p=int(st[0])
q=int(st[1])
else:
p=int(st[0])
q=1
an= getAns(n,m,p,q)
an=an.replace('x**2','x*x').replace(' ','')
print(f'Case {cnt}: {an}')
cnt+=1
r.sendline(an)
p.interactive()
|
不太分布式的软总线
Flag1
直接调用GetFlag1
方法即可。
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
| #include <gio/gio.h>
#include <glib.h>
int main(int argc, char *argv[]) {
GError *error = NULL;
GDBusConnection *connection;
GVariant *result;
// 连接到系统总线
connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
if (error != NULL) {
g_printerr("Error connecting to system bus: %s\n", error->message);
g_clear_error(&error);
return 1;
}
// 调用 GetFlag1 方法
result = g_dbus_connection_call_sync(
connection,
"cn.edu.ustc.lug.hack.FlagService", // 服务名称
"/cn/edu/ustc/lug/hack/FlagService", // 对象路径
"cn.edu.ustc.lug.hack.FlagService", // 接口名称
"GetFlag1", // 方法名称
g_variant_new("(s)", "Please give me flag1"), // 参数
G_VARIANT_TYPE("(s)"), // 返回类型
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error);
if (error != NULL) {
g_printerr("Error calling GetFlag1: %s\n", error->message);
g_clear_error(&error);
return 1;
}
// 解析返回值
const gchar *flag1;
g_variant_get(result, "(&s)", &flag1);
g_print("Flag1: %s\n", flag1);
// 清理
g_variant_unref(result);
g_object_unref(connection);
return 0;
}
|
Flag2
题目要求我们给出一个文件描述符,然后服务端会向这个文件描述符写入flag2,但由于我们并没有创建文件的权限,所以我们可以使用管道来实现。
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
| #include <unistd.h>
#include <fcntl.h>
#include <gio/gio.h>
#include <glib.h>
#include <stdio.h>
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
// 写入数据到管道
const char *pipe_message = "Please give me flag2\n"; // 更改变量名
write(pipefd[1], pipe_message, strlen(pipe_message));
close(pipefd[1]); // 关闭写端
// 创建一个 GDBusMessage 并设置文件描述符
GDBusMessage *dbus_message = g_dbus_message_new_method_call( // 更改变量名
"cn.edu.ustc.lug.hack.FlagService", "/cn/edu/ustc/lug/hack/FlagService",
"cn.edu.ustc.lug.hack.FlagService", "GetFlag2");
GUnixFDList *fd_list = g_unix_fd_list_new();
g_unix_fd_list_append(fd_list, pipefd[0], NULL);
g_dbus_message_set_unix_fd_list(dbus_message, fd_list);
g_object_unref(fd_list);
// 设置参数
GVariant *params = g_variant_new("(h)", 0); // 传递文件描述符索引
g_dbus_message_set_body(dbus_message, params);
// 调用方法并获取响应
GError *error = NULL;
GDBusConnection *connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
if (error != NULL) {
g_printerr("Error getting bus: %s\n", error->message);
g_error_free(error);
return 1;
}
GDBusMessage *response = g_dbus_connection_send_message_with_reply_sync(
connection, dbus_message, G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL,
&error);
if (error != NULL) {
g_printerr("Error sending message: %s\n", error->message);
g_error_free(error);
return 1;
}
// 处理响应
GVariant *body = g_dbus_message_get_body(response);
const gchar *flag;
g_variant_get(body, "(&s)", &flag);
g_print("Received flag: %s\n", flag);
// 清理
g_object_unref(response);
g_object_unref(connection);
close(pipefd[0]); // 关闭读端
return 0;
}
|
Flag3
题目只能给名为GetFlag3
的进程返回flag3,
1
| prctl(PR_SET_NAME, "getflag3", 0, 0, 0);
|
关灯
Flag1~Flag3
Light Out 问题本质上就是一个线性代数问题,我们可以通过构造矩阵来解决。
灯(i,j,k)开关能影响到的灯有(i,j,k), (i+1,j,k), (i-1,j,k), (i,j+1,k), (i,j-1,k), (i,j,k+1), (i,j,k-1)。
我们可以通过构造一个矩阵,对于一个灯(i,j,k),我们定义他的下标为i*n*n+j*n+k
,那么我们可以构造一个n^3*n^3
的矩阵,对于每一个灯,我们可以构造一个n^3
的向量,表示这个灯控制的灯。
最后解这个异或线性方程组即可。
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
| import os
import numpy
import zlib
import base64
import time
import hashlib
os.environ['TERM'] = 'xterm'
from pwn import *
F = GF(2)
n = 11
A = Matrix(F, n**3, n**3, sparse=True)
B = vector(F, n**3)
dx = [-1,1,0,0,0,0]
dy = [0,0,-1,1,0,0]
dz = [0,0,0,0,-1,1]
for i in range(n):
for j in range(n):
for k in range(n):
u = i*n*n + j*n + k
A[u,u] = 1
for l in range(6):
x = i + dx[l]
y = j + dy[l]
z = k + dz[l]
if x >= 0 and x < n and y >= 0 and y < n and z >= 0 and z < n:
v = x*n*n + y*n + z
pwnlib.term.term_mode = False
p = remote
#p.interactive()
# Receive the welcome message
print(p.recvline())
token = 't
# 发送数据
p.sendline(token)
print(p.recv(1000))
p.sendline('4')
st01=p.recvline().decode()
print(st01)
print(p.recv(4096))
for i in range(n**3):
B[i] = int(st01[i])
solutions = A.solve_right(B)
ans = ""
for i in range(n**3):
ans += str(solutions[i])
p.sendline(ans)
p.interactive()
|
禁止内卷
题目中强调了--reload
参数,那么应该是要修改py源文件,考虑如何写入文件.
阅读源码发现有一个/submit
接口,可以上传文件,没有处理文件名,存在路径穿越漏洞,在上传文件时修改POST
请求的filename
字段为../app.py
,即可直接覆盖app.py
,实现RCE。