stihl не предоставил(а) никакой дополнительной информации.
Before falling into the rabbit hole that is SCCM, my gateway into abusing endpoint management software came when I was just getting started in offensive security. I was going through the usual network traffic poisoning when a service account began hammering my testing device and filling ntlmrelayx with admin SOCKS sessions. When I took a look at the sessions I found I’d managed to capture the credentials for a PDQ inventory service account which ended up allowing me to elevate privileges in the environment. If you ask around your shop you’ll probably run into someone else that’s had a similar interaction with PDQ software and abusing it seems to be a Для просмотра ссылки Войди или Зарегистрируйся.
Just so it’s known, PDQ is Для просмотра ссылки Войдиили Зарегистрируйся of the security risks around all the credential distribution but considers them a Microsoft problem. From the article:
But that’s not the point of this post. Really this is the result of boredom on a long flight and since I’ve been trying to get more comfortable with reversing cryptography I figured PDQ might be a good target since endpoint software is always littered with credentials. So, in this post we’ll briefly touch on how the software operates and then do a bit of .NET “reversing” to decrypt some credentials.
или Зарегистрируйся provide similar services to what you’d find in SCCM just at a more affordable price for small and medium sized businesses. Deploy handles tasks like software deployment, operating system patching, and configuration management while inventory is geared more toward asset management. The two complement each other as inventory will keep track of things like missing software or patches on an endpoint that deploy can then get pushed to the client.
Operationally, both services run a background service in the context of a user service account. This account must be a local administrator on the software’s host machine and must be a console user. The key thing is the account does not require admin privileges on any target “managed” systems.
During any remote tasks like a package install or inventory scan, since PDQ doesn’t use an agent, each service uses a privileged service account (“deploy user” and “scan user” respectively) to authenticate via SMB and execute tasks. These accounts are required to have local admin on the target hosts but do not require local admin on the service’s host OS. If this sounds confusing here’s how PDQ displays it:
PDQ does Для просмотра ссылки Войдиили Зарегистрируйся using LAPS as the more secure deployment configuration which we’ll touch on later and also does warn against granting the these accounts domain admin privileges. Seen this myself, not the best idea.
или Зарегистрируйся, each of the credentials stored by the application are protected by “three separate AES-encrypted keys” stored in the application’s database, registry, and the app binaries. This is reinforced from the product’s security guide shown below:
To start collecting the keys, I started with the registry first and found the “Secure Key” value at Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Admin Arsenal\PDQ Deploy\Secure Key. I expected binary data but instead found a GUID which I thought may just be an identifier for the actual key.
Then, from the sqlite database stored at C:\ProgramData\Admin Arsenal\PDQ Deploy\Database.db, a similarly named column in the DatabaseInfo table contained another GUID identifier.
For the app key, nearly the entire application is written in C# so “reversing” the binaries should be easy. Searching for the string SecureKey in dnSpy lands on the DatabaseEncryptionMethod enum that defines two methods for encryption. The enum is used by a few methods in the SqliteDatabaseManager class related to encryption but don’t actually contain the app key. What it does show though is how the app recovers the registry and database keys (2, 3) and then concatenates all three keys into one “EncryptionKey” (1).
Finally, searching for the string “EncryptionKey” across all the service dependencies hits in the AdminArsenal.PDQDeploy.dll to reveal the third and final “key”.
The decryption setting is determined by the GetEncryptionVersion method based on the header bytes parsed from encrypted ciphertext.
Looking at the encrypted blob in the database, the (encrypted) header confirms it’s a “type 2” decryption setting.
Next, the TryDecryptSetting method calls Decrypt with the ciphertext and assembled key. There’s a bit of redundancy here as the method again checks the encryption version but if it’s a match on our type 2 version, the method strips the header bytes (1) off the ciphertext then calls GetStandardEncryptionStream to decrypt the remainder (2) with the decryption “key”. The first 4 bytes of the returned value identify the length of the decrypted plaintext string (3) which is extracted from the remaining bytes and returned (4).
GetStandardEncryptionStream is used by both the Encrypt and Decrypt methods. If it is called for decryption, it reads the first byte of the ciphertext to determine the IV length then extracts that length of bytes as the IV. Finally, we learn where the actual AES key comes from. Since we’re on version type 2, the decryption key is derived from the first 16 bytes of the sha256 hash of the combined GUID string. Neat.
Here’s an example of a hex blob pulled from my testing environment to show how it’s structured based on what we just learned.
Since we found out earlier how to parse the plaintext out of the decrypted data all the pieces are together to decrypt the password. You can grab the python POC Для просмотра ссылки Войдиили Зарегистрируйся or checkout the Для просмотра ссылки Войди или Зарегистрируйся Для просмотра ссылки Войди или Зарегистрируйся whipped up.
Для просмотра ссылки Войдиили Зарегистрируйся
Just so it’s known, PDQ is Для просмотра ссылки Войди
But that’s not the point of this post. Really this is the result of boredom on a long flight and since I’ve been trying to get more comfortable with reversing cryptography I figured PDQ might be a good target since endpoint software is always littered with credentials. So, in this post we’ll briefly touch on how the software operates and then do a bit of .NET “reversing” to decrypt some credentials.
What are PDQ Deploy/Inventory?
Для просмотра ссылки ВойдиOperationally, both services run a background service in the context of a user service account. This account must be a local administrator on the software’s host machine and must be a console user. The key thing is the account does not require admin privileges on any target “managed” systems.
During any remote tasks like a package install or inventory scan, since PDQ doesn’t use an agent, each service uses a privileged service account (“deploy user” and “scan user” respectively) to authenticate via SMB and execute tasks. These accounts are required to have local admin on the target hosts but do not require local admin on the service’s host OS. If this sounds confusing here’s how PDQ displays it:

