For the complete documentation index, see llms.txt. This page is also available as Markdown.
ADGET-WRITEABLEGITGPO ABUSEGPPKERBEROSLAPSLDAPGMSAPASSWORD PROFILINGPASSWORD REUSEPASSWORD SPRAYINGSENSITIVE FILE DISCLOSUREWINDOWSTARGETED KERBEROASTINGTOMBSTONE REANIMATIONWINDOWS-PRIVESC

Sentinel

Challenge Lab (Hard) - by investigato

The following post by 0xb0b is licensed under CC BY 4.0


Scenario

Objective

You have been hired to perform a red team engagement against Trask Industries. Trask is a forward-thinking technology firm, with a heavy focus on research and development (R&D).

Trask Industries is not aware of the engagement; instead, you have been hired as a new employee in the R&D department. Your task is to begin as a "new employee" and demonstrate impact by elevating your privileges to Domain Admin (if possible).

Initial Access

Below is your onboarding document. Download the document, and use the information for initial access.

In this document, we find the credentials for the user k.pryde.

Hash Cracking

To save you time on any hash cracking, a custom wordlist is provided.

Summary

Summary

In Sentinel, we enumerate a single Domain Controller DC01.trask.hsm with rustscan piped into Nmap, finding DNS, Kerberos, LDAP, SMB with signing required, WinRM, a Caddy web server on port 80, and a Microsoft HTTPAPI service on the non-standard port 45985. SMB shows NTLM is unsupported, so Kerberos must be used via the -k flag, and we generate a hosts file and krb5.conf with NetExec. The guest account and welcome-letter credentials both fail against the domain, so we pivot to the web. VHOST fuzzing with FFuF discovers the onboarding portal, where the welcome-letter credentials log us in as k.ryan. The portal exposes a password policy and a password-protected ZIP; rockyou.txt fails, but we turn the policy into a hashcat mask attack to crack it. The archive yields employee names, and spraying the recovered passwords authenticates us as k.ryan.

BloodHound shows no direct path, but a readable share contains a .git folder. We pull it recursively with NetExec's spider_plus module and review the git history to recover a deleted username and password, authenticating as e.parsons. As a member of R&D_Auditors, e.parsons can decrypt LAPS passwords, which we confirm by reading the SYSVOL Registry.pol files and find a LAPS policy naming the lab-admin account. We query msLAPS-EncryptedPassword with bloodyAD, recognize the Windows LAPS v2.0 DPAPI-NG blob, and decrypt it with a Python dpapi-ng script. The target SENTINEL-PROTO never appeared in scans, but recalling the forwarded port 45985, we connect via evil-winrm as lab-admin and grab the user flag.

On SENTINEL-PROTO, a DCSecure application runs every 30 minutes via a scheduled task; querying it with schtasks exposes the svc_dcsecure_agent credentials. This account has CREATE_CHILD on an OU, while svc_dcsecure_core sits tombstoned under our write permission, so we reanimate it with bloodyAD into that OU and inherit the same password. As svc_dcsecure_core, a member of Remote Management Users, we get a WinRM shell on the DC. There, a SentinelSecurity.Client binary provisions a temporary machine account whose group holds WriteOwner over Sentinel Service Account Readers. We chain set owner, GenericAll, and add member, then dump the gMSA password of svc_mmold with NetExec.

As svc_mmold, we have write access over m.mold but no SPN to Kerberoast, so we register one ourselves; the account is AES-only, so we patch Impacket's kerberosv5.py to request AES instead of RC4, then crack the resulting ticket and authenticate as m.mold. With no further paths, we dump the directory with an authenticated GSSAPI ldapsearch and find a base58-encoded unixUserPassword belonging to b.trask, which we decode in CyberChef. The user b.trask is also a member of Remote Management Users, granting another WinRM session. There we re-run SentinelSecurity.Client.exe and find a deploy command that applies configuration to a Sentinel Action policy; we craft a Group Policy Preferences XML adding b.trask to the Administrators group, test it, then activate it. Now a local administrator, we reconnect via evil-winrm and retrieve the root flag at C:\Users\Administrator\Desktop\root.txt.

