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
319 changes: 319 additions & 0 deletions pocs/linux/kernelctf/CVE-2025-37797_lts_cos_mitigation/docs/exploit.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# CVE-2025-37797
## Overview
- Requirements:
- Capabilites: CAP_NET_ADMIN
- Kernel configuration: CONFIG_NET_SCHED=y CONFIG_NET_SCH_HFSC=y
- User namespaces required: Yes
- Introduced by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=21f4d5cc25ec0e6e8eb8420dd2c399e6d2fc7d14
- Fixed by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=3df275ef0a6ae181e8428a6589ef5d5231e58b5c
- Affected Version: v4.14-rc2 - v6.15-rc3
- Affected Component: netfilter
- Syscall to disable: unshare
- URL: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-37797
- Cause: Improper Update of Reference Count
- Description: A use-after-free vulnerability in the Linux Kernel net scheduler subsystem can be exploited to achieve local privilege escalation. In the hfsc_change_class routine, it is possible to peek and empty a child qdisc. Subsequently, there is no check that the child qdisc is empty before adding the class to the hfsc qdisc's internal trees. This causes a use-after-free vulnerability. We recommend upgrading past commit 3df275ef0a6ae181e8428a6589ef5d5231e58b5c


## Analysis
The issue occurs due to a time-of-check/time-of-use condition in hfsc_change_class() when working with certain child qdiscs like netem or codel.

The vulnerability works as follows:
1. `hfsc_change_class()` checks if a class has packets (`q.qlen != 0`)
2. It then calls `qdisc_peek_len()`, which for certain qdiscs (e.g. codel, netem) might drop packets and empty the queue
3. The code continues assuming the queue is still non-empty, adding the class to vttree
4. This breaks HFSC scheduler assumptions that only non-empty classes are in vttree
5. Later, when the class is destroyed, this can lead to a Use-After-Free

### Details
The `hfsc_change_class()` function is triggered when the user tries to modify a hfsc class's properties, for instance adding a FSC curve.
When the qdisc attached to the target class has packets ([1]), `qdisc_peek_len()` is triggered on the child qdisc ([2]).
```c
static int
hfsc_change_class(struct Qdisc *sch, u32 classid, u32 parentid,
struct nlattr **tca, unsigned long *arg,
struct netlink_ext_ack *extack)
{
// [...]
if (cl->qdisc->q.qlen != 0) { // [1]
int len = qdisc_peek_len(cl->qdisc); // [2]

if (cl->cl_flags & HFSC_RSC) {
if (old_flags & HFSC_RSC)
update_ed(cl, len);
else
init_ed(cl, len);
}

if (cl->cl_flags & HFSC_FSC) {
if (old_flags & HFSC_FSC)
update_vf(cl, 0, cur_time);
else
init_vf(cl, len); // [3]
}
}
// [...]
```

Then, `qdisc_peek_len()` calls the qdisc's peek function ([4]):
```c
static unsigned int
qdisc_peek_len(struct Qdisc *sch)
{
struct sk_buff *skb;
unsigned int len;

skb = sch->ops->peek(sch); // [4]
// [...]
```

Different qdiscs have different peek functions, but most use `qdisc_peek_dequeued()`, which calls dequeue on the qdisc ([5]).
```c
static inline struct sk_buff *qdisc_peek_dequeued(struct Qdisc *sch)
{
struct sk_buff *skb = skb_peek(&sch->gso_skb);

/* we can reuse ->gso_skb because peek isn't called for root qdiscs */
if (!skb) {
skb = sch->dequeue(sch); // [5]

if (skb) {
__skb_queue_head(&sch->gso_skb, skb);
/* it's still part of the queue */
qdisc_qstats_backlog_inc(sch, skb);
sch->q.qlen++;
}
}

return skb;
```

This is usually safe because an invariant is maintained: the qlen decrement in `dequeue()` is counteracted by the subsequent `sch->q.qlen++`, and the dequeued packet is stored in a special property `sch->gso_skb`. Subsequent calls to `dequeue()` handle this case properly, always checking if such a packet exists.

