水磨沟区网络与信息安全管理员职业技能竞赛部分WP

水磨沟区网络与信息安全管理员职业技能竞赛部分WP

webshell分析

日志分析

恶意流量分析

发现加密注入参数

响应页面中包含:

在登录页源码中发现 AES 参数:
const key = CryptoJS.enc.Utf8.parse(‘5366971250864259’);
const iv = CryptoJS.enc.Utf8.parse(‘9111356600987654’);
用 AES-CBC + PKCS7 解密 id 参数,可得到盲注 payload:

1’and ascii(substr((select password from config),1,1))=90#

说明攻击者通过布尔盲注读取了Success的请求,按位置还原得到:
ZmxhZ3tmNTM2Yzg3OTAyYTg1ODE5ZjQ4ZTYxNDRkOTA3MTY5OX0=

不死马清除

删除/var/www/html/.add.php 发现会被重复创建,结束掉 nobody 的两个进程 删除成功

系统后门排查

依旧结束进程 删文件

RE

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
from struct import pack, unpack

def rc4(data: bytes, key: bytes) -> bytes:
S = list(range(256))
j = 0

for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xff
S[i], S[j] = S[j], S[i]

i = j = 0
out = bytearray()

for b in data:
i = (i + 1) & 0xff
j = (j + S[i]) & 0xff
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) & 0xff]
out.append(b ^ k)

return bytes(out)


def tea_decrypt_block(block: bytes, key):
v0, v1 = unpack("<2I", block)

delta = 0x9E3779B9
s = (delta * 32) & 0xffffffff

k0, k1, k2, k3 = key

for _ in range(32):
v1 = (v1 - (((v0 << 4) + k2) ^ (v0 + s) ^ ((v0 >> 5) + k3))) & 0xffffffff
v0 = (v0 - (((v1 << 4) + k0) ^ (v1 + s) ^ ((v1 >> 5) + k1))) & 0xffffffff
s = (s - delta) & 0xffffffff

return pack("<2I", v0, v1)


def tea_decrypt(data: bytes) -> bytes:
key = [17, 34, 51, 68]

return b"".join(
tea_decrypt_block(data[i:i+8], key)
for i in range(0, len(data), 8)
)


def main():
with open("chall1.txt", "r", encoding="utf-8") as f:
hex_data = f.read().strip()

cipher = bytes.fromhex(hex_data)

# 加密流程:plain -> RC4 -> TEA
# 解密流程:TEA^-1 -> RC4
after_tea = tea_decrypt(cipher)
plain = rc4(after_tea, b"just_for_crypto")

print("inner =", plain)
print("flag{" + plain.decode(errors="ignore") + "}")


if __name__ == "__main__":
main()

数据溯源

拿到附件后,先看目录结构,可以发现几个关键文件:

  • upfile.php
  • logs/access.log.1698364800
  • httpd.dmp
  • checkme.exe
  • upload/file.png

这说明题目并不是单纯的代码审计,而是需要结合:

  • Web 上传点源码
  • 访问日志
  • 内存转储
  • 可执行程序

进行一次完整的数据溯源。

第一步:分析上传点

查看 upfile.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
//U-Mail demo ...
if(isset($_POST['submit'])){
$filename = $_POST['filename'];
$filename = preg_replace("/[^\w]/i", "", $filename);
$upfile = $_FILES['file']['name'];
$upfile = str_replace(';',"",$upfile);
$upfile = preg_replace("/[^(\w|\:|\$|\.|\<|\>)]/i", "", $upfile);
$tempfile = $_FILES['file']['tmp_name'];
$ext = trim(get_extension($upfile)); // null
if(in_array($ext,array('php','php3','php5'))){
echo 'Warning ! File type error..';
}
if($ext != 'asp' or $ext != 'asa' or $ext != 'cer' or $ext != 'cdx' or $ext != 'aspx' or $ext != 'htaccess') $ext = 'file';
$savefile = 'upload/'.$filename.".".$ext;
if(move_uploaded_file($tempfile,$savefile)){
die('Success upload..path :'.$savefile);
}else{
die('Upload failed..');
}
}
function get_extension($file){
return strtolower(substr($file, strrpos($file, '.')+1));
}

这里有两个关键点:

  1. 对 php/php3/php5 只告警,不阻止上传
1
2
3
if(in_array($ext,array('php','php3','php5'))){
echo 'Warning ! File type error..';
}

这里只是输出警告,并没有 exit 或 die,所以上传流程会继续执行。

  1. 扩展名判断逻辑恒成立