PDQ does Для просмотра ссылки Войди
Secure Credential Storage
According to Для просмотра ссылки Войди
To start collecting the keys, I started with the registry first and found the “Secure Key” value at Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Admin Arsenal\PDQ Deploy\Secure Key. I expected binary data but instead found a GUID which I thought may just be an identifier for the actual key.

Then, from the sqlite database stored at C:\ProgramData\Admin Arsenal\PDQ Deploy\Database.db, a similarly named column in the DatabaseInfo table contained another GUID identifier.

For the app key, nearly the entire application is written in C# so “reversing” the binaries should be easy. Searching for the string SecureKey in dnSpy lands on the DatabaseEncryptionMethod enum that defines two methods for encryption. The enum is used by a few methods in the SqliteDatabaseManager class related to encryption but don’t actually contain the app key. What it does show though is how the app recovers the registry and database keys (2, 3) and then concatenates all three keys into one “EncryptionKey” (1).

Finally, searching for the string “EncryptionKey” across all the service dependencies hits in the AdminArsenal.PDQDeploy.dll to reveal the third and final “key”.

Decryption Flow
So now that we know it’s not 3 separate AES keys, let’s see how the credentials are decrypted with this “key”. The TryDecryptSetting method in the SqliteDatabaseManager class gets things started. First, the method determines the encryption version being used for the encrypted password (1) then calls a decryption routine based on that result for type 0 or type 2 (2).
The decryption setting is determined by the GetEncryptionVersion method based on the header bytes parsed from encrypted ciphertext.

Looking at the encrypted blob in the database, the (encrypted) header confirms it’s a “type 2” decryption setting.

Next, the TryDecryptSetting method calls Decrypt with the ciphertext and assembled key. There’s a bit of redundancy here as the method again checks the encryption version but if it’s a match on our type 2 version, the method strips the header bytes (1) off the ciphertext then calls GetStandardEncryptionStream to decrypt the remainder (2) with the decryption “key”. The first 4 bytes of the returned value identify the length of the decrypted plaintext string (3) which is extracted from the remaining bytes and returned (4).

GetStandardEncryptionStream is used by both the Encrypt and Decrypt methods. If it is called for decryption, it reads the first byte of the ciphertext to determine the IV length then extracts that length of bytes as the IV. Finally, we learn where the actual AES key comes from. Since we’re on version type 2, the decryption key is derived from the first 16 bytes of the sha256 hash of the combined GUID string. Neat.

Here’s an example of a hex blob pulled from my testing environment to show how it’s structured based on what we just learned.
Код:
Example blob: 28656e63727970746564290010644d18eb7817dad6de5f531b1b0b60113087662f3cf0ffdaa7760418c15ee6ea
Header: 28656e637279707465642900 (encrypted) + \x0
IVlen: \x10 (16)
IV: 644d18eb7817dad6de5f531b1b0b6011
Encdata: 3087662f3cf0ffdaa7760418c15ee6ea
Since we found out earlier how to parse the plaintext out of the decrypted data all the pieces are together to decrypt the password. You can grab the python POC Для просмотра ссылки Войди

Attacking PDQ
I won’t go into too much detail but I’ll share a few notes I kept track of while I was digging into all of this.- Hunting down the PDQ server itself can be a pain. I have seen guidance for setting up SPNs for the service Для просмотра ссылки Войди
или Зарегистрируйся which could help find it in AD. Otherwise, I’ve seen most of the service accounts and hostnames use “PDQ” in the AD object’s name. - If you compromise the host OS you can pull the background service account out of LSA secrets which will give you console access and permissions to decrypt creds from the database.
- The service does something weird with UAC when you’re logged onto the host interactively as an admin. I’m assuming it’s suppressing it for any user that doesn’t have permissions for the service which is cool. Might look into a bypass here if you’re curious. That’s not a blocker though. The service performs database Для просмотра ссылки Войди
или Зарегистрируйся regularly and maintains 10 backups. If you’re admin and don’t want to dump LSA you can read all the keys remotely and then pull one of the backups down. Pulling the active database will get you a sharing violation. I’m sure there’s ways around it but might try the easy road first. - Для просмотра ссылки Войди
или Зарегистрируйся via PDQ requires disabling Для просмотра ссылки Войдиили Зарегистрируйся on endpoints. The LAPS reader is in the database so do with that what you will. - I’m not certain, but across a few installations the app SecureKey has been static. Curious how far back it’s been static and if it changes on versions iterations.
- There are Для просмотра ссылки Войди
или Зарегистрируйся exclusion recommendations for PDQ related file paths.
Для просмотра ссылки Войди