commit 1109a88447e1c5cefe6ed93eccc8dcf8cd595d0e
parent ea33a19c447df8cc3748a8b6dc8e45121fdc3570
Author: Louis Burda <quent.burda@gmail.com>
Date: Thu, 20 May 2021 02:49:09 +0200
implemented rest of checker functionality
Diffstat:
12 files changed, 274 insertions(+), 114 deletions(-)
diff --git a/checker/.dockerignore b/checker/.dockerignore
@@ -3,4 +3,5 @@ Dockerfile
docker-compose.yml
.dockerignore
.gitignore
-.env
-\ No newline at end of file
+.env
+test.sh
diff --git a/checker/src/checker.py b/checker/src/checker.py
@@ -1,65 +1,96 @@
#!/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
+import random, string, struct, logging, selectors, time, socket
+import pwnlib
import numpy as np
logging.getLogger("faker").setLevel(logging.WARNING)
+logging.getLogger("pwnlib").setLevel(logging.WARNING)
from faker import Faker
-fake = Faker(["en_US"])
+def ensure_bytes(v):
+ if type(v) == bytes:
+ return v
+ elif type(v) == str:
+ return v.encode("latin-1")
+ else:
+ raise BrokenServiceException("Tried to pass non str/bytes to bytes arg")
class STLDoctorChecker(BaseChecker):
- flag_variants = 1
- noise_variants = 1
+ flag_variants = 2
+ noise_variants = 2
havoc_variants = 4
service_name = "stldoctor"
port = 9000
- def login_user(self, conn: SimpleSocket, password: str):
- self.debug("Sending command to login.")
- conn.write(f"login\n{password}\n")
- conn.readline_expect(b"logged in!", read_until=b"$", exception_message="Failed to log in")
-
- def binwrite(conn: SimpleSocket, buf: bytes):
- conn.sock.sendall(buf)
-
- def fake_filename(self):
- allowed = "abcdefghijklmopqrstuvwxyz0123456789-+.!"
- return "".join([c for c in fake.name().lower().replace(" ", "-") if c in allowed][:60]).ljust(5, "!")
+ debuglog = True
- def generate_ascii_file(self, solidname: str):
- if solidname != "":
- content = f"solid {solidname}\n"
+ 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")
+
+ def write(self, conn, buf):
+ if self.debuglog:
+ print("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().lower().replace(" ", "-") if c in allowed][:60]).ljust(10, "!")
+
+ def havocid(self):
+ return "".join([chr(int(random.randint(ord(' '), 255))) for i in range(60)])
+
+ def do_auth(self, conn: SimpleSocket, authstr: str):
+ self.write(conn, f"auth {authstr}\n")
+ resp = conn.recvuntil("$")
+ 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("$")
+ assert_in(modelid, resp, f"Uploaded model is missing from list command")
+
+ def generate_ascii_file(self, solidname):
+ solidname = ensure_bytes(solidname)
+
+ if len(solidname) != 0:
+ content = b"solid " + solidname + b"\n"
else:
- content = "solid\n"
- facet_count = int(random.random() * 30) + 4
+ content = b"solid\n"
+ facet_count = random.randint(4, 30)
for fi in range(facet_count):
- content += "facet normal "
+ content += b"facet normal "
vs = [[random.random() for i in range(3)] for k in range(3)]
norm = np.cross(np.subtract(vs[1], vs[0]), np.subtract(vs[2],vs[0]))
norm = norm / np.linalg.norm(norm)
- content += " ".join([f"{v:.2f}" for v in norm]) + "\n"
- content += "outer loop\n"
+ content += " ".join([f"{v:.2f}" for v in norm]).encode() + b"\n"
+ content += b"outer loop\n"
for i in range(3):
- content += "vertex " + " ".join([f"{v:.2f}" for v in vs[i]]) + "\n"
- content += "endloop\n"
- content += "endfacet\n"
+ content += b"vertex " + " ".join([f"{v:.2f}" for v in vs[i]]).encode() + b"\n"
+ content += b"endloop\n"
+ content += b"endfacet\n"
if solidname != b"":
- content += f"endsolid {solidname}\n"
+ content += b"endsolid " + solidname + b"\n"
else:
- content += "endsolid\n"
+ content += b"endsolid\n"
- return content.encode("latin1")
+ return content
+
+ def generate_bin_file(self, solidname):
+ solidname = ensure_bytes(solidname)
- def generate_bin_file(self, solidname: str):
- if len(solidname.encode()) > 78:
+ if len(solidname) > 78:
raise EnoException("Solidname to embed in header is larger than header itself")
if solidname != "":
- content = b"#" + solidname.encode("ascii").ljust(78, b"\x00") + b"\x00"
+ content = b"#" + solidname.ljust(78, b"\x00") + b"\x00"
else:
content = b"#" + b"\x00" * 79
- facet_count = int(random.random() * 30) + 4
+ facet_count = random.randint(4, 30)
content += struct.pack("<I", facet_count)
for fi in range(facet_count):
vs = [[random.random() for i in range(3)] for k in range(3)]
@@ -72,7 +103,7 @@ class STLDoctorChecker(BaseChecker):
content += b"\x00\x00"
return content
- def generate_file(self, filetype: str, solidname: str):
+ def generate_file(self, filetype, solidname):
if filetype == "ascii":
return self.generate_ascii_file(solidname = solidname)
elif filetype == "bin":
@@ -80,39 +111,68 @@ class STLDoctorChecker(BaseChecker):
else:
raise EnoException("Invalid file type supplied");
- def putfile(self, conn: SimpleSocket, solidname: str, modelname: str, filetype: str):
+ def putfile(self, conn, solidname, modelname, filetype):
+ solidname = ensure_bytes(solidname)
+ modelname = ensure_bytes(modelname)
+
# Generate file contents
stlfile = self.generate_file(filetype = filetype, solidname = solidname)
# Upload file
self.debug("Sending command to submit file")
- conn.write("submit\n")
- conn.write(f"{len(stlfile)}\n")
- binwrite(conn, stlfile)
- conn.write(f"{modelname}\n")
- self.debug(b":::RESPONSE:::\n" + conn.read_until(b"with ID "))
+ self.write(conn, "upload\n")
+ self.write(conn, f"{len(stlfile)}\n")
+ self.write(conn, stlfile)
+ self.write(conn, modelname + b"\n")
# Parse ID
- fileid = conn.read_until(b"!")
- if fileid == b"":
+ print(conn.recvuntil("with ID "))
+ modelid = conn.recvuntil(b"!")[:-1]
+ if modelid == b"":
raise BrokenServiceException("Unable to upload file!")
- self.debug(f"Got ID {fileid}")
+ self.debug(f"Got ID {modelid}")
+
+ conn.recvuntil(b"$")
+
+ if self.debuglog:
+ print(f"PUT FILE: {modelid}")
- conn.read_until(b"$")
+ return stlfile, modelid
- return stlfile, fileid
+ def getfile(self, conn, modelname):
+ modelname = ensure_bytes(modelname)
- def getfile(self, conn: SimpleSocket, modelname: str):
if modelname != "":
- self.debug(f"Sending command to retrieve file with '{modelname}'")
- conn.write(f"query\n{modelname}\n0\ny\n")
+ 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
else:
self.debug(f"Sending command to retrieve file")
- conn.write(f"query\n0\ny\n")
+ self.write(conn, f"search\n0\ny\n")
- resp = conn.read_until(b"$")
+ 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")
- return resp
+ 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
+
+ def check_getfile(self, conn, modelname, solidname, contents, modelid = None):
+ resp = self.getfile(conn, modelname = modelname)
+ if modelid:
+ assert_in(ensure_bytes(modelid), resp, f"Model id {modelid} not returned / correctly parsed")
+ assert_in(ensure_bytes(modelname), resp, f"Model name {modelname} not returned / correctly parsed")
+ assert_in(ensure_bytes(solidname), resp, f"Solid name {solidname} not returned / correctly parsed")
+ assert_in(ensure_bytes(contents), resp, f"STL File contents not returned / correctly parsed")
def querydb(self, *args):
self.debug("Querying db contents");
@@ -128,107 +188,137 @@ class STLDoctorChecker(BaseChecker):
def postdb(self, vdict):
self.chain_db = vdict
- def havoc_upload(self, filetype: str, registered: bool):
+ def havoc_upload(self, filetype, register):
+ solidname = self.havocid()
+ # these should not be havoc, since they are hashed
+ # and this could trigger the buffer overflow: part of exploit 2
+ modelname = self.fakeid()
+ authstr = self.fakeid()
+
+ # create new session and user and upload file
conn = self.openconn()
- if registered:
- pass # TODO: auth
- modelname = self.fake_filename()[:50]
- modelname += "".join([chr(127 + int(random.random() * 128)) for i in range(10)]) # noise
- contents, mid = self.putfile(conn, filetype = filetype, modelname = modelname, solidname = self.fake_filename())
- resp = self.getfile(conn, modelname = modelname)
- assert_in(modelname.encode(), resp, f"Model name '{modelname}' not returned / correctly parsed")
- assert_in(solidname.encode(), resp, f"Solid name '{modelname}' not returned / correctly parsed")
- assert_in(contents, resp, f"STL File contents not returned / correctly parsed")
+ if register:
+ self.do_auth(conn, authstr)
+ contents, modelid = self.putfile(conn, solidname, modelname, filetype)
+ self.check_getfile(conn, modelname, solidname, contents)
+ if register:
+ self.check_listed(conn, modelid)
self.closeconn(conn)
+ # try getting file from a new session
conn = self.openconn()
- resp = self.getfile(conn, modelname = modelname)
- assert_in(modelname.encode(), resp, f"Model name '{modelname}' not returned / correctly parsed")
- assert_in(solidname.encode(), resp, f"Solid name '{modelname}' not returned / correctly parsed")
- assert_in(contents, resp, f"STL File contents not returned / correctly parsed")
+ if register:
+ self.do_auth(conn, authstr)
+ self.check_getfile(conn, modelname, solidname, contents)
+ if register:
+ self.check_listed(conn, modelid)
self.closeconn(conn)
def openconn(self):
self.debug("Connecting to service")
- conn = self.connect()
- conn.read_until("$") # ignore welcome
+ conn = pwnlib.tubes.remote.remote(self.address, self.port)
+ conn.recvuntil("$") # ignore welcome
+ if self.debuglog:
+ self.write(conn, "echo\n")
+ conn.recvuntil("$")
return conn
- def closeconn(self, conn: SimpleSocket):
+ def closeconn(self, conn):
self.debug("Sending exit command")
- conn.write("exit\n")
+ self.write(conn, "exit\n")
conn.close()
def putflag(self): # type: () -> None
if self.variant_id == 0:
conn = self.openconn()
- modelname = self.fake_filename()
- stlfile, fileid = self.putfile(conn, solidname = self.flag, modelname = modelname, filetype = "ascii")
+ modelname = self.fakeid()
+ stlfile, modelid = self.putfile(conn, self.flag, modelname, filetype = "ascii")
+ self.closeconn(conn)
+ self.postdb({ "modelid": modelid, "modelname": modelname })
+ elif self.variant_id == 1:
+ conn = self.openconn()
+ modelname = self.fakeid()
+ authstr = self.fakeid()
+ self.do_auth(conn, authstr)
+ stlfile, modelid = self.putfile(conn, self.flag, modelname, filetype = "bin")
self.closeconn(conn)
- self.postdb({ "fileid": fileid, "modelname": modelname })
+ self.postdb({ "modelid": modelid, "modelname": modelname, "auth": authstr })
else:
raise EnoException("Invalid variant_id provided")
def getflag(self): # type: () -> None
if self.variant_id == 0:
- fileid, modelname = self.querydb("fileid", "modelname")
+ modelid, modelname = self.querydb("modelid", "modelname")
conn = self.openconn()
- resp = self.getfile(conn, modelname)
+ resp = self.getfile(conn, modelname.encode())
+ self.debug(resp)
+ assert_in(self.flag.encode(), resp, "Resulting flag was found to be incorrect")
+ self.closeconn(conn)
+ elif self.variant_id == 1:
+ modelid, modelname, authstr = self.querydb("modelid", "modelname", "auth")
+ conn = self.openconn()
+ self.do_auth(conn, authstr)
+ resp = self.getfile(conn, modelname.encode())
assert_in(self.flag.encode(), resp, "Resulting flag was found to be incorrect")
self.closeconn(conn)
else:
raise EnoException("Invalid variant_id provided")
-
def putnoise(self): # type: () -> None
if self.variant_id == 0:
conn = self.openconn()
- modelname = self.fake_filename()
- solidname = self.fake_filename()
- contents, fileid = self.putfile(conn, modelname = modelname, solidname = solidname, filetype = "bin")
+ modelname = self.fakeid()
+ solidname = self.fakeid()
+ contents, modelid = self.putfile(conn, solidname, modelname, filetype = "bin")
self.closeconn(conn)
- self.postdb({ "fileid": fileid, "modelname": modelname, "solidname": solidname, "contents": contents })
+ self.postdb({ "modelid": modelid, "modelname": modelname, "solidname": solidname, "contents": contents })
+ elif self.variant_id == 1:
+ conn = self.openconn()
+ authstr = self.fakeid()
+ modelname = self.fakeid()
+ solidname = self.fakeid()
+ self.do_auth(conn, authstr)
+ contents, modelid = self.putfile(conn, solidname, modelname, filetype = "ascii")
+ self.closeconn(conn)
+ self.postdb({ "modelid": modelid, "modelname": modelname, "solidname": solidname, "contents": contents, "auth": authstr })
else:
raise EnoException("Invalid variant_id provided")
def getnoise(self): # type: () -> None
if self.variant_id == 0:
- fileid, modelname, solidname, contents = self.querydb("fileid", "modelname", "solidname", "contents")
+ modelid, modelname, solidname, contents = self.querydb("modelid", "modelname", "solidname", "contents")
conn = self.openconn()
- resp = self.getfile(conn, modelname)
- # assert_in(contents, resp, "File content returned by service found to be incorrect")
- assert_in(solidname.encode(), resp, "Solid name returned by service found to be incorrect")
- assert_in(modelname.encode(), resp, "Model name returned by service found to be incorrect")
+ self.check_getfile(conn, modelname, solidname, contents, modelid)
+ self.closeconn(conn)
+ elif self.variant_id == 1:
+ modelid, modelname, solidname, contents, authstr = self.querydb("modelid", "modelname", "solidname", "contents", "auth")
+ conn = self.openconn()
+ self.do_auth(conn, authstr)
+ self.check_getfile(conn, modelname, solidname, contents, modelid)
self.closeconn(conn)
else:
raise EnoException("Invalid variant_id provided")
def havoc(self): # type: () -> None
if self.variant_id == 0:
- self.havoc_upload(filetype = 'ascii', registered = False)
+ self.havoc_upload('ascii', False)
elif self.variant_id == 1:
- self.havoc_upload(filetype = 'bin', registered = False)
+ self.havoc_upload('bin', False)
elif self.variant_id == 2:
- self.havoc_upload(filetype = 'ascii', registered = True)
+ self.havoc_upload('ascii', True)
elif self.variant_id == 3:
- self.havoc_upload(filetype = 'bin', registered = True)
+ self.havoc_upload('bin', True)
else:
raise EnoException("Invalid variant_id provided");
- # TODO!
- conn = self.openconn()
- self.closeconn(conn)
-
def exploit(self):
- """
- This method was added for CI purposes for exploits to be tested.
- Will (hopefully) not be called during actual CTF.
- :raises EnoException on Error
- :return This function can return a result if it wants
- If nothing is returned, the service status is considered okay.
- The preferred way to report Errors in the service is by raising an appropriate EnoException
- """
- # TODO: We still haven't decided if we want to use this function or not. TBA
+ if self.variant_id == 0:
+ pass
+ elif self.variant_id == 1:
+ pass
+ else:
+ raise EnoException("Invalid variant_id provided")
+
pass
diff --git a/checker/src/requirements.txt b/checker/src/requirements.txt
@@ -23,3 +23,4 @@ urllib3==1.26.4
Werkzeug==1.0.1
numpy==1.20.1
Faker==8.1.4
+pwntools==4.5.0
diff --git a/checker/test.sh b/checker/test.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+ipstr="$1"
+
+try() {
+ cmd="$1"
+ if [ $# -lt 2 ]; then
+ variant=0
+ else
+ 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
+}
+
+
+if [ $# -lt 1 ]; then
+ echo "USAGE: test.sh <IP>"
+elif [ $# -eq 3 ]; then
+ try "$2" "$3"
+else
+ try putflag 0
+ try getflag 0
+
+ try putflag 1
+ try getflag 1
+
+ try putnoise 0
+ try getnoise 0
+
+ try putflag 1
+ try getflag 1
+
+ try havoc 0
+ try havoc 1
+ try havoc 2
+ try havoc 3
+
+# try exploit 0
+# try exploit 1
+fi
diff --git a/service/container/.gitignore b/service/container/.gitignore
@@ -1 +1,2 @@
src/
+data/
diff --git a/service/container/Dockerfile b/service/container/Dockerfile
@@ -19,6 +19,6 @@ WORKDIR /service/
RUN make clean && make
EXPOSE 9000
-ENV RESULTDIR=/data/scans
+ENV RESULTDIR=/data/uploads
ENTRYPOINT ["/entrypoint.sh"]
diff --git a/service/container/cleaner.sh b/service/container/cleaner.sh
@@ -12,7 +12,12 @@ if [ -f "$timeref" ]; then
echo "$files" | while read path; do
rm -rf "$path"
done
- echo "[ $(date +%T) ] Removed $(echo -n "$files" | wc -l) old files!"
+ if [ -z "$files" ]; then
+ filecount=0
+ else
+ filecount=$(echo "$files" | wc -l)
+ fi
+ echo "[ $(date +%T) ] Removed $filecount old files!"
fi
touch "$timeref"
diff --git a/service/container/entrypoint.sh b/service/container/entrypoint.sh
@@ -6,9 +6,9 @@ mkdir -p "$RESULTDIR"
while [ 1 ]; do
/cleaner.sh
- sleep 180
+ sleep 200
done &
-servicecmd='socat -T30 -s TCP-LISTEN:9000,reuseaddr,fork EXEC:"/service/stldoctor",raw,pty,echo=0,stderr'
+servicecmd='socat -T30 -s TCP-LISTEN:9000,reuseaddr,fork EXEC:"/service/build/stldoctor",raw,pty,echo=0,stderr'
su -s /bin/sh -c "$servicecmd" service
diff --git a/service/data/.keep b/service/data/.keep
diff --git a/service/do.sh b/service/do.sh
@@ -52,7 +52,7 @@ elif [ "$1" == "cleansrc" ]; then
fi
done
elif [ "$1" == "test" ]; then
- SRCDIR="$PWD/src" DATADIR="$PWD/data" bash "tests/test.sh" ${@:2}
+ SRCDIR="$PWD/src" DATADIR="$PWD/container/data" bash "tests/test.sh" ${@:2}
elif [ "$1" == "make" ]; then
# build a normal version
cd src
diff --git a/service/src/main.c b/service/src/main.c
@@ -171,9 +171,9 @@ void
search_cmd(const char *arg)
{
char *end, *scandir = NULL, *infopath = NULL, *modelpath = NULL;
- const char *hash;
+ int i, which, dirstart, ishidden;
+ const char *hash, *name;
struct dirent *de;
- int i, which, dirstart;
DIR *d = NULL;
FILE *f = NULL;
size_t size;
@@ -190,9 +190,13 @@ 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));) {
- if (!strpfcmp(hash, de->d_name) && *de->d_name != '.') {
+ name = de->d_name;
+ if (loggedin && *name == '.' && !strpfcmp(hash, name + 1)
+ || !loggedin && *name != '.' && !strpfcmp(hash, name)) {
printf("%i : %s\n", i, de->d_name);
i++;
}
@@ -211,7 +215,9 @@ search_cmd(const char *arg)
seekdir(d, dirstart);
for (i = 0; (de = readdir(d));) {
- if (!strpfcmp(hash, de->d_name) && *de->d_name != '.') {
+ name = de->d_name;
+ if (loggedin && *name == '.' && !strpfcmp(hash, name + 1)
+ || !loggedin && *name != '.' && !strpfcmp(hash, name)) {
if (i == which) {
scandir = aprintf("%s/%s", resultdir, de->d_name);
break;
diff --git a/service/tests/test.sh b/service/tests/test.sh
@@ -47,10 +47,13 @@ connect() {
fi
}
-[ ! -z "$RESULTDIR" ] && rm -rf "$RESULTDIR"
-mkdir -p "$RESULTDIR"
+cleanuploads() {
+ [ ! -z "$RESULTDIR" ] && rm -rf "$RESULTDIR"
+ mkdir -p "$RESULTDIR"
+}
if [ "$1" == "stl" ]; then
+ cleanuploads
announce "Testing ASCII STL Parsing"
(
@@ -71,6 +74,7 @@ if [ "$1" == "stl" ]; then
) | checkleaks
elif [ "$1" == "vuln1" ]; then
+ cleanuploads
announce "Testing Flagstore 1"
@@ -121,6 +125,7 @@ elif [ "$1" == "vuln1" ]; then
) | connect
elif [ "$1" == "vuln2" ]; then
+ cleanuploads
announce "Testing Flagstore 2"
@@ -145,6 +150,8 @@ elif [ "$1" == "vuln2" ]; then
) | connect
elif [ "$1" == "authupload" ]; then
+ cleanuploads
+
(
echo "echo"
@@ -153,7 +160,14 @@ elif [ "$1" == "authupload" ]; then
cat "$TESTDATA/sample-ascii.stl" | wc -c
cat "$TESTDATA/sample-ascii.stl"
echo "testname"
+ ) | connect
+
+ (
+ echo "echo"
+
+ echo "auth test"
echo "list"
+ echo "search testname"
) | connect
else
connect