Skip to content

Commit

Permalink
add xss-bot sample challenge
Browse files Browse the repository at this point in the history
  • Loading branch information
sroettger committed Feb 7, 2020
1 parent aa55fd4 commit 441c208
Show file tree
Hide file tree
Showing 23 changed files with 619 additions and 0 deletions.
Binary file not shown.
29 changes: 29 additions & 0 deletions infrastructure/kctf/samples/xss-bot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM kctf-nsjail

RUN apt-get -y update
RUN apt-get -y upgrade

RUN mkdir /home/user

RUN apt-get install -y nodejs npm
RUN cd /home/user && npm install puppeteer

# See https://crbug.com/795759
RUN apt-get install -yq libgconf-2-4

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update && apt-get install -y wget --no-install-recommends \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst \
--no-install-recommends

RUN cd /home/user && npm install proof-of-work

COPY files /home/user

# Since we don't need nsjail here, we could also run as user in the k8s config
CMD exec setpriv --init-groups --reset-env --reuid user --regid user --inh-caps=-all -- /usr/bin/bash -c 'while true; do /usr/bin/node /home/user/bot.js; done'
227 changes: 227 additions & 0 deletions infrastructure/kctf/samples/xss-bot/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
.PHONY: start stop docker ip status logs ssh clean .deploy .maybe-expose .cluster-config .deployment .autoscaling .secrets .config .healthcheck-secrets .healthcheck-exploit-key-secret .healthcheck-config .FORCE

SHELL := bash
.ONESHELL:
.SHELLFLAGS = -e -c

PROJECT:=CONFIGMISSING
CLUSTER_NAME:=CONFIGMISSING
ZONE:=CONFIGMISSING
-include $(HOME)/.config/kctf/cluster.conf
KUBECONFIG=$(HOME)/.config/kctf/kube.conf
export KUBECONFIG

CHALLENGE_NAME:=$(shell basename ${CURDIR})
CLUSTER_GEN=gen/${PROJECT}_${ZONE}_${CLUSTER_NAME}
DEPLOYMENT_CONF_DIR=${CLUSTER_GEN}/deployment-conf

docker: gen/docker-image

start:
source chal.conf
if [ $${DEPLOY} = "true" ]; then
$(MAKE) .deploy
else
echo "skipping deployment: DEPLOY=\"$${DEPLOY}\""
fi

stop: .cluster-config
for resource_type in deployment service configMap hpa; do
kubectl get "$${resource_type}/${CHALLENGE_NAME}" >/dev/null 2>&1 && kubectl delete "$${resource_type}/${CHALLENGE_NAME}" || true
done
kubectl get "secret/${CHALLENGE_NAME}-secrets" >/dev/null 2>&1 && kubectl delete "secret/${CHALLENGE_NAME}-secrets" || true
for resource in "secret/${CHALLENGE_NAME}-healthcheck-exploit-key" "secret/${CHALLENGE_NAME}-healthcheck-secrets" "configMap/${CHALLENGE_NAME}-healthcheck-config"; do
kubectl get "$${resource}" >/dev/null 2>&1 && kubectl delete "$${resource}" || true
done

ip: .cluster-config
LB_IP=""
while [ -z "$${LB_IP}" ]; do
LB_IP=$$(kubectl get "service/${CHALLENGE_NAME}" -o=jsonpath='{.status.loadBalancer.ingress[0].ip}')
sleep 3
done
echo "$${LB_IP}"

status: .cluster-config
@echo "= INSTANCES / PODs ="
@echo
@echo "Challenge execution status"
@echo "This shows you how many instances of the challenges are running."
@echo
@kubectl get pods -l "app=${CHALLENGE_NAME}" -o wide
@echo
@echo
@echo "= DEPLOYMENTS ="
@echo
@echo "Challenge deployment status"
@echo "This shows you if the challenge was deployed to the cluster."
@echo
@kubectl get deployments -l "app=${CHALLENGE_NAME}" -o wide
@echo
@echo "= EXTERNAL SERVICES ="
@echo
@echo "Challenge external status"
@echo "This shows you if the challenge is exposed externally."
@echo
@kubectl get services -l "app=${CHALLENGE_NAME}" -o wide
@echo

logs: .cluster-config
kubectl logs -l "app=${CHALLENGE_NAME}" -c challenge

ssh: .cluster-config
kubectl exec deployment/${CHALLENGE_NAME} -c challenge -it /bin/bash