However, the vulnerability arises when the `dequeue()` function has side effects. For instance, if the dequeue function empties the qdisc while returning null (i.e. dropping all enqueued packets), the `hfsc_change_class()` routine does not check for this. If we specify the `HFSC_FSC` flag (i.e. try to add a FSC curve to the class), it unconditionally adds the class to the internal tree using `init_vf()` ([3]), treating it as a newly activated class, which is assumed to be non-empty.

Subsequently, we can delete the class while the hfsc qdisc continues referencing it. This is because the hfsc qdisc expects class deactivation to happen via `qlen_notify()` or via the `hfsc_dequeue()` code path, neither of which we trigger.

This gives us a UAF.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*build/*
gdb_stuff/
out/
tools/
exp
exploit_debug
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# taken from: https://github.com/google/security-research/blob/1bb2f8c8d95a34cafe7861bc890cfba5d85ec141/pocs/linux/kernelctf/CVE-2024-0193_lts/exploit/lts-6.1.67/Makefile

LIBMNL_DIR = $(realpath ./)/libmnl_build
LIBNFTNL_DIR = $(realpath ./)/libnftnl_build
LIBNFNETLINK_DIR = $(realpath ./)/libnfnetlink_build
LIBNETFILTER_QUEUE_DIR = $(realpath ./)/libnetfilterqueue_build
LIBIPTC_DIR = $(realpath ./)/libiptc_build

LIBS = -L$(LIBNFTNL_DIR)/install/lib -L$(LIBMNL_DIR)/install/lib -L$(LIBNFNETLINK_DIR)/install/lib -L$(LIBNETFILTER_QUEUE_DIR)/install/lib -L$(LIBIPTC_DIR)/install/lib -lxtables -lip4tc -lnftnl -lmnl -lnetfilter_queue -lnfnetlink
INCLUDES = -I$(LIBNFTNL_DIR)/libnftnl-1.2.5/include -I$(LIBMNL_DIR)/libmnl-1.0.5/include -I$(LIBNFNETLINK_DIR)/libnfnetlink-1.0.2/include -I$(LIBNETFILTER_QUEUE_DIR)/libnetfilter_queue-1.0.5/include -I$(LIBIPTC_DIR)/iptables-1.8.9/include
CFLAGS = -static -s

exp: exploit
cp exploit exp

exploit: exploit.c *.h
gcc -g -o exploit exploit.c -Wall -Wextra -Wno-unused -Werror -Wno-int-to-pointer-cast $(LIBS) $(INCLUDES) $(CFLAGS)

prerequisites: libnftnl-build libnetfilter-queue-build libiptc-build

libiptc-build : libiptc-download #libmnl-build libnftnl-build
tar -C $(LIBIPTC_DIR) -xvf $(LIBIPTC_DIR)/iptables-1.8.9.tar.xz
cd $(LIBIPTC_DIR)/iptables-1.8.9 && PKG_CONFIG_PATH=$(LIBMNL_DIR)/install/lib/pkgconfig:$(LIBNFTNL_DIR)/install/lib/pkgconfig ./configure --enable-static --prefix=`realpath ../install`
cd $(LIBIPTC_DIR)/iptables-1.8.9 && C_INCLUDE_PATH=$(C_INCLUDE_PATH):$(LIBMNL_DIR)/install/include::$(LIBNFTNL_DIR)/install/include LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(LIBMNL_DIR)/install/lib:$(LIBNFTNL_DIR)/install/lib make -j`nproc`
cd $(LIBIPTC_DIR)/iptables-1.8.9 && make install

libiptc-download:
mkdir $(LIBIPTC_DIR)
wget -P $(LIBIPTC_DIR) https://netfilter.org/projects/iptables/files/iptables-1.8.9.tar.xz


libnfnetlink-build : libnfnetlink-download
tar -C $(LIBNFNETLINK_DIR) -xvf $(LIBNFNETLINK_DIR)/libnfnetlink-1.0.2.tar.bz2
cd $(LIBNFNETLINK_DIR)/libnfnetlink-1.0.2 && ./configure --enable-static --prefix=`realpath ../install`
cd $(LIBNFNETLINK_DIR)/libnfnetlink-1.0.2 && make -j`nproc`
cd $(LIBNFNETLINK_DIR)/libnfnetlink-1.0.2 && make install

libnetfilter-queue-build : libnetfilter-queue-download libnfnetlink-build #libmnl-build
tar -C $(LIBNETFILTER_QUEUE_DIR) -xvf $(LIBNETFILTER_QUEUE_DIR)/libnetfilter_queue-1.0.5.tar.bz2
cd $(LIBNETFILTER_QUEUE_DIR)/libnetfilter_queue-1.0.5 && PKG_CONFIG_PATH=$(LIBNFNETLINK_DIR)/install/lib/pkgconfig:$(LIBMNL_DIR)/install/lib/pkgconfig ./configure --enable-static --prefix=`realpath ../install`
cd $(LIBNETFILTER_QUEUE_DIR)/libnetfilter_queue-1.0.5 && C_INCLUDE_PATH=$(C_INCLUDE_PATH):$(LIBNFNETLINK_DIR)/install/include:$(LIBMNL_DIR)/install/include LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(LIBNFNETLINK_DIR)/install/lib:$(LIBMNL_DIR)/install/lib make -j`nproc`
cd $(LIBNETFILTER_QUEUE_DIR)/libnetfilter_queue-1.0.5 && make install

libnetfilter-queue-download:
mkdir $(LIBNETFILTER_QUEUE_DIR)
wget -P $(LIBNETFILTER_QUEUE_DIR) https://netfilter.org/projects/libnetfilter_queue/files/libnetfilter_queue-1.0.5.tar.bz2

libnfnetlink-download:
mkdir $(LIBNFNETLINK_DIR)
wget -P $(LIBNFNETLINK_DIR) https://netfilter.org/projects/libnfnetlink/files/libnfnetlink-1.0.2.tar.bz2

libmnl-build : libmnl-download
tar -C $(LIBMNL_DIR) -xvf $(LIBMNL_DIR)/libmnl-1.0.5.tar.bz2
cd $(LIBMNL_DIR)/libmnl-1.0.5 && ./configure --enable-static --prefix=`realpath ../install`
cd $(LIBMNL_DIR)/libmnl-1.0.5 && make -j`nproc`
cd $(LIBMNL_DIR)/libmnl-1.0.5 && make install

libnftnl-build : libmnl-build libnftnl-download
tar -C $(LIBNFTNL_DIR) -xvf $(LIBNFTNL_DIR)/libnftnl-1.2.5.tar.xz
cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && PKG_CONFIG_PATH=$(LIBMNL_DIR)/install/lib/pkgconfig ./configure --enable-static --prefix=`realpath ../install`
cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && C_INCLUDE_PATH=$(C_INCLUDE_PATH):$(LIBMNL_DIR)/install/include LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(LIBMNL_DIR)/install/lib make -j`nproc`
cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && make install

libmnl-download :
mkdir $(LIBMNL_DIR)
wget -P $(LIBMNL_DIR) https://netfilter.org/projects/libmnl/files/libmnl-1.0.5.tar.bz2

libnftnl-download :
mkdir $(LIBNFTNL_DIR)
wget -P $(LIBNFTNL_DIR) https://netfilter.org/projects/libnftnl/files/libnftnl-1.2.5.tar.xz

run:
./exp

clean:
rm -f exp
rm -rf $(LIBMNL_DIR)
rm -rf $(LIBNFTNL_DIR)
rm -rf $(LIBNFNETLINK_DIR)
rm -rf $(LIBNETFILTER_QUEUE_DIR)
rm -rf $(LIBIPTC_DIR)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail
make exp

mkdir -p out
cp exp out/
Binary file not shown.
Loading
Loading