Santhacklaus 2019 - Naughty Docker
Posted on mar. 31 décembre 2019 in CTF
It looks like a naughty developer has been deploying a Docker image on a Santa production server a few days before Christmas. He was in a rush and was not able to properly pass all security checks on the built Docker image. Would be a shame if this image could give you an SSH access to the production server... http://46.30.204.47"
Première étape alors, se rendre sur le site internet indiqué.
On a alors le nom de l'image Docker (santactf/app
) ainsi que la commande pour le lancer. Ce que l'on fait de suite :).
$ docker run --rm -p 3000:3000 -d santactf/app
Unable to find image 'santactf/app:latest' locally
latest: Pulling from santactf/app
844c33c7e6ea: Pull complete
ada5d61ae65d: Pull complete
f8427fdf4292: Pull complete
f025bafc4ab8: Pull complete
7a9577c07934: Pull complete
add4f74c413b: Pull complete
1ee7a33fb93f: Pull complete
08ab1881dcea: Pull complete
96f3027f0dbd: Pull complete
cb67eac57f41: Pull complete
bf44330d5df8: Pull complete
4932e843cace: Pull complete
f0b9c596601c: Pull complete
Digest: sha256:621c884f7ddd0351fbb114e0b9c1d4d3b0e309cb5c5efc9ce872fd201af79cad
Status: Downloaded newer image for santactf/app:latest
8c5aff2e1ca7ee420ed7599494d53a3d6fbdeab47f6a034c6c52ea2e6b3ba329
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8c5aff2e1ca7 santactf/app "docker-entrypoint.s…" 24 seconds ago Up 17 seconds 0.0.0.0:3000->3000/tcp festive_easley
Ok. On a notre docker de démarrer. Voyant ce que nous pouvons obtenir sur le port 3000 :
Rien de concluant en soit...
Essayons de voir le contenu du docker et le fonctionnement de l'application :
# On se connect directement en root via le -u 0
$ docker exec -u 0 -it 8c5aff2e1ca7 bash
root@8c5aff2e1ca7:/home/node# id
uid=0(root) gid=0(root) groups=0(root)
root@8c5aff2e1ca7:/home/node# ls
node_modules package-lock.json package.json server.js
root@8c5aff2e1ca7:/home/node# ls -al
total 44
drwxr-xr-x 1 root root 4096 Dec 18 20:55 .
drwxr-xr-x 1 root root 4096 Dec 16 07:28 ..
-rw-r--r-- 1 node node 220 May 15 2017 .bash_logout
-rw-r--r-- 1 node node 675 May 15 2017 .profile
drwxr-xr-x 45 root root 4096 Dec 18 20:55 node_modules
-rw-r--r-- 1 root root 12606 Dec 16 23:34 package-lock.json
-rw-r--r-- 1 root root 241 Dec 16 23:34 package.json
-rw-r--r-- 1 root root 458 Dec 18 20:53 server.js
C'est donc une application nodejs qui est lancé. Le fichier principal est donc server.js :
const fastify = require('fastify')({
logger: true
});
fastify.get('/', function (request, reply) {
reply.send('Some production Santa CTF app');
});
fastify.listen(3000, '0.0.0.0', function (err, address) {
if (err) {
fastify.log.error(err);
process.exit(1);
}
fastify.log.info(`Server listening on ${address}`);
});
process.on('SIGTERM', function () {
fastify.close(function(){
process.exit(0);
});
});
C'est juste un serveur web basique affichant la phrase vue plus haut. Rien côté fichier et dans le docker.
Regardons du coup comment ce docker est construit. On voit lors de sa récupération qu'il est composer de 13 couches en tout. N'ayant pas de dockerfile à disposition, la commande docker history
peut nous aider à le reconstruire et comprendre l'enchaînement des commandes qui ont permis sa réalisation :
$ docker history --no-trunc santactf/app:latest
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:ddde36e2209357c424cca26ac5a0b46c2f864be797c053bed700422177ba7261 11 days ago /bin/sh -c #(nop) CMD ["node" "server.js"] 0B
<missing> 11 days ago /bin/sh -c #(nop) USER node 0B
<missing> 11 days ago /bin/sh -c #(nop) COPY file:8b53431519dafa70baa13c0dd04861e8688090bfece040ae71244d2e14a66845 in /home/node/ 458B
<missing> 11 days ago /bin/sh -c npm ci 5.59MB
<missing> 11 days ago /bin/sh -c #(nop) COPY multi:2f093554c78265fc6aeb1cb343015e8e8e7227fee6a0504f55721b9af13a16a6 in /home/node/ 12.8kB
<missing> 11 days ago /bin/sh -c #(nop) WORKDIR /home/node 0B
<missing> 11 days ago /bin/sh -c #(nop) EXPOSE 3000 0B
<missing> 11 days ago /bin/sh -c #(nop) CMD ["node"] 0B
<missing> 11 days ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entrypoint.sh"] 0B
<missing> 11 days ago /bin/sh -c #(nop) COPY file:6781e799bed1693e0357678a6692f346b66879c2248ff055a2ff51cc0a83288b in /usr/local/bin/ 116B
<missing> 11 days ago /bin/sh -c ln -s /usr/local/bin/node /usr/local/bin/nodejs && rm /home/node/.bashrc /home/node/.bash_history && rm -rf /usr/share/prod-common 19B
<missing> 11 days ago /bin/sh -c ARCH= && dpkgArch="$(dpkg --print-architecture)" && case "${dpkgArch##*-}" in amd64) ARCH='x64';; ppc64el) ARCH='ppc64le';; s390x) ARCH='s390x';; arm64) ARCH='arm64';; armhf) ARCH='armv7l';; i386) ARCH='x86';; *) echo "unsupported architecture"; exit 1 ;; esac && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" && tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1 --no-same-owner && rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" SHASUMS256.txt.asc 67.2MB
<missing> 11 days ago /bin/sh -c #(nop) COPY dir:795933707ce316a3189ec6fd11f015b1acbc4eae6d5f01185625a86edaa2c5c4 in / 18.6kB
<missing> 11 days ago /bin/sh -c #(nop) ENV NODE_VERSION=12.13.1 0B
<missing> 11 days ago /bin/sh -c groupadd --gid 1000 node && useradd --uid 1000 --gid node --shell /bin/bash --create-home node 333kB
<missing> 5 weeks ago /bin/sh -c set -ex; apt-get update; apt-get install -y --no-install-recommends autoconf automake bzip2 dpkg-dev file g++ gcc imagemagick libbz2-dev libc6-dev libcurl4-openssl-dev libdb-dev libevent-dev libffi-dev libgdbm-dev libglib2.0-dev libgmp-dev libjpeg-dev libkrb5-dev liblzma-dev libmagickcore-dev libmagickwand-dev libmaxminddb-dev libncurses5-dev libncursesw5-dev libpng-dev libpq-dev libreadline-dev libsqlite3-dev libssl-dev libtool libwebp-dev libxml2-dev libxslt-dev libyaml-dev make patch unzip xz-utils zlib1g-dev $( if apt-cache show 'default-libmysqlclient-dev' 2>/dev/null | grep -q '^Version:'; then echo 'default-libmysqlclient-dev'; else echo 'libmysqlclient-dev'; fi ) ; rm -rf /var/lib/apt/lists/* 562MB
<missing> 5 weeks ago /bin/sh -c apt-get update && apt-get install -y --no-install-recommends bzr git mercurial openssh-client subversion procps && rm -rf /var/lib/apt/lists/* 142MB
<missing> 5 weeks ago /bin/sh -c set -ex; if ! command -v gpg > /dev/null; then apt-get update; apt-get install -y --no-install-recommends gnupg dirmngr ; rm -rf /var/lib/apt/lists/*; fi 7.81MB
<missing> 5 weeks ago /bin/sh -c apt-get update && apt-get install -y --no-install-recommends ca-certificates curl netbase wget && rm -rf /var/lib/apt/lists/* 23.2MB
<missing> 5 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 5 weeks ago /bin/sh -c #(nop) ADD file:152359c10cf61d80091bfd19e7e1968a538bebebfa048dca0386e35e1e999730 in / 101MB
Ah ! Enfin des choses intéressantes qui remontent. Il faut prendre du bas vers le haut. Les premières commandes mettent à jour le système et installent les dépendances. La suite est plus intéressantes :
/bin/sh -c ln -s /usr/local/bin/node /usr/local/bin/nodejs && rm /home/node/.bashrc /home/node/.bash_history && rm -rf /usr/share/prod-common
On remarque la suppression de deux fichiers et d'un dossier. Essayons donc de voir comment on peut récupérer le contenu :).
On cherchant de la documentation sur internet, on tombe sur cet article Medium : https://medium.com/@jessgreb01/digging-into-docker-layers-c22f948ed612. Il nous parle d'un dossier /var/lib/docker/aufs
. Cependant, ce dossier n'existe pas sur mon serveur :
$ ls /var/lib/docker -l
total 48
drwx------ 2 root root 4096 Sep 27 14:43 builder
drwx--x--x 4 root root 4096 Sep 27 14:43 buildkit
drwx------ 4 root root 4096 Dec 30 17:21 containers
drwx------ 3 root root 4096 Sep 27 14:43 image
drwxr-x--- 3 root root 4096 Sep 27 14:43 network
drwx------ 35 root root 4096 Dec 30 17:21 overlay2
drwx------ 4 root root 4096 Sep 27 14:43 plugins
drwx------ 2 root root 4096 Dec 21 18:26 runtimes
drwx------ 2 root root 4096 Sep 27 14:43 swarm
drwx------ 2 root root 4096 Dec 30 17:21 tmp
drwx------ 2 root root 4096 Sep 27 14:43 trust
drwx------ 5 root root 4096 Sep 27 14:48 volumes
Cependant, un dossier overlay2
est présent et évoqué en fin d'article comme un possible successeur de AUFS. On vérfie lequel docker utilise :
$ docker info
Client:
Debug Mode: false
Server:
Containers: 2
Running: 1
Paused: 0
Stopped: 1
Images: 2
Server Version: 19.03.5
Storage Driver: overlay2
Ok, on sait donc qu'il faut non pas chercher sur AUFS
mais sur overlay2
. La page dans la documentation officielle docker nous aide sur son fonctionnement : https://docs.docker.com/storage/storagedriver/overlayfs-driver/#how-the-overlay2-driver-works.
Il nous faut donc retrouver le dossier prod-common
ainsi que les deux fichiers .bashrc
et .bash_history
dans cette ensemble de dossier. Un find est c'est fait :) :
$ find . -name ".bash_history" | grep node
./beb357bfdfd498ff0fbb507996c034316381dc3a7c163890412f33fc3323c84b/diff/home/node/.bash_history
./71f44852a81b6d28dbf5c6d8d0d64857aa9d00ed5b647c4471c2e3df27cb5855/diff/home/node/.bash_history
$ wc -l ./71f44852a81b6d28dbf5c6d8d0d64857aa9d00ed5b647c4471c2e3df27cb5855/diff/home/node/.bash_history
wc: ./71f44852a81b6d28dbf5c6d8d0d64857aa9d00ed5b647c4471c2e3df27cb5855/diff/home/node/.bash_history: No such device or address
$ wc -l ./beb357bfdfd498ff0fbb507996c034316381dc3a7c163890412f33fc3323c84b/diff/home/node/.bash_history
153 ./beb357bfdfd498ff0fbb507996c034316381dc3a7c163890412f33fc3323c84b/diff/home/node/.bash_history
BINGO ! On a retrouvé le dossier contenu les fichiers recherchés. Un tree
sur le dossier nous le confirme :
$ tree -a
.
├── home
│ └── node
│ ├── .bash_history
│ └── .bashrc
└── usr
└── share
└── prod-common
├── dev_081219_backup.zip
├── dev_091219_backup.zip
├── dev_101219_backup.zip
├── dev_111219_backup.zip
├── dev_121219_backup.zip
├── dev_131219_backup.zip
├── dev_141219_backup.zip
├── dev_151219_backup.zip
└── dev_161219_backup.zip
5 directories, 11 files
Le zip se trouvant dans le dossier prod-common
peut indiqué la piste souhaité pour se connecter sur le serveur de production. La commande zipinfo
va nous donner des informations sur leur contenu :
$ zipinfo usr/share/prod-common/dev_081219_backup.zip
Archive: usr/share/prod-common/dev_081219_backup.zip
Zip file size: 1290 bytes, number of entries: 2
-rw------- 3.0 unx 821 TX defN 19-Dec-18 21:31 id_santa_production
-rw-r--r-- 3.0 unx 296 TX defN 19-Dec-18 21:31 id_santa_production.pub
2 files, 1117 bytes uncompressed, 872 bytes compressed: 21.9%
Parfait. Des clés SSH (publique et privée) pour y accéder. On extrait donc son contenu :
$ unzip dev_081219_backup.zip
Archive: dev_081219_backup.zip
[dev_081219_backup.zip] id_santa_production password:
Cela ne pouvait pas être aussi facil :(. Regardons si nous pouvons avoir des indications ou même, le mot de passe dans un des deux autres fichiers que nous avons récupérer. Une recherche sur les deux fichiers avec pass
peut nous mettre sur la voie :
$ grep -rHin 'pass' .
./.bash_history:49:vncpasswd
./.bash_history:50:vncpasswd -type
./.bash_history:54:vncpasswd -type Password
./.bash_history:55:vncpasswd -type "Password"
./.bash_history:115:zip --password "$ARCHIVE_PIN" "$PRODUCTION_BACKUP_FILE" id_santa_production*
$ grep -rHin 'ARCHIVE_PIN' .
./.bash_history:61:export ARCHIVE_PIN=25362
./.bash_history:115:zip --password "$ARCHIVE_PIN" "$PRODUCTION_BACKUP_FILE" id_santa_production*
Et c'est le cas. On a le code PIN pour l'archive mais on ne sait cependant pas quelle archive est rattachée à ce PIN. On peut donc essayer sur l'ensemble avec la commande find
:
$ find usr/share/prod-common -exec unzip -P "25362" {} \;
unzip: cannot find or open ., ..zip or ..ZIP.
Archive: ./dev_081219_backup.zip
skipping: id_santa_production incorrect password
skipping: id_santa_production.pub incorrect password
Archive: ./dev_131219_backup.zip
skipping: id_santa_production incorrect password
skipping: id_santa_production.pub incorrect password
Archive: ./dev_151219_backup.zip
skipping: id_santa_production incorrect password
skipping: id_santa_production.pub incorrect password
Archive: ./dev_161219_backup.zip
skipping: id_santa_production incorrect password
skipping: id_santa_production.pub incorrect password
Archive: ./dev_141219_backup.zip
inflating: id_santa_production
inflating: id_santa_production.pub
Archive: ./dev_121219_backup.zip
skipping: id_santa_production incorrect password
skipping: id_santa_production.pub incorrect password
Archive: ./dev_091219_backup.zip
skipping: id_santa_production incorrect password
skipping: id_santa_production.pub incorrect password
Archive: ./dev_101219_backup.zip
skipping: id_santa_production incorrect password
skipping: id_santa_production.pub incorrect password
Archive: ./dev_111219_backup.zip
skipping: id_santa_production incorrect password
skipping: id_santa_production.pub incorrect password
$ ls -l
[...]
-rw------- 1 root root 821 Dec 18 21:31 id_santa_production
-rw-r--r-- 1 root root 296 Dec 18 21:31 id_santa_production.pub
Parfait. On a bien les clés. Mais on a toujours pas la commande SSH pour ce connecter. Comme pour le PIN, un grep
suffit à retrouver l'information.
$ grep -rHin 'ssh' home/node/
.bash_history:56:sudo nano /etc/ssh/sshd_config
.bash_history:57:sudo service restart ssh
.bash_history:58:sudo service ssh restart
.bash_history:62:ls ~/.ssh
.bash_history:64:ssh-keygen -t rsa -C jmding0714@gmail.com
.bash_history:65:cd ~/.ssh/
.bash_history:129:nano ~/.ssh/authorized_keys
.bash_history:130:ssh -p 5700 rudolf-the-reindeer@46.30.204.47
On peut donc essayer de se connecter avec la clé et les informations trouvées :
$ ssh -p 5700 -i usr/share/prod-common/id_santa_production rudolf-the-reindeer@46.30.204.47
Enter passphrase for key 'usr/share/prod-common/id_santa_production':
Bon. Nouveau mot de passe à trouvé. Cela ne pouvait pas être aussi simple :(. On reprend donc la recherche avec grep
:
$ grep -rHin 'password' home/node/
home/node/.bash_history:54:vncpasswd -type Password
home/node/.bash_history:55:vncpasswd -type "Password"
home/node/.bash_history:115:zip --password "$ARCHIVE_PIN" "$PRODUCTION_BACKUP_FILE" id_santa_production*
$ grep -rHin 'pwd' home/node/
home/node/.bash_history:78:pwd
home/node/.bashrc:68: export PRD_PWD='HoHoHo2020!NorthPole'
Et une fois de plus, c'est une victoire pour grep
!
On relance notre commande ssh
:
$ ssh -p 5700 -i usr/share/prod-common/id_santa_production rudolf-the-reindeer@46.30.204.47
Enter passphrase for key 'usr/share/prod-common/id_santa_production':
___ _ _ _ _____ _ ___ _____ ___
/ __| /_\ | \| |_ _/_\ / __|_ _| __|
\__ \/ _ \| .` | | |/ _ \ | (__ | | | _|
|___/_/ \_\_|\_| |_/_/ \_\ \___| |_| |_|
Well done, the flag is SANTA{NeverTrustDockerImages7263}
You may now log out of this server with "exit"
-bash-5.0$
Et c'est le FLAG !
Très bon challenge pour découvrir et mieux comprendre le fonctionnement de Docker. Merci <3.