本文最后更新于 2025年8月5日 中午
前言 原由是上周nepctf2025出了一道cs的流量题,刚好我没刷过类似题目,虽然写出来了,但是还是系统学习一下比较好的。
参考博客:
Cobalt Strike流量解密 - 1cePeak
(●´3`●)やれやれだぜ
基础前置
本人使用的cs4.7,同时teamserver放在本地kali上面
1
| ./teamserver [服务端ip] [密码]
|
比如说
1
| ./teamserver 192.168.71.3 123456
|
然后创建一个监听器,生成一个可执行程序然后在本地的虚拟机上线

最后类似就是这样的内容
加密算法
Cobalt Strike(CS)在通信过程中采用多层加密机制,以实现 Beacon 和 TeamServer 间的加密通讯与流量伪装。其加密算法与版本、Profile 配置有关
AES-128-CBC(Beacon 加密 Payload 和通信数据)
- 用途:对 Beacon 的任务、回传数据、Stage payload 等进行加密。
- Key 生成方式:Beacon 在初始化时从 TeamServer 获取对称密钥,通常为预设或通过 RSA 握手生成。
- IV:随机或固定,取决于 Malleable C2 配置。
- 实现语言:Java(TeamServer)与 C(Beacon)
通常这种是默认的,也是最常见的加密算法,本文也是围绕这一块来谈
RSA-2048(Beacon 与 TeamServer 握手)
- 用途:在 Beacon 初次上线时使用 RSA 公钥加密 AES 会话密钥。
- 流程:
- Beacon 内置 RSA 公钥(由 TeamServer 生成)。
- Beacon 使用该公钥加密 AES 密钥并发送。
- TeamServer 用私钥解密并获取 AES 会话密钥。
- 密钥位置:
- 存储在
.cobaltstrike.beacon_keys
或内嵌于 Payload。

流量传递
Cobalt Strike 流量传递流程简述:
- 握手阶段
Beacon 启动后,用 Team Server 公钥加密 AES 会话密钥发送给服务器,完成密钥交换。
- 加密通信
后续数据用 AES-128-CBC 加密,数据再经 Base64 等编码,伪装成正常 HTTP/DNS/SMB 流量。
- 定时请求
Beacon 按配置的 sleep 和 jitter 定时发起请求,获取任务并回传结果。
- Malleable C2
通过配置 HTTP 头、URI、编码方式等,实现流量伪装,降低检测风险。
Cobalt Strike 的 Beacon 在初始通信中常通过 HTTP GET 请求伪装为正常浏览行为,访问如 /dpixel
、/__utm.gif
、/pixel.gif
等路径,并将包含 AES 会话密钥等元数据的信息使用 RSA 公钥加密后,通过 Base64 编码写入 Cookie 头中发送给 C2 服务器,从而完成安全信道的建立。

这里抓了一下本地的流量,可以注意到伪装的ga.js
(也可以伪装成其他的内容)然后一段长的cookie值

下发指令的时候会请求 /submit.php?id=一串数字
同时post一段一串 0000
开头的16进制数据,这是 cs 流量的发送任务数据,这里我执行的是getuid的命令
流量解密
方法一
这里假如CTF题目提供了.cobaltstrike.beacon_keys
可以通过脚本来获取内容
cs-scripts/parse_beacon_keys.py at master · Slzdude/cs-scripts
参考这个脚本,我贴一下我微调的脚本
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
| import base64 import javaobj
def to_pem(key_bytes: bytes, header: str) -> str: pem_body = base64.encodebytes(key_bytes).decode().replace('\n', '') lines = [f"-----BEGIN {header}-----"] lines += [pem_body[i:i+64] for i in range(0, len(pem_body), 64)] lines.append(f"-----END {header}-----") return '\n'.join(lines)
with open(".cobaltstrike.beacon_keys", "rb") as f: pobj = javaobj.load(f)
key_entry = pobj.array.value
priv_raw = key_entry.privateKey.encoded._data pub_raw = key_entry.publicKey.encoded._data
priv_bytes = bytes([b & 0xFF for b in priv_raw]) pub_bytes = bytes([b & 0xFF for b in pub_raw])
private_pem = to_pem(priv_bytes, "PRIVATE KEY") public_pem = to_pem(pub_bytes, "PUBLIC KEY")
print(private_pem) print(public_pem)
|
这样可以获得公钥和私钥
再拿这个项目WBGlIl/CS_Decrypt,这个代码不兼容,我也贴一下我微调的代码,把心跳的时候的cookie贴进去
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
| from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import padding import base64 import hashlib import hexdump
PRIVATE_KEY = b"""-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY----- """
encode_data = "QGhgfFHXZOUsjC8YcgAdkB6fAgepIFJDM9iO7j39Q8aHelwnzqRILSMxM9gPUy3b+TfLpbNd33HdSXisl36A2Qj9qYFEaM8F3DTzAPFHpXs91tEaBUavdSRFLk13sUFAI7Cd41FlAc/ChNRidbGApbjB08JPpv0nvQE44gTuao0="
private_key = serialization.load_pem_private_key(PRIVATE_KEY, password=None)
ciphertext = private_key.decrypt( base64.b64decode(encode_data), padding=padding.PKCS1v15() )
def isFlag(var, flag): return (var & flag) == flag
def toIP(var): return ".".join([str((var >> (8 * i)) & 0xff) for i in reversed(range(4))])
def getName(var0): mapping = { 936: "gbk", 950: "big5", 65001: "utf-8", 1252: "windows-1252", 0: "ISO8859-1" } return mapping.get(var0, "ISO8859-1")
if ciphertext[0:4] == b'\x00\x00\xBE\xEF': raw_aes_keys = ciphertext[8:24] var9 = int.from_bytes(ciphertext[24:26], "little") var9_name = getName(var9)
beacon_id = int.from_bytes(ciphertext[28:32], "big") pid = int.from_bytes(ciphertext[32:36], "big") port = int.from_bytes(ciphertext[36:38], "big") flag = int.from_bytes(ciphertext[38:39], "big")
arch = "x64" if isFlag(flag, 2) else "x86" is64 = "1" if isFlag(flag, 4) else "0" bypass_uac = "True" if isFlag(flag, 8) else "False"
win_major = ciphertext[39] win_minor = ciphertext[40] win_build = int.from_bytes(ciphertext[41:43], "big")
ip = toIP(int.from_bytes(ciphertext[55:59], "little")) ip = ip if ip != "0.0.0.0" else "unknown"
try: ddata = ciphertext[59:].decode(var9_name, errors="replace") except: ddata = ciphertext[59:].decode("ISO8859-1", errors="replace")
fields = ddata.split("\t") computer = fields[0] if len(fields) > 0 else "" username = fields[1] if len(fields) > 1 else "" process = fields[2] if len(fields) > 2 else ""
print(f"Beacon ID: {beacon_id}") print(f"PID: {pid}") print(f"Port: {port}") print(f"Arch: {arch}, is64: {is64}, BypassUAC: {bypass_uac}") print(f"Windows version: {win_major}.{win_minor}.{win_build}") print(f"Host IP: {ip}") print(f"Computer: {computer}, Username: {username}, Process: {process}")
digest = hashlib.sha256(raw_aes_keys).digest() aes_key = digest[:16] hmac_key = digest[16:] print(f"AES key: {aes_key.hex()}") print(f"HMAC key: {hmac_key.hex()}")
print("\nFull metadata hexdump:") print(hexdump.hexdump(ciphertext))
else: print("Invalid Beacon metadata header")
|

成功拿到被控主机信息和两个需要用到的 key
1 2
| AES key: b3fbe5014705d989f005deb2f759fb80 HMAC key: af2bcf97ce60a201446fe44c3e7d2cbe
|
然后再解密CS流量,既然存在通信,那么必然有数据传输,所以直接查看存在data字段的数据包。
还是用上面的项目的CS_Task_AES_Decrypt.py
代码
需要先fromhex再to base64
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
| ''' cobaltstrike任务解密 ''' import hmac import binascii import base64 import struct
import hexdump from Crypto.Cipher import AES
def compare_mac(mac, mac_verif): if mac == mac_verif: return True if len(mac) != len(mac_verif): print "invalid MAC size" return False
result = 0
for x, y in zip(mac, mac_verif): result |= x ^ y
return result == 0
def decrypt(encrypted_data, iv_bytes, signature, shared_key, hmac_key): if not compare_mac(hmac.new(hmac_key, encrypted_data, digestmod="sha256").digest()[0:16], signature): print("message authentication failed") return
cypher = AES.new(shared_key, AES.MODE_CBC, iv_bytes) data = cypher.decrypt(encrypted_data) return data
def readInt(buf): return struct.unpack('>L', buf[0:4])[0]
shell_whoami="G2ZeKrJpjQiBVl8KiUhPm0Gcc8iWcq/mbiCtn0uPTqE/cD9OsYRO9J2riLj1CXrC"
if __name__ == "__main__": SHARED_KEY = binascii.unhexlify("b3fbe5014705d989f005deb2f759fb80") HMAC_KEY = binascii.unhexlify("af2bcf97ce60a201446fe44c3e7d2cbe")
enc_data = base64.b64decode(shell_whoami) print("数据总长度:{}".format(len(enc_data))) signature = enc_data[-16:] encrypted_data = enc_data[:-16]
iv_bytes = bytes("abcdefghijklmnop",'utf-8')
dec = decrypt(encrypted_data,iv_bytes,signature,SHARED_KEY,HMAC_KEY)
counter = readInt(dec) print("时间戳:{}".format(counter))
decrypted_length = readInt(dec[4:]) print("任务数据包长度:{}".format(decrypted_length))
data = dec[8:len(dec)] print("任务Data") print(hexdump.hexdump(data))
Task_Sign=data[0:4] print("Task_Sign:{}".format(Task_Sign))
Task_file_len = int.from_bytes(data[4:8], byteorder='big', signed=False) print("Task_file:{}".format(Task_file_len))
with open('data.bin', 'wb') as f: f.write(data[8:Task_file_len])
print(hexdump.hexdump(data[Task_file_len:]))
|

可以得到数据,然后?id=那个的数据包用Beacon_Task_return_AES_Decrypt.py
来解密,也是先from hex再to base64,大体流程就是这样。
方法二
参考这个项目
minhangxiaohui/CSthing: somthing about Cobaltstrike
参考文章
Cobalt Strike:使用已知私钥解密流量 – 第 2 部分 – NVISO Labs — Cobalt Strike: Using Known Private Keys To Decrypt Traffic – Part 2 – NVISO Labs
这种情况一般是在没有提供密钥也就是没有提供.cobaltstrike.beacon_keys
,可以尝试用这个方法
利用wireshark提取出来

很明显这里不是已知的cskey库里的,所以没给出密钥,拿到信标依然没有作用
还是得拿到本地的 key 文件才能解密流量,这里没啥用
(或者这里可以根据n在factordb进行分解,可以得到p和q的话,或者使用rsactftool弱公钥解,那么可以构造私钥,进一步也能实现,也是概率问题,不一定能成功)
那在如果有的情况下,可以继续执行
1
| python3 cs-decrypt-metadata.py -p 私钥 cookie
|
得到 Raw key、AES key 和 HMAC key
接下来用
1
| python3 cs-parse-http-traffic.py -r [Raw key] xxx.pcapng
|
这样就能得到一定的数据,在不规定其它参数的情况下解密出流量包中的通信流量,解决的比较快,但是大概率在实际情况还是要配合其他方法一起食用。
方法三
通过进程转储(dump)直接从内存中提取加密密钥(AES key、HMAC key和RAW key),从而绕过传统的私钥解密过程。
这里需要用到的脚本
Beta/cs-extract-key.py at master · DidierStevens/Beta
3.x
在 Cobalt Strike 3.x 中,信标的关键元数据(如 AES Key、HMAC Key、IV)在内存中以明文形式存储,且以特征标志 0x0000BEEF
开头。通过对信标进程进行内存转储,可在早期阶段直接提取这三组未加密的密钥信息,适用于内存取证与样本分析等场景。
1
| python3 cs-extract-key.py test.dmp
|
之后获得key之后大体的逻辑是差不多的
4.x
Cobalt Strike 3.x 中信标的元数据以明文形式存储于内存,易通过特征标志如 0x0000BEEF
提取关键密钥;而在 4.x 中,这些数据被加密并进行了结构混淆,内存中不再直接暴露明文 key,大幅提升了对抗分析与取证的难度,是从易识别向高隐匿演进的重要转变。
具体例题可以参考nepCTF2025的misc题客服小美,我没上传博客
首先先用上文提到的 cs-parse-http-traffic.py 脚本对抓取的流量包进行提取加密数据的操作
1
| python cs-parse-http-traffic.py -k unknown DESKTOP.pcapng
|
这时候可以获得数据,得到加密数据之后,再用动态信标的进程转储文件进行爆破解密,得到 key
然后执行
1
| python3 cs-extract-key.py -t 加密data test.dmp
|

差不多就是这样的例子,那么最后也可以照着前面的进行解密得到内容
比如利用 RAWkey 使用 cs-parse-http-traffic.py 对通信流量进行解密了,不同的是这里使用的是 SHA256 Raw Key,参数为 - k
1
| python cs-parse-http-traffic.py -k [SHA256 Raw key] test.pcapng
|
当然也可以带入到CS_Task_AES_Decrypt.py
这个脚本使用,具体问题具体分析。
后话
配合食用几个题目,大体逻辑应该就是了解了,这篇就水到这里了。