Recon

We use rustscan -b 500 -a 10.0.0.5 --top -- -sC -sV -Pn to enumerate all TCP ports on the target machine, piping the discovered results into Nmap which runs default NSE scripts -sC, service and version detection -sV, and treats the host as online without ICMP echo -Pn.

A batch size of 500 trades speed for stability, the default 1500 balances both, while much larger sizes increase throughput but risk missed responses and instability.

The target 10.0.0.5 is a Domain Controller, DC01.trask.hsm, in the trask.hsm domain. DNS is exposed on port 53 and Kerberos on port 88, alongside Kerberos password change on port 464. LDAP and LDAPS services are available on ports 389 and 636, with the Global Catalog on ports 3268 and 3269. SMB is exposed via ports 139 and 445 with message signing enabled and required. Remote management is available through WinRM on port 5985. The host also exposes RPC over HTTP on ports 593 and 57487, and several MSRPC endpoints on ports 135, 49664, 49667, and the 57494–57520 range. A Microsoft HTTPAPI service is additionally listening on the non-standard port 45985. An HTTP server on port 80 is running Caddy, serving a site.

SMB

We will generate a hosts file entry with the following command and see that NTLM authentication is not supported, meaning we may need to use Kerberos authentication via the -k flag if we find any credentials.

We add the following entry to our /etc/hosts file.

We generate the krb5.conf file using NetExec so that Kerberos knows which domain controller to contact for authentication in all future Kerberos-based requests.

We try to authenticate as guest again but now receive the error message KDC_ERR_CLIENT_REVOKED, which means the account's credentials have been revoked by the KDC. This indicates that the guest account exists on the domain but has been disabled.

We will now use the credentials provided in the attached welcome letter, but here, too, we receive an error message that reads KDC_ERR_PREAUTH_FAILED, which means Kerberos pre-authentication failed for the account. So the password is probably incorrect.

WEB

Since we can't use these credentials to authenticate against the domain, let's take a look at the web site. This seems to be just a static one.

Also a directory scan does not reveal anything.

We're trying to enumerate additional VHOSTs using the usual list we normally use. However, we're only finding the VHOST www, which takes us to the same static page after editing our /etc/hosts file.

We're testing more lists from the SecLists collection and with our initial filtering method we do not find another VHOST again...

We try to filter for redirects now and go through every list from the seclists collection and we will find another VHOST with the bitquark-subdomains-top100000.txt list. We identify onboarding as another VHOST.

Our /etc/hosts file looks now as follows:

We visit the site hosted on the VHOSTS onboarding and are redirected to a sign-in page.

We enter the credentials found in the attached welcome letter...

... and we are able to log in.

Access as k.ryan

The onboarding portal provides documentation for new employees that includes, among other things, a password policy with an example.

In addition, there is a download link to download the onboarding materials in a ZIP file.

We download it and notice that it is password-protected. Using Zip2John, we generate a hash so we can crack the password.

