Writer is definitely one of the toughest boxes I have ever solved at the time of writing this. It features a website that is vulnerable to SQL injection, which leads to authentication bypass. Once you have access, there is a feature that allows you to add and edit stories, which is vulnerable to command injection in the filename, but will be tricky to spot without using the SQL injection flaw and reading the python code of the web application.
Once inside the box, there are two local users, kyle
, and john
. Access to Kyle’s account was obtained after cracking a hash obtained from a local MySQL database. Once you have access as Kyle, you can exploit a write permission to /etc/postfix/disclaimer
to move laterally to John’s account, and from there exploit another write permission to create a malicious configuration file that will be by apt
through a cron job to gain code execution as root
.
Info
Recon
NMAP
# Nmap 7.70 scan initiated Thu Oct 14 07:14:55 2021 as: nmap -sC -sV -oN nmap.txt -v 10.10.11.101
Nmap scan report for 10.10.11.101
Host is up (0.22s latency).
Not shown: 995 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
| http-methods:
|_ Supported Methods: OPTIONS HEAD GET
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Story Bank | Writer.HTB
139/tcp open netbios-ssn Samba smbd 4.6.2
445/tcp open netbios-ssn Samba smbd 4.6.2
3809/tcp filtered apocd
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Host script results:
|_clock-skew: mean: 14m21s, deviation: 0s, median: 14m21s
| nbstat: NetBIOS name: WRITER, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
| Names:
| WRITER<00> Flags: <unique><active>
| WRITER<03> Flags: <unique><active>
| WRITER<20> Flags: <unique><active>
| \x01\x02__MSBROWSE__\x02<01> Flags: <group><active>
| WORKGROUP<00> Flags: <group><active>
| WORKGROUP<1d> Flags: <unique><active>
|_ WORKGROUP<1e> Flags: <group><active>
| smb2-security-mode:
| 2.02:
|_ Message signing enabled but not required
| smb2-time:
| date: 2021-10-14 07:30:13
|_ start_date: N/A
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Oct 14 07:15:57 2021 -- 1 IP address (1 host up) scanned in 61.77 seconds
A complete port scan didn’t yield any additional port.
Web
Found an email address admin@writer.htb
in the about page;
The contact us page has a form that appears to be non-responsive at first, but looking at my proxy, a request was actually made that returned a 404 Not found;
Removing the PHP extension from the form submission path above resulted in a 200 response, but the response obtained is no different from the original page.
Bruteforcing the web root using ffuf
showed some interesting paths;
The path /dashboard
redirects to the homepage, which I am guessing is due to access control. The /administrative
page provide a login page;
Testing common usernames and passwords did not work. Bruteforcing the blog posts numeric ID did not reveal any hidden post.
The /static
page has directory listing, but nothing of interest was found inside;
SMB
Attaempting null authentication and intentionally wrong credentials kept giving me this error;
Quick googling showed this could happen due to permission errors or non-existing shares. Since I don’t know any share name I assumed it was the latter.
Using smbmap
(Impacket), I was able to list some shares with null authentication, but I have access to none of them;
Dumping the password policy of the SMB server using CrackMapExec, the server does not have any auto-locking policy for accounts, which means we can bruteforce creds with locking accounts;
Bruteforce attampts with CrackMapExec kept giving false postives;
I’m guessing the error is due to lack of SMBv1 support, which is what hydra
reported also;
Even metasploit’s SMB login bruteforce module errored out;
The Web Login Form
The login form at /administrative
page is submitted via POST request with the parameters uname
and password
. Changing the request method to GET does not appear to affect the way the web app process the form. Deleting the password
parameter raised a 500 INTERNAL SERVER ERROR, but deleting the uname
parameter didn’t display any error. This indicate a difference in the way the two parameters are handled by the web application. Attempts to bruteforce the login page using the discovered email address admin@writer.htb, the username admin, and a wordlist made by crawling the web pages using CeWL
didn’t work.
Fuzzing the login parameter username
with SQL Injection payloads worked, leading to a successful authentication bypass, which redirected me to the /dashboard
page;
Dashboard
In the settings page, there is an option that allows the title, description, and logo of the site to be changed. This could be an interesting target for stored XSS when targeting other users, but I don’t think I need it since I can access all accounts using the SQL injection flaw in the login page.
However, clicking the save button in the bottom of the page did not send any request. Going to the users page, it appears the only user account in the web app is that of the admin;
Going into the stories page from the navigation menu, a list of all the stories were displayed, along with an option that allow stories to be updated;
During this update, user can change the title, tagline, image, and content of the story;
The image upload functionality validates uploaded files by checking the extension of the file submitted in the POST request, and rejects it if it doesn’t end with a .jpg
extension. Saving a PHP file as test.jpg
allows the file to be uploaded, but couldn’t be executed due to the extension not being .php
;
By changing the file extension to .jpg.php
, the upload succeeded, which indicate the web app merely checks for the presence of .jpg
in the filename;
When the above script was requested, however, the server simply sends the file for download without executing it;
I assumed it’s some .htaccess-like config preventing the server from executing the PHP file, so I tried testing the filename
POST parameter for path traversal, which the web app is vulnerable to;
Multple attempts to have the code executed did not work. Since the login page is vulnerable to SQL injection, I dumped the login request to a text file and passed it to sqlmap
to work it’s magic.
SQLMap
After exporting the login request from Burp Suite, I passed it to sqlmap
with the --threads=10
to speed things up a bit since it’s a blind SQL injection. It identified 2 databases in the server; information_schema
, which is default for MySQL servers, and writer
;
Tables found in the writer
database;
After sqlmap
screemed at me all the way about invalid characters, the table users
was dumped successfully, which was found to contain hash of the user admin
;
The above hash looks corrupted since the inital length of the field as reported by sqlmap was 32 characters long, but after dumping, the hash in the CSV file was 37 characters long. This made sense since sqlmap warned me about reliability issues when using multiple threads for blind injections. So I used the --sql-shell
argument to run specific SQL command that retrieve only the target hash;
Although the hash looks like an MD5 hash, john
identified it as multiple other formats, all of which were tested. But the hash could not be cracked.
Foothold
With the admin
hash proving to be uncrackable, I went back to the file upload vulnerability on the admin dashboard. The path traversal vulnerability allows writing to top level directories that are writable to the web server user like /tmp
and /dev/shm
. Attempts to write to the default apache web root /var/www/html
failed. Multiple command injection payloads in the filename parameter for the uploaded image also failed, and I just can’t get the server to execute uploaded PHP codes. So I moved on.
Going back to the SQL injection using sqlmap
, I realized I may be able to read local files using the --file-read
argument. So I tested it by requesting /etc/hostname
, and it worked. Since it’s a blind injection, the retrieval was slow and will be slower when dealing with larger files;
SInce there is a path traversal vulnerability when editing stories in the admin dashboard that I couldn’t exploit to gain code execution as I couldn’t figure out the web root of the server, and I know the target is a linux host, I tried dumping the file /etc/apache2/sites-enabled/000-default.conf
, which is the default configuration file for Apache2, and will contain the web root. This was a very slow process with sqlmap, so I had to switch to manual SQL queries that use substring()
to download chunks of the file, and it worked;
And just like that, web root has been identified. The .wsgi
is for python web applications, which explains some the difficulties I had enumerating paths earlier. Uploading a .php and a .txt files to the above path didn’t show any error message, but the uploaded files could not be requested. By checking the size of the uploaded files using the length(load_file('/path/to/file'))
, I verified the upload was successful. This is most likely a deadend because all requests to the path are being handled by the writer.wsgi
script, and even if I managed to overwrite it, I will need to reboot the Apache server for the script to be loaded again. So I went back to all my previous findings in hope of finding something I missed earlier.
The add new story feature in the admin page of the web app is the one thing I haven’t tested before;
Going through the request it generated in Burp Suite, I noticed an empty parameter named image_url
. Adding any sort of value to the parameter result in a blank response from the server after it hangs for a few seconds;
I needed to view the source code to understand what’s going on. The contents of the apache2 config file for enabled sites/vhosts showed /static
path in the web app was mapped to /var/www/writer.htb/writer/static
;
Since /static
is accessible in the web root of the website, that means /var/www/writer.htb/writer
will likely be the root of the website. Reading up a bit on how python web apps handle paths, I learned that the file __init__.py
is usually used to handle the path mappings of a directory. So I used SQLMap to dump the file, which will be at /var/www/writer.htb/writer/__init__.py
in this case, and I got the python source code. Going through it, I found a possible command injection flaw on how the web app handles URL uploads when adding a story in the admin dashboard;
The urlib.request.urlretrieve
method returns two values, the path to which the given URL has been downloaded, which will be random name inside /tmp
, and the headers returned. Notice that the the local_filename
is injected directly into a system command used to move the image. Testing the urlretreive()
function locally showed that it supports the file://
protocol, and that the returned local_filename
when using the file://
protocol is not random, but the absolute path of the file on disk.
With the above info, I was able to develop an RCE exploit for the web app after running multiple test locally on my python3 installation. By creating a story on the dashboard and setting the image name to;
And the URL upload to;
I got a reverse shell after sending the request;
User
Two local users were identified: john and kyle. Kyle got the user flag.
Open ports;
Going through the source file of the web application, I found a MySQL credential for the web app;
The password did not give access to any of the local accounts on the host, nor the SMB service running on the host.
Running linPEAS
on the host, it found credential for the database named dev
;
I was able to connect to the dev
database using the above credential, and found a hash for the user kyle;
john
is unable to load the hash when attempting to crack;
Using haschat
, the password was cracked successfully, which is marcoantonio
. Using the credential, I gained access to the account of kyle over ssh;
PrivEsc
User kyle does not have sudo access on the host. However, he is a member of smbgroup
, and filter
. Attempting to access the SMB service using his credentials failed. Searching the filesystem for files with group smbgroup
and filter
, I found an interesting match for the filter group;
The file /etc/postfix/disclaimer
looks interesting, and I have write permission to it. I had to do a bit of reading online to know what it’s used for. I found out that for any email in /etc/postfix/disclaimer_addresses
that send or receive mail, the file /etc/postfix/disclaimer
will be executed. There are two (2) emails in the disclaimer_addresses
file;
This means all I need to do is inject a reverse shell in the disclaimer
file, and send a message to any of the listed email addresses to gain code execution. Using nano
and netcat
, I was able to achieve this after a few tries as the box is restoring the contents of the disclaimer
file frequently;
The user john has SSH public key authentication configured, so I shipped his private key to my attack host for a more stable access over SSH.
John
I can’t list sudo permissions for this user as I still do not have his password. The user belongs to the group management. Searching for files belonging to the group, I got only one hit;
Checking the outputs of pspy
, I noticed a possible cron job that’s running apt-get update
;
After reading up on the purpose of /etc/apt/apt.conf.d
, I learned that it is a directory used to store configuration files of APT. The files can be configured with commands be executed by apt
on certain conditions. None of the existing files in the directory is writeable. But since all contents of the directory are treated as configuration files, and I have write access to directory, I created a simple configuration file (with a little help from https://blog.ikuamike.io/posts/2021/package_managers_privesc/) that execute a shell command when the apt update
command was invoked;
I then setup a listener, and wait for the update command to be executed by the cron task. It worked, and I got a reverse shell as root
;
Summary
- Identfied running services using
nmap
- Found an admin panel by bruteforcing with
ffuf
- Exploited an SQL Injection flaw to bypass authentication.
- Exploited a Command Injection flaw in the Add Story feature to gain a foothold into the box as
www-data
- Inside the box as www-data;
- Found and cracked a hash for kyle in the backend MySQL database used by Django.
- The hash gave me access to the box as kyle over SSH.
- Inside the box as kyle;
- The user belongs to the
filter
group, and have write permission on the file/etc/postfix/disclaimer
, which get executed when sending/receiving emails to/from certain addresses. - Injected a reverse shell into the file, and gained access to the box as the user john
- The user belongs to the
- Inside the box as john;
pspy
identified a cron job that performs package update usingapt-get update
- john belong to the group management, which gave him write access to
/etc/apt/apt.conf.d
directory. - Created a malicious configuration file in
/etc/apt/apt.conf.d
that execute a reverse shell command whenapt-get update
was invoked, and gained access to the box as root.