clean:
rm -R gen/* || true
rm -R healthcheck/gen/* || true

.deploy: .autoscaling .deployment .secrets .config .healthcheck-secrets .healthcheck-exploit-key-secret .healthcheck-config .cluster-config .maybe-expose

.maybe-expose: | .cluster-config
source chal.conf
if [ "$${PUBLIC}" = "true" ]; then
kubectl apply -f k8s/network.yaml
else
kubectl get "service/${CHALLENGE_NAME}" >/dev/null 2>&1 && kubectl delete "service/${CHALLENGE_NAME}" || true
fi

.autoscaling: | .cluster-config
kubectl apply -f k8s/autoscaling.yaml

.deployment: ${DEPLOYMENT_CONF_DIR} ${CLUSTER_GEN}/image-pushed ${CLUSTER_GEN}/remote-image ${CLUSTER_GEN}/healthcheck-image-pushed ${CLUSTER_GEN}/remote-healthcheck-image | .cluster-config
kubectl apply -k ${DEPLOYMENT_CONF_DIR}
# update the challenge container if the image changed
PUSHED_IMAGE="$$(cat ${CLUSTER_GEN}/image-tagged)"
CHAL_IMAGE="$$(cat ${CLUSTER_GEN}/remote-image)"
if [ $${CHAL_IMAGE} != $${PUSHED_IMAGE} ]; then
kubectl set image "deployment/${CHALLENGE_NAME}" "challenge=$${PUSHED_IMAGE}"
fi
# update the healthcheck container if the image changed
PUSHED_HEALTHCHECK_IMAGE="$$(cat ${CLUSTER_GEN}/healthcheck-image-tagged)"
HEALTHCHECK_IMAGE="$$(cat ${CLUSTER_GEN}/remote-healthcheck-image)"
if [ $${HEALTHCHECK_IMAGE} != $${PUSHED_HEALTHCHECK_IMAGE} ]; then
kubectl set image "deployment/${CHALLENGE_NAME}" "healthcheck=$${HEALTHCHECK_IMAGE}"
fi

.secrets: $(shell find secrets) | .cluster-config
kubectl apply -k secrets

.config: $(shell find config) | .cluster-config
kubectl apply -k config

gen/docker-image: Dockerfile files gen/src $(shell find files) ../kctf-conf/base/nsjail-docker/gen/docker-image
docker build -t "kctf-chal-${CHALLENGE_NAME}" .
echo $$(docker image ls "kctf-chal-${CHALLENGE_NAME}" -q) > $@

gen/src: .FORCE
$(MAKE) -C src ../gen/src

${CLUSTER_GEN}/image-tagged: gen/docker-image | .cluster-config
IMAGE_ID="$$(cat gen/docker-image)"
IMAGE_TAG="eu.gcr.io/${PROJECT}/${CHALLENGE_NAME}:$${IMAGE_ID}"
docker tag "kctf-chal-${CHALLENGE_NAME}" "$${IMAGE_TAG}"
echo -n "$${IMAGE_TAG}" > $@

${CLUSTER_GEN}/image-pushed: ${CLUSTER_GEN}/image-tagged
IMAGE_TAG="$$(cat ${CLUSTER_GEN}/image-tagged)"
docker push "$${IMAGE_TAG}"
touch $@

gen/healthcheck-docker-image: healthcheck/Dockerfile healthcheck/gen/exploit.cpio.enc $(shell find healthcheck/files)
docker build -t "kctf-healthcheck-${CHALLENGE_NAME}" healthcheck
echo $$(docker image ls "kctf-healthcheck-${CHALLENGE_NAME}" -q) > $@

healthcheck/gen/exploit.cpio.enc: healthcheck/gen/exploit.cpio healthcheck/gen/exploit.key
openssl aes-256-cbc -e -in healthcheck/gen/exploit.cpio -out $@ -K "$$(cat healthcheck/gen/exploit.key)" -nosalt -iv 00000000000000000000000000000000

healthcheck/gen/exploit.cpio: $(shell find healthcheck/exploit)
pushd ${@D}
rm -R exploit 2>/dev/null || true
cp -R ../exploit .
for f in $$(find exploit); do
TZ="UTC" touch -a -m -t 198001010000.00 $$f
done
rm exploit.cpio || true
find exploit -print0 | sort -z | cpio -0 --reproducible -R 0:0 -o > exploit.cpio
rm -R exploit
popd

healthcheck/gen/exploit.key: healthcheck/gen/exploit.cpio
sha256sum healthcheck/gen/exploit.cpio | awk '{print $$1}' > $@

${CLUSTER_GEN}/healthcheck-image-tagged: gen/healthcheck-docker-image | .cluster-config
IMAGE_ID="$$(cat gen/healthcheck-docker-image)"
IMAGE_TAG="eu.gcr.io/${PROJECT}/${CHALLENGE_NAME}-healthcheck"
docker tag "kctf-healthcheck-${CHALLENGE_NAME}" "$${IMAGE_TAG}"
echo -n "$${IMAGE_TAG}" > $@

${CLUSTER_GEN}/healthcheck-image-pushed: ${CLUSTER_GEN}/healthcheck-image-tagged
IMAGE_TAG="$$(cat ${CLUSTER_GEN}/healthcheck-image-tagged)"
docker push "$${IMAGE_TAG}"
touch $@

healthcheck/gen/exploit-key.yaml: healthcheck/gen/exploit.key | .cluster-config
kubectl create secret generic "${CHALLENGE_NAME}-healthcheck-exploit-key" --from-file=exploit.key=healthcheck/gen/exploit.key --dry-run -o yaml > $@

.healthcheck-exploit-key-secret: healthcheck/gen/exploit-key.yaml | .cluster-config
kubectl apply -f healthcheck/gen/exploit-key.yaml

.healthcheck-secrets: $(shell find healthcheck/secrets) | .cluster-config
kubectl apply -k healthcheck/secrets

.healthcheck-config: $(shell find healthcheck/config) | .cluster-config
kubectl apply -k healthcheck/config

../kctf-conf/base/nsjail-docker/gen/docker-image: .FORCE
$(MAKE) -C ${@D}/.. gen/docker-image

define CLUSTER_KUSTOMIZATION
bases:
- ../../../k8s
patchesStrategicMerge:
- update_image_name.yaml
endef

define UPDATE_IMAGE_NAME
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${CHALLENGE_NAME}
spec:
template:
spec:
containers:
- name: challenge
image: CHAL_IMAGE_PLACEHOLDER
- name: healthcheck
image: HEALTHCHECK_IMAGE_PLACEHOLDER
endef

export CLUSTER_KUSTOMIZATION
export UPDATE_IMAGE_NAME
${DEPLOYMENT_CONF_DIR}: ${CLUSTER_GEN}/remote-image ${CLUSTER_GEN}/remote-healthcheck-image | .cluster-config
mkdir -p $@
echo "$${CLUSTER_KUSTOMIZATION}" > "${DEPLOYMENT_CONF_DIR}/kustomization.yaml"
CHAL_IMAGE="$$(cat ${CLUSTER_GEN}/remote-image)"
HEALTHCHECK_IMAGE="$$(cat ${CLUSTER_GEN}/remote-healthcheck-image)"
echo "$${UPDATE_IMAGE_NAME}" | sed "s#CHAL_IMAGE_PLACEHOLDER#$${CHAL_IMAGE}#" | sed "s#HEALTHCHECK_IMAGE_PLACEHOLDER#$${HEALTHCHECK_IMAGE}#" > "${DEPLOYMENT_CONF_DIR}/update_image_name.yaml"
touch $@

${CLUSTER_GEN}/remote-image: ${CLUSTER_GEN}/image-tagged .FORCE
PUSHED_IMAGE="$$(cat ${CLUSTER_GEN}/image-tagged)"
(kubectl get deployment/${CHALLENGE_NAME} -o jsonpath='{.spec.template.spec.containers[?(@.name == "challenge")].image}' 2>/dev/null || echo -n "$${PUSHED_IMAGE}") > $@

${CLUSTER_GEN}/remote-healthcheck-image: ${CLUSTER_GEN}/healthcheck-image-tagged .FORCE
PUSHED_IMAGE="$$(cat ${CLUSTER_GEN}/healthcheck-image-tagged)"
(kubectl get deployment/${CHALLENGE_NAME} -o jsonpath='{.spec.template.spec.containers[?(@.name == "healthcheck")].image}' 2>/dev/null || echo -n "$${PUSHED_IMAGE}") > $@

.cluster-config:
@if [ "${PROJECT}" = "CONFIGMISSING" ]; then
@ echo 'error: config not loaded'
@ exit 1
@fi
kubectl config use-context "kctf_${PROJECT}_${ZONE}_${CLUSTER_NAME}"
mkdir -p ${CLUSTER_GEN}

.FORCE:
3 changes: 3 additions & 0 deletions infrastructure/kctf/samples/xss-bot/chal.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# this file will be sourced by the deployment scripts
DEPLOY="true"
PUBLIC="false"
10 changes: 10 additions & 0 deletions infrastructure/kctf/samples/xss-bot/config/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
configMapGenerator:
- name: xss-bot-config
files:
- pow
generatorOptions:
disableNameSuffixHash: true
labels:
type: generated
annotations:
note: generated
1 change: 1 addition & 0 deletions infrastructure/kctf/samples/xss-bot/config/pow
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
100 changes: 100 additions & 0 deletions infrastructure/kctf/samples/xss-bot/files/bot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const puppeteer = require('puppeteer');
const process = require('process');
const fs = require('fs');
const net = require('net');
const pow = require('proof-of-work');
const crypto = require("crypto");

(async function(){
const browser = await puppeteer.launch();

function ask_for_url(socket) {
socket.state = 'URL';
socket.write('Please send me a URL to open.\n');
}

function ask_for_pow(socket) {
let pow_difficulty = Number.parseInt(fs.readFileSync('/config/pow'));
if (pow_difficulty) {
socket.write('Proof-of-work enabled.\n');
verifier = new pow.Verifier({
size: 1024,
n: 16,
complexity: pow_difficulty,
prefix: crypto.randomBytes(2),
validity: 180000
});
socket.state = 'POW';
socket.write(`Please solve a proof-of work with difficulty ${pow_difficulty} and prefix ${verifier.prefix.toString('hex')} using https://www.npmjs.com/package/proof-of-work\n`);
} else {
socket.write('Proof-of-work disabled.\n');
ask_for_url(socket);
}
}

function validate_pow(socket, data) {
if (verifier.check(Buffer.from(data.toString().trim(), 'hex'))) {
socket.write('Proof-of-work verified.\n');
ask_for_url(socket);
} else {
socket.state = 'ERROR';
socket.write('Proof-of-work invalid.\n');
socket.destroy();
}
}

async function load_url(socket, data) {
let url = data.toString().trim();
console.log(`checking url: ${url}`);
if (!url.startsWith('http://') && !url.startsWith('https://')) {
socket.state = 'ERROR';
socket.write('Invalid scheme (http/https only).\n');
socket.destroy();
return;
}
socket.state = 'LOADED';
let cookie = JSON.parse(fs.readFileSync('/secrets/cookie'));

const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
await page.setCookie(cookie);
socket.write(`Loading page ${url}.\n`);
await page.goto(url);
setTimeout(()=>{
try {
page.close();
socket.write('timeout\n');
socket.destroy();
} catch (err) {
console.log(`err: ${err}`);
}
}, 60000);
}

var server = net.createServer();
server.listen(1337);
console.log('listening on port 1337');

server.on('connection', socket=>{
socket.captcha = 'foo';

socket.on('data', data=>{
try {
if (socket.state == 'POW') {
validate_pow(socket, data);
} else if (socket.state == 'URL') {
load_url(socket, data);
}
} catch (err) {
console.log(`err: ${err}`);
}
});

try {
ask_for_pow(socket);
} catch (err) {
console.log(`err: ${err}`);
}
});
})();

3 changes: 3 additions & 0 deletions infrastructure/kctf/samples/xss-bot/gen/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# this directory is for Makefile generated outputs
*
!.gitignore
17 changes: 17 additions & 0 deletions infrastructure/kctf/samples/xss-bot/healthcheck/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM ubuntu:19.10

RUN apt-get -y update
RUN apt-get -y upgrade

RUN apt-get -y install python2.7 python-pip
RUN pip install pwntools

RUN apt-get -y install cpio

COPY gen/exploit.cpio.enc /

RUN /usr/sbin/useradd --no-create-home -u 1000 user
COPY files /home/user
RUN mkdir /home/user/.pwntools-cache && echo never > /home/user/.pwntools-cache/update

CMD openssl aes-256-cbc -d -in exploit.cpio.enc -out exploit.cpio -K "$(cat /keys/exploit.key)" -nosalt -iv 00000000000000000000000000000000 && < exploit.cpio cpio -i && rm /exploit.cpio.enc && rm /exploit.cpio && setpriv --init-groups --reuid user --regid user --inh-caps=-all -- /exploit/run.sh & /home/user/healthz.py
Loading

0 comments on commit 441c208

Please sign in to comment.