Upgrade to Pro — share decks privately, control downloads, hide ads and more …

コンテナ上シェル悪用の話とPure Bashでcurlが作れた話

コンテナ上シェル悪用の話とPure Bashでcurlが作れた話

Ryoto Saito

March 16, 2025
Tweet

More Decks by Ryoto Saito

Other Decks in Technology

Transcript

  1. $ whoami
 • Ryoto (@systemctl_ryoto) ◦ MSS(マネージドセキュリティサービス)の仕事をしています • CTF歴 ◦

    2017年〜(ほぼWeb⼀筋) ◦ 所属:Katagaitai(2020〜) • シェル芸 ◦ 2019年〜 シェル芸勉強会(旧USP友の会)に参加 ◦ 好きな領域:Bash、POSIX、sed ◦ 記号難読化シェル芸を知っている⼈がいるかも 2 #katagaitai
  2. 最も単純な悪⽤⽅法 forkbomb :(){ :|:& };: 雑翻訳すると↓ async function colon() {

    colon(); colon(); } colon(); このオーケストレーション時代に、わざわざリソース飽和させるメリットは薄い (リソース制限を付けられることが多いのと、コンテナは破棄されて再起動されるだけなので) →もう少し⽬的を持った悪⽤ができないか 6 第1部 コンテナでシェルを取れた場合にできること
  3. 現在の主要なコンテナベースイメージ Debian:Ubuntuの元になっているので、apt, dpkgがそのまま使える Debian-slim:Debianイメージから不要なものを取っ払ったもの  →doc, locale, manなど。詳細なリストは https://github.com/debuerreotype/debuerreotype/blob/master/scripts/.slimify-excludes Alpine Linux:軽量が売りということで、イメージサイズを⼩さくしたいときに⽤いられがち

     →ライブラリがmuslであることに注意が必要 8 パッケージ管理 ライブラリ ローダ シェル Debian apt, dpkg glibc ld-linux-{arch} bash, dash Alpine apk musl ld-musl-{arch} busybox(ash互換) 第1部 コンテナでシェルを取れた場合にできること | コンテナベースイメージ
  4. Alpine Linuxイメージ内の実⾏ファイル⼀覧 / # find /usr/local/bin /usr/sbin /usr/bin /sbin /bin

    -type f -executable /usr/bin/scanelf /usr/bin/getconf /usr/bin/ldd /usr/bin/iconv /usr/bin/ssl_client /usr/bin/getent /sbin/apk # パッケージ管理ツール /sbin/ldconfig /bin/busybox # ほぼbusyboxが担っている / # ls -l $0 lrwxrwxrwx 1 root root 12 Feb 13 23:04 /bin/sh -> /bin/busybox 11 第1部 コンテナでシェルを取れた場合にできること | コンテナベースイメージ | Alpine Linux
  5. Busybox BusyBox combines tiny versions of many common UNIX utilities

    into a single small executable. The utilities in BusyBox generally have fewer options than their full-featured GNU cousins; however, the options that are included provide the expected functionality and behave very much like their GNU counterparts. • 主要なコマンドが詰め込まれたシングルバイナリ • Alpine Linuxイメージに同梱されている • busybox ls と引数でコマンド指定したり、 ln -s /bin/busybox /bin/ls のようにsymlinkを作る • ⼀部コマンドでは頻出オプションのみの実装になっている • Busyboxのシェル(sh/ash)はash互換のように⾒えるが、若⼲の独⾃拡張実装(Bash寄せ)がある [おまけ] シェル実装の超ざっくり系譜 (参考:https://qiita.com/ko1nksm/items/e7f43428352c0b4c78f9) Bourne shell -- ash(original) -- dash -- busybox ash \_ ksh -- bash \_ zsh 12 第1部 コンテナでシェルを取れた場合にできること | コンテナベースイメージ | Alpine Linux
  6. Busyboxにはどんなコマンドがある? Busyboxマニュアル https://www.busybox.net/downloads/BusyBox.html コマンドは結構ある パッケージ:rpm, dpkg 編集:ed, sed, awk, vi

    通信:wget, nc, telnet, (t)ftp, sendmail アーカイブ: cpio, b(un)zip2, g(un)zip, unzip, (un)lzma, tar, uncompress エンコード系:uuencode/uudecodeのみ coreutilsも多数あるが、完備はしていない 13 [, [[, acpid, addgroup, adduser, adjtimex, ar, arp, arping, ash, awk, basename, beep, blkid, brctl, bunzip2, bzcat, bzip2, cal, cat, catv, chat, chattr, chgrp, chmod, chown, chpasswd, chpst, chroot, chrt, chvt, cksum, clear, cmp, comm, cp, cpio, crond, crontab, cryptpw, cut, date, dc, dd, deallocvt, delgroup, deluser, depmod, devmem, df, dhcprelay, diff, dirname, dmesg, dnsd, dnsdomainname, dos2unix, dpkg, du, dumpkmap, dumpleases, echo, ed, egrep, eject, env, envdir, envuidgid, expand, expr, fakeidentd, false, fbset, fbsplash, fdflush, fdformat, fdisk, fgrep, find, findfs, flash_lock, flash_unlock, fold, free, freeramdisk, fsck, fsck.minix, fsync, ftpd, ftpget, ftpput, fuser, getopt, getty, grep, gunzip, gzip, hd, hdparm, head, hexdump, hostid, hostname, httpd, hush, hwclock, id, ifconfig, ifdown, ifenslave, ifplugd, ifup, inetd, init, inotifyd, insmod, install, ionice, ip, ipaddr, ipcalc, ipcrm, ipcs, iplink, iproute, iprule, iptunnel, kbd_mode, kill, killall, killall5, klogd, last, length, less, linux32, linux64, linuxrc, ln, loadfont, loadkmap, logger, login, logname, logread, losetup, lpd, lpq, lpr, ls, lsattr, lsmod, lzmacat, lzop, lzopcat, makemime, man, md5sum, mdev, mesg, microcom, mkdir, mkdosfs, mkfifo, mkfs.minix, mkfs.vfat, mknod, mkpasswd, mkswap, mktemp, modprobe, more, mount, mountpoint, mt, mv, nameif, nc, netstat, nice, nmeter, nohup, nslookup, od, openvt, passwd, patch, pgrep, pidof, ping, ping6, pipe_progress, pivot_root, pkill, popmaildir, printenv, printf, ps, pscan, pwd, raidautorun, rdate, rdev, readlink, readprofile, realpath, reformime, renice, reset, resize, rm, rmdir, rmmod, route, rpm, rpm2cpio, rtcwake, run-parts, runlevel, runsv, runsvdir, rx, script, scriptreplay, sed, sendmail, seq, setarch, setconsole, setfont, setkeycodes, setlogcons, setsid, setuidgid, sh, sha1sum, sha256sum, sha512sum, showkey, slattach, sleep, softlimit, sort, split, start-stop-daemon, stat, strings, stty, su, sulogin, sum, sv, svlogd, swapoff, swapon, switch_root, sync, sysctl, syslogd, tac, tail, tar, taskset, tcpsvd, tee, telnet, telnetd, test, tftp, tftpd, time, timeout, top, touch, tr, traceroute, true, tty, ttysize, udhcpc, udhcpd, udpsvd, umount, uname, uncompress, unexpand, uniq, unix2dos, unlzma, unlzop, unzip, uptime, usleep, uudecode, uuencode, vconfig, vi, vlock, volname, watch, watchdog, wc, wget, which, who, whoami, xargs, yes, zcat, zcip 第1部 コンテナでシェルを取れた場合にできること | コンテナベースイメージ | Alpine Linux
  7. [おまけ] Coreutils = Fileutils + Shellutils + Textutils Busybox公式ページ (https://www.busybox.net/about.html)

    より It provides replacements for most of the utilities you usually find in GNU fileutils, shellutils, etc. Wikipedia (https://ja.wikipedia.org/wiki/GNU_Core_Utilities) より GNU Core UtilitiesまたはCoreutilsはUnix系のOSで中核をなすcat、ls、rmなどのユーティリティの プログラム群、ないし、その開発とメンテナンスを⾏うGNUプロジェクトのサブプロジェクトであ る。以前はfileutils、textutils、shellutilsに分かれていた。 確かにhead, fold, nlのようなコマンドはBusyboxには実装されていない シェル芸するときには重宝するが、Busyboxにはsedとawkがあるからそこまで痛⼿ではないか 14 第1部 コンテナでシェルを取れた場合にできること | コンテナベースイメージ | Alpine Linux
  8. Debianベースイメージ ← aptでインストールされていたパッケージから抽出 • パッケージ: apt, dpkg • ユーティリティ: coreutils,

    diffutils, findutils • 便利ツール: grep, gzip, mawk, perl-base, sed, tar • リソース管理: sysvinit-utils, util-linux(プロセス、ディスク関連など) • シェル: bash, dash PerlやBashが入っているのがAlpineとの主な違い ただし、wgetは⼊っていない × telnet, nc, curl, wget, openssl あり物で通信したい場合はperlでゴリ押す? IO::socketは⼊っているが、Net::HTTPは⼊っていない Bashでもできないことはない(第2部でお話) 16 apt bash bsdutils coreutils dash debianutils diffutils dpkg findutils grep gzip mawk perl-base sed sysvinit-utils tar util-linux-extra util-linux 第1部 コンテナでシェルを取れた場合にできること | コンテナベースイメージ | Debian
  9. シェルから任意のバイナリファイルを書き込む⽅法 "abc" (0x616263) というデータを書き込む例 いずれも追記リダイレクト(>>)を使⽤すれば複数回に実⾏分割可能 1. 外部コマンドに頼る $ echo YWJj

    | base64 -d >> out.bin $ echo 616263 | xxd -p -r >> out.bin 2. echoのエスケープシーケンスでゴリ押す $ echo -en "\x61\x62\x63" >> out.bin # 16進数 $ echo -en "\0141\0142\0143" >> out.bin # 8進数 ※Bash/Busyboxでは-eオプションが必要。POSIXやDashでは-eオプション無しで8進数のみ対応 -nオプションは末尾に改⾏⽂字を出⼒しないために必要 3. 展開記法でゴリ押す(Dollar-Single-Quote) $ echo -n $'\x61\x62\x63' >> out.bin ※Dollar-Single-QuoteはPOSIX-1.2024から定義された。 Bash, Busybox shellでは使⽤可能。Dashでは使⽤不可。 18 第1部 コンテナでシェルを取れた場合にできること | 任意のバイナリを実⾏する
  10. +xができなくても、動的リンクバイナリは実⾏できる root@34d0c4a136ee:/# date Fri Mar 7 09:37:22 UTC 2025 root@34d0c4a136ee:/#

    file $(which date) /usr/bin/date: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c89b11207f6479603b0d49bf291c092c2b719293, for GNU/Linux 3.2.0, stripped root@34d0c4a136ee:/# ls -l $(which date) -rwxr-xr-x 1 root root 121904 Sep 20 2022 /usr/bin/date root@34d0c4a136ee:/# cp $(which date) ./date root@34d0c4a136ee:/# chmod -x ./date root@34d0c4a136ee:/# ./date bash: ./date: Permission denied root@34d0c4a136ee:/# /lib64/ld-linux-x86-64.so.2 ./date Fri Mar 7 09:38:09 UTC 2025 20 ローダを実⾏して バイナリを読ませる xフラグを抜くと 実⾏できない ネタゾーン 第1部 コンテナでシェルを取れた場合にできること | 任意のバイナリを実⾏する
  11. read権限だけあれば実⾏できる危険性 root@34d0c4a136ee:/# /lib64/ld-linux-x86-64.so.2 ./date Fri Mar 7 09:38:09 UTC 2025

    21 $ /bin/bash ./script.sh #スクリプトに実⾏権限なくてもbash経由で実⾏できるのと似ている [おまけ] Shebangを付けたファイルの実⾏は、上記とやっていることは同じ #!/bin/bash #!/usr/env/sed -f ←PATHからsedの実⾏ファイルを解決してくれる #!/bin/echo ←このファイルだけ作っておけば、実⾏すると$0を出⼒する 実はbashに限り、実⾏フラグがないファイルを実⾏するもう⼀つの⽅法がある(第2部に続く) ネタゾーン 第1部 コンテナでシェルを取れた場合にできること | 任意のバイナリを実⾏する
  12. Bash 現在の主要なLinuxディストリビューションのデフォルトシェル ashやPOSIXシェルと⽐べた特徴 • declare (typeset) により変数型指定が可能 ◦ 数値、名前参照、配列、連想配列などが使⽤可能 •

    基本コマンドの多くがビルトインとして使⽤可能 • 拡張パス展開(extglob)が使⽤可能 • TCP, UDP通信が使⽤可能 ◦ /dev/tcp/<address>/<port> ◦ /dev/udp/<address>/<port> ◦ 実際には存在しないデバイスファイルだが、Bashが認識してTCP/UDP通信をリダイレクト先にする • 変数⽂字列の置換が可能(patternはパス展開表現に従う) ◦ ${parameter/pattern/string} 23 第2部 コンテナでbashを取れた場合にできること
  13. 実⾏フラグがないファイルを実⾏する⽅法 builtinコマンドは⾃作できる bashの組込みコマンド⾃作によるスクリプトの⾼速化 (satさんのzenn) https://zenn.dev/satoru_takeuchi/articles/fb824d7c59ccdb6a0b38 • 特定の構造体で作成したCコードをShared Objectにビルドする $ cc

    -I/usr/include/bash/ -I/usr/include/bash/include \  -fpic -shared -o object.so source.c • 作成したShared Objectを読み込んでビルトイン化する $ enable -f ./object.so command-name enableしたShared Objectに限り、実⾏フラグがなくても実⾏可能 事前ビルドしたsoファイルをどうにかして取り寄せれば、 fork/execveなしで実⾏できる chmodコマンドをこの形式で取り寄せれば、後は好き放題できる? もっと⾔うと、Cで書けることは実質何でもできてしまう 25 #include <builtins.h> #include <shell.h> #include <stdio.h> static int myhello(WORD_LIST *list) { puts("Hello world!"); fflush(stdout); return EXECUTION_SUCCESS; } static char *desc[] = { "Show a greeting message.", "", "It's far faster than launching executable file", "because it't not necessary to call exec() and fork().", (char *)NULL }; struct builtin myhello_struct = { "myhello", // builtin command name myhello, // function called when issueing this command BUILTIN_ENABLED, // initial flag desc, // long description "myhello", // short description 0, }; 引⽤:https://zenn.dev/satoru_takeuchi/articles/fb824d7c59ccdb6a0b38 第2部 コンテナでbashを取れた場合にできること | バイナリを実⾏する(bash編) ネタゾーン
  14. /dev/tcp/<address>/<port> <address>部分はIPアドレスでもドメインでもよいため、名前解決実装が不要。神。 基本的な使い⽅ $ echo > /dev/tcp/127.0.0.1/80 ただ、これだとメッセージを送った後すぐコネクションが切れちゃう $ exec

    3<>/dev/tcp/127.0.0.1/80 $ echo >&3 $ cat <&3 こうすればコネクションを維持したまま送受信が可能 $ exec {peer}<>/dev/tcp/127.0.0.1/80 # $peerには11などの数値が格納される $ echo >&$peer fd番号が変数に格納されるので、こっちのほうが使いやすい 27 第2部 コンテナでbashを取れた場合にできること | BashでHTTP通信をする
  15. 最もシンプルなHTTPリクエスト製造コマンド 28 $ bash --norc --noprofile # 実験のため⼀旦カレントシェルから切り離す $ exec

    {peer}<>/dev/tcp/www.example.com/80 # コネクション維持のため、fdとして登録 $ cat << EOF | sed 's/$/\r/' >&$peer # ⾏末をCRLFにする GET / HTTP/1.0 Host: www.example.com User-Agent: curl Connection: close EOF $ cat <&$peer $ exit HTTP/1.0 200 OK Content-Type: text/html ETag: "84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134" Last-Modified: Mon, 13 Jan 2025 20:11:20 GMT Cache-Control: max-age=2377 Date: Sat, 15 Mar 2025 15:42:22 GMT Content-Length: 1256 Connection: close X-N: S <!doctype html> (以下略) 第2部 コンテナでbashを取れた場合にできること | BashでHTTP通信をする
  16. Bashだけでcurlをつくる pcurl.bash(pseudo cURL)を作ってみた シェルの本来の機能である「PATH内の外部コマンド呼び出し」を⼀切しないという縛りプレイ →sed, awk, perl, iconv, nkf に頼らない

    /bin に bash しかない状態でどこまでできるのかの検証 ソースはこちら:https://github.com/ryotosaito/pcurl ⼤前提:pcurl.bashの限界 • TLS通信(https://)はできない  →opensslコマンドが使えれば、 openssl s_client で後はどうにでもなる • HTTP/1.0 のみ使⽤可能  →HTTP/1.1では Transfer-Encoding: chunk をパースできる必要がある  (実はできるかも?未検証) 31 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた
  17. pcurl.bashで実装できたオプション $ ./pcurl.bash -h cURL clone made using only bash

    Usage: ./workspace/pcurl.bash/pcurl.bash [options]... <url> -A, --user-agent <name> Send User-Agent <name> to Server -d, --data <data> HTTP POST data -H, --header <header> Insert HTTP Header -h, --help Print help -L, --location Continue request after receiving Location header -o, --output <file> Output Response to OUTPUT file -v, --verbose Verbose output -X, --request <method> Request using <method> -x, --proxy <proxy> Use proxy --connect-to <host1:port1:host2:port2> connect to host2:port2 instead 32 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた https://github.com/ryotosaito/pcurl
  18. pcurl.bashで実⾏できるコマンド [1/2] # 以下は ./pcurl.bash を curl に差し替えても動作する $ ./pcurl.bash

    http://wttr.in/Tokyo # wttr.inはUser-Agent:curlでアクセスすると、CLIで天気予報を返してくれる $ ./pcurl.bash -o index.html example.com:80 # http://やURLの/が不⾜していたり、ポート番号が指定されていても処理できる # -oフラグでレスポンスボディを書き込む $ ./pcurl.bash -v -L http://httpbin.org/redirect/5 # -vオプションはデバッグ出⼒(極⼒curlに寄せている) # -LオプションはリダイレクトのLocationに⾃動アクセスする $ ./pcurl.bash -X PUT -d "key1=val1&key2=val2" http://httpbin.org/put # -Xオプションでメソッド指定 # -dオプションでリクエストボディを付加 33 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた https://github.com/ryotosaito/pcurl
  19. pcurl.bashで実⾏できるコマンド [2/2] $ ./pcurl.bash -x http://localhost:3128 http://www.example.com/index.html # プロキシを経由するアクセス #

    プロキシに対して GET http://www.example.com/index.html HTTP/1.0 を投げる $ ./pcurl.bash -v --connect-to hb.org:80:httpbin.org:80 http://hb.org/get # --connect-toは、Hostヘッダと実際の接続先を揃えずにアクセスできる⽅法 # DNSをいじったり/etc/hostsを変更する⼿間いらずでアクセスできる $ ./pcurl.bash -vx localhost:3128 --connect-to hb.org:80:httpbin.org:80 http://hb.org/get # プロキシと--connect-toの合わせ技 # プロキシに対して CONNECT httpbin.org:80 HTTP/1.0 # httpbin.orgとのコネクションが成⽴するので、そこからは通常のアクセス # GET /get HTTP/1.1 # HOST hb.org 34 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた https://github.com/ryotosaito/pcurl
  20. ⽂字列編集:パラメータ展開 POSIXのパラメータ展開(⽂字列の部分削除) ${param#word} ${param##word} 前方一致のwordを削除(#は最短一致、##は最長一致) ${param%word} ${param%%word} 後方一致のwordを削除(%は最短一致、%%は最長一致) word部分は、パス展開の glob表現が使える(*,

    ?, [) • *   任意の⽂字列 • ?  任意の1⽂字 • [...] カッコ内の各⽂字のいずれか1⽂字 ◦ [!...] [^...] カッコ内のいずれの⽂字でもない⽂字 ◦ [:class:]  ⽂字クラスclassに含まれる⽂字(詳細割愛) 38 $ URL="http://www.example.com/" $ SCHEME="${URL%%://*}" # ://以降の⽂字列を取っ払うとhttpだけが残る 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  21. ⽂字列編集:置換 Bashの追加機能に、⽂字列置換がある ${param/pattern/string} 変数の⽂字列中のpatternをstringに置換する ここでもpatternはパス展開のglobが使える(*, ?, [) 最も⻑いマッチが1つだけ置換される 置換のバリエーション •

    ${param/#pattern/string} 変数内で最初に出現したpattern置換する • ${param/%pattern/string} 変数内に最後に出現したpattern置換する • ${param//pattern/string} 変数内に出現したpatternをすべて置換する(/g) • ${param/pattern} 置換する代わりに削除する(${param/pattern/}と同じ) 39 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  22. Bashの限界:grepができない ここまで扱ったのは • globにマッチする⽂字列の削除 • globにマッチする⽂字列の置換 マッチした⽂字列のみ抽出をする、いわば grep -o のような操作はbashだけではできない

    そのため、⽂字列を抽出するときは⼀癖ある操作をする必要がある 「Content-Type: text/html」 から 「Content-Type」 を抽出するには →grepの場合:grep -oE '^[^:]+' (⾏頭から:までを抽出する) →bashの場合:${var%%:*} (:から⾏末までを削除する) 40 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  23. 実装の壁と解決策 41 複雑なパースを⾏う -H オプションで任意のヘッダを管理する TCP通信をする ⽂字列を編集する /dev/tcp/<address>/<port> パラメータ展開 ${param#word} ${param%word}

    パターン置換  ${param/pattern/string} 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  24. 複雑なパース:拡張パス展開 extglob 通常のパス展開ではPOSIXと同様のglobしか使えない(*, ?, [) extglobを有効にすると、下記の拡張記法が使える pattern-listは pattern1|pattern2|... のようにパイプで区切られた複数のパターン •

    ?(pattern-list) :pattern-listのうちどれかが0…1回 • *(pattern-list) :pattern-listのうちどれかが0…*回 • +(pattern-list) :pattern-listのうちどれかが1…*回 • @(pattern-list) :pattern-listのうちどれかが1回 • !(pattern-list) :pattern-listのうちどれにも該当しない 42 $ shopt -s extglob # extglobの有効化 $ RESP_LINE="HTTP/1.1 200 OK" $ RESP_STATUS="${RESP_LINE##HTTP/+([[:digit:].]) }" # 0-9か"."のいずれかが1回以上マッチ # RESP_STATUS="200 OK" 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  25. 実装の壁と解決策 43 TCP通信をする ⽂字列を編集する 複雑なパースを⾏う -H オプションで任意のヘッダを管理する /dev/tcp/<address>/<port> パラメータ展開 ${param#word} ${param%word}

    パターン置換  ${param/pattern/string} extglob 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  26. ヘッダ管理:連想配列 Bashにはdeclare, typeset(declareと同じ), local(ローカルスコープ)があり、変数の型が指定できる 例:declare -nで名前参照、declare -rでreadonly declare -Aで連想配列を定義できる(Bashの配列は1次元のみなので注意) 44

    $ declare -A assoc_arr=( [key1]=val1 [key2]=val2 ) # 初期化 $ assoc_arr[key3]=val3 # 追加 $ echo ${assoc_arr[key2]} # アクセス val2 $ for key in "${!assoc_arr[@]}"; do echo "assoc_arr[$key]=${assoc_arr[$key]}"; done # key列挙 assoc_arr[key2]=val2 assoc_arr[key3]=val3 assoc_arr[key1]=val1 # key列挙の順番は辞書順や挿⼊順ではない 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  27. # 規定ヘッダを先に定義しておく declare -A HEADERS=( [Accept-Encoding]="identity" [Connection]="Close" [User-Agent]="curl" ) #

    コマンドライン引数を使って、Headerの引数の上書きをする while getopts H: OPT; do case "$OPT" in -H) # コロンの前後でKeyとValueを区切って代⼊している HEADERS[${OPTARG%%:*}]="${OPTARG#*: }" shift;; esac done # 中略 exec {stdout}>&1 # オリジナルの標準出⼒を別fdに保存 exec >&$peer # 以降の出⼒はサーバに⾶んでいく # リクエスト⾏ echo -en "$METHOD $URL HTTP/1.0\r\n" # ヘッダ⾏ for key in "${!HEADERS[@]}" do echo -en "$key: ${HEADERS[$key]}\r\n" done echo -en "\r\n" # 標準出⼒復活 exec >&$stdout # レスポンス出⼒ cat <&$peer 連想配列を⽤いたヘッダ管理の実装例 Headerを連想配列管理しているのは、規定ヘッダの上書きを容易にするため →pcurlは現時点で同じヘッダを複数⼊れられる実装にはなっていないです。許して 45 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  28. declare -pの便利な使い⽅ declare -pの出⼒は、そのままdeclareコマンドとして叩けるようになっている 関数の出⼒として使⽤すると、構造体を返すような実装ができる 46 # 引数URLの構成要素を連想配列にして返す parse_url() {

    URL="$1" # 中略 local -A return=( [URL]="$URL" [SCHEME]="$SCHEME" [HOST]="$HOST" [PORT]="$PORT" [PATH]="$PATH" ) declare -p return } $ parse_url "http://www.example.com/index.html" declare -A return=([PORT]="80" [SCHEME]="http" [URL]="http://www.example.com/index.html" [HOST]="www.example.com" [PATH]="/index.html" ) $ parsed="$(parse_url "http://www.example.com/index.html")" # declareコマンド⽂字列を変数parsedに格納 $ eval "${parsed/return/TARGET}" # 変数名をreturnからTARGETに変更してから評価 $ declare -p TARGET declare -A TARGET=([PORT]="80" [SCHEME]="http" [URL]="http://www.example.com/index.html" [HOST]="www.example.com" [PATH]="/index.html" ) 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  29. 実装の壁と解決策 47 TCP通信をする ⽂字列を編集する 複雑なパースを⾏う -H オプションで任意の ヘッダを管理する /dev/tcp/<address>/<port> パラメータ展開 ${param#word}

    ${param%word} パターン置換  ${param/pattern/string} extglob 連想配列(declare -A) 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | 実装の壁とbashならではの対策 https://github.com/ryotosaito/pcurl
  30. pcurl.bashの材料 • ⼊出⼒コマンド ◦ echo, cat, read • [[ (test)

    コマンドオプション ◦ ==, =~, -eq, -le, -gt, -n, -z, -v • ⽂字列削除、置換 ◦ ${param#word}, ${param%word}, ${param/pattern/string} ◦ *, [...], [:digit:], $'\r', +(pattern) • 特別なパラメータ展開 ◦ ${#param}, ${!param[@]} • 変数宣⾔ ◦ declare(local), -A, -i, -p • リダイレクト ◦ exec, >&, <&, {word}<> • その他 ◦ shopt, true, getopts, shift, eval, exit, $() 48 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた https://github.com/ryotosaito/pcurl
  31. 他にも実装できそうなこと 準備時間の都合で⼀部オプションしか実装できなかったが、他にも多数の実装⾒込みがある • -I, --head HEADメソッド+ヘッダを標準出⼒ • -i, --include ヘッダを標準出⼒

    • -d, --data @<filename> Bodyデータをファイルから読み込み • -F --form <name=content> multipart/form-dataのボディ形式 • -f, --fail エラー時にボディを出⼒しない • -r, --range, -e, --referer -H 実装を転⽤するだけ • --post301, --post302, --post303 リダイレクト先にもPOSTする • -b, --cookie, -c, --cookie-jar Cookieデータの参照(-b)、保存(-c) • -O, --remote-name URLと同じファイル名で保存する • -J, --remote-header-name Content-Dispositionヘッダ指定のファイル名で保存する • --output-dir <dir> ファイルの保存先ディレクトリを指定 • --proxy-header プロキシ接続時のヘッダ 49 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた https://github.com/ryotosaito/pcurl
  32. HTTPが実装できたらできること できること • curl/wgetが封殺されただけの環境でも、迂回的にHTTP通信ができる  →他のコマンドに制限がなければrpm/debファイルを落として、そのままrpm/dpkgなど…  →chmod.soをホストしてDL、builtin化してもよい • IMDSへのアクセス  →実⾏環境がクラウドなら169.254.169.254にHTTPアクセスできる •

    サイドカープロキシがPlain HTTP対応なら他のアプリにもアクセスできる? できないこと • k8s API serverへのアクセス(基本的にHTTPSアクセスが必須なので) • 署名が必要なアクセス(AWS Sigv4、Oauth1.0など) 「必要最低限の実⾏ファイルしか置いていない」→Bashがあるけど⼤丈夫?(過剰) 51 第2部 コンテナでbashを取れた場合にできること | Bashでcurlを再現してみた | pcurl.bashの使い道 https://github.com/ryotosaito/pcurl
  33. rbash、またはbash -r $ ls -l $(which rbash) lrwxrwxrwx 1 root

    root 4 3月 14 2024 /usr/bin/rbash -> bash 53 rbashやbash -rで起動すると制限(restricted)モードになり、下記ができなくなる(⼀例) • cd • enable • exec • PATHやENVの更新 • /を含む⽂字列でのコマンド起動(絶対パス‧相対パス) • リダイレクト(>, <, >&, <&等)の実施 • set +rによる制限の解除 シェルインタフェースとしてはじめからrbashを⽤意する場合には有効だが、 バイナリがbashと同⼀である以上シェルを取られるシナリオを考慮する場合はあまり効果がない 第2部 コンテナでbashを取れた場合にできること | おまけ:rbash
  34. minimalなベースイメージ Distroless(https://github.com/GoogleContainerTools/distroless) OSとして最低限のファイルのみ(/etc/passwdなど)が同梱されたイメージ 実⾏ファイルは⼀切なく、標準ではシェルすら含まれていない 基本のタグの他、有名な⾔語処理系のためのイメージも⽤意されている python3, java17, java21, nodejs18, nodejs20,

    nodejs22 →次ページ Scratch(https://hub.docker.com/_/scratch) ファイルが⼀切含まれていない、最も基本的なイメージ 実⾏時に /dev /proc /sys といった特別なディレクトリ配下のファイルだけは⽣える 56 第3部 コンテナハードニングを考える
  35. Distroless Imageごとに同梱ファイルが異なる TagごとにユーザとBusyboxが異なる ⾔語環境イメージをそのまま起動すると、 シェルがなくても処理系コマンドが実⾏する Python, Node.jsならREPLが使える 57 Tag→ latest

    nonroot debug debug- nonroot User root (1) nonroot (63352) root (1) nonroot (63352) Busybox × × ◦ ◦ Image→ (余⽩のため 省略表記) static base-nossl base cc java python nodejs ca-certificates ◦ ◦ ◦ ◦ ◦ /etc/passwd ◦ ◦ ◦ ◦ ◦ /tmp ◦ ◦ ◦ ◦ ◦ tzdata ◦ ◦ ◦ ◦ ◦ glibc × ◦ ◦ ◦ ◦ libssl × × ◦ ◦ ◦ libgcc1 × × × ◦ × ⾔語環境 × × × × ◦ 第3部 コンテナハードニングを考える
  36. Distrolessの実⾏ファイル⼀覧 $ docker run --rm -it gcr.io/distroless/base-debian12:debug / # find

    / -type f -executable /etc/update-motd.d/10-uname /etc/ssl/certs/ca-certificates.crt /usr/share/doc/ca-certificates/copyright /usr/lib/os-release /lib/x86_64-linux-gnu/libc.so.6 # baseに同梱 /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 # baseに同梱 /.dockerenv /busybox/busybox # debugに同梱 58 第3部 コンテナハードニングを考える
  37. それでも有事の際にいろいろなコマンドを使いたい! 運⽤中のコンテナに障害調査したいときはどうするか 60 デプロイ種別 ネットワーク プロセス ファイルシステム 単⼀コンテナ ‧(nsenter -n)

    ‧デバッグコンテナをアタッチ ‧(nsenter -p) ‧デバッグコンテナをアタッチ デバッグコンテナをアタッチ docker-compose services.name.network_mode を設定したデバッグコンテナ services.name.pid を設定したデバッグコンテナ services.name.pid を設定したデバッグコンテナ Kubernetes サイドカーに デバッグコンテナを作成 サイドカーに デバッグコンテナを作成 (shareProcessNamespace: true) サイドカーに デバッグコンテナを作成 (shareProcessNamespace: true) Kubernetes > 1.25 エフェメラルコンテナ エフェメラルコンテナ エフェメラルコンテナ 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング
  38. コンテナを実装する技術:Namespace プロセス同⼠がお互いの存在に⼲渉しないように隔離できる技術 コンテナを起動するとPID=1のプロセスが実⾏されるが、ホストから⾒たときは別のプロセスIDになる 61 $ docker run --rm --name nstest

    -itd busybox sleep infinite ed6cb808df1376c1bbcc8dff4f031bd23cbcc437e81ef62f8b1e8a72691644c1 $ docker exec -it nstest ps PID USER TIME COMMAND 1 root 0:00 sleep infinite 7 root 0:00 ps $ docker inspect --format '{{.State.Pid}}' nstest # コンテナの実PIDを取得 7735 $ ps 7735 PID TTY STAT TIME COMMAND 7735 pts/0 Ss+ 0:00 sleep infinite 同じプロセスなのに IDが違う 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング
  39. Namespaceは複数の領域に分かれている • mount namespace ◦ プロセスにマウントされるファイルシステムを分離する • network namespace ◦

    プロセスに割り当てられるネットワークやIPアドレスを分離する • PID namespace ◦ プロセスIDのテーブルを分離する 他にも下記namespaceがある • UTS namespace :hostnameやdomainname • IPC namespace :プロセス間通信(IPC) • user namespace :UID, GIDやその権限 • cgroup namespace :cgroup(プロセスのリソース制限) • time namespace :プロセスが認識する時刻 62 話半分ゾーン 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング
  40. nsenter -n:network namespaceに⼊る $ docker run -d --rm --name python-server

    gcr.io/distroless/python3-debian12 python3 -m http.server c19a9495cb6335fdee0c6d5e43c05061f9fd00df7bdec2fe6d374163118dcae5 $ docker exec python-server curl -I http://localhost:8000 # コンテナ内にcurlはない OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown $ docker inspect --format '{{.State.Pid}}' python-server # コンテナのPIDを取得 10152 $ sudo nsenter -t 10152 -n curl -I http://localhost:8000 # ホストのcurlをコンテナNWで実⾏ HTTP/1.0 200 OK Server: SimpleHTTP/0.6 Python/3.11.2 Date: Thu, 13 Mar 2025 15:30:29 GMT Content-type: text/html; charset=utf-8 Content-Length: 741 64 話半分ゾーン 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング | 単⼀コンテナのデバッグ
  41. nsenter:コンテナホストからのアクセス nsenterコマンドを使うと、指定プロセスのnamespaceにアクセスできる nsenter -t PID <option> コマンド • -m, --mount:mount

    namespaceにアクセスする • -n, --net:network namespaceにアクセスする • -p, --pid:PID namespaceにアクセスする • etc… 前ページのコマンドは -n オプションでnetwork namespaceに⼊っていた $ sudo nsenter -t 10152 -n curl -I http://localhost:8000 65 話半分ゾーン 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング | 単⼀コンテナのデバッグ
  42. minimal環境のnsenterはクセがある:実⽤に難あり 実質mount namespaceにアクセスできない ⼀般的に、コンテナにnsenterするときは -m (--mount) オプションを付けてfilesystemに⼊る $ sudo nsenter

    -t 10152 -m ls / # コンテナ内のファイルへアクセスする が、どうやらマウント→コマンド実⾏の順序のため、コンテナ内のPATHから実⾏ファイルを探し出すらしい → distrolessのmount namespaceに⼊ると当然コマンドが⼀切⼊っていないので、デバッグできない ※奥の⼿として、ホストのbind mountやvolumeパスへ直接アクセスするという⼿がある(邪道なので割愛) ネットワークデバッグにも⽀障がある ネットワークのデバッグのために -n (--net) だけを使⽤しても、⼗分なデバッグができないことがある 例)ホストの /etc/resolv.conf がsystemd-resolved を向いているため、名前解決ができない 66 話半分ゾーン 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング | 単⼀コンテナのデバッグ
  43. おまけ:docker debug Docker Desktopの有償ライセンス版では、 docker debug コマンドが⽤意されている https://docs.docker.com/reference/cli/docker/debug/ ⾮常に便利そうだが、動作確認ができないので割愛 67

    話半分ゾーン 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング | 単⼀コンテナのデバッグ
  44. 改めて、単⼀コンテナのデバッグ docker run コマンドには、PID namespace, network namespaceを共有するオプションがある nsenter に⽐べると取り回しがしやすい→評価◯ 68

    $ docker run --rm -it --pid=container:python-server --network=container:python-server busybox / # wget localhost:8000 # --network にアクセスしているとネットワークを共有できる Connecting to localhost:8000 (127.0.0.1:8000) saving to 'index.html' index.html 100% |******************************************| 741 0:00:00 ETA 'index.html' saved / # ls /proc/1/root/usr/bin # --pid にアクセスしているとfilesystemにもアクセスできる python python3 python3.11 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング | 単⼀コンテナのデバッグ
  45. docker-composeでのNamespace共有 compose.yaml内で指定可能なnamespace • network_mode:network namespaceの指定 • pid:PID namespaceの指定 各項⽬に設定可能な値 •

    host:ホストと共有 • service:<name>:指定サービスと共有 デバッグコンテナの起動 70 $ docker compose --profile debug up -d # docker compose start debug $ docker compose exec debug sh $ docker compose stop debug services: app: image: gcr.io/distroless/python3-debian12 command: -m http.server # デバッグ用コンテナ debug: image: busybox command: sleep infinity profiles: ["debug"] network_mode: "service:app" pid: "service:app" depends_on: - app dockerと同じデバッグ⽅法を取るのもあり 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング | docker-composeのデバッグ
  46. $ kubectl exec -it deployment/myapp -c debug-sidecar -- sh /

    # wget localhost:8000 # ネットワークデバッグ Connecting to localhost:8000 (127.0.0.1:8000) saving to 'index.html' index.html 100% |*****************| 741 0:00:00 ETA 'index.html' saved / # ps PID USER TIME COMMAND 1 65535 0:00 /pause 7 root 0:00 python3 -m http.server 14 root 0:00 sleep infinity 20 root 0:00 sh 26 root 0:00 ps / # ls /proc/7/root/usr/bin # ファイルシステムにアクセス python python3 python3.11 Kubernetesの場合:デバッグ⽤サイドカー 72 apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: replicas: 1 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: shareProcessNamespace: true containers: - name: main-container image: gcr.io/distroless/python3 command: ["python3", "-m", "http.server"] - name: debug-sidecar image: busybox command: ["sleep", "infinity"] もう少し良い⽅法がある(次ページ) 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング | Kubernetes Podのデバッグ
  47. Kubernetesの秘技:エフェメラルコンテナ エフェメラルコンテナ:⼀時的に作成するサイドカーコンテナ 前に紹介したデバッグ⽤サイドカーを使い捨てで特定podだけにアタッチできる https://kubernetes.io/ja/docs/concepts/workloads/pods/ephemeral-containers/ v1.16からalpha、v1.25からstable(2025/3/11時点でlatestはv1.32) コンテナ作成以降のデバッグ⼿順は前ページと同じ 73 $ kubectl debug

    -it myapp-6d9d8fb966-x4mqt --image=busybox --target main-container / # wget localhost:8000 Connecting to localhost:8000 (127.0.0.1:8000) saving to 'index.html' index.html 100% |*****************| 741 0:00:00 ETA 'index.html' saved / # ls /proc/7/root/usr/bin python python3 python3.11 第3部 コンテナハードニングを考える | minimalコンテナのトラブルシューティング | Kubernetes Podのデバッグ