Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions pocs/linux/kernelctf/CVE-2026-23209_cos/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
## Setup

Macvlan interfaces need to be created on top of a "real" (loopback won't work) ethernet interface, so first we need to create a veth pair.

Next step is to create a macvlan interface "mv0" in a source mode. In this mode destination vlan is chosen based on the source mac address of the packet.
When creating mv0 we set the source address to "00:00:00:00:00:90", so only packets from this address will be handled by mv0.

Now we try to create a second macvlan interface on the same physical interface as mv0, but with 2 differences:
- source address is set to 00:00:00:00:00:20 - this is the address of the veth0 interface, so all packets sent from veth0 will be handled by our newly created macvlan instance
- we specify an invalid interface name to trigger register_netdevice() failure

This causes the newly created net_device object to be freed while it's still referenced in the source hash of the veth0's macvlan port.
net_device is allocated from kmalloc-cg-4k and can be easily reallocated using msg_msg primitive.

## Triggering the use-after-free

Sending a packet through veth0 will result in a use-after-free in macvlan_forward_source_one():

```
static void macvlan_forward_source_one(struct sk_buff *skb,
struct macvlan_dev *vlan)
{
struct sk_buff *nskb;
struct net_device *dev;
int len;
int ret;

dev = vlan->dev;
if (unlikely(!(dev->flags & IFF_UP)))
return;

pr_debug("dev is up\n");
nskb = skb_clone(skb, GFP_ATOMIC);
if (!nskb)
return;

len = nskb->len + ETH_HLEN;
nskb->dev = dev;

if (ether_addr_equal_64bits(eth_hdr(skb)->h_dest, dev->dev_addr))
nskb->pkt_type = PACKET_HOST;

ret = __netif_rx(nskb);
macvlan_count_rx(vlan, len, ret == NET_RX_SUCCESS, false);
}

```

vlan argument points to a freed net_device object that is controlled by us, so we also control the dev field of the skb passed to ` __netif_rx()`.

## Getting RIP control

The skb will be enqueued to backlog and eventually `__netif_receive_skb_core` will be called. This function looks at skb->dev->ptype_all to check for handlers registered to receive all packets:

```
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
```

If skb->dev->ptype_all has a valid entry, deliver_skb calls ->func pointer of the ptype_all entry:

```
static inline int deliver_skb(struct sk_buff *skb,
struct packet_type *pt_prev,
struct net_device *orig_dev)
{
if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
return -ENOMEM;
refcount_inc(&skb->users);
return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
```

Since we control skb->dev->ptype_all we can point it to our fake struct packet_type and get RIP control, but we have to place net_device and packet_type at a known address in kernel address space.
This is accomplished by leaking page_offset_base though a side-channel and allocating a lot of large simple_xattr objects to fill most of physical memory with copies of our payload.
We can then point skb->dev to (page_offset_base + arbitrary offset) and have a good chance of finding our payload.

## Pivot to ROP

When .func is called in deliver_skb RSI points to our fake net_device object.

Following gadgets are used to pivot stack to our ROP chain:

```
push rsi
jmp qword [rsi + 0xf]
```

and

```
pop rsp
ret
```

## Privilege escalation
Our ROP is executed from the ksoftirqd kernel thread context, so we can't do a traditional commit_creds() to modify the current process's privileges.

To solve this issue we patch the kernel code, creating a backdoor that will grant root privileges to any process that executes a given syscall.

We chose a rarely used kexec_file_load() syscall and overwrote its code with our get_root function that does all traditional privileges escalation/namespace escape stuff: commit_creds(init_cred), switch_task_namespaces(pid, init_nsproxy) etc.

This function also returns a special value (0x777) that our user space code can use to detect if the system was already compromised.

Patching the kernel function is done rop_patch_kernel_code() - it calls set_memory_rw() on destination memory and uses memcpy() to write new code there.
58 changes: 58 additions & 0 deletions pocs/linux/kernelctf/CVE-2026-23209_cos/docs/vulnerability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
## Requirements to trigger the vulnerability

- CAP_NET_ADMIN and CAP_NET_RAW in a namespace is required
- Kernel configuration: CONFIG_PACKET
- User namespaces required: yes

## Commit which introduced the vulnerability

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=aa5fd0fb77486b8a6764ead8627baa14790e4280

## Commit which fixed the vulnerability

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f8db6475a83649689c087a8f52486fcc53e627e9

## Affected kernel versions

Introduced in 4.9. Fixed in 6.1.163 and other stable trees.

## Affected component, subsystem

net/packet

## Description

The issue is triggered when a new macvlan link is created with
MACVLAN_MODE_SOURCE mode and MACVLAN_MACADDR_ADD (or
MACVLAN_MACADDR_SET) parameter, lower device already has a macvlan
port and register_netdevice() called from macvlan_common_newlink()
fails (e.g. because of the invalid link name).

In this case macvlan_hash_add_source is called from
macvlan_change_sources() / macvlan_common_newlink():

This adds a vlan reference to the port's vlan_source_hash using
macvlan_source_entry.

vlan is a pointer to the priv data of the link that is being created.

When register_netdevice() fails, the error is returned from
macvlan_newlink() to rtnl_newlink_create():

...
if (ops->newlink)
err = ops->newlink(dev, &params, extack);
else
err = register_netdevice(dev);
if (err < 0) {
free_netdev(dev);
goto out;
}
...

and free_netdev() is called, causing a kvfree() on the struct
net_device that is still referenced in the source entry attached to
the lower device's macvlan port.

Now all packets sent on the macvlan port with a matching source mac
address will trigger a use-after-free in macvlan_forward_source().
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
INCLUDES =
LIBS = -pthread -lkernelXDK
CFLAGS = -fomit-frame-pointer -static -fcf-protection=none -Wno-writable-strings

exploit: exploit.cpp kernelver_18244.521.55.h kaslr.c target.kxdb
g++ -o $@ exploit.cpp kaslr.c $(INCLUDES) $(CFLAGS) $(LIBS)
objcopy --add-section tools=tools.tar.gz $@
Binary file not shown.
Loading
Loading