Hack The Box - Laboratory
In this write-up, I’m going over Laboratory from Hack the Box, a fun and enjoyable machine that got labelled as an easy box. The whole challenge mainly revolves around two exploits in a GitLab instance that will give you a user shell on the box. Especially the second vulnerability is pretty exciting and shows the importance of keeping your application secrets safe. Many apps will serialize data and send it over to the user in the form of a cookie. They mainly do this to keep their app stateless. That way, they don’t have to worry about keeping track of the whole session state themselves. But that puts a lot of trust in the fact that the user can’t modify this cookie, which is often considered safe because it got signed with a secret unknown to the user. However, if this secret is somehow leaked, like for example, in this case through an LFI, then as a malicious actor, you can send a custom payload and execute a deserialization attack. The first exploit will trigger the LFI and use the GitLab app secret to sign a payload to get a shell on the box. Then via a custom setuid binary with path file checking, we’ll get access to the root account. With all that said, let’s dive right in and start our enumeration.
Enumeration
Starting of our investigation with an nmap scan. This will allow us to get a better overview of what is exposed on our target machine. We’ll use -sV
to enumerate versions, -sC
to run all default scripts, and store the output in a file named nmap.txt
. This way, we can always refer back to it later.
So we have ssh open on port 22
, apache on port 80
that redirects to https
so port 443
. The certificate returned on port 443
does give us a couple of domains (laboratory.htb
and git.laboratory.htb
), so let’s add them to our hosts file:
Digging into laboratory.htb
, we get presented with a fairly basic HTML page. The whole website seems to mostly contain static content. Other than a couple of possible usernames, there isn’t much here.
Let’s turn our focus to the other domain, git.laboratory.htb
, as it turns out this seems to be a self-hosted instance of the GitLab Community Edition. GitLab is a fully open-source alternative to GitHub. It gives you access to a git repository manager, wiki, issue tracker, CI pipelines, and much more. It basically gives an organization or individual access to all the tools required to enforce a healthy DevOps life cycle with a single application. The current tech stack is a mix of Go, Ruby on Rails and Vue.js. The instance is just open and allows us to register a new user for ourselves. The only validation it has is that it checks that the email you use is from laboratory.htb
. So let’s create ourselves a user, something like:
- Username: yolo123
- Email: [email protected]
- Password: password
And we’ve got access to this GitLab instance. For this newly created user, this GitLab instance seems pretty empty. Let’s dig up the current version and see if there are any know vulnerabilities that can give us a foothold on the machine or allow us to escalate our privileges. GitLab has a pretty rich API that it exposes, which is also neatly documented here. We can leverage the API to see what version it is currently running:
When looking around online for known exploits for this particular GitLab version I bumped into CVE-2020-10977. It’s an arbitrary file read exploit that allows reading any file present on the server. Especially this HackerOne is really interesting. It’s where the issue initially got reported and contains very detailed step by step instructions on how to exploit and get RCE.
Foothold
Triggering the LFI is actually fairly trivial. The HackerOne article explains everything you need to know in pretty great detail, but let me walk you through the steps. To get started, we need to create 2 projects on GitLab with the user we just created. Let’s call them project-a
and project-b
, now in project-a
create a new issue with the following description:
That’s a pretty funky looking URL, right? GitLab allows you to upload files and add them to an issue, which is pretty convenient if you manage a project. If someone reports a problem, they can add some visual clarification by attaching an image. These files are added to an upload directory scoped to the current project and given a unique id to prevent collisions. It then automatically adds a markdown link to the description. So far, so good.
Mistakes happen, so GitLab has a feature that allows transferring issue from one repo to another. At this point, GitLab needs to rewrite the upload and move it to the new project. And that’s where the exploit happens. It does not validate the file, allowing arbitrary files to be copied via directory traversal to project-b
in our case. This means we now got /etc/passwd
as an attachment to our moved issue in project-b
:
Clicking on that link allows you to download the attachment. We are now able to download any file from the our target machine:
This file on its own isn’t all too interesting. But doing a bit of research, you will learn that every GitLab instance will contain this file /opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
, which has all the secrets required to start signing custom cookies. This is the crucial piece we need to transform our LFI exploit into an RCE.
The HackerOne article contains a snippet of code that allows us to generate the serialized payload we can inject into the cookie. To create this payload, we’ll need access to the GitLab Rails Console. The best way to get access to this console is via the GitLab docker image. This even allows us to run the exact same version as our target machine is running. To get a shell in the container, you can run:
This might take a while, so go grab a coffee, this local GitLab instance should be running in just a few. When the instance is up and running, use docker ps
to find the id of the container. You can then use docker exec -it <container-id> bash
to get a shell inside this container. Now let’s replace the secret_key_base
in /opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
in our docker image with the secret we just leaked from our target machine. This will allow us to create a valid signed cookie with our reverse shell embedded within. Before we can do this, restart GitLab with gitlab-ctl restart
. When that is finish we can launch the rails console with gitlab-rails console
and paste in the following payload:
This will print out a base64 encoded string that is the experimentation_subject_id cookie. Which we can now send over via curl
to get ourselves a reverse shell. Before make sure you have netcat listening on 6666
with nc -lnvp 6666
.
And we’ve got our reverse shell:
That’s a lot of manual steps that you need to go through. Given that the HackerOne report is very detailed and GitLab exposed a pretty rich API, I decided to write up this exploit in Python. You can find the code in this GitHub repository. The repository contains all the instructions to get up and running. But in short, you’ll need to have pipenv
installed on your path. This can be done via pip install pipenv
. Then running pipenv sync
will install all required dependencies in a virtual environment managed by pipenv. Allowing you to kick of the LFI with:
This will print out the value of secret_key_base
. The script also allows you to chain the LFI into an RCE, giving you the ability to execute any command you want on the target GitLab instance:
Lateral Movement
We got access to our target machine but at the moment we are not a privileged user and are currently logged in as the git
user. But just like in our container just earlier, we have access to the gitlab-rails console
. There is this GitLab support article that explains how you can use the console to reset a users password. Let’s use this same technique to give us admin permissions:
Going back to our browser, we should now have access to the admin section. If you don’t see the wrench icon give it a few seconds to propagate, GitLab caches certain parts quite heavily. On the admin dashboard, we see 2 projects we now have access to. These projects are from the Dexter
user, and if you snooped around the gitlab-rails console a bit, you might have noticed that this user is also an admin.
The SecureDocker
project contains a .ssh
folder with an ssh key we can use to try and log in.
Logging in with ssh as the Dexter
gives us access to the user flag:
Let’s run linpeas on the machine to get an idea of where we need to focus our efforts next. Hack the box machine don’t have internet access, so the best way to get this up and running is to download linpeas.sh
on your host machine and expose it by hosting a webserver with python -m http.server
. Then we can use curl to download the script and pipe it directly to bash to let it do its thing. The results show an interesting SUID binary that isn’t usually on a Linux system, and the dexter
user owns that. I truncated the output, but a SUID binary can be detected by the s
within the file permissions of the output of an ls
command. A SUID binary gets executed as the root user without asking for a password, meaning it is an exciting target for us to try and exploit.
When running this binary, you’ll notice that nothing really happened. There is also no output written to stdout, so it’s pretty hard to know what it actually did. Let’s see what this binary is doing under the hood by running it with `ltrace. ltrace is a program that simply runs the specified command until it exits. It intercepts and records the dynamic library calls which are called by the executed process and the signals which are received by that process. It can also intercept and print the system calls executed by the program, which is exactly what we need in this case. Looking at the output it seems to be using a system call to execute a command from the path:
If you use sudo
to run a command with elevated permissions it has a concept of secure paths. Meaning it changes the PATH
variable to make it harder to tamper with. The configuration for this is defined in the /etc/sudoers
file and often includes the same known paths but prefixed with an s like /sbin
or /usr/sbin
.
But SUID binaries don’t have anything like that in place, meaning we can highjack the path add our own chmod
to a folder we add to the very start of the PATH variable. The following image shows how I created an executable file called chmod
in /dev/shm
that executes a single command id
. This will show us the user we have access to when called from the docker-security
binary. I then add /dev/shm
as the first entry to the PATH
variable and execute docker-security
again. This shows that the command was run as the `root user.
All we have to do now is edit our chmod
file again with vim
or nano
and swap out id
for bash
, which will give us a bash shell running as the root user. Allowing us to read the final flag.
Conclusion
And that’s the whole box.