Container là 1 tiến trình sandbox, sử dụng chung kernel với host và cô lập với toàn bộ tiến trình khác trên host. Với những ứng dụng web thiết kế theo mô hình microservice, mỗi service sẽ chạy trong một container, vì thế khi RCE được một service thực chất ta chỉ đang ở trong một tiến trình bị giới hạn truy cập tới thông tin, tài nguyên khác trên server. Trong trường hợp này ta chỉ có thể lấy được source code của servie đó, một vài thông tin biến môi trường,… Để chiếm quyền điều khiển server và tấn công sâu hơn nữa, ta cần thêm một bước đó là container breakout, hay container escape.
    Container breakout là thuật ngữ chỉ hành vi một người dùng hợp pháp hoặc một kẻ tấn công bypass được sự cô lập của container để truy cập toàn bộ tài nguyên (file system, processes, network interfaces,…) của host. Trong bài viết này mình sẽ đi tìm hiểu các phương pháp chính để breakout khi đã drop được shell vào trong container.
    Nội dung bài viết sẽ gồm 3 phần
        – Phần 1: tổng quan về Linux Kernel và chroot, cgroup, namespace
        – Phần 2: kiến thức cơ bản cần nắm được về docker container
        – Phần 3: các attack vector breakout container

Phần 1: tổng quan về Linux Kernel và chroot, cgroup, namespace

Figure 1.Linux kernel
    Linux kernel hiểu một cách trừu tượng là lớp giao tiếp trung gian giữa người dùng và phần cứng máy tính. Linux kernel cung cấp khoảng 380 system call như read, open, write, mmap, close, exit,… gọi trực tiếp đến phần cứng máy tính. Linux Kernel còn có 1 số subsystem như lập lịch tiến trình (process scheduling), giao tiếp đa tiến trình IPC (inter-process communication), quản lí bộ nhớ (memory management), quản lí file (virtual files), mạng (networking). Ngoài ra Linux kernel còn có một số component và 1 số module bảo mật như SELinux, AppArmor, TOMOYO, Smack
Figure 2. Docker vs Hypervisor
    Hypervisor là công nghệ ảo hóa, tạo ra các máy ảo bằng cách sử dụng image OS riêng, sử dụng tài nguyên riêng biệt so với host
    Docker sử dụng docker engine tạo ra các tiến trình cô lập, sử dụng OS và tài nguyên của host.
https://www.geeksforgeeks.org/chroot-command-in-linux-with-examples/
    Command chroot (change root) dùng để thay đổi thư mục root của một tiến trình trong Linux bằng cách sử dụng system call. Cả docker và chroot đều tạo 1 môi trường cô lập trên Linux.
        – Chroot không cho phép truy cập ra bên ngoài root directory mới, docker hỗ trợ mount – điểm chia sẻ chung giữa container và host.
        – Chroot không thay đổi thông tin userID, groupID của process, vì thế process vẫn có cùng permission với trước khi change root, docker hỗ trợ user namespace.
        – Chroot không có cơ chế kiểm soát tài nguyên mà 1 tiến trình sử dụng, docker lợi dụng tính năng cgroup của linux kernel khắc phục điểm bất lợi này.
        – Ngoài ra docker còn hỗ trợ việc cấu hình mạng riêng, quản lí image dễ dàng, chia sẻ image giữa các user,…
    Control group – cgroup là một tính năng của Linux kernel. Cgroups chia các tiến trình thành các nhóm , thêm các cấu hình kiểm soát và thống kê tài nguyên sử dụng (CPU, memory, disk I/O, network,…) của mỗi nhóm tiến trình đó. Ví dụ về 1 cgroups trong Linux, các folder trong cgroup chính là các cgroup con của cgroup đó.
    Để hiểu thêm về nguyên lí hoạt động của cgroup, ta thử vào trong 1 vài file xem nó có gì. cgroup.controllers chứa danh sách các controller khả dụng trong cgroups, nghĩa là nếu muốn giới hạn memory sử dụng của 1 tiến trình thì trước tiên ta phải thêm memory controller vào vào file này.
    cgroup.procs là danh sách các process ID trong cgroup, hiện tại có 2 tiến trình process ID là 1 và 10 (không phải 1 và 9 lí do là khi chạy lệnh cat, tiến trình PID 9 ps -ef đã stop, PID bằng 10 là PID tiến trình cat)
    pids.current là tổng số process đang nằm trong cgroup
    pids.max là số lượng process tối đa được phép chứa trong cgroup, giá trị max là không bị giới hạn
    Giới hạn về memory mà tất cả các tiến trình trong cgroup được phép sử dụng nằm trong file memory.max
    Các thông số cấu hình khác tham khảo thêm tại
        – https://docs.kernel.org/admin-guide/cgroup-v1/index.html
        – https://docs.kernel.org/admin-guide/cgroup-v2.html
    Một mình cgroup không đảm bảo được tính cô lập (isolation) của một container, vì thế container cần phải sử dụng thêm namespace.
    Namespace là một tính năng của Linux Kernel. Nó như là 1 vách ngăn để đảm bảo rằng các tiến trình trong namespace này không thể truy cập tài nguyên của các tiến trình trong namespace khác.
