comm and improvements

This commit is contained in:
Kristal-g
2021-04-21 13:35:11 +03:00
parent b987870e56
commit 1e401b89f4
6 changed files with 626 additions and 57 deletions
+13 -8
View File
@@ -1,26 +1,31 @@
# CobaltStrikeParser
Python parser for CobaltStrike Beacon's configuration
## Background
Use `parse_beacon_config.py` for stageless beacons or on memory dumps.
Many stageless beacons are PEs where the beacon code itself is stored in the `.data` section and xored with 4-byte key.
## Description
Use `parse_beacon_config.py` for stageless beacons, memory dumps or C2 urls with metasploit compatibility mode (default true).
Many stageless beacons are PEs where the beacon code itself is stored in the `.data` section and xored with 4-byte key.
The script tries to find the xor key and data heuristically, decrypt the data and parse the configuration from it.
This is designed so it can be used as a library too.
The repo now also includes a small commuincation module (comm.py) that can help with communcating to a C2 server as a beacon.
## Usage
```
usage: parse_beacon_config.py [-h] [--json] [--quiet] [--version VERSION] path
usage: parse_beacon_config.py [-h] [--json] [--quiet] [--version VERSION] beacon
Parses CobaltStrike Beacon's configuration from PE or memory dump.
Parses CobaltStrike Beacon's configuration from PE, memory dump or URL.
positional arguments:
path Beacon file path
beacon This can be a file path or a url (if started with http/s)
optional arguments:
-h, --help show this help message and exit
--json Print as json
--quiet Do not print missing or empty settings
--version VERSION Try as specific cobalt version (3 or 4). If not specified, tries both.
```
```
## Extra
To use the communication poc copy it to the main folder and run it from there.
For installing the M2Crypto library (a requirement for the poc) on Windows, it's easiest with installers found online, and not through pip.
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/python3
'''
By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal
Refs:
https://github.com/RomanEmelyanov/CobaltStrikeForensic/blob/master/L8_get_beacon.py
https://github.com/nccgroup/pybeacon
'''
import requests, struct, urllib3
import argparse
from urllib.parse import urljoin
import socket
import json
from base64 import b64encode
from struct import unpack, unpack_from
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
EMPTY_UA_HEADERS = {"User-Agent":""}
URL_PATHS = {'x86':'ab2g', 'x64':'ab2h'}
class Base64Encoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, bytes):
return b64encode(o).decode()
return json.JSONEncoder.default(self, o)
def _cli_print(msg, end='\n'):
if __name__ == '__main__':
print(msg, end=end)
def read_dword_be(fh):
data = fh.read(4)
if not data or len(data) != 4:
return None
return unpack(">I",data)[0]
def get_beacon_data(url, arch):
full_url = urljoin(url, URL_PATHS[arch])
try:
resp = requests.get(full_url, timeout=30, headers=EMPTY_UA_HEADERS, verify=False)
except requests.exceptions.RequestException as e:
_cli_print('[-] Connection error: ', e)
return
if resp.status_code != 200:
_cli_print('[-] Failed with HTTP status code: ', resp.status_code)
return
buf = resp.content
# Check if it's a Trial beacon, therefore not xor encoded (not tested)
eicar_offset = buf.find(b'EICAR-STANDARD-ANTIVIRUS-TEST-FILE')
if eicar_offset != -1:
return buf
offset = buf.find(b'\xff\xff\xff')
if offset == -1:
_cli_print('[-] Unexpected buffer received')
return
offset += 3
key = struct.unpack_from('<I', buf, offset)[0]
size = struct.unpack_from('<I', buf, offset+4)[0] ^ key
head_enc = struct.unpack_from('<I', buf, offset+8)[0] ^ key
head = head_enc & 0xffff
# Taken directly from L8_get_beacon.py
if head == 0x5a4d or head ==0x9090:
decoded_data = b''
for i in range(2+offset//4, len(buf)//4-4):
a = struct.unpack_from('<I', buf, i*4)[0]
b = struct.unpack_from('<I', buf, i*4+4)[0]
с = a ^ b
decoded_data += struct.pack('<I', с)
return decoded_data
+253
View File
@@ -0,0 +1,253 @@
#!/usr/bin/python3
'''
By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal
These were awesome reference material:
https://research.nccgroup.com/2020/06/15/striking-back-at-retired-cobalt-strike-a-look-at-a-legacy-vulnerability/
https://github.com/nccgroup/pybeacon
'''
import struct
import base64
import hashlib
import random
import os
import string
import M2Crypto
import requests
PUBLIC_KEY_TEMPLATE = "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----"
class Metadata(object):
"""
Class to represent a beacon's metadata.
This is specific to Cobalt 4 and up
"""
def __init__(self, public_key, aes_source_bytes):
"""
Generates a random beacon entry
Args:
public_key (bytes): The extracted public key from beacon configuration
aes_source_bytes (bytes): 16 bytes used to generate AES keys from
"""
self.public_key = public_key
self.port = random.randint(40000,50000)
self.ciphertext = ""
self.charset = 20273
self.ver = random.randint(1,10)
self.ip = os.urandom(4)
self.comp = ''.join(random.choices(string.ascii_uppercase + string.digits, k=7))
self.user = ''.join(random.choices(string.ascii_uppercase + string.digits, k=7))
self.pid = random.randint(1,50000) * 4 - 2 # ;)
self.bid = random.randint(1,1000000) * 2
self.barch = 1
self.is64 = False
self.high_integrity = False
self.aes_source_bytes = aes_source_bytes
self.junk = os.urandom(14)
d = hashlib.sha256(aes_source_bytes).digest()
self.aes_key = d[0:16]
self.hmac_key = d[16:]
def rsa_encrypt(self, data):
"""Encrypt given data the way Cobalt's server likes
Args:
data (bytes): Data to encrypt
Returns:
bytes:
"""
bio = M2Crypto.BIO.MemoryBuffer(PUBLIC_KEY_TEMPLATE.format(base64.b64encode(self.public_key).decode()).encode())
pubkey = M2Crypto.RSA.load_pub_key_bio(bio)
# Format is: magic + dataLength + data
# beef is the magic used by the server
packed_data = b'\x00\x00\xBE\xEF' + struct.pack('>I', len(data)) + data
return pubkey.public_encrypt(packed_data, M2Crypto.RSA.pkcs1_padding)
def pack(self):
data = self.aes_source_bytes + struct.pack('>hhIIHBH', self.charset, self.charset, self.bid, self.pid, self.port, self.is64, self.ver) + self.junk
data += struct.pack('4s', self.ip)
data += b'\x00' * (51 - len(data))
data += '\t'.join([self.comp, self.user]).encode()
return self.rsa_encrypt(data)
TERMINATION_STEPS = ['header', 'parameter', 'print']
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"}
# Could probably just be b'\x00'*4 + data
def mask(arg, data):
key = os.urandom(4)
data = data.encode('latin-1')
return key.decode('latin-1') + ''.join(chr(c ^ key[i%4]) for i, c in enumerate(data))
def demask(arg, data):
key = data[:4].encode('latin-1')
data = data.encode('latin-1')
return ''.join(chr(c ^ key[i%4]) for i, c in enumerate(data[4:]))
def netbios_decode(name, case):
i = iter(name.upper())
try:
return ''.join([chr(((ord(c)-ord(case))<<4)+((ord(next(i))-ord(case))&0xF)) for c in i])
except:
return ''
func_dict_encode = {"append": lambda arg, data: data + arg,
"prepend": lambda arg, data: arg + data,
"base64": lambda arg, data: base64.b64encode(data),
"netbios": lambda arg, data: ''.join([chr((ord(c)>>4) + ord('a')) + chr((ord(c)&0xF) + ord('a')) for c in data]),
"netbiosu": lambda arg, data: ''.join([chr((ord(c)>>4) + ord('A')) + chr((ord(c)&0xF) + ord('A')) for c in data]),
"base64": lambda arg, data: base64.b64encode(data.encode('latin-1')).decode('latin-1'),
"base64url": lambda arg, data: base64.urlsafe_b64encode(data.encode('latin-1')).decode('latin-1').strip('='),
"mask": mask,
}
func_dict_decode = {"append": lambda arg, data: data[:-len(arg)],
"prepend": lambda arg, data: data[len(arg):],
"base64": lambda arg, data: base64.b64decode(data),
"netbios": lambda arg, data: netbios_decode(data, 'a'),
"netbiosu": lambda arg, data: netbios_decode(data, 'A'),
"base64": lambda arg, data: base64.b64decode(data.encode('latin-1')).decode('latin-1'),
"base64url": lambda arg, data: base64.urlsafe_b64decode(data.encode('latin-1')).decode('latin-1').strip('='),
"mask": demask,
}
class Transform(object):
def __init__(self, trans_dict):
"""An helper class to tranform data according to cobalt's malleable profile
Args:
trans_dict (dict): A dictionary that came from packedSetting data. It's in the form of:
{'ConstHeaders':[], 'ConstParams': [], 'Metadata': [], 'SessionId': [], 'Output': []}
"""
self.trans_dict = trans_dict
def encode(self, metadata, output, sessionId):
"""
Args:
metadata (str): The metadata of a Beacon, usually given from Metadata.pack()
output (str): If this is for a Beacon's response, then this is the response's data
sessionId (str): the Beacon's ID
Returns:
(str, dict, dict): This is to be used in an HTTP request. The tuple is (request_body, request_headers, request_params)
"""
params = {}
headers = {}
body = ''
for step in self.trans_dict['Metadata']:
action = step.split(' ')[0].lower()
arg = step.lstrip(action).strip().strip('"')
if action in TERMINATION_STEPS:
if action == "header":
headers[arg] = metadata
elif action == "parameter":
params[arg] = metadata
elif action == "print":
body = metadata
else:
metadata = func_dict_encode[action](arg, metadata)
for step in self.trans_dict['Output']:
action = step.split(' ')[0].lower()
arg = step.lstrip(action).strip().strip('"')
if action in TERMINATION_STEPS:
if action == "header":
headers[arg] = output
elif action == "parameter":
params[arg] = output
elif action == "print":
body = output
else:
output = func_dict_encode[action](arg, output)
for step in self.trans_dict['SessionId']:
action = step.split(' ')[0].lower()
arg = step.lstrip(action).strip().strip('"')
if action in TERMINATION_STEPS:
if action == "header":
headers[arg] = sessionId
elif action == "parameter":
params[arg] = sessionId
elif action == "print":
body = sessionId
else:
sessionId = func_dict_encode[action](arg, sessionId)
for step in self.trans_dict['ConstHeaders']:
offset = step.find(': ')
header, value = step[:offset], step[offset+2:]
headers[header] = value
for step in self.trans_dict['ConstParams']:
offset = step.find('=')
param, value = step[:offset], step[offset+1:]
params[param] = value
return body, headers, params
def decode(self, body, headers, params):
"""
Parses beacon's communication data from an HTTP request
Args:
body (str): The body of an HTTP request
headers (dict): Headers dict from the HTTP request
params (dict): Params dict from the HTTP request
Returns:
(str, str, str): The tuple is (metadata, output, sessionId)
"""
metadata = ''
output = ''
sessionId = ''
for step in self.trans_dict['Metadata'][::-1]:
action = step.split(' ')[0].lower()
arg = step.lstrip(action).strip().strip('"')
if action in TERMINATION_STEPS:
if action == "header":
metadata = headers[arg]
elif action == "parameter":
metadata = params[arg]
elif action == "print":
metadata = body
else:
metadata = func_dict_decode[action](arg, metadata)
for step in self.trans_dict['Output'][::-1]:
action = step.split(' ')[0].lower()
arg = step.lstrip(action).strip().strip('"')
if action in TERMINATION_STEPS:
if action == "header":
output = headers[arg]
elif action == "parameter":
output = params[arg]
elif action == "print":
output = body
else:
output = func_dict_decode[action](arg, output)
for step in self.trans_dict['SessionId'][::-1]:
action = step.split(' ')[0].lower()
arg = step.lstrip(action).strip().strip('"')
if action in TERMINATION_STEPS:
if action == "header":
sessionId = headers[arg]
elif action == "parameter":
sessionId = params[arg]
elif action == "print":
sessionId = body
else:
sessionId = func_dict_decode[action](arg, sessionId)
return metadata, output, sessionId
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/python3
'''
By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal
Refs:
https://github.com/RomanEmelyanov/CobaltStrikeForensic/blob/master/L8_get_beacon.py
https://github.com/nccgroup/pybeacon
'''
import requests, struct, sys, os, urllib3
import argparse
from parse_beacon_config import cobaltstrikeConfig
from urllib.parse import urljoin
from io import BytesIO
from Crypto.Cipher import AES
import hmac
import urllib
import socket
from comm import *
HASH_ALGO = hashlib.sha256
SIG_SIZE = HASH_ALGO().digest_size
CS_FIXED_IV = b"abcdefghijklmnop"
EMPTY_UA_HEADERS = {"User-Agent":""}
URL_PATHS = {'x86':'ab2g', 'x64':'ab2h'}
def get_beacon_data(url, arch):
full_url = urljoin(url, URL_PATHS[arch])
try:
resp = requests.get(full_url, timeout=30, headers=EMPTY_UA_HEADERS, verify=False)
except requests.exceptions.RequestException as e:
print('[-] Connection error: ', e)
return
if resp.status_code != 200:
print('[-] Failed with HTTP status code: ', resp.status_code)
return
buf = resp.content
# Check if it's a Trial beacon, therefore not xor encoded (not tested)
eicar_offset = buf.find(b'EICAR-STANDARD-ANTIVIRUS-TEST-FILE')
if eicar_offset != -1:
return cobaltstrikeConfig(BytesIO(buf)).parse_config()
offset = buf.find(b'\xff\xff\xff')
if offset == -1:
print('[-] Unexpected buffer received')
return
offset += 3
key = struct.unpack_from('<I', buf, offset)[0]
size = struct.unpack_from('<I', buf, offset+4)[0] ^ key
head_enc = struct.unpack_from('<I', buf, offset+8)[0] ^ key
head = head_enc & 0xffff
# Taken directly from L8_get_beacon.py
if head == 0x5a4d or head ==0x9090:
decoded_data = b''
for i in range(2+offset//4, len(buf)//4-4):
a = struct.unpack_from('<I', buf, i*4)[0]
b = struct.unpack_from('<I', buf, i*4+4)[0]
с = a ^ b
decoded_data += struct.pack('<I', с)
return cobaltstrikeConfig(BytesIO(decoded_data)).parse_config()
def register_beacon(conf):
"""Registers a random beacon and sends a task data.
This is a POC that shows how a beacon send its metadata and task results with Malleable profiles
Args:
conf (dict): Beacon configuration dict, from cobaltstrikeConfig parser
"""
# Register new random beacon
urljoin('http://'+conf['C2Server'].split(',')[0], conf['C2Server'].split(',')[1])
aes_source = os.urandom(16)
m = Metadata(conf['PublicKey'], aes_source)
t = Transform(conf['HttpGet_Metadata'])
body, headers, params = t.encode(m.pack().decode('latin-1'), '', str(m.bid))
print('[+] Registering new random beacon: comp=%s user=%s' % (m.comp, m.user))
try:
req = requests.request('GET', urljoin('http://'+conf['C2Server'].split(',')[0], conf['C2Server'].split(',')[1]), params=params, data=body, headers=dict(**headers, **{'User-Agent':''}), timeout=5)
except Exception as e:
print('[-] Got excpetion from server: %s' % e)
return
# This is how to properly encrypt a task:
# Tasks are encrypted with the session's aes key, decided and negotiated when we registered the beacon (it's part of the metadata)
## Here is where you'll build a proper task struct ##
random_data = os.urandom(50)
# session counter = 1
data = struct.pack('>II', 1, len(random_data)) + random_data
pad_size = AES.block_size - len(data) % AES.block_size
data = data + pad_size * b'\x00'
# encrypt the task data and wrap with hmac sig and encrypted data length
cipher = AES.new(m.aes_key, AES.MODE_CBC, CS_FIXED_IV)
enc_data = cipher.encrypt(data)
sig = hmac.new(m.hmac_key, enc_data, HASH_ALGO).digest()[0:16]
enc_data += sig
enc_data = struct.pack('>I', len(enc_data)) + enc_data
# task data is POSTed so we need to take the transformation steps of http-post.client
t = Transform(conf['HttpPost_Metadata'])
body, headers, params = t.encode(m.pack().decode('latin-1'), enc_data.decode('latin-1'), str(m.bid))
print('[+] Sending task data')
try:
req = requests.request('POST', urljoin('http://'+conf['C2Server'].split(',')[0], conf['HttpPostUri'].split(',')[0]), params=params, data=body, headers=dict(**headers, **{'User-Agent':''}), timeout=5)
except Exception as e:
print('[-] Got excpetion from server while sending task: %s' % e)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Parse CobaltStrike Beacon's configuration from C2 url and registers a beacon with it")
parser.add_argument("url", help="Cobalt C2 server (e.g. http://1.1.1.1)")
args = parser.parse_args()
x86_beacon_conf = get_beacon_data(args.url, 'x86')
x64_beacon_conf = get_beacon_data(args.url, 'x64')
if not x86_beacon_conf and not x64_beacon_conf:
print("[-] Failed finding any beacon configuration")
exit(1)
print("[+] Got beacon configuration successfully")
conf = x86_beacon_conf or x64_beacon_conf
register_beacon(conf)
+149 -49
View File
@@ -1,6 +1,7 @@
#!/usr/bin/python3
'''
Parses CobaltStrike Beacon's configuration from PE file or memory dump.
By Gal Kristal from SentinelOne (gkristal.w@gmail.com)
By Gal Kristal from SentinelOne (gkristal.w@gmail.com) @gal_kristal
Inspired by https://github.com/JPCERTCC/aa-tools/blob/master/cobaltstrikescan.py
@@ -9,35 +10,28 @@ TODO:
2. Dynamic size parsing
'''
from struct import unpack
from beacon_utils import *
from struct import unpack, unpack_from
from socket import inet_ntoa
from collections import OrderedDict
from netstruct import unpack as netunpack
import json
from base64 import b64encode
from sys import argv
import argparse
import io
import re
import pefile
import os
import hashlib
from io import BytesIO
THRESHOLD = 1100
COLUMN_WIDTH = 35
SUPPORTED_VERSIONS = (3, 4)
SILENT_CONFIGS = ['PublicKey', 'ProcInject_Stub', 'smbFrameHeader', 'tcpFrameHeader', 'SpawnTo']
class Base64Encoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, bytes):
return b64encode(o).decode()
return json.JSONEncoder.default(self, o)
def cli_print(msg, end='\n'):
def _cli_print(msg, end='\n'):
if __name__ == '__main__':
print(msg, end=end)
class confConsts:
MAX_SETTINGS = 64
TYPE_NONE = 0
@@ -56,12 +50,6 @@ class confConsts:
4: 0x2e
}
def read_dword_be(fh):
data = fh.read(4)
if not data or len(data) != 4:
return None
return unpack(">I",data)[0]
class packedSetting:
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):
@@ -78,6 +66,8 @@ class packedSetting:
self.hashBlob = hashBlob
self.enum = enum
self.mask = mask
self.transform_get = None
self.transform_post = None
if datatype == confConsts.TYPE_STR and length == 0:
raise(Exception("if datatype is TYPE_STR then length must not be 0"))
@@ -98,8 +88,62 @@ class packedSetting:
self_repr[4:6] = self.length.to_bytes(2, 'big')
return self_repr
def parse_transformdata(self, data):
'''
Args:
data (bytes): Raw communication transforam data
Returns:
dict: Dict of transform commands that should be convenient for communication forging
'''
dio = io.BytesIO(data)
trans = {'ConstHeaders':[], 'ConstParams': [], 'Metadata': [], 'SessionId': [], 'Output': []}
current_category = 'Constants'
# TODO: replace all magic numbers here with enum
while True:
tstep = read_dword_be(dio)
if tstep == 7:
name = read_dword_be(dio)
if self.pos == 12: # GET
current_category = 'Metadata'
else: # POST
current_category = 'SessionId' if name == 0 else 'Output'
elif tstep in (1, 2, 5, 6):
length = read_dword_be(dio)
step_data = dio.read(length).decode()
trans[current_category].append(BeaconSettings.TSTEPS[tstep] + ' "' + step_data + '"')
elif tstep in (10, 16, 9):
length = read_dword_be(dio)
step_data = dio.read(length).decode()
if tstep == 9:
trans['ConstParams'].append(step_data)
else:
trans['ConstHeaders'].append(step_data)
elif tstep in (3, 4, 13, 8, 11, 12, 15):
trans[current_category].append(BeaconSettings.TSTEPS[tstep])
else:
break
if self.pos == 12:
self.transform_get = trans
else:
self.transform_post = trans
return trans
def pretty_repr(self, full_config_data):
data_offset = full_config_data.find(self.binary_repr())
if data_offset < 0 and self.datatype == confConsts.TYPE_STR:
self.length = 16
while self.length < 2048:
data_offset = full_config_data.find(self.binary_repr())
if data_offset > 0:
break
self.length *= 2
if data_offset < 0:
return 'Not Found'
@@ -128,7 +172,7 @@ class packedSetting:
return inet_ntoa(conf_data)
else:
conf_data = unpack('>I', conf_data)[0]
conf_data = unpack('>i', conf_data)[0]
if self.is_date and conf_data != 0:
fulldate = str(conf_data)
return "%s-%s-%s" % (fulldate[0:4], fulldate[4:6], fulldate[6:])
@@ -203,9 +247,7 @@ class packedSetting:
return conf_data
if self.is_headers:
conf_data = conf_data.strip(b'\x00')
conf_data = [chunk[1:].decode() for chunk in conf_data.split(b'\x00') if len(chunk) > 1]
return conf_data
return self.parse_transformdata(conf_data)
conf_data = conf_data.strip(b'\x00').decode()
return conf_data
@@ -216,13 +258,13 @@ class BeaconSettings:
BEACON_TYPE = {0x0: "HTTP", 0x1: "Hybrid HTTP DNS", 0x2: "SMB", 0x4: "TCP", 0x8: "HTTPS", 0x10: "Bind TCP"}
ACCESS_TYPE = {0x1: "Use direct connection", 0x2: "Use IE settings", 0x4: "Use proxy server"}
EXECUTE_TYPE = {0x1: "CreateThread", 0x2: "SetThreadContext", 0x3: "CreateRemoteThread", 0x4: "RtlCreateUserThread", 0x5: "NtQueueApcThread", 0x6: None, 0x7: None, 0x8: "NtQueueApcThread-s"}
#TRANSFORMSTEP = {1: "append", 2: "prepend", 3: "base64", 4: "print", 5: "parameter", 6: "header", 7: "build", 8: "netbios", 9: "_parameter", 10: "_header",
# 11: "netbiosu", 12: "uri_append", 13: "base64_url", 14: "strrep", 15: "mask"}
ALLOCATION_FUNCTIONS = {0: "VirtualAllocEx", 1: "NtMapViewOfSection"}
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"}
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" ]
def __init__(self, version):
if version not in SUPPORTED_VERSIONS:
cli_print("Error: Only supports version 3 and 4, not %d" % version)
_cli_print("Error: Only supports version 3 and 4, not %d" % version)
return
self.version = version
self.settings = OrderedDict()
@@ -235,17 +277,23 @@ class BeaconSettings:
self.settings['MaxGetSize'] = packedSetting(4, confConsts.TYPE_INT)
self.settings['Jitter'] = packedSetting(5, confConsts.TYPE_SHORT)
self.settings['MaxDNS'] = packedSetting(6, confConsts.TYPE_SHORT)
# Silencing for now
#self.settings['PublicKey'] = packedSetting(7, confConsts.TYPE_STR, 256, isBlob=True)
# Silenced config
self.settings['PublicKey'] = packedSetting(7, confConsts.TYPE_STR, 256, isBlob=True)
self.settings['PublicKey_MD5'] = packedSetting(7, confConsts.TYPE_STR, 256, isBlob=True, hashBlob=True)
self.settings['C2Server'] = packedSetting(8, confConsts.TYPE_STR, 256)
self.settings['UserAgent'] = packedSetting(9, confConsts.TYPE_STR, 128)
# TODO: Concat with C2Server?
self.settings['HttpPostUri'] = packedSetting(10, confConsts.TYPE_STR, 64)
# This is how the server transforms its communication to the beacon
# ref: https://www.cobaltstrike.com/help-malleable-c2 | https://usualsuspect.re/article/cobalt-strikes-malleable-c2-under-the-hood
# TODO: Switch to isHeaders parser logic
self.settings['Malleable_C2_Instructions'] = packedSetting(11, confConsts.TYPE_STR, 256, isBlob=True,isMalleableStream=True)
# This is the way the beacon transforms its communication to the server
# TODO: Change name to HttpGet_Client and HttpPost_Client
self.settings['HttpGet_Metadata'] = packedSetting(12, confConsts.TYPE_STR, 256, isHeaders=True)
self.settings['HttpPost_Metadata'] = packedSetting(13, confConsts.TYPE_STR, 256, isHeaders=True)
self.settings['SpawnTo'] = packedSetting(14, confConsts.TYPE_STR, 16, isBlob=True)
self.settings['PipeName'] = packedSetting(15, confConsts.TYPE_STR, 128)
# Options 16-18 are deprecated in 3.4
@@ -264,6 +312,7 @@ class BeaconSettings:
self.settings['HttpPostChunk'] = packedSetting(28, confConsts.TYPE_INT)
self.settings['Spawnto_x86'] = packedSetting(29, confConsts.TYPE_STR, 64)
self.settings['Spawnto_x64'] = packedSetting(30, confConsts.TYPE_STR, 64)
# Whether the beacon encrypts his communication, should be always on (1) in beacon 4
self.settings['CryptoScheme'] = packedSetting(31, confConsts.TYPE_SHORT)
self.settings['Proxy_Config'] = packedSetting(32, confConsts.TYPE_STR, 128)
self.settings['Proxy_User'] = packedSetting(33, confConsts.TYPE_STR, 64)
@@ -288,16 +337,29 @@ class BeaconSettings:
# If True then allocation is using NtMapViewOfSection
self.settings['ProcInject_AllocationMethod'] = packedSetting(52, confConsts.TYPE_SHORT, enum=self.ALLOCATION_FUNCTIONS)
# Unknown data, silencing for now
#self.settings['ProcInject_Stub'] = packedSetting(53, confConsts.TYPE_STR, 16, isBlob=True)
# Unknown data, silenced for now
self.settings['ProcInject_Stub'] = packedSetting(53, confConsts.TYPE_STR, 16, isBlob=True)
self.settings['bUsesCookies'] = packedSetting(50, confConsts.TYPE_SHORT, isBool=True)
self.settings['HostHeader'] = packedSetting(54, confConsts.TYPE_STR, 128)
# Silencing as I've yet to test it on a sample with those options
#self.settings['smbFrameHeader'] = packedSetting(57, confConsts.TYPE_STR, 128, isBlob=True)
#self.settings['tcpFrameHeader'] = packedSetting(58, confConsts.TYPE_STR, 128, isBlob=True)
# Silenced as I've yet to test it on a sample with those options
self.settings['smbFrameHeader'] = packedSetting(57, confConsts.TYPE_STR, 128, isBlob=True)
self.settings['tcpFrameHeader'] = packedSetting(58, confConsts.TYPE_STR, 128, isBlob=True)
self.settings['headersToRemove'] = packedSetting(59, confConsts.TYPE_STR, 64)
# DNS Beacon
self.settings['DNS_Beaconing'] = packedSetting(60, confConsts.TYPE_STR, 33)
self.settings['DNS_get_TypeA'] = packedSetting(61, confConsts.TYPE_STR, 33)
self.settings['DNS_get_TypeAAAA'] = packedSetting(62, confConsts.TYPE_STR, 33)
self.settings['DNS_get_TypeTXT'] = packedSetting(63, confConsts.TYPE_STR, 33)
self.settings['DNS_put_metadata'] = packedSetting(64, confConsts.TYPE_STR, 33)
self.settings['DNS_put_output'] = packedSetting(65, confConsts.TYPE_STR, 33)
self.settings['DNS_resolver'] = packedSetting(66, confConsts.TYPE_STR, 15)
self.settings['DNS_strategy'] = packedSetting(67, confConsts.TYPE_SHORT, enum=self.ROTATE_STRATEGY)
self.settings['DNS_strategy_rotate_seconds'] = packedSetting(68, confConsts.TYPE_INT)
self.settings['DNS_strategy_fail_x'] = packedSetting(69, confConsts.TYPE_INT)
self.settings['DNS_strategy_fail_seconds'] = packedSetting(70, confConsts.TYPE_INT)
class cobaltstrikeConfig:
def __init__(self, f):
@@ -348,24 +410,45 @@ class cobaltstrikeConfig:
if as_json:
continue
if conf_name in SILENT_CONFIGS:
continue
if parsed_setting == 'Not Found' and quiet:
continue
if type(parsed_setting) != list:
if quiet and type(parsed_setting) == str and parsed_setting.strip() == '':
conf_type = type(parsed_setting)
if conf_type in (str, int, bytes):
if quiet and conf_type == str and parsed_setting.strip() == '':
continue
cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=parsed_setting))
_cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=parsed_setting))
elif parsed_setting == []:
if quiet:
continue
cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val='Empty'))
else:
cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=parsed_setting[0]))
_cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val='Empty'))
elif conf_type == dict: # the beautifulest code
conf_data = []
for k in parsed_setting.keys():
if parsed_setting[k]:
conf_data.append(k)
for v in parsed_setting[k]:
conf_data.append('\t' + v)
if not conf_data:
continue
_cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=conf_data[0]))
for val in conf_data[1:]:
_cli_print(' ' * COLUMN_WIDTH, end='')
_cli_print(val)
elif conf_type == list: # list
_cli_print("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH-3, val=parsed_setting[0]))
for val in parsed_setting[1:]:
cli_print(' ' * COLUMN_WIDTH, end='')
cli_print(val)
_cli_print(' ' * COLUMN_WIDTH, end='')
_cli_print(val)
if as_json:
cli_print(json.dumps(parsed_config, cls=Base64Encoder))
_cli_print(json.dumps(parsed_config, cls=Base64Encoder))
return parsed_config
@@ -401,7 +484,7 @@ class cobaltstrikeConfig:
pe = pefile.PE(data=self.data)
data_sections = [s for s in pe.sections if s.Name.find(b'.data') != -1]
if not data_sections:
cli_print("Failed to find .data section")
_cli_print("Failed to find .data section")
return False
data = data_sections[0].get_data()
@@ -433,12 +516,29 @@ class cobaltstrikeConfig:
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Parses CobaltStrike Beacon's configuration from PE or memory dump.")
parser.add_argument("path", help="Beacon file path")
parser = argparse.ArgumentParser(description="Parses CobaltStrike Beacon's configuration from PE, memory dump or URL.")
parser.add_argument("beacon", help="This can be a file path or a url (if started with http/s)")
parser.add_argument("--json", help="Print as json", action="store_true", default=False)
parser.add_argument("--quiet", help="Do not print missing or empty settings", action="store_true", default=False)
parser.add_argument("--version", help="Try as specific cobalt version (3 or 4). If not specified, tries both.", type=int)
args = parser.parse_args()
if not cobaltstrikeConfig(args.path).parse_config(version=args.version, quiet=args.quiet, as_json=args.json):
if not cobaltstrikeConfig(args.path).parse_encrypted_config(version=args.version, quiet=args.quiet, as_json=args.json):
print("Failed to find any beacon configuration")
if os.path.isfile(args.beacon):
if cobaltstrikeConfig(args.beacon).parse_config(version=args.version, quiet=args.quiet, as_json=args.json) or \
cobaltstrikeConfig(args.beacon).parse_encrypted_config(version=args.version, quiet=args.quiet, as_json=args.json):
exit(0)
elif args.beacon.lower().startswith('http'):
x86_beacon_data = get_beacon_data(args.beacon, 'x86')
x64_beacon_data = get_beacon_data(args.beacon, 'x64')
if not x86_beacon_data and not x64_beacon_data:
print("[-] Failed to find any beacon configuration")
exit(1)
conf_data = x86_beacon_data or x64_beacon_data
if cobaltstrikeConfig(BytesIO(conf_data)).parse_config(version=args.version, quiet=args.quiet, as_json=args.json) or \
cobaltstrikeConfig(BytesIO(conf_data)).parse_encrypted_config(version=args.version, quiet=args.quiet, as_json=args.json):
exit(0)
print("[-] Failed to find any beacon configuration")
exit(1)
+2
View File
@@ -1,2 +1,4 @@
netstruct==1.1.2
pefile==2019.4.18
#M2Crypto==0.37.1
#pycryptodome==3.10.1