We are not able to crack the hash using rockyou.txt. Since rockyou.txt fails to crack the hash, we turn to the password policy from the onboarding portal, which gives a very specific 16-character construction: an uppercase starting letter, any mix of letters and numbers, the full year of hire, and a terminating special character from (!,@,#,$) giving us a masked example ProtoTr****1973! too, which has exactly 4 unknown characters. Because those 4 positions can each be an uppercase letter, lowercase letter, or digit, we can feed this pattern to hashcat as a mask attack, defining a custom charset ?u?l?d and placing it in the four missing slots as ?1. We are able to crack the hash...

... and are now able to unzip the onboarding documents.

From one of the documents in the zip file, named orientation-schedule.md, we can identify a few names. Since we now have at least two valid passwords (from the welcome letter and the zip file), we'll try them on potential users.

We derive a users list by the names we found...

... and spray the passwords using NetExec and are able to authenticate as k.ryan with the password used to protect the zip file.

BloodHound Enumeration

Using a valid user account, we are now attempting to enumerate the domain using Kerberos authentication via bloodhound-ce.py ingestor. We request a TGT and export the ccache file. However, the attempt using bloodhound-ce.py fails. We try another aproach with the alternative rusthound and are successful.

We mark the compromised user k.ryan as owned. We see that the user is member of the it group, the newemployeeonboarding group and the non_r&d_staff. The Bloodhound data itself does not reveal any path to lateral movement or privilege escalation for now.

We only identify the Administrator user as the only Domain Admin.

Access as e.parsons

We use NetExec to list the shares that k.ryan can read from and write to, and find the New_Employee_Onboarding share, to which we have read access.

If this hasn't been done yet, we request a TGT and export the ccache file

Next, we connect to the New_Employee_Onboarding using smbclient.py and find a .git folder.

We inspect the Git reflog by reading logs/HEAD from the .git directory, which reveals the repository's commit history. This shows a commit about redacting the trainer password from the Trainer_Guide. We could make further progress here. The idea is to view the history and changes made and thereby reveal the password, which we could then use for a password spray attack for example.

Unfortunately, we cannot use smbclient.py to recursively download the entire folder structure. Authentication using smbclient also fails. As an alternative, we can use the spider_plus module from NetExec to not only enumerate all shares but also download them recursively.

After downloading all the shares using the spider_plus module, we navigate to the .git folder and review the change history as follows.

We scroll through the output and find not only a password but also a corresponding username that was deleted from the guide.

We test the credentials and are able to authenticate.

Shell as lab-admin

Next, we revisit our BloodHound data and mark the user e.parsons as owned. We see that the user is member of the r&d and the r&d_auditors group. But here, too, we don't see a direct path in our Bloodhound data to move laterally or escalate our privileges. Nevertheless, these groups are interesting to start with.

After analyzing BloodHound data, we identified that the compromised user is a member of both the R&D and R&D_Auditors groups. No further attack paths were discovered through BloodHound, and a PowerView.py connection was not possible. As a last resort, we manually inspected the SYSVOL Group Policy objects. Before that we grep for R&D as a quick win and discover a LAPS policy ({4AF28D0B-7E1C-4AF6-8635-53F1AFFE15CB}) that designates TRASK\R&D_Auditors as the ADPasswordEncryptionPrincipal, meaning members of this group can decrypt the LAPS-managed local administrator password (lab-admin) on machines where this policy applies. Grepping SYSVOL for specific strings in Registry.pol files requires handling UTF-16LE encoding, as standard grep silently skips binary content.

The -iRlaP flags tell grep to search recursively through all files (R), treating binary files as text (a), in a case-insensitive manner (i), printing only matching filenames (l), using Perl-compatible regex (P) to match the null bytes present in UTF-16LE encoded files.

Since the user e.parsons is able to decrypt the LAPS-managed local administrator password for lab-admin we try to query such. We request a Kerberos TGT for e.parsons using getTGT.py. We then export the KRB5CCNAME environment variable to point at the newly created e.parsons.ccache file, so that our tooling uses this ticket for authentication.

Finally, we use bloodyAD with Kerberos authentication -k to query the DC over LDAP for all computer objects and retrieve their msLAPS-EncryptedPassword attribute, leveraging our R&D_Auditors membership to decrypt the LAPS-managed local administrator password. But we only receive an encrypted password.

We give it another try with the bloodyAD module msldap. This time we get the results in hex (still encrypted).

We recognize the blob as LAPS v2.0 because the password is encrypted, not plaintext. The original Microsoft LAPS stores passwords in cleartext, while v2.0 wraps them in a DPAPI‑NG blob.

The giveaway is the 4b44534b bytes, which spell out KDSK (Key Distribution Services Key), the signature of the Crypto‑NG encryption that only LAPS v2.0 uses.

The second line is probably a list of 5 separate DPAPI-NG blobs, likely from msLAPS-EncryptedPasswordHistory.

After retrieving the DPAPI-NG encrypted LAPS blob for SENTINEL-PROTO using bloodyAD, a short Python script leveraging the dpapi-ng library is used to strip the 16-byte Windows LAPS v2 header and decrypt the CMS envelope via ncrypt_unprotect_secret(), authenticating to DC01 with e.parsons' Kerberos ccache to recover the local administrator credentials. With the following attempt we are able to recover one password.

To check for additional credentials, the bloodyAD LAPS output (which included the msLAPS-EncryptedPasswordHistory) attribute containing five historical DPAPI-NG blobs is saved to a file and parsed with a second script that iterates over each entry, strips the LAPS v2 header, and decrypts them all using e.parsons' Kerberos ticket, revealing previous lab-admin password rotations for SENTINEL-PROTO.

We save the output into a file...

... and run the second script.

We have gathered now five passwords of a LOCAL admin account lab-admin for a specific machine called SENTINEL-PROTO which we do not have identified yet. We were not able to discover it through a scan of the network. The account also do not appear in our Bloodhound data and a spray of that password to prior identified users is not fruitful.

We try to get a remote session on the DC via evil-winrm, but without success. We recall the open port 45985. By trying to authenticate against that service, which seems also to be a remote management service, we are able to connect to the SENTINEL-PROTO machine. It seems like the port is forwarded thorugh the DC.

We are the local admin on that machine.

We find the users flag at C:\Users\Administrator\Desktop\user.txt.

Access as svc_dcsecure_agent

During the enumeration of the machine, we notice that an unusual piece of software called DCSecure is installed in the Program Files (x86) folder.

Running this piece of software requires a username and a password.

We provide the labuser credentials and the see a successful run.

After running the application we inspect the log files.

We see every 30 miuntes a check is performed, so a service or scheduled task might run this piece of software. Furthermore we see that the credentials of the svc_dcsecure_agent user were used, but the credentials are redataced and connection attempts failed to check dcSecureSvc and svc_dcsecure_core failed.

At the end of the log file we see our attempt.

Since the application runs every 30 minutes using the credentials of the svc_dcsecure_agent user, let's check the scheduled tasks and services and then try to review them to see if we can retrieve the credentials. We do find a DCSecure-Compliance-Check scheduled task.

We query the scheduled task an find the used credentials.

We test the credentials and are able to authenticate.

Shell as svc_dcsecure_core

We request a Kerberos TGT for svc_dcsecure_agent using getTGT.py. We then export the KRB5CCNAME environment variable to point at the newly created svc_dcsecure_agent.ccache file, so that our tooling uses this ticket for authentication.

We are performing a new enumeration with the new user of the Active Directory using rusthound, since these newly identified users were not included in our original scan.

We can see that this user is in the dc_securemaintenance group. But here, too, we don't see a direct path in our Bloodhound data to move laterally or escalate our privileges

In addition to the BloodHound data, we also examine which permissions such as GetWriteable each user holds that are not reflected in BloodHound.

We see that svc_dcsecure_core currently sits in the CN=Deleted Objects container as a tombstoned object, over which we hold write permission. We also see that svc_dcsecure_agent has CREATE_CHILD permission specifically on the OU=Legacy Service Compatible Access OU.

These two findings combine into a restore path. Reanimating a tombstoned object in Active Directory is essentially moving it out of the CN=Deleted Objects container back into a live OU, which requires CREATE_CHILD in the destination container. Since our CREATE_CHILD right applies only to OU=Legacy Service Compatible Access, that is the only OU we can reanimate the object into — attempting to restore it anywhere else would fail. So that is where we restore svc_dcsecure_core.

Using our CREATE_CHILD permission on the OU=Legacy Service Compatible Access OU, we reanimate the tombstoned svc_dcsecure_core object by restoring it into that container.

After restoring the account we try to authenticate with the same password of svc_dcsecure_agent and are successful.

We request a Kerberos TGT for svc_dcsecure_core using getTGT.py. We then export the KRB5CCNAME environment variable to point at the newly created svc_dcsecure_core.ccache file, so that our tooling uses this ticket for authentication.

We are performing a new enumeration with the new user of the Active Directory using rusthound, since these newly identified users were not included in our original scan.

We see that the user svc_dcsecure_core is member of the legacy access continuity group which is member of the remote management users which would allow us to gain a remote session on the DC using evil-winrm.

Next, we try to connect to the DC using evil-winrm and are successful.

We enumerate the installation folders again and find an uninstall log file in C:/Program Files (x86)/DCSecure/.

In addition to the files found in Program Files (x86), we find another unusual installation in the SentinelSecurity.Client directory within Program Files.

Access as sentinel-xxxxx$

It looks like we can create temporary accounts using the binary.

We run the create command and receive a newly created Sentinel user along with its corresponding hash.

We request a Kerberos TGT for sentinel-ekHZ47 using getTGT.py. We then export the KRB5CCNAME environment variable to point at the newly created sentinel-ekHZ47.ccache file, so that our tooling uses this ticket for authentication.

And we are able to authenticate as that user against the domain.

Access as svc_mmold$

We'll run another domain enumeration using rusthound after we've created the user. Let's see if these temporary users have been assigned any interesting permissions or groups.

We see that our temporary Sentinel user is a member of the sentinel service accounts group, which in turn has WriteOwner privileges over the sentinel service account readers group.

We can set the temporary user as the owner of the sentinel service account readers group, grant ourselves GenericAll on the group, and then add a user we control to the group, thereby gaining all the permissions of the sentinel service account readers group.

With the membership of the sentinel service account readers group we will be able to read the GMSA password of svc_mmold$ gaining control over that account.

WriteOwner -> change ourself to owner

We change the ownership of the target Active Directory object sentinel service account readers to our controlled account sentinel-ekHZ47$ so we gain full control over it.

Grant us GenericAll over the Group

We grant the account sentinel-ekHZ47$ GenericAll permissions over the target group sentinel service account readers, giving us full control over the object and its attributes.

AddMember: Add sentinel-ekHZ47$ to the group

We add the account sentinel-ekHZ47$ as a member of the sentinel service account readers group to inherit its privileges.

This abuse can be carried out when controlling an object that has a GenericAll, GenericWrite, Self, AllExtendedRights or Self-Membership, over the target group.

Now that we have added our temporary sentinel user to the sentinel service account readers group, we request a new TGT so that the updated group membership is reflected in the ticket's PAC and our newly inherited permissions take effect.

Dump gMSA

Finally we dump the GMSA password using NetExec and gain the NTLM hash vo svc_mmold$.

Now we request a Kerberos TGT for svc_mmold$ using getTGT.py. We then export the KRB5CCNAME environment variable to point at the newly created svc_mmold$.ccache file, so that our tooling uses this ticket for authentication.

We are able to authenticate agains the DC as svc_mmold$.

Access as m.mold

Unfortunately, we can't find any further path in Bloodhound. But here, too, we'll take also a look the write permission unsing bloodyAD. We see we have write permissions over Matt Mold which would porbably allow us to perform a targetKerberoast attack or a shadow credentials attack to take over the account.

We try to perform a targetedKerberoast attack using GetUserSPNs but we get a KDC_ERR_S_PRINCIPAL_UNKNOWN error.

KDC_ERR_S_PRINCIPAL_UNKNOWN means m.mold has no SPN set, so the KDC has no service principal to issue a ticket for. Kerberoasting only works against accounts with registered SPNs.

Kerberoasting works by requesting a TGS (Ticket Granting Service) ticket for a service, which the KDC encrypts using the target account's password hash. We then take that ticket offline and crack the hash.

The catch is that the KDC will only issue a TGS ticket for an account that has an SPN registered. If the account has no SPN, there is no service to request a ticket for, and Kerberoasting is not possible.

In our case, we discover we have write privileges over the target account, but it has no SPN set, so we cannot Kerberoast it yet. Since we can write to the account, we simply add an arbitrary SPN ourselves (e.g. fake/whatever).

We query for the detailed output on get writeable and see that we are alos allowed to edit the SPN.

We use bloodyAD's msldap addspn module to register the Service Principal Name sentinel/mmold on the Matt Mold user object.

The SPN is set and the KDC found it, but it's rejecting the requested encryption type. By default GetUserSPNs.py requests RC4 (etype 23), but the DC or the account's msDS-SupportedEncryptionTypes is configured to only allow AES.

The following tool might be worht a try, but could not be used on my machine. https://gitlab.com/Zer1t0/cerbero

To perform a the targetedKerberoast we patch the impacket tooling to support AES.

First we locate the encryption type used for the TGS requests inside Impacket's Kerberos implementation, since it hardcodes RC4 which the target rejects.

Make a copy of the file prior to be able to revert the changes.

Lines 183 and 194 handle the TGS-REQ, so we patch both to request aes256_cts_hmac_sha1_96 instead of rc4_hmac, forcing Impacket to negotiate AES.

We verify that the patch took effect by confirming the AES encryption type is now referenced in the file.

With the patched library in place, we run GetUserSPNs.py to Kerberoast svc_mmold$, and the request now succeeds against the AES-only target. The attack returns an AES256 $krb5tgs$18$ ticket for m.mold that we can attempt to crack offline.

We crack the hash using Hashcat mode 19700 and recover the plaintext password for m.mold using the provided wordlist.

With the credentials in hand, we request a TGT for m.mold and export the ccache file.

We test the credentials against SMB using the cached ticket and confirm we can authenticate as m.mold.

Shell as b.trask

As m.mold, we don't see any other paths for escalation in our Bloodhound data, nor do we find anything using get writable.

Since neither BloodHound nor bloodyAD get writable surfaced anything useful for m.mold, we take a sledgehammer to crack a nut and dump the entire directory with ldapsearch, requesting every attribute including operational ones via + over an authenticated GSSAPI bind and writing the unwrapped LDIF to a file for offline review.

Rather than scrolling through the full dump by hand, we grep the LDIF for any attribute containing "pass" and get lucky: A password-related field called unixUserPassword is exposed in the directory data.

It belongs to the user b.trask.

It looks like base58 and we can confirm it in Cyberchef and retrieve a password.

With the credentials in hand, we request a TGT for b.trask and export the ccache file.

We test the credentials against SMB using the cached ticket and confirm we can authenticate as b.trask.

We look b.trask up in BloodHound and see that the user is a member of the Legacy Access Continuity group — the same group svc_dcsecure_core belongs to — which in turn grants membership in the Remote Management Users group, opening a path to an interactive session on the DC again...

Shell as Administrator

Enumeration through Bloodhound, bloodyAD, or LDAP did not yield any direct results.

As b.trask, we'll now get a remote management session using evil-winrm.

We re-run the SentinelSecurity.Client.exe application and we see a new option. We are now able to use the deploy command to provide new configuration to available Sentinel Groups.

The deploy command expects a configuration file to be applied to a so called Sentinel Action policy. It seems like test can be performed without applying them directly. The online tag activates the deployed policy.

We leverage claude to write a gpp to add b.trask to Administrators group.

We upload the configuration...

... and test the configuraiton. It seems valid.

Now we re-run the previous command with the -o parameter to activate the policy.

The user b.trask is now member of the local Administrators.

We request a new TGT for b.trask and export the ccache file so that the updated group membership is reflected in the ticket's PAC.

We connect via evil-winrm using the cached ticket and, now a member of the Administrators group, find the final flag at C:\Users\Administrator\Desktop\root.txt.

Last updated