Thông tin namespace của 1 tiến trình trong linux
    Có nhiều loại namespace: cgroup namespace, ipc namespace, mnt namespace,…
        – cgroup namespace là không gian chứa thông tin về cgroup (đề cập ở trên) trong kernel. Mỗi cgroup namespace chứa 1 tập các cgroup root directory. Khi 1 process tạo mới sử dụng clone hoặc unshare với flag CLONE_NEWCGROUP thì cgroup directory hiện tại sẽ trở thành cgroup root directory của cgroup namespace mới. Sau quá trình boot, mở shell lên thì là mặc định ta đang ở trong initial cgroup namespace, khi 1 cgroup namespace con được tạo thì nó sẽ bị cô lập từ đây, nghĩa là nó chỉ có thể thấy toàn bộ process trong namepace này và namespace con chứ không thể thấy được process ở cgroup namespace cha.
https://man7.org/linux/man-pages/man7/cgroup_namespaces.7.html

Hình ảnh trên là mô hình hóa 2 cgroup namespace trong linux, vòng tròn lớn là cgroup namespace lớn nhất (initial cgroup namespace), root directory của nó sẽ là /sys/fs/cgroup. Các folder cpuacct, devices, user.slice, system.slice,… là các cgroup con tuy nhiên vẫn cùng cgroup namespace. Ở đây ta cần hiểu được rằng cgroup và cgroup namespace là khác nhau. Vòng tròn nhỏ chính là cgroup namespace của 1 container, cgroup root directory của nó sẽ là /sys/fs/cgroup/system.slice/docker-4dd…..8a6.scope. Các tiến trình trong cgroup namespace này không thể truy cập được các tiến trình, tài nguyên bên ngoài nó.

    – mỗi pid namespace là 1 tập process id độc lập, bắt đầu từ 1. Pid namespace có thể lồng nhau, vì thế pid namespace cha (pid namespace ban đầu) có thể thấy toàn bộ process. Ví dụ về tập process id trong container.
    – user namespace là 1 không gian chứa thông tin về user. Nó chứa một bảng ánh xạ (table mapping) chuyển giá trị userId trong container với giá trị userId tương ứng trong host. Ví dụ user root trong container có userId 0, nhưng host khi kiểm tra quyền sở hữu (ownership) thì nó sẽ lấy giá trị userId là giá trị userId được config trong /proc/[pid]/uid_map
Đọc thêm: https://en.wikipedia.org/wiki/Linux_namespaces
    Như vậy tính năng cgroup đảm bảo được việc quản lí tiến trình, tính năng namespace đảm bảo được tính cô lập của tiến trình –> Docker engine lợi dụng 2 tính năng này để tạo ra container một cách dễ dàng và thuận tiện cho người dùng.

