├── CSMOCK.png ├── README.md ├── beacon_utils.py ├── cs.png ├── cs_mock.py ├── grab_beacon_config_rsa.nse ├── parse_beacon_config.py └── rsa.png /CSMOCK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jas502n/CS_mock/307df5c4b9bfba4e4058dbc2266b5781f2794d48/CSMOCK.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS_mock 2 | 模拟cobalt strike beacon上线包. Simulation cobalt strike beacon connection packet. 3 | 4 | 拿到c2通信使用的RSA public key和提交metedata的url 即可模拟上线 5 | 6 | Use the CobaltStrikeParser extract public key from the payload https://github.com/Sentinel-One/CobaltStrikeParser parse_beacon_config.py payload_url --json 7 | 8 | Remember to remove the extra padding from the public key 9 | ![](CSMOCK.png) 10 | 11 | 12 | ### 注意事项 13 | 14 | 经过检测,nmap脚本存在解析字段 prepend 丢失,建议使用 parse_beacon_config.py 脚本,能正确解析 `Metadata` 原数据 提交字段的参数值 `__cfduid=` 15 | 16 | 例如: 17 | 18 | 19 | ``` 20 | curl -A O -o 4Ovd -L http://c2.x.x.x/4Ovd 21 | 22 | python3 parse_beacon_config.py --version 4 --json 4Ovd 23 | ``` 24 | 25 | ``` 26 | "Metadata": ["base64url", "prepend \"__cfduid=\"" 27 | ``` 28 | 29 | ### 0x01 nmap scan from c2 server 30 | 31 | `$ cScan 10.10.26.164 80` 32 | 33 | ``` 34 | Nmap scan report for 10.10.26.164 35 | Host is up (0.0019s latency). 36 | 37 | PORT STATE SERVICE 38 | 80/tcp open http 39 | | grab_beacon_config_rsa: 40 | | x86: 41 | | time: 1630913440646.5 42 | | sha256: e85b3ab8b81cf843b02b8c4e2a25766cd1ccc347dfa48e9ede3f1f128aad5ff7 43 | | config: 44 | | RSA Public Key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoeNuV/KkCl7dHwdyl8CIn1o5nHvVxquEs3k58509cojk+arW8dSzfPa2eVrjHtc4rMd7WGLif4AA9FaBwHgIdZ8J9K4xU1V9wWxF6iIFHcOT04KcFdZnJ4nXgMFrI7j4TYK1ugS9qV8u7C3Necrl38vRvOPi0kMYMiRO5KtT0KwIDAQAB 45 | | Jitter: 0 46 | | C2 Server: 10.10.26.164,/load 47 | | Spawn To x64: %windir%\sysnative\rundll32.exe 48 | | C2 Host Header: 49 | | Polling: 60000 50 | | Port: 80 51 | | Watermark: 1234567890 52 | | Method 2: POST 53 | | Method 1: GET 54 | | HTTP Method Path 2: /submit.php 55 | | Spawn To x86: %windir%\syswow64\rundll32.exe 56 | | Beacon Type: 0 (HTTP) 57 | | md5: 20e1378670b0d882f8ffb4e5e3ee0d9d 58 | | uri_queried: /HjIa 59 | | sha1: 48aecd9afe30e2dcafc456cbe4cd5fa2986c2217 60 | | x64: 61 | | time: 1630913441918.8 62 | | sha256: f4e4f1f97aed0eea7ddaf2d3c5b60005fac38befba13d0804732e45eecec22fd 63 | | config: 64 | | RSA Public Key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoeNuV/KkCl7dHwdyl8CIn1o5nHvVxquEs3k58509cojk+arW8dSzfPa2eVrjHtc4rMd7WGLif4AA9FaBwHgIdZ8J9K4xU1V9wWxF6iIFHcOT04KcFdZnJ4nXgMFrI7j4TYK1ugS9qV8u7C3Necrl38vRvOPi0kMYMiRO5KtT0KwIDAQAB 65 | | Jitter: 0 66 | | C2 Server: 10.10.26.164,/__utm.gif 67 | | Spawn To x64: %windir%\sysnative\rundll32.exe 68 | | C2 Host Header: 69 | | Polling: 60000 70 | | Port: 80 71 | | Watermark: 1234567890 72 | | Method 2: POST 73 | | Method 1: GET 74 | | HTTP Method Path 2: /submit.php 75 | | Spawn To x86: %windir%\syswow64\rundll32.exe 76 | | Beacon Type: 0 (HTTP) 77 | | md5: 8478e44aff0b52772697f5bfcf4e24b0 78 | | uri_queried: /4Ovd 79 | |_ sha1: c6bdfdacd266e6024da8cccd7c7b251bad247e3f 80 | 81 | Nmap done: 1 IP address (1 host up) scanned in 3.41 seconds 82 | 83 | ``` 84 | 85 | RSA Public Key 86 | 87 | ``` 88 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoeNuV/KkCl7dHwdyl8CIn1o5nHvVxquEs3k58509cojk+arW8dSzfPa2eVrjHtc4rMd7WGLif4AA9FaBwHgIdZ8J9K4xU1V9wWxF6iIFHcOT04KcFdZnJ4nXgMFrI7j4TYK1ugS9qV8u7C3Necrl38vRvOPi0kMYMiRO5KtT0KwIDAQAB 89 | ``` 90 | 91 | ### c2-profile 92 | 93 | ``` 94 | header = { 95 | 'Cookie': "__cfduid=" + base64.b64encode(enpack).decode('utf-8') 96 | } 97 | ``` 98 | 99 | #### Default: 100 | 101 | ``` 102 | GET /jquery-3.3.1.min.js HTTP/1.1 103 | Accept-Encoding: identity 104 | Host: 10.10.26.164 105 | User-Agent: Python-urllib/3.9 106 | Cookie: od8aPOC86rrPtnIJVmt5a4VjQMyjjzZxZrtuU2sOlGVZvO08c6od6PIjoS65qPrrX7Op036kjBB4QmAJd5pJUvGXpOYRB02v7reZZdAwpPEWwaxxUJxDBWovNcfYexw5dgCrbpVv/kKnAuemh1JYrAaYyvikRJvBrgQzjcOBBJM= 107 | ``` 108 | 109 | #### Cookie cfduid 110 | ``` 111 | GET /jquery-3.3.1.min.js HTTP/1.1 112 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 113 | Referer: http://code.jquery.com/ 114 | Accept-Encoding: gzip, deflate 115 | Cookie: __cfduid=e4kfutNGa060S9mZiqDfitie2GtdNNctRVVxSktYsgO7mQ-w5QEAlr_PZVzr-U4NYky9FTh1-fMGvi1Jjm7bQRpQrhUR4Et0CSWcFQK9fn0RqKJp6oREleOddJ9avbI9PLZQHzIBqo1qfgCYrGsGvRMBn90usUMOK4cDjQ6UF24 116 | User-Agent: Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko 117 | Host: 10.10.26.164 118 | Connection: Keep-Alive 119 | Cache-Control: no-cache 120 | ``` 121 | 122 | Beacon 通信原理: 123 | https://wbglil.gitbook.io/cobalt-strike/cobalt-strike-yuan-li-jie-shao/cs-mu-biao-shang-xian-guo-cheng 124 | 125 | `java -cp cobaltstrike.jar: DumpKeys` 126 | 127 | ``` 128 | Private Key: MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKh425X8qQKXt0fB3KXwIifWjmce9XGq4SzeTnznT1yiOT5qtbx1LN89rZ5WuMe1zisx3tYYuJ/gAD0VoHAeAh1nwn0rjFTVX3BbEXqIgUdw5PTgpwV1mcnideAwWsjuPhNgrW6BL2pXy7sLc15yuXfy9G84+LSQxgyJE7kq1PQrAgMBAAECgYEAjLzJ07ZKChxrw8ozZXwBTH50X7kBGX/CtBTSRI2HQr9SSs4iG9lXLvb7fva7TjqWjIcQvJHSSdx61oymhoLLCB33l5dqynLWOSIR1fXuJ63aC4e9/VKdzU2ee9ebOeYch98r78xlMGNWnHBxYw0+1Q5t0O0TWRn7ogDEhzRW7OECQQD0dhVQetTfcEtOqs7i3PFZhE7Lw6DZciIib97BsvCufqgCwi8szRs04UEig5/RS0jW2yufCyv89QTb+OpGWv3XAkEAsGyRrqd6P5Y+zWR7ZGIxHRyPZ3VyVfadNCzws4JMiyG3vHfLC8omP0tdBE95NUtiUKlvxC271qknQ+IxEKnpzQJAGvBgTOwcRTsksViVT961If44iK/YK94t8MmPLJ1BdJk0folTlYZMtzkTanBCzleTgUnJts8OW+PMU0lM18/zJQJBAKsHhS9IwpVUJOvehlRpcG1lW4wTalT1r65+BpYvMd8LP+CgTSHPxcZEXBcqqDlHLDdAGs6W+2r9Lsh7Rc9+uN0CQEoitgXTiPRKbi9ieqan67uJUuWN7ZNmFdKmKTw2Vel4bNqAD4+Aw+dDM5N16QZKg/J32dzp73fSqCLcEYdh+aQ= 129 | 130 | Public Key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoeNuV/KkCl7dHwdyl8CIn1o5nHvVxquEs3k58509cojk+arW8dSzfPa2eVrjHtc4rMd7WGLif4AA9FaBwHgIdZ8J9K4xU1V9wWxF6iIFHcOT04KcFdZnJ4nXgMFrI7j4TYK1ugS9qV8u7C3Necrl38vRvOPi0kMYMiRO5KtT0KwIDAQAB 131 | 132 | ``` 133 | 134 | ![](./rsa.png) 135 | RSA 私钥解密 136 | 137 | https://the-x.cn/zh-cn/cryptography/Rsa.aspx 138 | 139 | ``` 140 | BArv+naOZOElxtW3xTdGiAli5OvocO6RjwZ8McZa6fJEoS6l0vsd2uiQB1UFXHxToOXaOP3OVNEfI5dr1vmdF+f423O8Kf0PlSgGdQH8UWD6C/ajW+jZL1UmIWZz1Rxo7fP1pCntQVVHB/apAifQPTFqLngc4opGtC5y/9tL4aU= 141 | ``` 142 | 143 | 144 | 格式: 145 | 146 | ``` 147 | computer_name =>> nY0Er 148 | 149 | user_name =>> eqB2JD 150 | 151 | process_name =>> wdUsWA 152 | ``` 153 | 154 | #### hex + ascil 155 | ``` 156 | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 157 | -------------------------------------------------------------------- 158 | 00 00 BE EF 00 00 00 47 08 09 01 09 03 09 02 00 | .......G........ 159 | 0C 08 04 06 0B 01 0E 0B A8 03 A8 03 00 57 03 6A | .............W.j 160 | 00 00 50 BB 00 00 04 06 02 23 F0 00 00 00 00 76 | ..P......#.....v 161 | 91 0A 60 76 90 F5 50 0D 0C 02 04 6E 59 30 45 72 | ..`v..P....nY0Er 162 | 48 09 65 71 42 32 4A 44 09 77 64 55 73 57 41 | H.eqB2JD.wdUsWA 163 | ``` 164 | 165 | 166 | #### Example: 167 | ![](./cs.png) 168 | 169 | ``` 170 | base64: S6/WGjAUXnLfW0d1tPRZ1E6Kjb8VN4VEW0s2EpBOPrIm4ulqKPyGbtcYOW2tnum5LwgeoBcSQ62v0bBndR8bUayzB4Dv3UZHXw55KyVb7Ct2ErCOh4ofotdROht9mtf64Z/dUDzR0u/JcTQ68jqIGgn4b5EnTCcK/GbIgMBH7mc= 171 | 00000000: 00 00 BE EF 00 00 00 4B 09 04 06 03 02 0A 09 0F .......K........ 172 | 00000010: 07 0D 00 09 02 0E 04 00 A8 03 A8 03 00 6E 5D 1C .............n]. 173 | 00000020: 00 00 15 8C 00 00 04 06 02 23 F0 00 00 00 00 76 .........#.....v 174 | 00000030: 91 0A 60 76 90 F5 50 01 08 00 00 48 66 35 39 42 ..`v..P....Hf59B 175 | 00000040: 70 09 67 77 39 46 64 79 09 51 4C 61 33 71 59 2E p.gw9Fdy.QLa3qY. 176 | 00000050: 65 78 65 exe 177 | 178 | ``` 179 | 180 | -------------------------------------------------------------------------------- /beacon_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal 4 | Refs: 5 | https://github.com/RomanEmelyanov/CobaltStrikeForensic/blob/master/L8_get_beacon.py 6 | https://github.com/nccgroup/pybeacon 7 | ''' 8 | 9 | import requests, struct, urllib3 10 | import argparse 11 | from urllib.parse import urljoin 12 | import socket 13 | import json 14 | from base64 import b64encode 15 | from struct import unpack, unpack_from 16 | 17 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 18 | EMPTY_UA_HEADERS = {"User-Agent":""} 19 | URL_PATHS = {'x86':'ab2g', 'x64':'ab2h'} 20 | 21 | class Base64Encoder(json.JSONEncoder): 22 | def default(self, o): 23 | if isinstance(o, bytes): 24 | return b64encode(o).decode() 25 | return json.JSONEncoder.default(self, o) 26 | 27 | 28 | def _cli_print(msg, end='\n'): 29 | if __name__ == '__main__': 30 | print(msg, end=end) 31 | 32 | 33 | def read_dword_be(fh): 34 | data = fh.read(4) 35 | if not data or len(data) != 4: 36 | return None 37 | return unpack(">I",data)[0] 38 | 39 | 40 | def get_beacon_data(url, arch): 41 | full_url = urljoin(url, URL_PATHS[arch]) 42 | try: 43 | resp = requests.get(full_url, timeout=30, headers=EMPTY_UA_HEADERS, verify=False) 44 | except requests.exceptions.RequestException as e: 45 | _cli_print('[-] Connection error: ', e) 46 | return 47 | 48 | if resp.status_code != 200: 49 | _cli_print('[-] Failed with HTTP status code: ', resp.status_code) 50 | return 51 | 52 | buf = resp.content 53 | 54 | # Check if it's a Trial beacon, therefore not xor encoded (not tested) 55 | eicar_offset = buf.find(b'EICAR-STANDARD-ANTIVIRUS-TEST-FILE') 56 | if eicar_offset != -1: 57 | return buf 58 | return decrypt_beacon(buf) 59 | 60 | 61 | def decrypt_beacon(buf): 62 | offset = buf.find(b'\xff\xff\xff') 63 | if offset == -1: 64 | _cli_print('[-] Unexpected buffer received') 65 | return 66 | offset += 3 67 | key = struct.unpack_from('H", content, port_index + 6) 52 | return port 53 | end 54 | 55 | 56 | end 57 | 58 | local function parse_field(key_name, contents, hex_flag, format) 59 | local index = "" 60 | 61 | local beacon_types = {} 62 | beacon_types[0] = "0 (HTTP)" 63 | beacon_types[1] = "1 (Hybrid HTTP DNS)" 64 | beacon_types[2] = "2 (SMB)" 65 | beacon_types[3] = "3 (Unknown)" 66 | beacon_types[4] = "4 (TCP)" 67 | beacon_types[5] = "5 (Unknown)" 68 | beacon_types[6] = "6 (Unknown)" 69 | beacon_types[7] = "7 (Unknown)" 70 | beacon_types[8] = "8 (HTTPS)" 71 | beacon_types[9] = "9 (Unkown)" 72 | beacon_types[10] = "10 (Bind TCP)" 73 | 74 | local access_types = {} 75 | access_types[0] = "0 (Unknown)" 76 | access_types[1] = "1 (Use direct connection)" 77 | access_types[2] = "2 (Use IE settings)" 78 | access_types[3] = "3 (Unknown)" 79 | access_types[4] = "4 (Use proxy server)" 80 | 81 | if key_name == "Beacon Type" then 82 | index = string.find(contents, hex_flag, 1, true) 83 | if index == nil then 84 | return nil 85 | end 86 | return beacon_types[string.unpack(format, contents, index + string.len(hex_flag))] 87 | 88 | elseif key_name == "Proxy Access Type" then 89 | index = string.find(contents, hex_flag, 1, true) 90 | 91 | if index == nil then 92 | return nil 93 | end 94 | return access_types[string.unpack(format, contents, index + string.len(hex_flag))] 95 | 96 | elseif key_name == "DNS Idle" then 97 | index = string.find(contents, hex_flag, 1, true) 98 | 99 | if index == nil then 100 | return nil 101 | end 102 | 103 | local address = "" 104 | address = address .. string.unpack(format, contents, index + string.len(hex_flag) + 0) .. "." 105 | address = address .. string.unpack(format, contents, index + string.len(hex_flag) + 1) .. "." 106 | address = address .. string.unpack(format, contents, index + string.len(hex_flag) + 2) .. "." 107 | address = address .. string.unpack(format, contents, index + string.len(hex_flag) + 3) 108 | return address 109 | 110 | elseif key_name == "RSA Public Key" then 111 | index = string.find(contents, hex_flag, 1, true) 112 | if index == nil then 113 | return nil 114 | end 115 | return string.sub(base64.enc(string.sub(contents, index + string.len(hex_flag), index + string.len(hex_flag) + 192)),0,216) 116 | 117 | else 118 | index = string.find(contents, hex_flag, 1, true) 119 | 120 | if index == nil then 121 | return nil 122 | end 123 | return string.unpack(format, contents, index + string.len(hex_flag)) 124 | end 125 | end 126 | 127 | local function grab_beacon(response) 128 | 129 | local output = {} 130 | 131 | local test_string = string.char(0xFF) .. string.char(0xFF) .. string.char(0xFF) 132 | local return_string = "" 133 | if (response.status == 200) then 134 | if (http.response_contains(response, test_string, true)) then 135 | local offset = string.find(response.rawbody, test_string) + 3 136 | local endian = "H") 154 | if not output["Beacon Type"] then 155 | output["Beacon Type"] = parse_field("Beacon Type", repacked2, "\x00\x01\x00\x01\x00\x02", ">H") 156 | repacked = repacked2 157 | end 158 | 159 | output["Port"] = parse_field("Port", repacked, "\x00\x02\x00\x01\x00\x02", ">H") 160 | output["Polling"] = parse_field("Polling", repacked, "\x00\x03\x00\x02\x00\x04", ">I") 161 | output["Jitter"] = parse_field("Jitter", repacked, "\x00\x05\x00\x01\x00\x02", ">H") 162 | output["Max DNS"] = parse_field("Max DNS", repacked, "\x00\x06\x00\x01\x00\x02", ">H") 163 | output["C2 Server"] = parse_field("C2 Server", repacked, "\x00\x08\x00\x03\x01\x00", "z") 164 | 165 | -- output["Unknown1"] = parse_field("Unknown1", repacked, "\x00\x04\x00\x02\x00\x04", ">I") 166 | -- output["Unknown3"] = parse_field("Unknown3", repacked, "\x00\x0b\x00\x03\x01\x00", ">I") 167 | -- output["Unknown4"] = parse_field("Unknown4", repacked, "\x00\x1c\x00\x02\x00\x04", ">I") 168 | -- output["Unknown5"] = parse_field("Unknown5", repacked, "\x00\x1f\x00\x01\x00\x02", ">I") 169 | 170 | output["User Agent"] = parse_field("User Agent", repacked, "\x00\x09\x00\x03\x00\x80", "z") 171 | output["HTTP Method Path 2"] = parse_field("HTTP Method Path 2", repacked, "\x00\x0a\x00\x03\x00\x40", "z") 172 | output["Header 1"] = parse_field("Header 1", repacked, "\x00\x0c\x00\x03\x01\x00", "z") 173 | output["Header 2"] = parse_field("Header 2", repacked, "\x00\x0d\x00\x03\x01\x00", "z") 174 | output["Injection Process"] = parse_field("Injection Process", repacked, "\x00\x0e\x00\x03\x00\x40", "z") 175 | output["Pipe Name"] = parse_field("Pipe Name", repacked, "\x00\x0f\x00\x03\x00\x80", "z") 176 | output["Year"] = parse_field("Year", repacked, "\x00\x10\x00\x01\x00\x02", ">H") 177 | output["Month"] = parse_field("Month", repacked, "\x00\x11\x00\x01\x00\x02", ">H") 178 | output["Day"] = parse_field("Day", repacked, "\x00\x12\x00\x01\x00\x02", ">H") 179 | output["DNS Idle"] = parse_field("DNS Idle", repacked, "\x00\x13\x00\x02\x00\x04", "B") 180 | output["DNS Sleep"] = parse_field("DNS Sleep", repacked, "\x00\x14\x00\x02\x00\x04", ">H") 181 | output["Method 1"] = parse_field("Method 1", repacked, "\x00\x1a\x00\x03\x00\x10", "z") 182 | output["Method 2"] = parse_field("Method 2", repacked, "\x00\x1b\x00\x03\x00\x10", "z") 183 | output["Spawn To x86"] = parse_field("Spawn To x86", repacked, "\x00\x1d\x00\x03\x00\x40", "z") 184 | output["Spawn To x64"] = parse_field("Spawn To x64", repacked, "\x00\x1e\x00\x03\x00\x40", "z") 185 | output["Proxy Hostname"] = parse_field("Proxy Hostname", repacked, "\x00\x20\x00\x03\x00\x80", "z") 186 | output["Proxy Username"] = parse_field("Proxy Username", repacked, "\x00\x21\x00\x03\x00\x40", "z") 187 | output["Proxy Password"] = parse_field("Proxy Password", repacked, "\x00\x22\x00\x03\x00\x40", "z") 188 | output["Proxy Access Type"] = parse_field("Proxy Access Type", repacked, "\x00\x23\x00\x01\x00\x02", "z") 189 | output["CreateRemoteThread"] = parse_field("CreateRemoteThread", repacked, "\x00\x24\x00\x01\x00\x02", "z") 190 | output["Watermark"] = parse_field("Watermark", repacked, "\x00\x25\x00\x02\x00\x04", ">I") 191 | output["C2 Host Header"] = parse_field("C2 Host Header", repacked, "\x00\x36\x00\x03\x00\x80", "z") 192 | output["RSA Public Key"] = parse_field("RSA Public Key", repacked, "\x00\x07\x00\x03\x01\x00", "") 193 | 194 | 195 | 196 | -- local write_out = io.open(stdnse.tohex(openssl.digest("sha256",response.body)) .. ".bin","w") 197 | -- io.output(write_out) 198 | -- io.write(response.body) 199 | 200 | if (stdnse.get_script_args("save")) == "true" then 201 | local write_out = io.open(stdnse.tohex(openssl.digest("sha256", response.body)) .. ".bin", "w") 202 | io.output(write_out) 203 | io.write(response.body) 204 | end 205 | end 206 | end 207 | return output 208 | end 209 | 210 | action = function(host, port) 211 | local json_output = {} 212 | 213 | local uri_x86 = generate_checksum(92) 214 | local uri_x64 = generate_checksum(93) 215 | 216 | local response_x86 = http.get(host, port, uri_x86) 217 | if response_x86.body == nil then 218 | return "No Valid Response" 219 | end 220 | 221 | json_output['x86'] = {} 222 | json_output['x86']['uri_queried'] = uri_x86 223 | json_output['x86']['sha256'] = stdnse.tohex(openssl.digest("sha256", response_x86.body)) 224 | json_output['x86']['sha1'] = stdnse.tohex(openssl.digest("sha1", response_x86.body)) 225 | json_output['x86']['md5'] = stdnse.tohex(openssl.digest("md5", response_x86.body)) 226 | json_output['x86']['time'] = stdnse.clock_ms() 227 | json_output['x86']['config'] = grab_beacon(response_x86) 228 | 229 | local response_x64 = http.get(host, port, uri_x64) 230 | 231 | if response_x64.body == nil then 232 | return "No Valid Response" 233 | end 234 | 235 | json_output['x64'] = {} 236 | json_output['x64']['uri_queried'] = uri_x64 237 | json_output['x64']['sha256'] = stdnse.tohex(openssl.digest("sha256", response_x64.body)) 238 | json_output['x64']['sha1'] = stdnse.tohex(openssl.digest("sha1", response_x64.body)) 239 | json_output['x64']['md5'] = stdnse.tohex(openssl.digest("md5", response_x64.body)) 240 | json_output['x64']['time'] = stdnse.clock_ms() 241 | json_output['x64']['config'] = grab_beacon(response_x64) 242 | 243 | -- https://nmap.org/nsedoc/lib/json.html 244 | 245 | -- 输出json格式 object 类型 246 | -- json_output = json.generate(json_output) 247 | 248 | -- json_output = json.make_array(json_output) 249 | 250 | -- json_output = json.make_object(json_output) 251 | 252 | -- =>> true 253 | -- json_output = json.parse(json_output) 254 | 255 | 256 | -- =>> object 257 | -- json_output = json.typeof(json_output) 258 | 259 | 260 | return json_output 261 | end 262 | -------------------------------------------------------------------------------- /parse_beacon_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | Parses CobaltStrike Beacon's configuration from PE file or memory dump. 4 | By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal 5 | 6 | Inspired by https://github.com/JPCERTCC/aa-tools/blob/master/cobaltstrikescan.py 7 | 8 | TODO: 9 | 1. Parse headers modifiers 10 | 2. Dynamic size parsing 11 | ''' 12 | 13 | from beacon_utils import * 14 | from struct import unpack, unpack_from 15 | from socket import inet_ntoa 16 | from collections import OrderedDict 17 | from netstruct import unpack as netunpack 18 | import argparse 19 | import io 20 | import re 21 | import pefile 22 | import os 23 | import hashlib 24 | from io import BytesIO 25 | import base64 26 | 27 | THRESHOLD = 1100 28 | COLUMN_WIDTH = 35 29 | SUPPORTED_VERSIONS = (3, 4) 30 | SILENT_CONFIGS = ['PublicKey', 'ProcInject_Stub', 'smbFrameHeader', 'tcpFrameHeader', 'SpawnTo'] 31 | 32 | def _cli_print(msg, end='\n'): 33 | if __name__ == '__main__': 34 | print(msg, end=end) 35 | 36 | class confConsts: 37 | MAX_SETTINGS = 64 38 | TYPE_NONE = 0 39 | TYPE_SHORT = 1 40 | TYPE_INT = 2 41 | TYPE_STR = 3 42 | 43 | START_PATTERNS = { 44 | 3: b'\x69\x68\x69\x68\x69\x6b..\x69\x6b\x69\x68\x69\x6b..\x69\x6a', 45 | 4: b'\x2e\x2f\x2e\x2f\x2e\x2c..\x2e\x2c\x2e\x2f\x2e\x2c..\x2e' 46 | } 47 | START_PATTERN_DECODED = b'\x00\x01\x00\x01\x00\x02..\x00\x02\x00\x01\x00\x02..\x00' 48 | CONFIG_SIZE = 4096 49 | XORBYTES = { 50 | 3: 0x69, 51 | 4: 0x2e 52 | } 53 | 54 | class packedSetting: 55 | 56 | def __init__(self, pos, datatype, length=0, isBlob=False, isHeaders=False, isIpAddress=False, isBool=False, isDate=False, boolFalseValue=0, isProcInjectTransform=False, isMalleableStream=False, hashBlob=False, enum=None, mask=None): 57 | self.pos = pos 58 | self.datatype = datatype 59 | self.is_blob = isBlob 60 | self.is_headers = isHeaders 61 | self.is_ipaddress = isIpAddress 62 | self.is_bool = isBool 63 | self.is_date = isDate 64 | self.is_malleable_stream = isMalleableStream 65 | self.bool_false_value = boolFalseValue 66 | self.is_transform = isProcInjectTransform 67 | self.hashBlob = hashBlob 68 | self.enum = enum 69 | self.mask = mask 70 | self.transform_get = None 71 | self.transform_post = None 72 | if datatype == confConsts.TYPE_STR and length == 0: 73 | raise(Exception("if datatype is TYPE_STR then length must not be 0")) 74 | 75 | self.length = length 76 | if datatype == confConsts.TYPE_SHORT: 77 | self.length = 2 78 | elif datatype == confConsts.TYPE_INT: 79 | self.length = 4 80 | 81 | 82 | def binary_repr(self): 83 | """ 84 | Param number - Type - Length - Value 85 | """ 86 | self_repr = bytearray(6) 87 | self_repr[1] = self.pos 88 | self_repr[3] = self.datatype 89 | self_repr[4:6] = self.length.to_bytes(2, 'big') 90 | return self_repr 91 | 92 | def parse_transformdata(self, data): 93 | ''' 94 | Args: 95 | data (bytes): Raw communication transforam data 96 | 97 | Returns: 98 | dict: Dict of transform commands that should be convenient for communication forging 99 | 100 | ''' 101 | dio = io.BytesIO(data) 102 | trans = {'ConstHeaders':[], 'ConstParams': [], 'Metadata': [], 'SessionId': [], 'Output': []} 103 | current_category = 'Constants' 104 | 105 | # TODO: replace all magic numbers here with enum 106 | while True: 107 | tstep = read_dword_be(dio) 108 | if tstep == 7: 109 | name = read_dword_be(dio) 110 | if self.pos == 12: # GET 111 | current_category = 'Metadata' 112 | else: # POST 113 | current_category = 'SessionId' if name == 0 else 'Output' 114 | elif tstep in (1, 2, 5, 6): 115 | length = read_dword_be(dio) 116 | step_data = dio.read(length).decode() 117 | trans[current_category].append(BeaconSettings.TSTEPS[tstep] + ' "' + step_data + '"') 118 | elif tstep in (10, 16, 9): 119 | length = read_dword_be(dio) 120 | step_data = dio.read(length).decode() 121 | if tstep == 9: 122 | trans['ConstParams'].append(step_data) 123 | else: 124 | trans['ConstHeaders'].append(step_data) 125 | elif tstep in (3, 4, 13, 8, 11, 12, 15): 126 | trans[current_category].append(BeaconSettings.TSTEPS[tstep]) 127 | else: 128 | break 129 | 130 | if self.pos == 12: 131 | self.transform_get = trans 132 | else: 133 | self.transform_post = trans 134 | 135 | return trans 136 | 137 | 138 | def pretty_repr(self, full_config_data): 139 | data_offset = full_config_data.find(self.binary_repr()) 140 | if data_offset < 0 and self.datatype == confConsts.TYPE_STR: 141 | self.length = 16 142 | while self.length < 2048: 143 | data_offset = full_config_data.find(self.binary_repr()) 144 | if data_offset > 0: 145 | break 146 | self.length *= 2 147 | 148 | if data_offset < 0: 149 | return 'Not Found' 150 | 151 | repr_len = len(self.binary_repr()) 152 | conf_data = full_config_data[data_offset + repr_len : data_offset + repr_len + self.length] 153 | if self.datatype == confConsts.TYPE_SHORT: 154 | conf_data = unpack('>H', conf_data)[0] 155 | if self.is_bool: 156 | ret = 'False' if conf_data == self.bool_false_value else 'True' 157 | return ret 158 | elif self.enum: 159 | return self.enum[conf_data] 160 | elif self.mask: 161 | ret_arr = [] 162 | for k,v in self.mask.items(): 163 | if k == 0 and k == conf_data: 164 | ret_arr.append(v) 165 | if k & conf_data: 166 | ret_arr.append(v) 167 | return ret_arr 168 | else: 169 | return conf_data 170 | 171 | elif self.datatype == confConsts.TYPE_INT: 172 | if self.is_ipaddress: 173 | return inet_ntoa(conf_data) 174 | 175 | else: 176 | conf_data = unpack('>i', conf_data)[0] 177 | if self.is_date and conf_data != 0: 178 | fulldate = str(conf_data) 179 | return "%s-%s-%s" % (fulldate[0:4], fulldate[4:6], fulldate[6:]) 180 | 181 | return conf_data 182 | 183 | if self.is_blob: 184 | if self.enum != None: 185 | ret_arr = [] 186 | i = 0 187 | while i < len(conf_data): 188 | v = conf_data[i] 189 | if v == 0: 190 | return ret_arr 191 | v = self.enum[v] 192 | if v: 193 | ret_arr.append(v) 194 | i+=1 195 | 196 | # Only EXECUTE_TYPE for now 197 | else: 198 | # Skipping unknown short value in the start 199 | string1 = netunpack(b'I$', conf_data[i+3:])[0].decode() 200 | string2 = netunpack(b'I$', conf_data[i+3+4+len(string1):])[0].decode() 201 | ret_arr.append("%s:%s" % (string1.strip('\x00'),string2.strip('\x00'))) 202 | i += len(string1) + len(string2) + 11 203 | 204 | 205 | if self.is_transform: 206 | if conf_data == bytes(len(conf_data)): 207 | return 'Empty' 208 | 209 | ret_arr = [] 210 | prepend_length = unpack('>I', conf_data[0:4])[0] 211 | prepend = conf_data[4 : 4+prepend_length] 212 | append_length_offset = prepend_length + 4 213 | append_length = unpack('>I', conf_data[append_length_offset : append_length_offset+4])[0] 214 | append = conf_data[append_length_offset+4 : append_length_offset+4+append_length] 215 | ret_arr.append(prepend) 216 | ret_arr.append(append if append_length < 256 and append != bytes(append_length) else 'Empty') 217 | return ret_arr 218 | 219 | if self.is_malleable_stream: 220 | prog = [] 221 | fh = io.BytesIO(conf_data) 222 | while True: 223 | op = read_dword_be(fh) 224 | if not op: 225 | break 226 | if op == 1: 227 | l = read_dword_be(fh) 228 | prog.append("Remove %d bytes from the end" % l) 229 | elif op == 2: 230 | l = read_dword_be(fh) 231 | prog.append("Remove %d bytes from the beginning" % l) 232 | elif op == 3: 233 | prog.append("Base64 decode") 234 | elif op == 8: 235 | prog.append("NetBIOS decode 'a'") 236 | elif op == 11: 237 | prog.append("NetBIOS decode 'A'") 238 | elif op == 13: 239 | prog.append("Base64 URL-safe decode") 240 | elif op == 15: 241 | prog.append("XOR mask w/ random key") 242 | 243 | conf_data = prog 244 | if self.hashBlob: 245 | conf_data = conf_data.strip(b'\x00') 246 | conf_data = hashlib.md5(conf_data).hexdigest() 247 | 248 | return conf_data 249 | 250 | if self.is_headers: 251 | return self.parse_transformdata(conf_data) 252 | 253 | conf_data = conf_data.strip(b'\x00').decode() 254 | return conf_data 255 | 256 | 257 | class BeaconSettings: 258 | 259 | BEACON_TYPE = {0x0: "HTTP", 0x1: "Hybrid HTTP DNS", 0x2: "SMB", 0x4: "TCP", 0x8: "HTTPS", 0x10: "Bind TCP"} 260 | ACCESS_TYPE = {0x1: "Use direct connection", 0x2: "Use IE settings", 0x4: "Use proxy server"} 261 | EXECUTE_TYPE = {0x1: "CreateThread", 0x2: "SetThreadContext", 0x3: "CreateRemoteThread", 0x4: "RtlCreateUserThread", 0x5: "NtQueueApcThread", 0x6: None, 0x7: None, 0x8: "NtQueueApcThread-s"} 262 | ALLOCATION_FUNCTIONS = {0: "VirtualAllocEx", 1: "NtMapViewOfSection"} 263 | TSTEPS = {1: "append", 2: "prepend", 3: "base64", 4: "print", 5: "parameter", 6: "header", 7: "build", 8: "netbios", 9: "const_parameter", 10: "const_header", 11: "netbiosu", 12: "uri_append", 13: "base64url", 14: "strrep", 15: "mask", 16: "const_host_header"} 264 | ROTATE_STRATEGY = ["round-robin", "random", "failover", "failover-5x", "failover-50x", "failover-100x", "failover-1m", "failover-5m", "failover-15m", "failover-30m", "failover-1h", "failover-3h", "failover-6h", "failover-12h", "failover-1d", "rotate-1m", "rotate-5m", "rotate-15m", "rotate-30m", "rotate-1h", "rotate-3h", "rotate-6h", "rotate-12h", "rotate-1d" ] 265 | 266 | def __init__(self, version): 267 | if version not in SUPPORTED_VERSIONS: 268 | _cli_print("Error: Only supports version 3 and 4, not %d" % version) 269 | return 270 | self.version = version 271 | self.settings = OrderedDict() 272 | self.init() 273 | 274 | def init(self): 275 | self.settings['BeaconType'] = packedSetting(1, confConsts.TYPE_SHORT, mask=self.BEACON_TYPE) 276 | self.settings['Port'] = packedSetting(2, confConsts.TYPE_SHORT) 277 | self.settings['SleepTime'] = packedSetting(3, confConsts.TYPE_INT) 278 | self.settings['MaxGetSize'] = packedSetting(4, confConsts.TYPE_INT) 279 | self.settings['Jitter'] = packedSetting(5, confConsts.TYPE_SHORT) 280 | self.settings['MaxDNS'] = packedSetting(6, confConsts.TYPE_SHORT) 281 | # Silenced config 282 | self.settings['PublicKey'] = packedSetting(7, confConsts.TYPE_STR, 256, isBlob=True) 283 | 284 | self.settings['PublicKey_MD5'] = packedSetting(7, confConsts.TYPE_STR, 256, isBlob=True, hashBlob=True) 285 | self.settings['C2Server'] = packedSetting(8, confConsts.TYPE_STR, 256) 286 | self.settings['UserAgent'] = packedSetting(9, confConsts.TYPE_STR, 128) 287 | # TODO: Concat with C2Server? 288 | self.settings['HttpPostUri'] = packedSetting(10, confConsts.TYPE_STR, 64) 289 | 290 | # This is how the server transforms its communication to the beacon 291 | # ref: https://www.cobaltstrike.com/help-malleable-c2 | https://usualsuspect.re/article/cobalt-strikes-malleable-c2-under-the-hood 292 | # TODO: Switch to isHeaders parser logic 293 | self.settings['Malleable_C2_Instructions'] = packedSetting(11, confConsts.TYPE_STR, 256, isBlob=True,isMalleableStream=True) 294 | # This is the way the beacon transforms its communication to the server 295 | # TODO: Change name to HttpGet_Client and HttpPost_Client 296 | self.settings['HttpGet_Metadata'] = packedSetting(12, confConsts.TYPE_STR, 256, isHeaders=True) 297 | self.settings['HttpPost_Metadata'] = packedSetting(13, confConsts.TYPE_STR, 256, isHeaders=True) 298 | 299 | self.settings['SpawnTo'] = packedSetting(14, confConsts.TYPE_STR, 16, isBlob=True) 300 | self.settings['PipeName'] = packedSetting(15, confConsts.TYPE_STR, 128) 301 | # Options 16-18 are deprecated in 3.4 302 | self.settings['DNS_Idle'] = packedSetting(19, confConsts.TYPE_INT, isIpAddress=True) 303 | self.settings['DNS_Sleep'] = packedSetting(20, confConsts.TYPE_INT) 304 | # Options 21-25 are for SSHAgent 305 | self.settings['SSH_Host'] = packedSetting(21, confConsts.TYPE_STR, 256) 306 | self.settings['SSH_Port'] = packedSetting(22, confConsts.TYPE_SHORT) 307 | self.settings['SSH_Username'] = packedSetting(23, confConsts.TYPE_STR, 128) 308 | self.settings['SSH_Password_Plaintext'] = packedSetting(24, confConsts.TYPE_STR, 128) 309 | self.settings['SSH_Password_Pubkey'] = packedSetting(25, confConsts.TYPE_STR, 6144) 310 | self.settings['SSH_Banner'] = packedSetting(54, confConsts.TYPE_STR, 128) 311 | 312 | self.settings['HttpGet_Verb'] = packedSetting(26, confConsts.TYPE_STR, 16) 313 | self.settings['HttpPost_Verb'] = packedSetting(27, confConsts.TYPE_STR, 16) 314 | self.settings['HttpPostChunk'] = packedSetting(28, confConsts.TYPE_INT) 315 | self.settings['Spawnto_x86'] = packedSetting(29, confConsts.TYPE_STR, 64) 316 | self.settings['Spawnto_x64'] = packedSetting(30, confConsts.TYPE_STR, 64) 317 | # Whether the beacon encrypts his communication, should be always on (1) in beacon 4 318 | self.settings['CryptoScheme'] = packedSetting(31, confConsts.TYPE_SHORT) 319 | self.settings['Proxy_Config'] = packedSetting(32, confConsts.TYPE_STR, 128) 320 | self.settings['Proxy_User'] = packedSetting(33, confConsts.TYPE_STR, 64) 321 | self.settings['Proxy_Password'] = packedSetting(34, confConsts.TYPE_STR, 64) 322 | self.settings['Proxy_Behavior'] = packedSetting(35, confConsts.TYPE_SHORT, enum=self.ACCESS_TYPE) 323 | # Option 36 is deprecated 324 | self.settings['Watermark'] = packedSetting(37, confConsts.TYPE_INT) 325 | self.settings['bStageCleanup'] = packedSetting(38, confConsts.TYPE_SHORT, isBool=True) 326 | self.settings['bCFGCaution'] = packedSetting(39, confConsts.TYPE_SHORT, isBool=True) 327 | self.settings['KillDate'] = packedSetting(40, confConsts.TYPE_INT, isDate=True) 328 | # Inner parameter, does not seem interesting so silencing 329 | #self.settings['textSectionEnd (0 if !sleep_mask)'] = packedSetting(41, confConsts.TYPE_INT) 330 | 331 | #TODO: dynamic size parsing 332 | #self.settings['ObfuscateSectionsInfo'] = packedSetting(42, confConsts.TYPE_STR, %d, isBlob=True) 333 | self.settings['bProcInject_StartRWX'] = packedSetting(43, confConsts.TYPE_SHORT, isBool=True, boolFalseValue=4) 334 | self.settings['bProcInject_UseRWX'] = packedSetting(44, confConsts.TYPE_SHORT, isBool=True, boolFalseValue=32) 335 | self.settings['bProcInject_MinAllocSize'] = packedSetting(45, confConsts.TYPE_INT) 336 | self.settings['ProcInject_PrependAppend_x86'] = packedSetting(46, confConsts.TYPE_STR, 256, isBlob=True, isProcInjectTransform=True) 337 | self.settings['ProcInject_PrependAppend_x64'] = packedSetting(47, confConsts.TYPE_STR, 256, isBlob=True, isProcInjectTransform=True) 338 | self.settings['ProcInject_Execute'] = packedSetting(51, confConsts.TYPE_STR, 128, isBlob=True, enum=self.EXECUTE_TYPE) 339 | # If True then allocation is using NtMapViewOfSection 340 | self.settings['ProcInject_AllocationMethod'] = packedSetting(52, confConsts.TYPE_SHORT, enum=self.ALLOCATION_FUNCTIONS) 341 | 342 | # Unknown data, silenced for now 343 | self.settings['ProcInject_Stub'] = packedSetting(53, confConsts.TYPE_STR, 16, isBlob=True) 344 | self.settings['bUsesCookies'] = packedSetting(50, confConsts.TYPE_SHORT, isBool=True) 345 | self.settings['HostHeader'] = packedSetting(54, confConsts.TYPE_STR, 128) 346 | 347 | # Silenced as I've yet to test it on a sample with those options 348 | self.settings['smbFrameHeader'] = packedSetting(57, confConsts.TYPE_STR, 128, isBlob=True) 349 | self.settings['tcpFrameHeader'] = packedSetting(58, confConsts.TYPE_STR, 128, isBlob=True) 350 | self.settings['headersToRemove'] = packedSetting(59, confConsts.TYPE_STR, 64) 351 | 352 | # DNS Beacon 353 | self.settings['DNS_Beaconing'] = packedSetting(60, confConsts.TYPE_STR, 33) 354 | self.settings['DNS_get_TypeA'] = packedSetting(61, confConsts.TYPE_STR, 33) 355 | self.settings['DNS_get_TypeAAAA'] = packedSetting(62, confConsts.TYPE_STR, 33) 356 | self.settings['DNS_get_TypeTXT'] = packedSetting(63, confConsts.TYPE_STR, 33) 357 | self.settings['DNS_put_metadata'] = packedSetting(64, confConsts.TYPE_STR, 33) 358 | self.settings['DNS_put_output'] = packedSetting(65, confConsts.TYPE_STR, 33) 359 | self.settings['DNS_resolver'] = packedSetting(66, confConsts.TYPE_STR, 15) 360 | self.settings['DNS_strategy'] = packedSetting(67, confConsts.TYPE_SHORT, enum=self.ROTATE_STRATEGY) 361 | self.settings['DNS_strategy_rotate_seconds'] = packedSetting(68, confConsts.TYPE_INT) 362 | self.settings['DNS_strategy_fail_x'] = packedSetting(69, confConsts.TYPE_INT) 363 | self.settings['DNS_strategy_fail_seconds'] = packedSetting(70, confConsts.TYPE_INT) 364 | 365 | 366 | class cobaltstrikeConfig: 367 | def __init__(self, f): 368 | ''' 369 | f: file path or file-like object 370 | ''' 371 | self.data = None 372 | if isinstance(f, str): 373 | with open(f, 'rb') as fobj: 374 | self.data = fobj.read() 375 | else: 376 | self.data = f.read() 377 | 378 | """Parse the CobaltStrike configuration""" 379 | 380 | @staticmethod 381 | def decode_config(cfg_blob, version): 382 | return bytes([cfg_offset ^ confConsts.XORBYTES[version] for cfg_offset in cfg_blob]) 383 | 384 | def _parse_config(self, version, quiet=False, as_json=False): 385 | ''' 386 | Parses beacon's configuration from beacon PE or memory dump. 387 | Returns json of config is found; else it returns None. 388 | 389 | :int version: Try a specific version (3 or 4), or leave None to try both of them 390 | :bool quiet: Whether to print missing or empty settings 391 | :bool as_json: Whether to dump as json 392 | ''' 393 | re_start_match = re.search(confConsts.START_PATTERNS[version], self.data) 394 | re_start_decoded_match = re.search(confConsts.START_PATTERN_DECODED, self.data) 395 | 396 | if not re_start_match and not re_start_decoded_match: 397 | return None 398 | encoded_config_offset = re_start_match.start() if re_start_match else -1 399 | decoded_config_offset = re_start_decoded_match.start() if re_start_decoded_match else -1 400 | 401 | if encoded_config_offset >= 0: 402 | full_config_data = cobaltstrikeConfig.decode_config(self.data[encoded_config_offset : encoded_config_offset + confConsts.CONFIG_SIZE], version=version) 403 | else: 404 | full_config_data = self.data[decoded_config_offset : decoded_config_offset + confConsts.CONFIG_SIZE] 405 | 406 | parsed_config = {} 407 | settings = BeaconSettings(version).settings.items() 408 | for conf_name, packed_conf in settings: 409 | parsed_setting = packed_conf.pretty_repr(full_config_data) 410 | 411 | parsed_config[conf_name] = parsed_setting 412 | 413 | # 获取正确的公钥长度值,AB结尾 414 | if conf_name == "PublicKey": 415 | PublicKey_str = base64.b64encode(parsed_config[conf_name]).decode('UTF-8') 416 | # print(PublicKey_str) 417 | PublicKey_ABstr = PublicKey_str.rfind("AB") + 2 418 | PublicKey_str = PublicKey_str[0 : PublicKey_ABstr] # 0-216 419 | parsed_config[conf_name] = base64.b64decode(PublicKey_str.encode("UTF-8")) 420 | 421 | if as_json: 422 | continue 423 | 424 | if conf_name in SILENT_CONFIGS: 425 | continue 426 | 427 | if parsed_setting == 'Not Found' and quiet: 428 | continue 429 | 430 | conf_type = type(parsed_setting) 431 | if conf_type in (str, int, bytes): 432 | if quiet and conf_type == str and parsed_setting.strip() == '': 433 | continue 434 | _cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=parsed_setting)) 435 | 436 | elif parsed_setting == []: 437 | if quiet: 438 | continue 439 | _cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val='Empty')) 440 | 441 | elif conf_type == dict: # the beautifulest code 442 | conf_data = [] 443 | for k in parsed_setting.keys(): 444 | if parsed_setting[k]: 445 | conf_data.append(k) 446 | for v in parsed_setting[k]: 447 | conf_data.append('\t' + v) 448 | if not conf_data: 449 | continue 450 | _cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=conf_data[0])) 451 | for val in conf_data[1:]: 452 | _cli_print(' ' * COLUMN_WIDTH, end='') 453 | _cli_print(val) 454 | 455 | elif conf_type == list: # list 456 | _cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=parsed_setting[0])) 457 | for val in parsed_setting[1:]: 458 | _cli_print(' ' * COLUMN_WIDTH, end='') 459 | _cli_print(val) 460 | 461 | if as_json: 462 | _cli_print(json.dumps(parsed_config, cls=Base64Encoder)) 463 | 464 | return parsed_config 465 | 466 | def parse_config(self, version=None, quiet=False, as_json=False): 467 | ''' 468 | Parses beacon's configuration from beacon PE or memory dump 469 | Returns json of config is found; else it returns None. 470 | 471 | :int version: Try a specific version (3 or 4), or leave None to try both of them 472 | :bool quiet: Whether to print missing or empty settings 473 | :bool as_json: Whether to dump as json 474 | ''' 475 | 476 | if not version: 477 | for ver in SUPPORTED_VERSIONS: 478 | parsed = self._parse_config(version=ver, quiet=quiet, as_json=as_json) 479 | if parsed: 480 | return parsed 481 | else: 482 | return self._parse_config(version=version, quiet=quiet, as_json=as_json) 483 | return None 484 | 485 | 486 | def parse_encrypted_config_non_pe(self, version=None, quiet=False, as_json=False): 487 | self.data = decrypt_beacon(self.data) 488 | return self.parse_config(version=version, quiet=quiet, as_json=as_json) 489 | 490 | def parse_encrypted_config(self, version=None, quiet=False, as_json=False): 491 | ''' 492 | Parses beacon's configuration from stager dll or memory dump 493 | Returns json of config is found; else it returns None. 494 | 495 | :bool quiet: Whether to print missing settings 496 | :bool as_json: Whether to dump as json 497 | ''' 498 | 499 | try: 500 | pe = pefile.PE(data=self.data) 501 | except pefile.PEFormatError: 502 | return self.parse_encrypted_config_non_pe(version=version, quiet=quiet, as_json=as_json) 503 | 504 | data_sections = [s for s in pe.sections if s.Name.find(b'.data') != -1] 505 | if not data_sections: 506 | _cli_print("Failed to find .data section") 507 | return False 508 | data = data_sections[0].get_data() 509 | 510 | offset = 0 511 | key_found = False 512 | while offset < len(data): 513 | key = data[offset:offset+4] 514 | if key != bytes(4): 515 | if data.count(key) >= THRESHOLD: 516 | key_found = True 517 | size = int.from_bytes(data[offset-4:offset], 'little') 518 | encrypted_data_offset = offset+16 - (offset % 16) 519 | break 520 | 521 | offset += 4 522 | 523 | if not key_found: 524 | return False 525 | 526 | # decrypt 527 | enc_data = data[encrypted_data_offset:encrypted_data_offset+size] 528 | dec_data = [] 529 | for i,c in enumerate(enc_data): 530 | dec_data.append(c ^ key[i % 4]) 531 | 532 | dec_data = bytes(dec_data) 533 | self.data = dec_data 534 | return self.parse_config(version=version, quiet=quiet, as_json=as_json) 535 | 536 | 537 | if __name__ == '__main__': 538 | parser = argparse.ArgumentParser(description="Parses CobaltStrike Beacon's configuration from PE, memory dump or URL.") 539 | parser.add_argument("beacon", help="This can be a file path or a url (if started with http/s)") 540 | parser.add_argument("--json", help="Print as json", action="store_true", default=False) 541 | parser.add_argument("--quiet", help="Do not print missing or empty settings", action="store_true", default=False) 542 | parser.add_argument("--version", help="Try as specific cobalt version (3 or 4). If not specified, tries both.", type=int) 543 | args = parser.parse_args() 544 | 545 | if os.path.isfile(args.beacon): 546 | if cobaltstrikeConfig(args.beacon).parse_config(version=args.version, quiet=args.quiet, as_json=args.json) or \ 547 | cobaltstrikeConfig(args.beacon).parse_encrypted_config(version=args.version, quiet=args.quiet, as_json=args.json): 548 | exit(0) 549 | 550 | elif args.beacon.lower().startswith('http'): 551 | x86_beacon_data = get_beacon_data(args.beacon, 'x86') 552 | x64_beacon_data = get_beacon_data(args.beacon, 'x64') 553 | if not x86_beacon_data and not x64_beacon_data: 554 | print("[-] Failed to find any beacon configuration") 555 | exit(1) 556 | 557 | conf_data = x86_beacon_data or x64_beacon_data 558 | if cobaltstrikeConfig(BytesIO(conf_data)).parse_config(version=args.version, quiet=args.quiet, as_json=args.json) or \ 559 | cobaltstrikeConfig(BytesIO(conf_data)).parse_encrypted_config(version=args.version, quiet=args.quiet, as_json=args.json): 560 | exit(0) 561 | 562 | print("[-] Failed to find any beacon configuration") 563 | exit(1) 564 | -------------------------------------------------------------------------------- /rsa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jas502n/CS_mock/307df5c4b9bfba4e4058dbc2266b5781f2794d48/rsa.png --------------------------------------------------------------------------------