1
if($ext != 'asp' or $ext != 'asa' or $ext != 'cer' or $ext != 'cdx' or $ext != 'aspx' or $ext != 'htaccess') $ext = 'file';

这里用了 or,导致条件恒为真,因此最终上传文件总会被保存成:

1
upload/<filename>.file

或者更准确地说,服务端会强制把扩展名改成 file。

这说明攻击者可能上传了恶意文件,但真正关键的是:

  • 上传过什么内容
  • 落地文件名是什么
  • 后续是否还有其他痕迹

第二步:查看访问日志

从 logs/access.log.1698364800 中定位可疑请求,发现 IP 95.214.55.115 的访问非常关键:

1
2
3
4
5
95.214.55.115 - - [27/Oct/2023:10:42:21 +0800] "GET / HTTP/1.1" 200 277
95.214.55.115 - - [27/Oct/2023:10:42:21 +0800] "GET /favicon.ico HTTP/1.1" 404 2659
95.214.55.115 - - [27/Oct/2023:10:42:28 +0800] "GET / HTTP/1.1" 304 -
95.214.55.115 - - [27/Oct/2023:10:43:25 +0800] "POST /upfile.php HTTP/1.1" 200 29
95.214.55.115 - - [27/Oct/2023:10:43:27 +0800] "POST /upfile.php HTTP/1.1" 200 39

可见这个 IP 在短时间内连续进行了两次上传。

其中:

  • 第一次响应长度是 29
  • 第二次响应长度是 39

这和源码中的两种输出非常吻合:

  • Warning ! File type error..
  • Success upload..path :...

说明这两次 POST 很可能分别对应:

  1. 一次带有警告的上传
  2. 一次真正成功并返回保存路径的上传

第三步:从内存转储中恢复上传内容

仅靠日志还不够,需要继续分析 httpd.dmp。

在转储中搜索 multipart/form-data、upfile.php、Success upload 等字符串,可以恢复出两次 POST 请求体。

第一次上传

恢复出的关键信息:

1
2
Content-Disposition: form-data; name="file"; filename="&#20225;&#19994;&#24494;&#20449;&#25130;&#22270;_16956346424665.png"
Content-Type: image/png

这次上传的是一个 PNG 图片,也就是附件中的:

  • upload/file.png

而且内存中还能看到响应字符串:

1
Warning ! File type error..

这说明第一次上传虽然是图片,但服务器执行过程中出现了这段警告字符串。

第二次上传

恢复出的第二次请求体更关键:

1
2
3
4
Content-Disposition: form-data; name="file"; filename="post.php"
Content-Type: image/png

Key:qazwsxerfvgy5uji

同时还能在转储中直接看到服务端响应:

1
Success upload..path :upload/file.png

这说明攻击者第二次上传的原始文件名是:

1
post.php

上传内容是:

1
Key:qazwsxerfvgy5uji

最终服务器返回的保存路径是:

1
upload/file.png

也就是说,真正需要溯源的核心数据已经从转储里恢复出来了。

第四步:分析 checkme.exe

继续分析 checkme.exe,在可执行文件字符串中能直接发现一条非常重要的信息:

1
upload\post.php

这说明程序真正想读取或校验的目标文件名是:

1
upload/post.php

而不是附件现成提供的 upload/file.png。

因此可以推断:

  • httpd.dmp 中恢复出的第二次上传内容,才是 checkme.exe 需要的输入
  • 题目实际上要求我们从日志和转储中“还原原始数据”

第五步:还原目标文件并运行程序

根据转储内容,手动重建文件:

路径:

1
upload/post.php

内容:

1
Key:qazwsxerfvgy5uji

然后运行:

1
checkme.exe

程序直接输出:

1
flag{FeiYangYangDeKuaiLeShengHuo#1}

国密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from base64 import b64encode
from gmssl.sm4 import CryptSM4, SM4_DECRYPT

key = bytes.fromhex("0123456789abcdeffedcba9876543210")

ct = bytes([
0xf1, 0x00, 0x6e, 0xe5, 0xbb, 0x2a, 0x02, 0xa4,
0x53, 0xbc, 0x7e, 0x46, 0x96, 0x02,
0x4c, 0x77
])

sm4 = CryptSM4()
sm4.set_key(key, SM4_DECRYPT)

# 不走 crypt_ecb,避免 unpadding
plain = bytes(sm4.one_round(sm4.sk, list(ct)))

print("plain hex:", plain.hex())
print("plain:", plain)
print("input:", b64encode(plain).decode())