Phần 2: kiến thức cơ bản cần nắm được về docker container

Docker container

    Docker container là 1 tiến trình sandbox. Nó cô lập với tất cả các tiến trình khác trên host.  Sự cô lập này lợi dụng tính năng cgroup và namespace (đã nói ở trên), 2 tính năng tồn tại trên Linux kernel từ lâu. Docker sử dụng docker engine để tiếp cận và sử dụng 2 tính năng này dễ dàng hơn.
    Tóm lại, một container là:
        – Một thực thể (instance) của docker image. Ta có thể tạo tạo, khởi chạy, dừng, di chuyển hoặc xóa container sử dụng Docker API hoặc CLI.
        – Có thể chạy ở máy local, máy ảo hoặc trên môi trường cloud.
        – Có thể chạy trên bất kỳ hệ điều hành nào.
        – Cô lập với các container khác, chạy ứng dụng riêng và chỉ cần các file binary, lib, file cấu hình đủ để chạy ứng dụng đó.

Docker image

    Khi chạy một container, nó sử dụng filesystem độc lập, riêng biệt so với host. Các filesystem này được cung cấp bởi docker image.  Vì vậy docker image phải chứa tất cả mọi thứ cần thiết để chạy một ứng dụng – all dependencies, configurations, scripts, binaries, libraries,…

Mount

    Giả sử bạn chạy một container có chức năng Ghi chép, những thông tin lưu ở phần Ghi chép sẽ được lưu ra 1 file nào đó trong container. Tuy nhiên khi khởi chạy lại container từ đầu, tất cả những thay đổi trước đó sẽ bị mất –> cần đến mount
Mount là điểm chia sẻ chung giữa host và container, khi ta thay đổi dữ liệu folder mount trên container như nào thì dữ liệu trong folder mount trên host cũng bị thay đổi như thế.
    Ví dụ mount type volume

Sử dụng command docker inspect để kiểm tra path source mount trên host

    Ghi file bất kỳ ra folder /tmp trên container

Kiểm tra source mount folder trên host

    Mount type bind tương tự như mount type volume, tuy nhiên ta sẽ chỉ định được source mount path
    Toàn bộ data trong folder /tmp trên host sẽ được đồng bộ với folder /tmp trên container
    Vì container là tiến trình cô lập nên dễ thấy mount point như là một entry point nên kiểm tra trước tiên khi muốn breakout 1 container.

Phần 3: các attack vector breakout container

Attack vector 1Mounted Docker Socket Escape/Exposed Docker Socket

    Docker hoạt động theo mô hình client/server, docker daemon đóng vai trò server nhận các yêu cầu từ client docker cli gửi đến và giao tiếp qua docker socket.Như mình đã đề cập ở trên mount point là điểm chia sẻ dữ liệu chung giữa host và container, khi docker socket được mount trong container, từ trong container attacker khi đó có thể giao tiếp với docker server thông qua socket này. Việc mount docker socket này thường được dùng trong trường hợp quản trị hoặc developer muốn chạy container bên trong 1 container hoặc muốn kiểm tra thông tin, log event của container nên cần giao tiếp với docker deamon từ trong container.
    Kiểm tra docker.socket được  mount trong container

Kiểm tra xem docker cli có được cài trong container không

    Trong trường hợp không thể cài đặt được docker trong container, ta sẽ nghĩ đến việc giao tiếp với docker socket bằng cách khác, dùng TCP socket hoặc Docker HTTP API. Tham khảo tại:
        – https://i.blackhat.com/USA-19/Thursday/us-19-Edwards-Compendium-Of-Container-Escapes-up.pdf (page 42)
        – https://docs.docker.com/engine/api/v1.41/
    Ví dụ command sau tương đương với docker container ls
    Sau khi cài đặt docker cli, ta tạo mới một container, mount toàn bộ thư mục / của host vào thư mục /host trong container, thay đổi root folder và gọi lệnh bash

Attack vector 2Capabilities Abuse Escape

