Nginx, a well-known Web server and reverse proxy server, has a serious vulnerability: the Nginx off-by-one heap write vulnerability, which exists in Nginx’s DNS parsing module ngx_resolver_copy(). Attackers can exploit this vulnerability to carry out remote DDos attacks, or even execute them remotely.

An off-by-one error occurred when ngx_resolver_copy() processed a DNS response, which allows a network attacker to write a dot character (. ‘, 0x2E) in the heap-allocated buffer, causing it to go out of range. All Nginx instances configured with parser syntax (resolver XXXX) can trigger this vulnerability through DNS responses that respond to DNS requests from Nginx. Tailor-made packets allow the use of 0x2E to overwrite the least significant bytes of metadata in the next heap block, enabling an attacker to implement Ddos denial of service and possibly even remote code execution.

Due to the lack of DNS spoofing mitigation in Nginx and the fact that vulnerable functions are invoked prior to checking the DNS transaction ID, a remote attacker may be able to inject a tainted DNS response to the infected server to exploit this vulnerability.

Holes affect

Severity level: High

Vulnerability vector: remote /DNS

Confirmed affected versions: 0.6.18-1.20.0

Confirmed fixes: 1.21.0, 1.20.1

Supplier: F5, Inc.

Status: Public

CVE: CVE – 2021-23017

CWE: 193

CVSS score: 8.1

The CVSS vector: CVSS: 3.1 / AV: N/AC: H/PR: N/UI: N/S: U/C: H/I: H/A: H/E: U/RL: O/RC: C

Vulnerability Analysis When resolver is configured in the Nginx configuration, the Nginx DNS resolver (core/ngx_resolver.c) is used to resolve the host names of multiple modules through DNS.

The ngx_resolver_copy() call in Nginx validates and decompresses each DNS domain name contained in the DNS response, receives the network packet as input and a pointer to the name being processed, and on success returns a pointer to the newly allocated buffer containing the uncompressed name. The whole call is done in two steps,

1) Calculate len of the uncompressed domain name size and verify the validity of the input packet. Discard domain names that contain more than 128 Pointers or contain domain names that exceed the input buffer boundary.

2) Allocate the output buffer and copy the uncompressed domain name into it.

A mismatch between the size calculation in Part 1 and the uncompressed domain name in Part 2 resulted in an off-by-one error in len, which allowed one dot character to be written in bytes beyond the name->data boundary.

A calculation error occurs when the last part of the compressed name contains a pointer to a NUL byte. Although the computation step considers only the points between labels, the decompression step writes a dot character each time the label is processed and the character that follows is non-NUL. When the label is followed by a pointer to a NUL byte, the decompression process will:

// 1) copy the label to the output buffer,

ngx_strlow(dst, src, n);

dst += n;

src += n;

// 2) read next character,

n = *src++;

// 3) as its a pointer, its not NUL,

if (n ! = 0) {

// 4) so a dot character that was not accounted for is written out of bounds

*dst++ = ‘.’;

}

// 5) Afterwards, the pointer is followed,

if (n & 0xc0) {

n = ((n & 0x3f) << 8) + *src;

src = &buf[n];

n = *src++;

}

// 6) and a NULL byte is found, signaling the end of the function

if (n == 0) {

name->len = dst – name->data;

return NGX_OK;

} If the calculated size happens to align with the heap block size, the out-of-range dot character overwrites the least significant byte of metadata for the next heap block length. This may directly cause the size of the next heap block to be written, but also overwrites three flags, causing PREV_INUSE to be cleared and IS_MMAPPED to be set.

==7863== Invalid write of size 1

==7863== at 0x137C2E: ngx_resolver_copy (ngx_resolver.c:4018)

==7863== by 0x13D12B: ngx_resolver_process_a (ngx_resolver.c:2470)

==7863== by 0x13D12B: ngx_resolver_process_response (ngx_resolver.c:1844)

==7863== by 0x13D46A: ngx_resolver_udp_read (ngx_resolver.c:1574)

==7863== by 0x14AB19: ngx_epoll_process_events (ngx_epoll_module.c:901)

==7863== by 0x1414D4: ngx_process_events_and_timers (ngx_event.c:247)

==7863== by 0x148E57: ngx_worker_process_cycle (ngx_process_cycle.c:719)

==7863== by 0x1474DA: ngx_spawn_process (ngx_process.c:199)

==7863== by 0x1480A8: ngx_start_worker_processes (ngx_process_cycle.c:344)

==7863== by 0x14952D: ngx_master_process_cycle (ngx_process_cycle.c:130)

==7863== by 0x12237F: main (Nginx.c:383)

==7863== Address 0x4bbcfb8 is 0 bytes after a block of size 24 alloc’d

==7863== at 0x483E77F: malloc (vg_replace_malloc.c:307)

==7863== by 0x1448C–4: ngx_alloc (ngx_alloc.c:22)

==7863== by 0x137AE4: ngx_resolver_alloc (ngx_resolver.c:4119)

==7863== by 0x137B26: ngx_resolver_copy (ngx_resolver.c:3994)

==7863== by 0x13D12B: ngx_resolver_process_a (ngx_resolver.c:2470)

==7863== by 0x13D12B: ngx_resolver_process_response (ngx_resolver.c:1844)

==7863== by 0x13D46A: ngx_resolver_udp_read (ngx_resolver.c:1574)

==7863== by 0x14AB19: ngx_epoll_process_events (ngx_epoll_module.c:901)

==7863== by 0x1414D4: ngx_process_events_and_timers (ngx_event.c:247)

==7863== by 0x148E57: ngx_worker_process_cycle (ngx_process_cycle.c:719)

==7863== by 0x1474DA: ngx_spawn_process (ngx_process.c:199)

==7863== by 0x1480A8: ngx_start_worker_processes (ngx_process_cycle.c:344)

==7863== by 0x14952D: ngx_MASTER_process_Cycle (ngx_process_cycle.c:130) Although no Poc is available, this vulnerability could theoretically be used for remote code execution.

Attack vector analysis DNS responses can trigger vulnerabilities in a number of ways.

First, Nginx must send the DNS request and must wait for the response. Poisoning can then be done in multiple parts of the DNS response:

DNS problem QNAME,

DNS answers the name,

DNS answers RDATA for CNAME and SRV responses,

By crafting responses with multiple poisoned QNAME, NAME, or RDATA values, you can hit vulnerable functions multiple times while processing the response, effectively performing multiple offline writes.

In addition, when an attacker provides a poisoned CNAME, it is resolved recursively, triggering an additional OOB write ngx_resolve_name_locked() call ngx_strlow() (ngx_resolver.c: 594) and other OOB reads during ngx_resolver_dup() (ngx_resolver.c: 790) and ngx_crc32_short() (ngx_resolver.c: 596).

Sample PAYLOAD of DNS response for a “example.net” request with contaminated Cnames:

A slightly different payload (the payload in Poc.py) fills enough bytes to cover the lowest significant byte dot next_chunk.mchunk_size:

The 24-byte label results in the allocation of a 24-byte buffer filled with 24 bytes + an out-of-range dot character.

Bug fixes and resolutionThis problem can be mitigated by assigning an extra byte to the forged dot character written at the end of the domain name when it is resolved.

Daemon off configuration affected by the bug;

http{

access_log logs/access.log;

server{

listen 8080;

location / {

Resolver 127.0.0.1:1053;

set $dns example.net;

proxy_pass $dns;

}

}

}

events {

worker_connections 1024;

}