diff --git a/Cargo.lock b/Cargo.lock index de9c19c..a0c7999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,7 @@ dependencies = [ "simplelog", "tera", "tokio", + "void", ] [[package]] @@ -2421,6 +2422,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 31f0c45..b0cadd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ serde_yaml = "0.9" tera = "1.19.1" simplelog = { version = "0.12.2", features = ["paris"] } fully_pub = "0.1.4" +void = "1" # kubernetes: kube = { version = "0.91.0", features = ["runtime", "derive"] } diff --git a/src/configparser/challenge.rs b/src/configparser/challenge.rs index d3ce291..ccc58ec 100644 --- a/src/configparser/challenge.rs +++ b/src/configparser/challenge.rs @@ -6,8 +6,11 @@ use simplelog::*; use std::collections::BTreeMap; use std::fs; use std::path::Path; +use std::str::FromStr; +use void::Void; use crate::configparser::config::Resource; +use crate::configparser::field_coersion::string_or_struct; pub fn parse_all() -> Vec> { // find all challenge.yaml files @@ -50,13 +53,25 @@ pub fn parse_one(path: &str) -> Result { struct ChallengeConfig { name: String, author: String, + description: String, + #[serde(default)] category: String, - description: String, + + #[serde(default = "default_difficulty")] difficulty: i64, + flag: FlagType, - provide: Vec, - pods: Vec, + + #[serde(default)] + provide: Vec, // optional if no files provided + + #[serde(default)] + pods: Vec, // optional if no containers used +} + +fn default_difficulty() -> i64 { + 1 } #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -98,8 +113,10 @@ struct FileVerifier { #[fully_pub] struct Pod { name: String, - build: BuildSpec, - image: String, + + #[serde(flatten)] + image_source: ImageSource, + env: Option, resources: Option, replicas: i64, @@ -108,20 +125,32 @@ struct Pod { } #[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] +#[serde(rename_all = "lowercase")] #[fully_pub] -enum BuildSpec { - Context(String), - Map(BTreeMap), +enum ImageSource { + #[serde(deserialize_with = "string_or_struct")] + Build(BuildObject), + Image(String), } #[derive(Debug, PartialEq, Serialize, Deserialize)] #[fully_pub] struct BuildObject { context: String, - dockerfile: String, - dockerfile_inline: String, - args: ListOrMap, + dockerfile: Option, + // dockerfile_inline: String, + #[serde(default)] + args: BTreeMap, +} +impl FromStr for BuildObject { + type Err = Void; + fn from_str(s: &str) -> std::result::Result { + Ok(BuildObject { + context: s.to_string(), + dockerfile: None, + args: Default::default(), + }) + } } #[derive(Debug, PartialEq, Serialize, Deserialize)] diff --git a/src/configparser/field_coersion.rs b/src/configparser/field_coersion.rs new file mode 100644 index 0000000..2c51e33 --- /dev/null +++ b/src/configparser/field_coersion.rs @@ -0,0 +1,55 @@ +// stuff to coerce bare string into full build context object +// (based on serde example: https://serde.rs/string-or-struct.html) + +use std::collections::BTreeMap as Map; +use std::fmt; +use std::marker::PhantomData; +use std::str::FromStr; + +use serde::de::{self, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer}; +use void::Void; + +pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + FromStr, + D: Deserializer<'de>, +{ + // This is a Visitor that forwards string types to T's `FromStr` impl and + // forwards map types to T's `Deserialize` impl. The `PhantomData` is to + // keep the compiler from complaining about T being an unused generic type + // parameter. We need T in order to know the Value type for the Visitor + // impl. + struct StringOrStruct(PhantomData T>); + + impl<'de, T> Visitor<'de> for StringOrStruct + where + T: Deserialize<'de> + FromStr, + { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or map") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(FromStr::from_str(value).unwrap()) + } + + fn visit_map(self, map: M) -> Result + where + M: MapAccess<'de>, + { + // `MapAccessDeserializer` is a wrapper that turns a `MapAccess` + // into a `Deserializer`, allowing it to be used as the input to T's + // `Deserialize` implementation. T then deserializes itself using + // the entries from the map visitor. + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) + } + } + + deserializer.deserialize_any(StringOrStruct(PhantomData)) +} diff --git a/src/configparser/mod.rs b/src/configparser/mod.rs index 79ab3ce..eb5aa45 100644 --- a/src/configparser/mod.rs +++ b/src/configparser/mod.rs @@ -1,5 +1,6 @@ pub mod challenge; pub mod config; +pub mod field_coersion; use anyhow::{anyhow, Error, Result}; pub use config::UserPass; // reexport diff --git a/tests/repo/pwn/notsh/.gitignore b/tests/repo/pwn/notsh/.gitignore new file mode 100644 index 0000000..5761abc --- /dev/null +++ b/tests/repo/pwn/notsh/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/tests/repo/pwn/notsh/Dockerfile b/tests/repo/pwn/notsh/Dockerfile new file mode 100644 index 0000000..d8bdd5a --- /dev/null +++ b/tests/repo/pwn/notsh/Dockerfile @@ -0,0 +1,42 @@ +# IMAGE 1: build challenge +# @AUTHOR: if your chal doesn't build seperately from being run (i.e. Python), +# delete all of the IMAGE 1 code +FROM ubuntu:18.04 AS builder + +# @AUTHOR: build requirements here +RUN apt-get -qq update && apt-get -qq --no-install-recommends install build-essential + +WORKDIR /build + +# @AUTHOR: make sure all source is copied in. If everything is in src/, no change needed +COPY src ./src/ +COPY Makefile . +RUN make container + +# IMAGE 2: run challenge +# @AUTHOR: feel free to change base image as necessary (i.e. python, node) +FROM ubuntu:18.04 + +# @AUTHOR: run requirements here +RUN apt-get -qq update && apt-get -qq --no-install-recommends install xinetd + +# copy binary +WORKDIR /chal +# @AUTHOR: make sure all build outputs are copied to the runner +# if there is no build output, replace this with the appropriate COPY stmts +# to pull files from the host +COPY --from=builder /build/notsh /chal/ + +# copy flag +COPY flag /chal/ + +# make user +RUN useradd chal + +# copy service info +COPY container_src/* / + +# run challenge +EXPOSE 31337 +RUN chmod +x /run_chal.sh +CMD ["/usr/sbin/xinetd", "-syslog", "local0", "-dontfork", "-f", "/xinetd.conf"] diff --git a/tests/repo/pwn/notsh/Makefile b/tests/repo/pwn/notsh/Makefile new file mode 100644 index 0000000..bdcdd8d --- /dev/null +++ b/tests/repo/pwn/notsh/Makefile @@ -0,0 +1,23 @@ +CC=gcc +C_FLAGS=-Wall # disable NX: -z execstack + # disable canary: -fno-stack-protector + # disable PIE: -no-pie +C_LIBS= # -lcrypto or something + +out=notsh + +.PHONY: all +all: $(out) + +$(out): src/*.c + $(CC) $(C_FLAGS) -o $@ $^ $(C_LIBS) + +# container builds this target +# make sure 'all' builds everything you need +# container builds on ubu1804 +.PHONY: container +container: all + +.PHONY: clean +clean: + $(RM) $(out) *.o diff --git a/tests/repo/pwn/notsh/build-artifacts b/tests/repo/pwn/notsh/build-artifacts new file mode 100644 index 0000000..b8e3f4e --- /dev/null +++ b/tests/repo/pwn/notsh/build-artifacts @@ -0,0 +1,5 @@ +# These need to be absolute paths +/chal/notsh + +# libc path on Ubuntu +/lib/x86_64-linux-gnu/libc.so.6 diff --git a/tests/repo/pwn/notsh/challenge.yaml b/tests/repo/pwn/notsh/challenge.yaml new file mode 100644 index 0000000..1cf42ce --- /dev/null +++ b/tests/repo/pwn/notsh/challenge.yaml @@ -0,0 +1,21 @@ +name: notsh +author: captainGeech +description: |- + This challenge isn't a shell + + `nc {{host}} {{port}}` + +provide: +- ./notsh.zip + +flag: + file: ./flag + +pods: + - name: main + build: . + replicas: 2 + ports: + - internal: 31337 + expose: + tcp: 30124 diff --git a/tests/repo/pwn/notsh/container_src/banner_fail b/tests/repo/pwn/notsh/container_src/banner_fail new file mode 100644 index 0000000..2cfe885 --- /dev/null +++ b/tests/repo/pwn/notsh/container_src/banner_fail @@ -0,0 +1 @@ +XINETD CONNECTION FAILED, PING @ADMIN \ No newline at end of file diff --git a/tests/repo/pwn/notsh/container_src/run_chal.sh b/tests/repo/pwn/notsh/container_src/run_chal.sh new file mode 100644 index 0000000..9baef62 --- /dev/null +++ b/tests/repo/pwn/notsh/container_src/run_chal.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# no stderr +exec 2>/dev/null + +# dir +cd /chal + +# timeout after 20 sec +# @AUTHOR: make sure to set the propery entry point +# <---| don't touch anything left +# | unless you need a longer timeout +timeout -k1 20 stdbuf -i0 -o0 -e0 ./notsh +# ^^ 20 sec timeout \ No newline at end of file diff --git a/tests/repo/pwn/notsh/container_src/xinetd.conf b/tests/repo/pwn/notsh/container_src/xinetd.conf new file mode 100644 index 0000000..fe08d56 --- /dev/null +++ b/tests/repo/pwn/notsh/container_src/xinetd.conf @@ -0,0 +1,19 @@ +service chal +{ + socket_type = stream + protocol = tcp + wait = no + user = chal + type = UNLISTED + bind = 0.0.0.0 + port = 31337 + server = /run_chal.sh + banner_fail = /banner_fail + + # these may need to be adjusted based on how resource + # intensive the challenge is (along with k8s scaling) + nice = 2 + rlimit_cpu = 10 + cps = 10000 10 + instances = 10 +} diff --git a/tests/repo/pwn/notsh/flag b/tests/repo/pwn/notsh/flag new file mode 100644 index 0000000..4fa6e91 --- /dev/null +++ b/tests/repo/pwn/notsh/flag @@ -0,0 +1 @@ +dam{good_test_chal_notsh} diff --git a/tests/repo/pwn/notsh/src/bestpwn.c b/tests/repo/pwn/notsh/src/bestpwn.c new file mode 100644 index 0000000..014009e --- /dev/null +++ b/tests/repo/pwn/notsh/src/bestpwn.c @@ -0,0 +1,32 @@ +#include +#include +#include +#include +#include +#include + +int main() { + char input[20] = {0}; + char flag[40] = {0}; + + puts("hello from notsh v1.0"); + printf("would you like a flag? "); + + fgets(input, 20, stdin); + + input[strcspn(input, "\n")] = 0; + + if (strcmp(input, "yes") == 0) { + puts("ok!"); + + int fd = open("./flag", O_RDONLY); + read(fd, flag, 40); + write(1, flag, 40); + } else if (strcmp(input, "shell") == 0) { + system("/bin/sh"); + } else { + puts("better luck next time!"); + } + + return 0; +} \ No newline at end of file diff --git a/tests/repo/web/bar/Containerfile b/tests/repo/web/bar/Containerfile index 6d7b659..0c82457 100644 --- a/tests/repo/web/bar/Containerfile +++ b/tests/repo/web/bar/Containerfile @@ -1,3 +1,3 @@ FROM nginx -COPY site_source/ /var/www/html/ +COPY site_source/ /usr/share/nginx/html/