Sử dụng command capsh –print để kiểm tra capabilities của container. Nếu có 1 trong số các capability sau thì có thể breakout container:
CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_SYS_MODULE, DAC_READ_SEARCH,
DAC_OVERRIDE, CAP_SYS_RAWIO, CAP_SYSLOG, CAP_NET_RAW, CAP_NET_ADMIN

CAP_SYS_MODULE

    Các container là các tiến trình cô lập, đảm bảo 1 tiến trình không thể truy cập vào tài nguyên của tiến trình khác và tài nguyên của host, thế nhưng các container vẫn sử dụng chung kernel với host.
    Khi 1 container có cap_sys_module capability, nó có thể inject – load/unload kernel module bất kỳ vào kernel của host sử dụng 3 system call: init_module, finit_module, delete_module
    Kiểm tra xem container có cap_sys_module capability không, nếu có, ta có thể inject malicious module vào linux kernel sử dụng command insmod. Để build reverse-shell.ko tham khảo tại https://book.hacktricks.xyz/linux-hardening/privilege-escalation/linux-capabilities#cap_sys_module
    Như vậy ta đã lấy được reverse shell của host.

DAC_READ_SEARCH

    Discretionary access control (DAC) kiểm soát quyền truy cập linh hoạt, sử dụng ACL access control list để kiểm soát quyền ghi, đọc, xóa,… của 1 object này đối với 1 object khác. Khi 1 container có cap_dac_read_search, nó có thể path traversal và đọc toàn bộ file của host. Nó cũng có permission gọi đến syscal open_by_handle_at để thực hiện việc này. Kiểm tra xem container có dac_read_search capability không
    Sử dụng exploit poc (tham khảo https://book.hacktricks.xyz/linux-hardening/privilege-escalation/linux-capabilities#cap_dac_read_search) để đọc file bất kỳ trên host

DAC_OVERRIDE

     Tiếp tục ở lab trên, lợi dụng cap_dac_override để ghi đè file bất kỳ trên host, tuy nhiên cap_dac_override yêu cầu cần phải có cap_dac_read_search capability. Kiểm tra container có cap_dac_read_searchcap_dac_override capability không

Khi có capability này, để breakout container, ta thường thêm user + password vào file /etc/passwd, sửa password của user trong file /etc/shadow, thêm user vào file /etc/sudoers, hoặc xóa ký tự x cạnh user name trong file /etc/passwd để ssh user đó mà không cần mật khẩu,…

    Sử dụng mã khai thác (tham khảo tại https://book.hacktricks.xyz/linux-hardening/privilege-escalation/linux-capabilities#cap_dac_override) để ghi đè file /etc/passwd và file /etc/shadow trên host. Ta có thể sử dụng lab trước để download các file này từ host, sau đó thêm user, password để tránh trường hợp bị conflict user.

    Sau khi ghi đè 2 file /etc/passwd và /etc/shadow, ta ssh đến host sử dụng user root, password user root là password của user root trong container (ta kiểm soát được giá trị này)

CAP_SYS_PTRACE

     Khi một container có cap_sys_ptrace nó có thể debug và inject vào một tiến trình khác.
    Khi đứng trong 1 container, để inject được tiến trình ngoài host thì container phải cùng namespace 
pid với host (–pid=host)
     Trong trường hợp host có enable AppArmor, container cần disable tính năng này, vì AppArmor chặn 
1 số system call từ trong container. Nếu AppArmor enable thì ta không thể inject được process của host.
    Trên host ta chạy 1 tiến trình test để làm tiến trình target cho việc inject
    Trong container, inject shellcode vào tiến trình test trên

     Shellcode là payload mở cổng 5600 để lắng nghe shell trên host (https://www.exploit-db.com/exploits/41128) hoặc có thể sử dụng msfvenom để tạo shellcode với payload tùy ý.

Code c để inject shellcode vào 1 tiến trình khác: https://github.com/W3ndige/linux-process-injection/blob/master/inject.c

Sau khi inject, kiểm tra trên host port 5600 đã được mở

    Từ trong container, sử dụng netcat kết nối đến cổng này trên host để lấy shell của host

Attack vector 3Escape from Privileged Containers

Bình thường khi 1 container được tạo, thì nó sẽ được quản lí bởi Docker engine. Docker engine như là 1 cầu nối trung gian giữa host và container, chuyển các syscall từ container đến kernel và giúp container sử dụng tài nguyên của host một cách hợp lí, hiệu quả.

    Khi một container chạy với mode privileged, nó có thể tương tác trực tiếp với host, kế thừa toàn bộ capabilities root user và filesystem trên host.
Chi tiết sự khác biệt giữa container với privileged container trong đường dẫn: https://book.hacktricks.xyz/linux-hardening/privilege-escalation/docker-security/docker-privileged
    Privileged container có thể được tạo ra bằng cách thêm flag –privileged hoặc bằng 1 số option sau:
        –cap-add=ALL –> thêm capabilities  cho container
        –security-opt apparmor=unconfined –> disable apparmor
        –security-opt seccomp=unconfined –> disable seccomp
        –security-opt label:disable –> disable selinux
        –pid=host –> sử dụng pid namespace giống host
        –userns=host –> sử dụng user namespace giống host
        –uts=host –> sử dụng uts namespace giống host
        –cgroupns=host –> sử dụng cgroup namespace giống host
        Mount /dev (mount phân vùng đĩa cứng vào container)

case 1: privileged container + –pid=host

    pid namespace của tiến trình trong container giống với pid namespace của tiến trình trên host
    Khi đó ta có thể chuyển tới namespace của process chạy trên host bằng command nsenter

case 2: privileged container

    Kiểm tra xem container có capabilities cap_sys_admin không? cap_sys_admin cho phép ta mount phân vùng đĩa cứng (/dev/sda*) của host vào container
    Sau đó sử dụng lệnh fdisk -l để list các đĩa cứng và phân vùng đĩa cứng. Đĩa cứng phát hiện đầu tiên sẽ là /dev/sda, thứ 2 sẽ là /dev/sdb,…. đĩa CD ROM phát hiện thứ nhất tên sẽ là /dev/scd0 (hoặc /dev/sr0),..
    Trong trường hợp container không hỗ trợ command fdisk, ta có thể tìm phân vùng đĩa cứng như sau
    Khi đó ta có thể mount đĩa cứng /dev/sda1 này lên container và truy cập vào filesystem của host
cmd: mount /dev/sda1 /mnt

Attack vector 4Share network namespace

     Mặc định container sẽ chạy với interface docker0, địa chỉ ip sẽ bắt đầu với 172.17.0
    Nên khi 1 container chạy với option –network host, nó sẽ sử dụng network interfaces của host, do đó sẽ cùng network namespace và cùng hostname với host.
     Hướng tấn công của trường hợp này là đi tìm và khai thác các dịch vụ khác chạy trên interface này, khi RCE được từ 1 dịch vụ thì đó cũng là RCE host.
    Mình dựng 1 php web server đơn giản trên host, trên đó có 1 shell php, để kiểm tra từ container gọi command, xem kết quả trả về sẽ là gì.
     Trong trường hợp container không cùng network namespace với host
    Trong trường hợp container cùng network namespace với host
   Kết quả mình đã RCE được host từ container cùng network namespace.

Lời kết

Mình đã trình bày những hướng tấn công chính để breakout container khi đã đặt được chân vào container, đó là exposed docker socket, capabilities abuse, privileged container và share network namespace. Còn 1 hướng tấn công nữa là khai thác các CVE ở linux kernel, Docker, containerd, K8s,… như dirty pipe, runC hoặc kiếm Zero days ở các thành phần này, trong phạm vi bài viết này mình sẽ không trình bày ở đây, hi vọng sau này mình sẽ có thời gian để nghiên cứu sâu hơn,

__ezekiel_from_VCI__

534 lượt xem