CVE-2022-32250
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:
| |
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"] | |
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.
| |
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:
| Step | Action | Result |
|---|---|---|
| 1 | Kernel allocates an object | Valid memory region |
| 2 | Kernel frees the object | Memory becomes available |
| 3 | Pointer still exists | Dangling reference |
| 4 | Attacker sprays objects | Heap is influenced |
| 5 | Freed memory is reused | Attacker data may land there |
| 6 | Kernel uses pointer | Controlled 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.
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 --> Eflowchart 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 --> Eflowchart 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:
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] | |
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.
| Item | Description |
|---|---|
| Vulnerability | Use-After-Free |
| Component | nf_tables / Netfilter |
| Trigger | Crafted nftables operations |
| Impact | Local privilege escalation |
| Goal | Turn memory corruption into useful primitives |
Simplified exploitation path:
| |
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.
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"] | |
Simplified pseudo-code:
| |
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:
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"] | |
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.
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"] | |
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.
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"] | |
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:
| |
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:
| |
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:
| |
Example skeleton:
| |
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.
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"] | |
A typical pattern is:
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"] | |
At this point, exploitation becomes less about guessing and more about calculation.
Stage 5 — KASLR Bypass
KASLR randomizes the kernel base address at boot.
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"] | |
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:
| |
If an exploit can overwrite it with an attacker-controlled path, the kernel may execute a user-controlled 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"]flowchart LR
A["Overwrite modprobe_path
→ /tmp/pullsec"]
--> B["Trigger unknown binary"]
--> C["Kernel invokes modprobe"]
--> D["Execute attacker script as root"] | |
Example payload:
| |
Trigger with an invalid binary:
| |
If the overwrite worked:
| |
Expected result:
| |
Practical Lab Notes
For this kind of topic, I prefer working in a disposable virtual machine.
| |
| |
| |
| |
Useful commands:
| |
Mitigations
Mitigation is mostly about reducing the attack surface and limiting the impact of kernel memory corruption.
| Category | Mitigation |
|---|---|
| Patching | Keep the kernel up to date |
| Isolation | Disable unprivileged user namespaces when possible |
| Access control | Restrict nftables usage |
| Hardening | Enable AppArmor / SELinux |
| Monitoring | Watch for unexpected privilege escalation behavior |
Example hardening checks:
| |
Example hardening values:
| |
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_pathoverwrite
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.
kofi