diff --git a/check_system/.editorconfig b/check_system/.editorconfig new file mode 100755 index 0000000..54bf397 --- /dev/null +++ b/check_system/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[VERSION] +insert_final_newline = false + +[*.py] +charset = utf-8 +indent_style = space +indent_size = 4 diff --git a/check_system/Dockerfile b/check_system/Dockerfile new file mode 100755 index 0000000..5aa8f87 --- /dev/null +++ b/check_system/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.7.14-slim +ADD src VERSION /dist/ +WORKDIR /dist + +# setup the services +RUN pip install --requirement aesthetic/requirements.txt +RUN pip install --requirement editor/requirements.txt +RUN pip install --requirement jinnice/requirements.txt +RUN pip install --requirement myblog/requirements.txt + +# start game simulation +CMD ["python", "-u", "main.py"] diff --git a/check_system/LICENSE b/check_system/LICENSE new file mode 100755 index 0000000..bbe6f76 --- /dev/null +++ b/check_system/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 VolgaCTF + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/check_system/README.md b/check_system/README.md new file mode 100755 index 0000000..32fee97 --- /dev/null +++ b/check_system/README.md @@ -0,0 +1,67 @@ +# VolgaCTF 2022 Final Homework + +This repo contains all the checkers from `VolgaCTF 2022 Final` along with a game-simulating script. + +Could be useful to do your _homework_. + +## Build + +```bash +$ docker build -t volgactf2022/homework-image . +``` + +## Run + +```bash +$ docker run \ + -e TEAM_IP= \ + -e ROUND_DURATION=10 \ + --rm \ + volgactf2022/homework-image +``` + +## Optional environment variables + +### Simulation-related variables +| Var name | Description | Default value | +|-----------------------------|------------------------------------------------------|:-------------------:| +| `ROUND_DURATION` | Round duration (time between two consecutive PUSHes) | 30 sec | +| `SKIP_EDITOR` | Skip `Editor` service | False | +| `SKIP_AESTHETIC` | Skip `Aesthetic` service | False | +| `SKIP_MYBLOG` | Skip `MyBlog` service | False | +| `SKIP_JINNICE` | Skip `Jinnice` service | False | +| `PULL_COUNT` | Number of PULLs for each round | 5 | +| `PRINT_STATS_EVERY_N_ROUND` | Output stats frequency | 1 | +| `PRINT_STATS_SINGLE_COLUMN` | Output stats in a single column | False (two columns) | + +### Checkers' variables +| Var name | Description | Default value | +|--------------------------------|------------------------------------------|:-------------:| +| `EDITOR_PORT` | `Editor` service port | 8080 | +| `EDITOR_TIMEOUT` | `Editor` service connection timeout | 30 | +| `EDITOR_N_MAX_IMAGES_PER_PUSH` | Max number of images to PUSH to `Editor` | 3 | +| `AESTHETIC_PORT` | `Aesthetic` service port | 8777 | +| `AESTHETIC_TIMEOUT` | `Aesthetic` service connection timeout | 15 | +| `MYBLOG_PORT` | `MyBlog` service port | 13377 | +| `MYBLOG_TIMEOUT` | `MyBlog` service connection timeout | 20 | +| `JINNICE_PORT` | `Jinnice` service port | 8888 | +| `JINNICE_TIMEOUT` | `Jinnice` service connection timeout | 30 | + +### Example with more options +Below is an example usage which assumes that only `Editor` and `MyBlog` services are spawned, +`Editor`'s port is `18080`, and `MyBlog` checker's connection timeout is increased (e.g. for debugging purposes): +```bash +$ docker run \ + -e TEAM_IP= \ + -e ROUND_DURATION=10 \ + -e SKIP_AESTHETIC= \ + -e SKIP_JINNICE= \ + -e EDITOR_PORT=18080 \ + -e MYBLOG_TIMEOUT=1800 \ + --rm \ + volgactf2022/homework-image +``` + +## License + +MIT @ [VolgaCTF](https://github.com/VolgaCTF) diff --git a/check_system/VERSION b/check_system/VERSION new file mode 100755 index 0000000..d156ce2 --- /dev/null +++ b/check_system/VERSION @@ -0,0 +1 @@ +42.0.0 \ No newline at end of file diff --git a/check_system/src/aesthetic/__init__.py b/check_system/src/aesthetic/__init__.py new file mode 100644 index 0000000..d4fb8de --- /dev/null +++ b/check_system/src/aesthetic/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from .main import push, pull diff --git a/check_system/src/aesthetic/ec_private.pem b/check_system/src/aesthetic/ec_private.pem new file mode 100644 index 0000000..f7943b6 --- /dev/null +++ b/check_system/src/aesthetic/ec_private.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIF/LZh6xyFkuNfwP349RlC4PBv4DyGPt6BzBiublvj1yoAoGCCqGSM49 +AwEHoUQDQgAEGySmc8D9mKY0VGif5el/bnbIlQeRhLEWYtvvINH/IM5W2BfgtrrZ +Vl5dyGy7tAWqpcqluIipcmcYcHqJndIneg== +-----END EC PRIVATE KEY----- diff --git a/check_system/src/aesthetic/external.py b/check_system/src/aesthetic/external.py new file mode 100644 index 0000000..d2279be --- /dev/null +++ b/check_system/src/aesthetic/external.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from random import choice +from string import ascii_letters, digits + + +def get_random_message(size=16): + return ''.join(choice(ascii_letters + digits) for _ in range(size)) diff --git a/check_system/src/aesthetic/main.py b/check_system/src/aesthetic/main.py new file mode 100644 index 0000000..ebdc160 --- /dev/null +++ b/check_system/src/aesthetic/main.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import base64 +import hashlib +import logging +import os +import socket + +import jwt +from volgactf.final.checker.result import Result + +from .utils import read_message, send_message + +logger = logging.getLogger(__name__) +SERVICE_PORT = int(os.getenv('AESTHETIC_PORT', 8777)) +SESSION_TOTAL_TIMEOUT = int(os.getenv('AESTHETIC_TIMEOUT', 15)) + + +async def push(endpoint, capsule: str, label, metadata): + try: + logger.debug('[%s on PUSH]: connecting', endpoint) + fd = socket.create_connection((endpoint, SERVICE_PORT), timeout=SESSION_TOTAL_TIMEOUT) + logger.debug('[%s on PUSH]: connected to service', endpoint) + except Exception as ex: + logger.error('[%s on PUSH]: failed to connect, reason: %s', endpoint, str(ex)) + return Result.DOWN, '', 'Failed to connect' + + try: + send_message(fd, b'PUSH') + + iv = b'\x70\x67\x4a\xd5\xaf\x53\x92\xf9\xb2\x94\xde\x78' + os.urandom(4) + + send_message(fd, capsule.encode('utf-8')) + send_message(fd, metadata.round.to_bytes(4, 'big')) + send_message(fd, iv) + + encrypted_capsule = read_message(fd) + ec_hash = hashlib.sha256(encrypted_capsule).digest() + auth_tag = read_message(fd) + + with open('ec_private.pem', 'rb') as jwtkey: + key = jwtkey.read() + signature = jwt.encode( + {'message': 'It\'s me, Mario!'}, + key=key, + algorithm='ES256' + ) + + send_message(fd, signature.encode('utf-8')) + + if read_message(fd) != b"+": + send_message(fd, b'EXIT') + read_message(fd) + return Result.MUMBLE, '', '' + + send_message(fd, b'EXIT') + read_message(fd) + + return Result.UP, \ + (base64.b64encode(iv) + b'::' + + base64.b64encode(auth_tag) + b'::' + + base64.b64encode(ec_hash)).decode(), \ + 'UP' + + except Exception as ex: + logger.error('[%s on PUSH]: failed on PUSH, reason: %s', endpoint, str(ex)) + return Result.MUMBLE, '', '' + + +async def pull(endpoint, capsule: bytes, label: str, metadata): + try: + logger.debug('[%s on PULL]: connecting', endpoint) + fd = socket.create_connection((endpoint, SERVICE_PORT), timeout=SESSION_TOTAL_TIMEOUT) + logger.debug('[%s on PULL]: connected to service', endpoint) + except Exception as ex: + logger.error('[%s on PULL]: failed to connect, reason: %s', endpoint, str(ex)) + return Result.DOWN, '' + + try: + b64_iv, b64_auth_tag, b64_ec_hash = label.encode().split(b'::') + iv = base64.b64decode(b64_iv) + auth_tag = base64.b64decode(b64_auth_tag) + ec_hash = base64.b64decode(b64_ec_hash) + + send_message(fd, b'PULL') + send_message(fd, metadata.round.to_bytes(4, 'big')) + + received_enc_capsule = read_message(fd) + rec_hash = hashlib.sha256(received_enc_capsule).digest() + + if rec_hash != ec_hash: + print(rec_hash, ec_hash) + send_message(fd, b'-') + send_message(fd, b'EXIT') + read_message(fd) + return Result.DOWN, 'Wrong hash' + else: + send_message(fd, b'+') + + send_message(fd, iv) + send_message(fd, auth_tag) + + recv_capsule = read_message(fd) + + if recv_capsule.decode('utf-8') != capsule: + send_message(fd, b'EXIT') + read_message(fd) + return Result.DOWN, 'Corrupted flag' + + send_message(fd, b'EXIT') + read_message(fd) + + return Result.UP, 'UP' + + except Exception as ex: + logger.error('[%s on PULL]: failed on PULL, reason: %s', endpoint, str(ex)) + return Result.MUMBLE, '' diff --git a/check_system/src/aesthetic/requirements.txt b/check_system/src/aesthetic/requirements.txt new file mode 100644 index 0000000..3c66021 --- /dev/null +++ b/check_system/src/aesthetic/requirements.txt @@ -0,0 +1 @@ +jwt==1.3.1 diff --git a/check_system/src/aesthetic/utils.py b/check_system/src/aesthetic/utils.py new file mode 100644 index 0000000..bd24805 --- /dev/null +++ b/check_system/src/aesthetic/utils.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +import logging +import struct + + +logger = logging.getLogger('service') + + +""" + Communication +""" + +class InputOverflowException(Exception): + pass + + +class InputUnderflowException(Exception): + pass + + +def read_message(s, max_input_length=1024*16) -> bytes: + received_buffer = s.recv(8) + if len(received_buffer) < 8: + raise InputUnderflowException('Failed to receive data: the received length is less than 8 bytes long') + to_receive = struct.unpack(' max_input_length: + raise InputOverflowException('Failed to receive data: requested to accept too much data') + received_buffer = b'' + + while len(received_buffer) < to_receive: + data = s.recv(to_receive - len(received_buffer)) + if len(data) == 0: + raise InputUnderflowException('Failed to receive data: the pipe must have been broken') + received_buffer += data + if len(received_buffer) > max_input_length: + raise InputOverflowException('Failed to receive data: accepted too much data') + + return received_buffer + + +def send_message(s, message: bytes): + send_buffer = struct.pack('?@[\\]^_`{|}~' + alphabet = ascii_letters + digits + requirements = [ + ascii_uppercase, # at least one uppercase letter + ascii_lowercase, # at least one lowercase letter + digits, # at least one digit + special, # at least one special symbol + *(length - 4) * [alphabet] # rest: letters digits and symbols + ] + return ''.join(secrets.choice(req) for req in random.sample(requirements, length)) + + +def generate_first_name(): + return fake.first_name() + + +def generate_last_name(): + return fake.last_name() + + +def generate_bio(): + return fake.sentence(nb_words=random.randrange(5, 20)) + + +def generate_image_name(): + return uuid4().hex[:random.randrange(12, 20)] + + +def image_raw_to_array(image_raw_data): + return imread(io.BytesIO(image_raw_data)) + + +def image_array_to_raw(image_array, image_format): + buf = io.BytesIO() + imsave(buf, image_array, format=image_format) + buf.seek(0, 0) + return buf.read() + + +def generate_fake_image(img_fmt, raw_data): + img_fmt = img_fmt if img_fmt != 'jpg' else 'jpeg' + width = random.randrange(512, 1024) + height = random.randrange(512, 1024) + image_data = fake.image(size=(width, height), image_format=img_fmt) + if raw_data: + return image_data, (width, height, 3) + else: + return image_raw_to_array(image_data), (height, width, 3) + + +def generate_image(assets_folder_path, image_format, raw_data=True): + if coin_flip(): + return generate_fake_image(image_format, raw_data) + + try: + images_folder_path = os.path.join(assets_folder_path, image_format) + image_file_path = random.choice(glob.glob(os.path.join(images_folder_path, '*.{}'.format(image_format)))) + image_array = imread(image_file_path) + image_shape = image_array.shape + if raw_data: + with open(image_file_path, 'rb') as f: + return f.read(), image_shape + else: + return image_array, image_shape + + except Exception: + return generate_fake_image(image_format, raw_data) + + +def embed_lsb(image_array, capsule): + def access_bit(_data, _num): + base = int(_num // 8) + shift = int(_num % 8) + return (_data[base] >> shift) & 0x1 + + # 1. flatten the image and check if capsule can be embedded + data = capsule.encode('utf-8') + data_length = len(data) + image_data = image_array.flatten() + image_length = len(image_data) + if image_length < data_length * 8 + 32: + raise Exception('Image data is too short: len(image)={0}, len(data)={1}'.format(image_length, data_length)) + + # 2. data bytes to array of bits + dl_bytes = int.to_bytes(data_length, byteorder='little', length=4, signed=True) + data_bit_string = np.array([access_bit(data, i) for i in range(len(data) * 8)], dtype=np.uint8) + dl_bit_string = np.array([access_bit(dl_bytes, i) for i in range(len(dl_bytes) * 8)], dtype=np.uint8) + padding = np.zeros(image_length - len(dl_bit_string) - len(data_bit_string), dtype=np.uint8) + payload = np.concatenate((dl_bit_string, data_bit_string, padding), axis=0) + + # 3. embed the payload and return the image + image_data = image_data & ~np.uint8(1) + image_data = image_data | payload + image_array = image_data.reshape(image_array.shape) + + return image_array + + +def extract_lsb(image_array): + def to_int(b): + val = 0 + for i, d in enumerate(b): + val = val | (d << i) + return val + + # 1. flatten the image and check its length + image_array = image_array.flatten() + lsb_bytes_length = int(len(image_array) / 8.0) + if len(image_array) < 4 * 8: + raise Exception('Image data is too short: len(lsb_bytes)={0} bytes'.format(lsb_bytes_length)) + + # 2. unpack message length + lsb = image_array & 0x1 + lsb = lsb[:int(len(lsb) / 8.0) << 3] + message_length_bytes = bytes([to_int(lsb[i:i + 8]) for i in range(0, 32, 8)]) + message_length = int.from_bytes(message_length_bytes, byteorder='little', signed=True) + if not 0 < message_length < lsb_bytes_length: + raise Exception('Incorrect packed message_length: expected 0 < message_length < {0}, but recv={1}' + .format(lsb_bytes_length, message_length)) + + # 3. extract the embedded message and check the capsule + message_bytes = bytes([to_int(lsb[i:i + 8]) for i in range(32, 32 + 8 * message_length, 8)]) + capsule_recv = message_bytes.decode('utf-8') + return capsule_recv diff --git a/check_system/src/jinnice/__init__.py b/check_system/src/jinnice/__init__.py new file mode 100644 index 0000000..d4fb8de --- /dev/null +++ b/check_system/src/jinnice/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from .main import push, pull diff --git a/check_system/src/jinnice/main.py b/check_system/src/jinnice/main.py new file mode 100755 index 0000000..e237036 --- /dev/null +++ b/check_system/src/jinnice/main.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +import logging +import os +import sqlite3 +from uuid import uuid4 + +import aiohttp +from faker import Faker +from unidecode import unidecode +from volgactf.final.checker.result import Result + +logger = logging.getLogger(__name__) +fake = Faker() + +# region Environment variables + +PORT = int(os.getenv('JINNICE_PORT', 8888)) +CONNECTION_TOTAL_TIMEOUT = int(os.getenv('JINNICE_TIMEOUT', 30)) +SAMPLES_DB_PATH = os.getenv('JINNICE_SAMPLES_DB_PATH', '/dist/jinnice/samples.db') + +PUSH_TASK_RET_CODE_OK = 200 +PUSH_CAPSULE_RET_CODE_OK = 200 +PULL_CAPSULE_RET_CODE_OK = 200 + +PUSH_TASK_URI_FMT = os.getenv('PUSH_TASK_URI_FMT', 'http://{endpoint}:{port}/task') +PUSH_CAPSULE_URI_FMT = os.getenv('PUSH_CAPSULE_URI_FMT', 'http://{endpoint}:{port}/push') +PULL_CAPSULE_URI_FMT = os.getenv('PULL_CAPSULE_URI_FMT', 'http://{endpoint}:{port}/pull/{task_id}') + + +# endregion Environment variables + + +# region Utils + +def decode_if_unicode(*args): + n_args = len(args) + if n_args == 0: + return None + elif n_args == 1: + s = args[0] + return unidecode(s) if s is not None else None + else: + return [unidecode(s) if s is not None else None for s in args] + + +def get_random_user_agent(): + return fake.user_agent() + + +# endregion Utils + + +async def do_push(endpoint, capsule, _label, _metadata): + # 1. preprocess the capsule + capsule = decode_if_unicode(capsule) + + async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar(unsafe=True), + timeout=aiohttp.ClientTimeout(total=CONNECTION_TOTAL_TIMEOUT), + skip_auto_headers={'User-Agent'}) as session: + + # 2. POST the next task and get result + logger.info('[%s] on PUSH: sending the next task via POST /task', endpoint) + try: + # select a task and generate a unique id + with sqlite3.connect(SAMPLES_DB_PATH) as conn: + c = conn.cursor() + c.execute('SELECT * FROM samples ORDER BY RANDOM() LIMIT 1') + task_content, task_result, task_comments = c.fetchone() + + task_content, task_result, task_comments = decode_if_unicode(task_content, task_result, task_comments) + task_id = uuid4().hex + + # make the request + headers = {'User-Agent': get_random_user_agent()} + url = PUSH_TASK_URI_FMT.format(endpoint=endpoint, port=PORT) + data = {'id': task_id, 'data': task_content} + if task_comments is not None and task_comments != '': + data['comments'] = task_comments + + async with session.post(url, headers=headers, json=data) as r: + if r.status != PUSH_TASK_RET_CODE_OK: + logger.info('[%s] on PUSH: uploading task failed, received code: %s', endpoint, r.status) + return Result.MUMBLE, '', 'Incorrect response code on POST /task' + response_record = await r.json() + if 'data' not in response_record: + return Result.MUMBLE, '', 'Incorrect response format on POST /task' + response_result = response_record['data'] + + except sqlite3.Error as ex: + logger.exception('[%s] on PUSH: Failed to get a random sample: %s', endpoint, ex) + return Result.INTERNAL_ERROR, '', '' + except aiohttp.ClientResponseError as ex: + logger.error('[%s] on PUSH: failed to proceed after server had responded: %s', endpoint, ex) + return Result.MUMBLE, '', 'Incorrect response on POST /task' + except aiohttp.ClientConnectionError as ex: + logger.error('[%s] on PUSH: failed to establish connection: %s', endpoint, ex) + return Result.DOWN, '', 'Connection error on POST /task' + except Exception as ex: + logger.error('[%s] on PUSH: Exception while POSTing task: %s', endpoint, ex) + return Result.DOWN, '', 'Connection error on POST /task' + + # 3. check if the answer is correct + logger.info('[%s] on PUSH: checking the received solution', endpoint) + if response_result != task_result: + logger.info('[%s] on PUSH: the received result is incorrect', endpoint) + logger.debug('[%s] on PUSH: received=%s', endpoint, response_result) + logger.debug('[%s] on PUSH: actual =%s', endpoint, task_result) + return Result.CORRUPT, '', 'Incorrect result' + + # 4. POST capsule and its id + logger.info('[%s] on PUSH: POSTing the capsule', endpoint) + try: + url = PUSH_CAPSULE_URI_FMT.format(endpoint=endpoint, port=PORT) + data = {'id': task_id, 'data': capsule} + async with session.post(url, headers=headers, json=data) as r: + if r.status != PUSH_CAPSULE_RET_CODE_OK: + logger.info('[%s] on PUSH: POSTing capsule failed, status=%d', endpoint, r.status) + return Result.MUMBLE, '', 'Incorrect response code on POST /push' + logger.info('[%s] on PUSH: POSTed capsule', endpoint) + + except aiohttp.ClientResponseError as ex: + logger.error('[%s] on PUSH: failed to proceed after server had responded: %s', endpoint, ex) + return Result.MUMBLE, '', 'Incorrect response on POST /push' + except aiohttp.ClientConnectionError as ex: + logger.error('[%s] on PUSH: failed to establish connection: %s', endpoint, ex) + return Result.DOWN, '', 'Connection error on POST /push' + except Exception as ex: + logger.error('[%s] on PUSH: Exception while POSTing capsule: %s', endpoint, ex) + return Result.DOWN, '', 'Connection error on POST /push' + + # 5. save the task id and return status UP + return Result.UP, task_id, 'UP' + + +async def do_pull(endpoint, capsule, label, _metadata): + # 1. get the task id + capsule = decode_if_unicode(capsule) + task_id = decode_if_unicode(label) + + async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar(unsafe=True), + timeout=aiohttp.ClientTimeout(total=CONNECTION_TOTAL_TIMEOUT), + skip_auto_headers={'User-Agent'}) as session: + # 2. GET capsule by task id + logger.info('[%s] on PULL: GETing capsule by task_id=%s', endpoint, task_id) + try: + headers = {'User-Agent': get_random_user_agent()} + url = PULL_CAPSULE_URI_FMT.format(endpoint=endpoint, port=PORT, task_id=task_id) + async with session.get(url, headers=headers) as r: + if r.status != PULL_CAPSULE_RET_CODE_OK: + logger.info('[%s] on PULL: failed to GET capsule, status=%s', endpoint, r.status) + return Result.MUMBLE, 'Incorrect response code on GET /pull/{id}' + response_record = await r.json() + if 'data' not in response_record: + return Result.MUMBLE, 'Incorrect response format on GET /pull/{id}' + response_capsule = response_record['data'] + + except aiohttp.ClientResponseError as ex: + logger.error('[%s] on PULL: failed to proceed after server had responded: %s', endpoint, ex) + return Result.MUMBLE, 'Incorrect response on GET /pull/{id}' + except aiohttp.ClientConnectionError as ex: + logger.error('[%s] on PULL: failed to establish connection: %s', endpoint, ex) + return Result.DOWN, 'Connection error on GET /pull/{id}' + except Exception as ex: + logger.error('[%s] on PULL: Exception while GETing capsule: %s', endpoint, ex) + return Result.DOWN, 'Connection error on GET /pull/{id}' + + # 3. check the capsule + if response_capsule != capsule: + logger.info('[%s] on PULL: the received capsule is incorrect', endpoint) + logger.info('[%s] on PULL: received=%s', endpoint, response_capsule) + logger.info('[%s] on PULL: actual =%s', endpoint, capsule) + return Result.CORRUPT, 'Incorrect flag' + + return Result.UP, 'UP' + + +async def push(endpoint, capsule, label, metadata): + try: + return await do_push(endpoint, capsule, label, metadata) + except Exception as ex: + # N.B. PARANOIA MODE ON!!! JAVA STYLE PROGRAMMING MODE ON!!! + logger.exception('[%s] on PUSH: Exception while PUSHing capsule: %s', endpoint, ex) + return Result.MUMBLE, '', 'Incorrect server response' + + +async def pull(endpoint, capsule, label, metadata): + try: + return await do_pull(endpoint, capsule, label, metadata) + except Exception as ex: + # N.B. PARANOIA MODE ON!!! JAVA STYLE PROGRAMMING MODE ON!!! + logger.exception('[%s] on PULL: Exception while PULLing capsule: %s', endpoint, ex) + return Result.MUMBLE, 'Incorrect server response' diff --git a/check_system/src/jinnice/requirements.txt b/check_system/src/jinnice/requirements.txt new file mode 100755 index 0000000..cadd470 --- /dev/null +++ b/check_system/src/jinnice/requirements.txt @@ -0,0 +1,5 @@ +aiohttp==3.7.4 +volgactf.final.checker.result==1.0.0 +unidecode==1.3.4 +Faker==14.2.0 +olefile==0.46 diff --git a/check_system/src/jinnice/samples.db b/check_system/src/jinnice/samples.db new file mode 100644 index 0000000..3901180 Binary files /dev/null and b/check_system/src/jinnice/samples.db differ diff --git a/check_system/src/main.py b/check_system/src/main.py new file mode 100755 index 0000000..38050a3 --- /dev/null +++ b/check_system/src/main.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +import asyncio +import hashlib +import logging +import os +import random +import string +import uuid + +import jinnice.main as jinnice +import editor.main as editor +import aesthetic.main as aesthetic +import myblog.main as myblog + +from volgactf.final.checker.result import Result + +# region Environment variables + +TEAM_IP = os.getenv('TEAM_IP', '0.0.0.0') +ROUND_DURATION = int(os.getenv('ROUND_DURATION', 30)) + +SKIP_JINNICE = False if os.getenv('SKIP_JINNICE') is None else True +SKIP_EDITOR = False if os.getenv('SKIP_EDITOR') is None else True +SKIP_AESTHETIC = False if os.getenv('SKIP_AESTHETIC') is None else True +SKIP_MYBLOG = False if os.getenv('SKIP_MYBLOG') is None else True + +PULL_COUNT = int(os.getenv('PULL_COUNT', 5)) +PRINT_STATS_EVERY_N_ROUND = int(os.getenv('PRINT_STATS_EVERY_N_ROUND', 1)) +PRINT_STATS_SINGLE_COLUMN = False if os.getenv('PRINT_STATS_SINGLE_COLUMN') is None else True + + +# endregion Environment variables + +# region Themis imitator + +class Metadata(object): + def __init__(self, round_number): + self.round_number = round_number + + @property + def round(self): + return self.round_number + + +def add_flag_to_pool(pool_flag_labels, flag, flag_adj, pull_count=5): + if len(pool_flag_labels) != pull_count: + del pool_flag_labels[:] + for _ in range(pull_count): + pool_flag_labels.append({'flag': flag, 'label': flag_adj}) + else: + del pool_flag_labels[0] + pool_flag_labels.append({'flag': flag, 'label': flag_adj}) + + +def gen_capsule(): + chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + # cur_flag = '{0}='.format(hashlib.md5(uuid.uuid4().bytes).hexdigest()) + return 'VolgaCTF{{{0}.{1}.{2}}}'.format( + ''.join(random.choice(chars) for _ in range(301)), + ''.join(random.choice(chars) for _ in range(301)), + ''.join(random.choice(chars) for _ in range(301)) + ) + + +def print_stats(services): + template = '''\ + Service **{name}** + Latest PUSH + status: {push_status} + message: {push_message} + Latest PULL + status: {pull_status} + message: {pull_message} + + Statistics (PUSH){padding} Statistics (PULL) + UP: {push_up: <{n}} UP: {pull_up: <{n}} + MUMBLE: {push_mumble: <{n}} MUMBLE: {pull_mumble: <{n}} + CORRUPT: {push_corrupt: <{n}} CORRUPT: {pull_corrupt: <{n}} + DOWN: {push_down: <{n}} DOWN: {pull_down: <{n}} + TOTAL: {push_total: <{n}} TOTAL: {pull_total: <{n}}\ +''' + stats = [] + for service_name, _, _, _, latest, push_stats, pull_stats in services: + n = max(6, *[len(s) for s in map(str, list(push_stats.values()) + list(pull_stats.values()))]) + m = n - 6 + s = template.format( + name=service_name, + push_status=latest['push']['status'], + push_message=latest['push']['message'], + pull_status=latest['pull']['status'], + pull_message=latest['pull']['message'], + push_up=push_stats[Result.UP], + padding=' ' * m, + push_mumble=push_stats[Result.MUMBLE], + push_corrupt=push_stats[Result.CORRUPT], + push_down=push_stats[Result.DOWN], + push_total=sum(push_stats.values()), + pull_up=pull_stats[Result.UP], + pull_mumble=pull_stats[Result.MUMBLE], + pull_corrupt=pull_stats[Result.CORRUPT], + pull_down=pull_stats[Result.DOWN], + pull_total=sum(pull_stats.values()), + n=n, + ) + stats.append(s) + + max_width = max([max([len(r) for r in s.split('\n')]) for s in stats]) + border = '=' * max_width + if PRINT_STATS_SINGLE_COLUMN or len(stats) < 2: + for s in stats: + print('{0}\n{1}\n{0}'.format(border, s)) + else: + for sl, sr in zip(stats[0::2], stats[1::2]): + print('{0:<{width}}{0:<{width}}'.format(border, width=max_width + 4)) + for sll, srl in zip(sl.split('\n'), sr.split('\n')): + print('{0:<{width}}{1:<{width}}'.format(sll, srl, width=max_width + 4)) + print('{0:<{width}}{0:<{width}}'.format(border, width=max_width + 4)) + if len(stats) & 1: + print('{0}\n{1}\n{0}'.format(border, stats[-1])) + + +async def main(team_ip, timeout, debug=False): + # 1. initialize logger + level = logging.DEBUG if debug > 3 else logging.INFO + logging.basicConfig(level=level, + format='[%(asctime)s %(name)-12s %(levelname)s]: %(message)s', + datefmt='%m/%d %H:%M:%S') + logger = logging.getLogger('checker') + logger.setLevel(level) + + # 2. initialize stats objects, skipping some services if required + if SKIP_EDITOR and SKIP_AESTHETIC and SKIP_MYBLOG and SKIP_JINNICE: + logger.info('All the four services are skipped - nothing to do...') + return + + services = [] + if not SKIP_EDITOR: + services.append(('editor', editor.push, editor.pull)) + if not SKIP_AESTHETIC: + services.append(('aesthetic', aesthetic.push, aesthetic.pull)) + if not SKIP_MYBLOG: + services.append(('myblog', myblog.push, myblog.pull)) + if not SKIP_JINNICE: + services.append(('jinnice', jinnice.push, jinnice.pull)) + services = [ + ( + service_name, + push_fn, + pull_fn, + [], + {'push': {'status': '', 'message': ''}, 'pull': {'status': '', 'message': ''}}, + {r: 0 for r in Result}, + {r: 0 for r in Result}, + ) + for service_name, push_fn, pull_fn in services + ] + + # 3. start the simulation + round_number = 0 + while True: + round_number += 1 + logger.info('Round %d', round_number) + + for service_name, push_fn, pull_fn, pool_flag_labels, latest, push_stats, pull_stats in services: + logger.info('[%d] Push-pulling service %s', round_number, service_name) + + md = Metadata(round_number) + label = hashlib.md5(uuid.uuid4().bytes).hexdigest()[:16] + cur_flag = gen_capsule() + + logger.info('[%d] Pushing flag %s', round_number, cur_flag) + cur_res, label, message = await push_fn(team_ip, cur_flag, label, md) + logger.info('[%d] Status=%s, message="%s"', round_number, cur_res, message) + push_stats[cur_res] += 1 + latest['push']['status'] = cur_res.name + latest['push']['message'] = message + if cur_res == Result.UP: + add_flag_to_pool(pool_flag_labels, cur_flag, label, pull_count=PULL_COUNT) + + for i in range(len(pool_flag_labels)): + cur_flag = pool_flag_labels[i]['flag'] + label = pool_flag_labels[i]['label'] + logger.info('[%d] Pulling flag %s', round_number, cur_flag) + cur_res, message = await pull_fn(team_ip, cur_flag, label, md) + logger.info('[%d] Status=%s, message="%s"', round_number, cur_res, message) + pull_stats[cur_res] += 1 + latest['pull']['status'] = cur_res.name + latest['pull']['message'] = message + logger.info('') + + if PRINT_STATS_EVERY_N_ROUND > 0 and round_number % PRINT_STATS_EVERY_N_ROUND == 0: + print_stats(services) + + await asyncio.sleep(timeout) + + +# endregion Themis imitator + + +if __name__ == '__main__': + # start the checker + loop = asyncio.get_event_loop() + loop.run_until_complete(main(TEAM_IP, ROUND_DURATION)) + loop.close() diff --git a/check_system/src/myblog/__init__.py b/check_system/src/myblog/__init__.py new file mode 100644 index 0000000..d4fb8de --- /dev/null +++ b/check_system/src/myblog/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from .main import push, pull diff --git a/check_system/src/myblog/external.py b/check_system/src/myblog/external.py new file mode 100644 index 0000000..8f52f7c --- /dev/null +++ b/check_system/src/myblog/external.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from random import choice +from string import ascii_letters, digits + + +def get_random_message(size=16): + return ''.join(choice(ascii_letters + digits) for _ in range(size)) + + +user_agents = [ +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.0", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36", +"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15", +"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36", +"Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0", +"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", +"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36", +"Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0", +"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36", +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36", +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 Edg/84.0.522.63" +] diff --git a/check_system/src/myblog/helper.py b/check_system/src/myblog/helper.py new file mode 100644 index 0000000..5af0b0c --- /dev/null +++ b/check_system/src/myblog/helper.py @@ -0,0 +1,21 @@ +import random +import string + + +def get_rand_element(collection): + limit = len(collection) + num = random.randint(0, limit-1) + return collection[num] + + +def random_str(length=10): + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(length)) + +def make_creds(): + username = f'{{{random_str(15)}}}' + password = random_str(20) + return dict(username=username, password=password) + + + diff --git a/check_system/src/myblog/main.py b/check_system/src/myblog/main.py new file mode 100644 index 0000000..45705e7 --- /dev/null +++ b/check_system/src/myblog/main.py @@ -0,0 +1,473 @@ +# -*- coding: utf-8 -*- +import asyncio +import json +import logging +import os +import random +import sys + +import aiohttp +from aiohttp import ClientSession, CookieJar, ClientTimeout, FormData +from volgactf.final.checker.result import Result + +from .external import user_agents +from .helper import get_rand_element, random_str + +logger = logging.getLogger(__name__) + +# ------------------------ SOME CONSTANTS ------------------------ + +TIMEOUT = int(os.getenv('MYBLOG_TIMEOUT', 20)) +PORT = int(os.getenv('MYBLOG_PORT', 13377)) + +# ------------------------ ANNOYING MESSAGES --------------------- + +NOT_WORKING_MESSAGE = "Why is your service not working?" +FLAG_WAS_LOST = "You have just lost your flag :(" +ALL_FINE = "You're doing good" +CORRUPTED = "Is your flag corruption an accident?" + + +def ping_enabled(): + return os.getenv('VOLGACTF_FINAL_PING_ENABLED', 'yes') == 'yes' + + +# -------------------------- HELP FUNCTION --------------------------------------- +async def post_request(url, headers, json_inp=None, data=None, cookies=None): + timeout = ClientTimeout(total=TIMEOUT) + jar = CookieJar(unsafe=True) + async with ClientSession(cookie_jar=jar, cookies=cookies, timeout=timeout, skip_auto_headers={"User-Agent"}) as session: + async with session.post(url, headers=headers, json=json_inp, data=data) as r: + data = "" + json_data = "" + if hasattr(r, "data"): + data = await r.data + if r.content_type == 'application/json': + json_data = await r.json() + return r.status, json_data, data +async def get_request(url, headers, cookies={}): + timeout = ClientTimeout(total=TIMEOUT) + jar = CookieJar(unsafe=True) + async with ClientSession(cookie_jar=jar, cookies=cookies, timeout=timeout, skip_auto_headers={"User-Agent"}) as session: + async with session.get(url, headers=headers) as r: + data = "" + json_data = "" + if hasattr(r, "data"): + data = await r.data + if r.content_type == 'application/json': + json_data = await r.json() + return r.status, json_data, data + +# ------------------------ SIGN-UP & SIGN-IN FUNCTION ---------------------------- + +async def register_user(endpoint, creds): + url = get_url(endpoint) + "/api/auth/sign_up" + timeout = ClientTimeout(total=TIMEOUT) + jar = CookieJar(unsafe=True) + headers = {'User-Agent:': get_rand_element(user_agents), 'content-type': 'application/json'} + + session = ClientSession(cookie_jar=jar, timeout=timeout, skip_auto_headers={"User-Agent"}) + try: + async with session.post(url, headers=headers, json=creds) as r: + if r.status == 200 or r.status == 403: # 403 stands for User Already Registered + await session.close() + return Result.UP + await session.close() + return Result.MUMBLE + except Exception: + logger.error('An exception occurred', exc_info=sys.exc_info()) + await session.close() + return Result.MUMBLE + return Result.MUMBLE + + +async def authN(endpoint, LOGIN_CREDS): + result, auth_token = await login_user(endpoint, LOGIN_CREDS) + if result != Result.UP: + return '' + return auth_token.value + + +async def login_user(endpoint, creds): + url = get_url(endpoint) + "/api/auth/sign_in" + timeout = ClientTimeout(total=TIMEOUT) + jar = CookieJar(unsafe=True) + headers = {'User-Agent:': get_rand_element(user_agents), 'content-type': 'application/json'} + + session = ClientSession(cookie_jar=jar, timeout=timeout, skip_auto_headers={"User-Agent"}) + try: + async with session.post(url, headers=headers, json=creds) as r: + if r.status == 200: + await session.close() + return Result.UP, r.cookies.get('session') + await session.close() + return Result.MUMBLE, None + except Exception: + logger.error('An exception occurred', exc_info=sys.exc_info()) + await session.close() + return Result.MUMBLE, None + return Result.MUMBLE, None + + +def get_url(endpoint): + return "http://{0}:{1}".format(endpoint, PORT) + + +async def ping_service(endpoint): + url = get_url(endpoint) +"/health_check" + timeout = ClientTimeout(total=TIMEOUT) + jar = CookieJar(unsafe=True) + headers = {'User-Agent:': get_rand_element(user_agents)} + + session = ClientSession(cookie_jar=jar, timeout=timeout, skip_auto_headers={"User-Agent"}) + try: + async with session.get(url, headers=headers) as r: + await session.close() + if r.status == 200: + return Result.UP + return Result.DOWN + except Exception: + logger.error('An exception occurred', exc_info=sys.exc_info()) + await session.close() + return Result.DOWN + return Result.MUMBLE + +async def check_another_func(endpoint, creads): + async def check_blogs_list(blog_id): + url = get_url(endpoint)+"/api/blogs" + headers = {'User-Agent:': get_rand_element(user_agents)} + status_code, json_data, data_data = await get_request(url, headers=headers) + if status_code == 200: + record = next(item for item in json_data if item["url"] == blog_id) + if record: + return True + else: + print("check_blogs_list - not successful") + return False + + async def check_image_upload(token): + folder = random_str(8) + url = get_url(endpoint)+f"/file/upload?path={folder}" + cookies = {'session': token} + file_bytes = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x08\x02\x00\x00\x00\xfc\x18\xed\xa3\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x05\x17IDATH\x89\x9dV\xfdO\x13g\x1c\xef\xdf\xb0_\xf6\xc3\x94\xb6\x14*\xd2^\xefzo\xed\xd1\xbb\xbeS\xdan\xcc\x01+\xacF\x8dd.LE\x9c! \x03\xa2AQ\x06\x03\t40:p"2 \x0ep\xbe-31:q\xcaL\x9dC\xb7\xfd\xb0(\x0cfp0\xb0Z\xc4\x96\x97\xf6n\xb9\x1e4\xa4-P|\xf2\xe4~\xb8\xdc\xf3\xf9|\xbe\x9f\xef\xcb=<&\xb6\xe5}\xedu\xdd\xb9\xd7\xe6\xf8\xbaxo\xa1\xddb3a\x06\xad\x8c4\xa1\xfa\x8f,\xb6\xc3\xfb\x8a\\\x83.\x86ah\x9a\x8e<\xc8\x8b\x05\xbd\xab\xed\x9c\xcd\x98AI\x08D\x00\xe2"X\xb5U\xa1\x96\xa6h\x00\x95Z\x9aB&+Q!\xa4\x96\xa6\xdc\xbe1\xc00L \x10\xd8\x18A x\xc0Q\xdd {\'Y\x0f\xa9\xf5\x90Z\x07RZ\x19\xa9\x01T\xdc\xd6\xcaH\x83\\\x93\x92\x84[\t\xd3\xd3\xb1\x7f"\xe3X\x87\x80\x0e~\xfd\xca3c3e\xa6$\xe1+\xa1Wn=\xa4F\xf8\xb2\x93e\x95\x91A\xaco\x91\xdf\xefg\x18\xa6\xe5T3"\x00\xf5\x90:*\x81VFR\x12\xc2\xac0N>\x9b\x08\x0bb}\x82@P\xd1\xc3_\x87(\t\x11\x15\x9d\xdb:\x90\xc2E\xf2\xab\xfdWB\x9ab%\xa0\x83r\xe6|>[\xea:.\xc9\xe3\x80\x86\xaa\xfa\r\x130\xcb\x1c%\xf9EX\xbc\\\x07R\xd1#\x80(,\x1e*+(\xd9\xb0E\xcc\xb2"Gu\x83<\x0eX-\rA\x8b\xe0\xcf>>\x10v66\x82E\x96\xa0\xb3\xadc]\x82CoH\xe0g\t\xfa\xbazQ!\xa8[\x8d\x80\xb5H^z\xe0\xf0\x9b[\xd4\xdby~\x8dJ\xd5Cj\x98\x0f\xd4VT\x87\xb5\xc2\x06,jo\xf9\x06\xda,\xd5\xcbW\'\x88\x03\xbaNw\x86\xbe\xdf\x00A \xa8\xe8\x8b\xf2\x13\x08_\xb6F\xaf\xa9\xb6*\x06\x07\xeel<\x02\x9a},..\xe6f\xee"\xc4\xa86Z\x99je$\x99\xac\xb4(M\xcf\xa7\x9e\xbfa\'?\xf9\xeb\x89\x0e\xa4\xd8!*[\xb5\x84\x0e\xe6\xe6G\x1e\x8fu\x16\xb5\xb7\x9cY;\xc3\x88\x00\xec\xef\xea\rk\xe3X\xa7\xe9\xfc\xfc\xfc\x8et;!\xc6\xa2\xfb\x03R\xc4\x16,\'-k\xf6\xd5,G@\xc7\x9e\x03\x7fP\xce\x0f\x17\xaeb\xa2U\x87\x84\x1eR\xa3B\xe8r\xefE\xd6O\xff\x12\xb4\xdf\xef\x9f}5\xfb\xc2\xfd\x82\xb7\xae\xfc\x19\xcf\xccvk6!\xc6\xa2\x12p\x7f\x82\xe2\xbd\x85\\\xb6F\x1e\x8f\xf4u\xf5\x9e(=\x9eg\xdfcV\x18K\x0bJV%\xa0i:T\x9d\xa8\x10\xd2A\xd1\xd1q\x11l\xb7d\xbft\xbf\x1c\x7f:\xfe\xe9\xf6O\x0c\xb0\x16\x13\xc9a\xbe\x8c\x10c\xd0&I\xf5\x91\x93\xbc\xb5\xd1[\x1b\x9d\xb8H\x1euD\xb3\xe8\tp\x96\xfe\x83\xd1\xe1Q\x86a\x16\x16\x16>\xcf/\xe6\n!\r7\xa4\x93\x16\xc9\xdb\xe2s\xadg\xa3\x10\xd0\x01\xd6\x19\x9a\xa6\x9bk\x1d\xb8\x08f\xd1e\xd1}\xb7[\xb3\xc7F\xc6B\xa9\x9a|6\x91\xa1K\x97\xc7\x01\xd5G\xaa\x18\x86\xa9;V38p7\x9c\x80\x13\xfe\xd2\xfd\xa2\xac\xa0\x04\x8b\x8f\xa2\x9d\xfb\xe9#|\xb0`\xf7\xfe\xa9\xc9\xa9\xd0\x11.\xbd\xb5\xc7j\xe08\xc0\xac0v8\xdb9b^\xa4-\xf7\x07]9iY\x98\x10\n\xcb\xaaVF\xea J)FUI\x8a\xa6/\x1dsss+\x9b\x96\x9b?\x1d\xcevv\xa4\x83\x14\xb4YZ[Q\xb3T\xa6!h\x9f\xd7\xdbr\xaa\x89LV\x12[\xb0\x95cY\x07RzH\x9d\x92\x84#Bhw\xc6N\xd7\xdd{\x9c\xf0\x95#\x81\x8b\xe0ha9*\x84\xb8\xdb\r"\x00\xaf\xf4]\xe6\xd1\xc1\xc50\xcc\x90\xeb\xb7\xdc\xac]\xa8\x00\xe4\xc4\x86p5\x80J\x91\x88`\xf1P\xb6)\xf3|G\x8f\xcf\xeb\r\x9b6\x81@\x80C\x1f\xb8~\x8b\xbb\x90-\xcd\xbe$\xdc\x96\x9a\xc1F0\xe3\xf18\xaa\x1b\x82\xaf\x14FX\xcb:#U\x91[\x95\xb8\x08F\x04\xa0\x16P\xe5\xd9\xf7\\\xe8\xee\x9b\xf1x8h\xae\nX\xdc\xe5\x8e\xa5i\xfa\xd2w\x17\x8d\xb0\x96LV\xaeL[*\xaa\xe3}\xdf\xd3oD\xb4\xe2\xb7\x84p\x1c\x00\xf3\x01\x98\x0fr=\xf5>e=\xb4\xa7\xe0L\xf3\xe9?\x1f\xfd\xc1\x15\tM\xd3\x8b\x0b\x8ba\xf7*\xef\xeb\xd77\xaf\xdd8\xb0{\xbf"\x01\xa1$D\x08]/\xd7 \x02\xb0\xa2\xe8(\xaf\xa7\xbd\xbb\xb9\xd6\xe1\xac\xff\xaa\xa9\xd6\xd1\xda\xe8\xec>\xf3\xed\xb5\xcb?>z\xf0\xd0=\xed\x8e\xbch2\xcbu<\xfd\xdf\xd4\xad\xeb?\xd5W\xd6\xd9-6B\x8c*\x13\x11]\xd0wn\x1b`\x8dR\x8cZ\t\xd3\xe8\xf0\xdf1\x8dk\x9fonb\xfc\xdf\xfb\xbf\xb8\xfa{\xfa\xeb+\xeb\xf6\xef\xcc\xb3*M\x84\x18\x93o\x96*\x13QJBpu\xa1LD\x15\tl\xb6\xc0M\x92\xf7H\xf3\x90\xeb\x01[E\xfe\xe0\n\xf8\x03\xf3s\xf3\xeei\xf7\xd8\xc8\xd8\xefC\x8f~\xbey\xfbJ\xef\xa5\xd6F\xe7\xf1\x92\x8a\x83\xb9\xf99\xe6\x0f\xd3p\x83\x06P\xe1"\x18\x15\x80\xaa$\x85\x0e\xa4\xcc\xb81\x9d4oS\xbf\x9be\xd8f\xb7f\xe7f\xee\xda\xb7#\xafxoaUyek\xa3s\xe4\xf10\'\xee\x7f\xc8C\xe2\x94\xb5\xb8\xaa\xe6\x00\x00\x00\x00IEND\xaeB`\x82' + data_file = FormData() + data_file.add_field('file', + file_bytes, + filename='favicon.png', + content_type='image/png') + headers = {'User-Agent:': get_rand_element(user_agents)} + status_code, json_data, data_data = await post_request(url, headers=headers, cookies=cookies, data=data_file) + if status_code == 200: + name = json_data.get("filename") + return folder, name + else: + print("check_image_upload - not successful") + return None, None + async def check_image_access(path, filename): + async def check_file_list_exist(): + url = get_url(endpoint)+f"/file/list?path={path}" + headers = {'User-Agent:': get_rand_element(user_agents)} + status_code, json_data, data_data = await get_request(url, headers=headers) + if (filename in json_data): + return True + else: + print("check_file_list_exist - not successful") + return False + + exist_in_list = await check_file_list_exist() + if exist_in_list: + url = get_url(endpoint)+f"/image/ss?another={path}&filename={filename}" #TODO WITHOUT SS -> payload + headers = {'User-Agent:': get_rand_element(user_agents)} + status_code, json_data, data_data = await get_request(url, headers=headers) + if status_code == 200: + return True + else: + print("check_image_access - not successful") + return False + + try: + auth_token = await authN(endpoint, {"username": creads.get("username"), + "password": creads.get("password")}) + if auth_token: + blog_url = await get_blog(endpoint, auth_token) + if blog_url: + if await check_blogs_list(blog_url): + path, filename = await check_image_upload(auth_token) + if filename and path: + check_image_result = await check_image_access(path, filename) + if check_image_result: + return Result.UP, "" + else: + return Result.MUMBLE, "check image access - failed" + else: + return Result.MUMBLE, "check file's name or path failed" + else: + return Result.MUMBLE, "check_blogs_list failed" + else: + return Result.MUMBLE, "GET blog_url failed" + else: + return Result.MUMBLE, "auth failed" + except: + logger.error('An exception occurred', exc_info=sys.exc_info()) + return Result.MUMBLE, "exception" + + + +# ------------------------ PUSH & PULL -------------------------- + +async def push_content_server_flag(endpoint, flag, TI_REGISTRATION_CREDS): + result = await register_user(endpoint, TI_REGISTRATION_CREDS) + if result == Result.UP: + auth_token = await authN(endpoint, {"username": TI_REGISTRATION_CREDS.get("username"), + "password": TI_REGISTRATION_CREDS.get("password")}) + cookies = {"session": auth_token} + url = get_url(endpoint) + "/file/upload?path=secrets" + timeout = ClientTimeout(total=TIMEOUT) + jar = CookieJar(unsafe=True) + headers = {'User-Agent:': get_rand_element(user_agents)} + + # with BytesIO() as myio: + # myio.write(flag.encode()) + form_data = aiohttp.FormData() + form_data.add_field('file', flag.encode(), filename=TI_REGISTRATION_CREDS.get("username")+".txt", content_type='multipart/form-data') + + session = ClientSession(cookie_jar=jar, cookies=cookies, timeout=timeout, skip_auto_headers={"User-Agent"}) + try: + async with session.post(url, headers=headers, data=form_data) as r: + if r.status == 200: + await session.close() + return Result.UP + await session.close() + return Result.MUMBLE + except Exception: + logger.error('An exception occurred', exc_info=sys.exc_info()) + await session.close() + return Result.MUMBLE + return Result.MUMBLE + +async def push_blog_flag(endpoint, capsule, TI_REGISTRATION_CREDS): + result = await register_user(endpoint, TI_REGISTRATION_CREDS) + if result == Result.UP: + try: + auth_token = await authN(endpoint, {"username":TI_REGISTRATION_CREDS.get("username"), "password":TI_REGISTRATION_CREDS.get("password")}) + return await send_create_site(endpoint, auth_token, capsule) + except: + logger.error('An exception occurred', exc_info=sys.exc_info()) + return Result.MUMBLE, '' + return Result.MUMBLE, '' + + + + +async def send_create_site(endpoint, token, ti_capsule) -> (int, str): + async def get_blog(): + url = get_url(endpoint) + "/api/blog" + headers = {'User-Agent:': get_rand_element(user_agents), 'content-type': 'application/json'} + cookies = {"session": token} + status, json_data, data_data = await get_request(url, headers, cookies=cookies) + if status == 200: + return json_data.get("url") + else: + return None + async def create_post(blog_id): + url = get_url(endpoint) + f"/api/blog/{blog_id}/create_post" + + headers = {'User-Agent:': get_rand_element(user_agents), 'content-type': 'application/json'} + json_data = {"title": "secret", + "body": ti_capsule} + cookies = {"session": token} + status, json_data, data_data = await post_request(url, headers, json_inp=json_data, cookies=cookies) + if status == 200: + return Result.UP, json_data.get("post_id") + return Result.MUMBLE, '' + blog_url = await get_blog() + if blog_url: + return await create_post(blog_url) + else: + return Result.MUMBLE, "" + +async def push(endpoint, capsule, label, metadata): + result = await ping_service(endpoint) + if result != Result.UP: + return result, label, NOT_WORKING_MESSAGE + + round_num = metadata.round # .get('round') TODO + round_remainder = round_num % 2 + + ad_username = random_str(random.randrange(6, 10)) # "test" + ad_password = random_str(random.randrange(8, 12)) # "test1337" + AD_REGISTRATION_CREDS = dict(username=ad_username, password=ad_password) + if round_remainder == 1: + print('push content_server') + #is_private == Null -> for server False + result = await push_content_server_flag(endpoint, capsule, AD_REGISTRATION_CREDS) + if result != Result.UP: + return result, json.dumps({"round_remainder": round_remainder}), FLAG_WAS_LOST + return result, json.dumps( + {"round_remainder": round_remainder, "username": ad_username, "password": ad_password}), ALL_FINE + else: + print('push blog private text') + result, post_id = await push_blog_flag(endpoint, capsule, AD_REGISTRATION_CREDS) + if result != Result.UP: + return result, json.dumps({"round_remainder": round_remainder}), FLAG_WAS_LOST + return result, json.dumps({"round_remainder": round_remainder, "post_id": post_id, "username": ad_username, + "password": ad_password}), ALL_FINE + +async def pull(endpoint, capsule, label, metadata): + result = await ping_service(endpoint) + if result != Result.UP: + return result, NOT_WORKING_MESSAGE + + round_num = metadata.round # .get('round') TODO + round_remainder = round_num % 2 + + + result, msg = await check_another_func(endpoint, json.loads(label)) + if result != Result.UP: + return result, msg + + + + if round_remainder == 1: + print('pull content_server') + result = await pull_content_server_flag(endpoint, capsule, json.loads(label)) + if result == Result.UP: + return result, ALL_FINE + elif result == Result.CORRUPT: + return result, CORRUPTED + else: + return result, NOT_WORKING_MESSAGE + else: + print('pull blog private text') + result, msg = await pull_blog_flag(endpoint, capsule, json.loads(label)) + if result == Result.UP: + return result, ALL_FINE + elif result == Result.CORRUPT: + return result, msg + else: + return result, msg + +async def pull_blog_flag(endpoint, capsule, label): + auth_token = await authN(endpoint, label) + return await get_blog_capsule(endpoint, auth_token, capsule, label.get('username')) + +async def pull_content_server_flag(endpoint, capsule, label): + auth_token = await authN(endpoint, label) + return await get_file_capsule(endpoint, auth_token, capsule, label.get('username')) + +async def get_blog(endpoint, token): + url = get_url(endpoint) + "/api/blog" + headers = {'User-Agent:': get_rand_element(user_agents), 'content-type': 'application/json'} + cookies = {"session": token} + status, json_data, data_data = await get_request(url, headers, cookies=cookies) + if status == 200: + return json_data.get("url") + else: + return None + +async def get_blog_capsule(endpoint, token, capsule, username): + + async def get_posts(): + url = get_url(endpoint) + f"/api/blog/{blog_id}" + headers = {'User-Agent:': get_rand_element(user_agents), 'content-type': 'application/json'} + cookies = {"session": token} + status, json_data, data_data = await get_request(url, headers, cookies=cookies) + if status == 200: + return json_data.get("posts") + else: + return None + + async def get_post(): + url = get_url(endpoint) + f"/api/blog/{blog_id}/post/{needed_post_id}" + headers = {'User-Agent:': get_rand_element(user_agents), 'content-type': 'application/json'} + cookies = {"session": token} + status, json_data, data_data = await get_request(url, headers, cookies=cookies) + if status == 200: + return json_data.get("body") + else: + return None + + try: + blog_id = await get_blog(endpoint, token) + if blog_id: + posts = await get_posts() + needed_post_id = next(item["id"] for item in posts if item["title"] == "secret") + if needed_post_id: + expected_capsule = await get_post() + if expected_capsule == capsule: + return Result.UP, '' + else: + return Result.CORRUPT, 'cant get flag in post' + + except: + return Result.MUMBLE, 'except' + + +async def get_file_capsule(endpoint, token, capsule, username): + async def check_file_secrets(filename): + url = get_url(endpoint) + f"/file/list?path=secrets" + headers = {'User-Agent:': get_rand_element(user_agents)} + status_code, json_data, data_data = await get_request(url, headers=headers) + if (filename in json_data): + return True + else: + print("check_file_secrets - not successful") + return False + + res = await check_file_secrets(username+'.txt') + if res: + url = get_url(endpoint) + f"/file/get/secrets?filename={username+'.txt'}" + timeout = ClientTimeout(total=TIMEOUT) + jar = CookieJar(unsafe=True) + headers = {'User-Agent:': get_rand_element(user_agents)} + cookies = {"session": token} + session = ClientSession(cookie_jar=jar, cookies=cookies, timeout=timeout, skip_auto_headers={"User-Agent"}) + try: + async with session.get(url, headers=headers) as r: + if r.status == 200: + data = await r.read() + expected_capsule = data.decode("utf-8").strip() + await session.close() + if expected_capsule == capsule: + return Result.UP + else: + return Result.CORRUPT + await session.close() + return Result.MUMBLE + except Exception: + logger.error('An exception occurred', exc_info=sys.exc_info()) + await session.close() + return Result.MUMBLE + return Result.MUMBLE + else: + return Result.MUMBLE + +# ------------------------ TEST MAIN ------------------------ + + +def play_round(endpoint): + metadata = {'round': 1} # site_prefix to push_site) + loop = asyncio.get_event_loop() + + total_rounds = 1 + + for i in range(2 * total_rounds): + label = "" + capsule = 'time {}'.format(i) + push_task = loop.create_task(push(endpoint, capsule, label, metadata)) + loop.run_until_complete(push_task) + status, label, msg = push_task.result() + print("push task", (status, label, msg)) + + pull_task = loop.create_task(pull(endpoint, capsule, label, metadata)) + loop.run_until_complete(pull_task) + status, msg = pull_task.result() + print("pull task", (status, msg)) + + metadata['round'] += 1 + + loop.close() + + +if __name__ == "__main__": + play_round(endpoint="127.0.0.1") diff --git a/check_system/src/myblog/requirements.txt b/check_system/src/myblog/requirements.txt new file mode 100644 index 0000000..8b15e1f --- /dev/null +++ b/check_system/src/myblog/requirements.txt @@ -0,0 +1 @@ +asyncio diff --git a/checker b/checker deleted file mode 160000 index 179b297..0000000 --- a/checker +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 179b297abcc7f545163d363d366e6af723c35856