This week Microsoft released an update for CVE-2020-1350 (SIGRed), a Remote Code Execution vulnerability that affects Windows Server versions from 2003 to Server 2019.
It was assigned a CVSS score of 10 due to its capability of spreading between other machines without user interaction. The DNS service is commonly found on Domain Controllers. Attacking these servers may compromise high level accounts.
The SIGRed vulnerability was researched and published by Sagi Tzadik from Check Point Research team. All the credits goes to them. Original research can be found here https://research.checkpoint.com/2020/resolving-your-way-into-domain-admin-exploiting-a-17-year-old-bug-in-windows-dns-servers/.
Given that this is a Windows DNS Server vulnerability we started by spin up a new virtual machine running Windows Server 2012 R2 x64.
We also need to setup our DNS server to be able to answer queries originated from vulnerable machine. The communication flow is similar to the diagram.
In summary, to trigger the bug in Windows DNS Server, the communication between the machines must be as follows:
- The attacker query the Windows DNS for his controlled domain (e.g. 41.sigred.pwn)
- Windows DNS doesn’t have the record; forwards to alternate DNS server (e.g 220.127.116.11)
- DNS at 18.104.22.168 has the record of 41.sigred.pwn and replies to Windows DNS
- Finally Windows DNS caches this response and answer to the attacker
This is a rather simplified description. Since we are testing over a controlled environment we can do actual requests directly to DNS server and even use the same DNS server to create records to point to our malicious server. The goal is to trigger the bug and not to simulate a real attack scenario.
The researchers started by creating a DNS configuration with two domains, setting the nameservers of one domain pointing to the malicious DNS of the other, making Windows DNS cache these nameservers for subsequent queries of the second domain.
The main goal is to force Windows DNS to query our custom server, i.e., acting as a client. A proper DNS setup could be particularly useful to trigger the bug on Windows DNS servers that are not reachable from the exterior (e.g. organization intranet, etc). The research also demonstrated one technique to smuggle the DNS query disguised as a HTTP packet.
Finished the operating system install, we need to setup our domain to establish DNS communication with the vulnerable server. Started by creating a New Forwarded Zone sigred.pwn in Windows DNS. Then create a New Delegated subdomain 41.sigred.pwn in order to transfer control to the zone hosted in a different DNS server. The 41.sigred.pwn will be pointing to attacker DNS host.
Final configuration looks like this.
Note that IP address 192.168.1.215 is the address of our custom DNS server, listening on port 53 (attacker machine host the server).
To test if the DNS is correctly setup, we can create a New Record at sigred.pwn zone (e.g. A record) and query to verify if we receive a valid response.
Obviously the address 192.168.1.216 is the host of vulnerable Windows DNS Server. We will send the initial DNS query with dig to force Windows DNS forward requests to our malicious server.
Attacker DNS Setup
Setting up a server to receive, process & reply to DNS requests was not trivial. All the components are based on Python dnslib, which is a great library but unfortunately the documentation wasn’t provided.
The lack of specification lead to some hours reverse engineering the library, modifying and adapting to our needs. On the other hand, some fundamental DNS internals were learned while understanding how this lib works (headers, RRs, RFC formats, etc).
All the code is based on dnslib and can be found here. The proxy is an excellent candidate as it supports both TCP/UDP by running different threads. It also includes handler and resolver methods that are sufficiently abstract to implement our solution.
The PassthroughDNSHandler class receives the requests, parses them into DNSRecord objects and forwards to the resolver (e.g. 22.214.171.124). The reply will be then sent to the original request (just like a proxy). However we don’t want to focus on implementation but on application to our particular problem: let it receive & send DNS packets to the vulnerable server.
Previously we setup a delegated zone 41.sigred.pwn that points to our DNS server/proxy. If we dig this domain, our malicious DNS will be in charge of processing and replying. By modifying the handler we can achieve that.
If we ask Windows DNS for a A record of 41.redsig.pwn, it will query our Python server, because 41.redsig.pwn is delegated to 192.168.1.215.
At this moment we already have control in what Windows DNS receives. It is possible to build answers via dnslib and send them back to the vulnerable server (or whoever requests it).
What if we reply with a crafted packet?
The researchers at Check Point Research found a vulnerability contained in dns.exe, responsible for answering DNS queries on Windows Server.
This particular issue cause an integer overflow when calculating the total size of memory to allocate, given a SIG Resource Record data. As a consequence, the allocated memory is way less than the needed to put the actual data. The size is then converted to 16 bit representation, thus requiring it to exceed 65536 bytes in order to provoke overflow.
When the allocated (small) memory address is used as a destination buffer, the memcpy will overwrite potential existing data, therefore leading to a Heap-Based Buffer Overflow.
Without entering in too much technical details, it is possible to cause this overflow by sending a sufficiently large response that uses compression in one field.
In DNS, strings are encoded as <size><value>. To avoid repetition of strings (thus, saving bytes when sending over the network) they can be compressed using the magic byte 0xc0[offset].
For example, if we have the following data encoded as <size><value>:
- \x00 \x00 \x03 F O O \x04 1 2 3 4 (…)
We can create a reference within the packet to FOO as 0xc002 and another reference to 1234 as 0xc006, where both 0x02 and 0x06 are the offsets for each string respectively, since the beginning of the data.
Researchers discovered that when setting the offset to hit one digit of the string, the value that will be taken in consideration into the sum of size of memory to allocate is the decimal value (ascii table) of that digit, and not the actual string size.
For example, if we make a pointer as 0xc007 it will hit the character 1 and sum 49 to the size of memory to allocate, given that 49 is the decimal value of character 1. Even though, the string only has a length of 4 bytes. (Credits to @maxpl0it for pointing this out)
Given the simple explanation of the vulnerability, we are now able to build an exploit that will cause the remote DNS service to crash (DoS).
First we should handle SIG resource type. We’ve previously tested the A response over our DNS proxy. At this time we just need to craft another record of the SIG type. (Note: RRSIG is also vulnerable since it uses the same dns!SigWireRead function to calculate size for malloc).
Our Python handler function looks like the following to reply with a SIG record. Note the parameters name and sig. They correspond to both Signer’s Name and Signature, respectively.
Other fields are not really important in this case. Maybe it is a good idea to set TTL to 0 when performing tests (avoid caching, quicker update reflection). However, increasing TTL could also be useful to store our record in Windows DNS memory for a longer time.
There is one more thing needed to successfully exploit this vulnerability. The DNS communication is usually/initially started as UDP, but DNS over UDP is limited to a payload of 512 bytes and that’s not enough to produce the overflow.
That said, we must move on to TCP which packet size limit is, indeed 65535.
In this case, we are testing in a controlled environment so we could just query with dig +tcp flag but that would be cheating. There’s another wise technique as demonstrated in original research.
The first incoming request from Windows DNS to our malicious DNS will be over UDP but we can then reply with the flag TC (truncate) set. The client will immediately repeat the query over TCP. And that’s exactly what we need to be able to reply with a packet of ~65535 bytes in length containing compressed strings.
- 192.168.1.215 - Attacker machine & Python malicious DNS
- 192.168.1.216 - Vulnerable Windows DNS Server
By intercepting the communication we can observe the following:
- No. 2 - Initial SIG query of 41.sigred.pwn to Windows DNS
- No. 5 - Windows DNS forwards to attacker DNS
- No. 6 - Attacker DNS replies to Windows DNS with TC flag set (truncate, force to repeat over TCP)
- No. 10 - The Windows DNS repeat the query but this time over TCP (Red)
- No. 12 - Our malicious DNS sends the payload, over TCP, to Windows DNS (Red)
To successfully trigger the bug, the malicious DNS response must be a large TCP packet: the signature field containing maximum no. of bytes, and the value of signame field containing a compressed string, pointing to 1-byte ahead of the beginning of encoded name 41.sigred.pwn.
This will cause the total size of the record data to be interpreted as a value greater than 65535, thus causing the overflow and crashing dns.exe.
The source code of the attacker DNS server is available at my GitHub: https://github.com/joaovarelas.