Intro to Whitebox Pentesting  

Command Execution

By now, we have confirmed the existence of a code injection vulnerability in the web application we are testing. Our next goal is to simplify the process of exploiting it and maximize the gains we can obtain from this vulnerability.

It would not be ideal or efficient if we downplay our findings by not reaching maximum exploitation, which is usually remote code execution for web applications. Anything beyond that would be beyond the web penetration testing scope.

So, going back to our plan, our next step is to reach command execution:

  • [x] Hit the validateString function
  • [x] Trace how our input looks within the function
  • [x] Obtain an admin role
  • [x] Confirm that we reach the eval function
  • [x] Prepare the payload
  • [x] Confirm the payload reaches the target function as intended
  • [x] Inject code and confirm code injection
  • [ ] Reach command execution or file writing
  • [ ] Blindly verify command execution/file writing
  • [ ] Automate the exploitation process by writing an exploit

Furthermore, as we have seen in the previous section, we know that our code output would be limited to the NodeJS console on the backend server. After reaching command execution, we would need to find a way to obtain the output of our commands to remotely verify whether our exploitation attempt has worked.

NodeJS System Command Execution

Since we should be able to inject any code we want, we should be able to use whatever function NodeJS uses to execute system commands. A quick search for a one-liner to execute system commands with NodeJs shows the following:

require("child_process").execSync("touch pwned");

Exercise: Write the above code in a test.js file and run it with node test.js, then check if a new file called pwned was created.

As an initial confirmation of reaching command execution through our vulnerability, we will attempt to create a new file called pwned, and then we can manually confirm that it was created. Now, to execute this command, we can place this into our payload from the previous section so that it would look like the following:

{
  "text": "'}) + require('child_process').execSync('touch pwned')//"
}

We can now try sending this payload and checking the code base directory to see if the pwned file is created:

As we can see, the file was indeed created, so we achieved command execution on the system. The next step is to remotely obtain the output of any commands we write without having access to the backend.

Note: In some NodeJS applications, the use of the require keyword may not be possible, such as when "type": "module" is specified within package.json. In such cases, we would need to find an alternative code to use for command execution, which shouldn't be difficult if you know JavaScript, or we can simply rely on already imported packages to do the same "if any".

Obtaining Command Output

With command execution possible, let's see if we can obtain any output of those commands. Let's consider the possible options for such cases:

  1. Log output to console "for local testing"
  2. Use a reverse shell
  3. Use HTTP exfiltration (through GET parameters)
  4. Use DNS exfiltration (or ping exfiltration)
  5. Store the output in the database
  6. Write the output to a file, then access that file
  7. Inject the output into the HTTP response
  8. Use sleep timers or boolean output to read the content

We may be able to easily modify our previous JavaScript system command execution code to capture the output and log it to the console as follows:

console.log(require("child_process").execSync("ls").toString());

If we use this code in our payload, it would indeed log the command output to the console:

However, this won't be useful in the real attack, as we won't have access to the backend console as we do now. So, we must think of other ways to obtain the commands' output. Furthermore, to make things even more complicated, the backend server prevents any outgoing connections, which makes it impossible to receive a reverse shell of any kind. As another security mechanism, the backend server does not even have internet access, so we can't rely on HTTP or DNS exfiltration to obtain the output "as we did in other modules".

Finally, as we already know, the web application we are testing is quite simple and does not rely on a database, so we can also rule out that option. This last one is relatively uncommon as most web applications use databases, so this option is usually valid, but it's another challenge we will need to bypass.

This leaves us with two final options, so let's consider each.

Output through File Read

We can attempt to direct the command output to a file (e.g. > ./file.txt) or through JavaScript code fs.writeFile. However, for any of this to work, two conditions need to be met:

  1. Find a directory the application has access to write into
  2. Find a way to read the content of this file through public access

As for the first one, the web application usually has access to its directory so we can rely on that. We can always verify this through our testing phase, as the local setup will likely require similar access privileges as the production server.

Once we can write the output to a file, we must publicly access its content. There are multiple ways to do so, such as:

  1. Write it to a publicly accessible file
  2. Read the file content through a file read vulnerability (e.g. LFI, XXE, SQLi etc)

The application we are testing only exposes pre-specified routes mapped to specific functions, such as the API endpoints. So, we cannot simply drop any file and access it. Still, most such applications provide a public directory that everyone can access, usually called ./public and contains things like css and js files that are necessary for the front-end web application to run. We would usually write the output to this directory and access it that way.

In this case, we cannot find any public directory specified in app.js. However, we could try another approach by overwriting one of the functions linked to an exposed route, such as generateQR, and turning it into a basic web shell. Or, we could even add extra code to app.js to expose a new route and map it to a web shell middleware we create. An example of this would be the following:

app.get("/api/cmd", (req, res) => {
  const cmd = require("child_process").execSync(req.query.cmd).toString();
  res.send(cmd);
});

This would require a lot of local testing to ensure that our payloads would work without messing up the application. In any case, this method may not be the best approach, as production web applications would need to be restarted to execute any new code.

Finally, we are left with reading the file content through another vulnerability. We have already reviewed the entire code and did not notice any other vulnerability or see any file read function calls. There could be such a public vulnerability in one of the packages being used by the web application, but we have already tested these packages, so we can also rule out this option.

In the next section, we will see if obtaining the command output through the HTTP response is possible.

/ 1 spawns left

Waiting to start...

Questions

Answer the question(s) below to complete this Section and earn cubes!

Click here to spawn the target system!

Target: Click here to spawn the target system!

+10 Streak pts

Previous

+10 Streak pts

Next