前言

其实这篇算是临时想写的~顺便记一记坑防止以后自己再踩进去

提权不成功?

这个坑主要是我在用了CVE-2017-16995原始的exp时,发现提权不成功才踩到的

在原始的exp中,作者将task_struct->cred->uid&&gid置0,于是在fork后的/bin/sh中,获得了root权限

这个乍一想没啥毛病,我那时候也是觉得没啥问题

但是,我在调试的时候,发现并不成功,仅仅修改uid&&gid且fork后的/bin/sh并没有获得root权限,而是如下情况

1
2
/ $ id
uid=0 gid=0 euid=1001(test1) egid=1001 groups=1001

我们知道,euid&&egid才是实际标示当前进程权限的值,也就是说,这个fork后的/bin/sh虽然uid&&gid为0,但不知为何,euid&&egid却没有同样被置零

我找了真机测试了一下,同样是仅修改uid&&gid为0,但在bash上却不会出现这种情况,fork后的/bin/bash进程的euid&&egid被bash置为了与uid&&gid一样的值:0

疑惑.jpg

思索了一阵子,我觉得是不是busybox有什么问题(说明一下,我是用busybox+linux kernel调试)

故而,我去找了下busybox的源码,一探究竟

真有你的,busybox

来看看busybox对于sh的实现(busybox version == 1.26.2)

先来看busybox_main

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
124
125
126
127
static int busybox_main(char **argv)
{
if (!argv[1]) {
/* Called without arguments */
const char *a;
int col;
unsigned output_width;
help:
output_width = 80;
if (ENABLE_FEATURE_AUTOWIDTH) {
/* Obtain the terminal width */
output_width = get_terminal_width(2);
}

dup2(1, 2);
full_write2_str(bb_banner); /* reuse const string */
full_write2_str(" multi-call binary.\n"); /* reuse */
full_write2_str(
"BusyBox is copyrighted by many authors between 1998-2015.\n"
"Licensed under GPLv2. See source distribution for detailed\n"
"copyright notices.\n"
"\n"
"Usage: busybox [function [arguments]...]\n"
" or: busybox --list"IF_FEATURE_INSTALLER("[-full]")"\n"
IF_FEATURE_INSTALLER(
" or: busybox --install [-s] [DIR]\n"
)
" or: function [arguments]...\n"
"\n"
IF_NOT_FEATURE_SH_STANDALONE(
"\tBusyBox is a multi-call binary that combines many common Unix\n"
"\tutilities into a single executable. Most people will create a\n"
"\tlink to busybox for each function they wish to use and BusyBox\n"
"\twill act like whatever it was invoked as.\n"
)
IF_FEATURE_SH_STANDALONE(
"\tBusyBox is a multi-call binary that combines many common Unix\n"
"\tutilities into a single executable. The shell in this build\n"
"\tis configured to run built-in utilities without $PATH search.\n"
"\tYou don't need to install a link to busybox for each utility.\n"
"\tTo run external program, use full path (/sbin/ip instead of ip).\n"
)
"\n"
"Currently defined functions:\n"
);
col = 0;
a = applet_names;
/* prevent last comma to be in the very last pos */
output_width--;
while (*a) {
int len2 = strlen(a) + 2;
if (col >= (int)output_width - len2) {
full_write2_str(",\n");
col = 0;
}
if (col == 0) {
col = 6;
full_write2_str("\t");
} else {
full_write2_str(", ");
}
full_write2_str(a);
col += len2;
a += len2 - 1;
}
full_write2_str("\n");
return 0;
}

if (is_prefixed_with(argv[1], "--list")) {
unsigned i = 0;
const char *a = applet_names;
dup2(1, 2);
while (*a) {
# if ENABLE_FEATURE_INSTALLER
if (argv[1][6]) /* --list-full? */
full_write2_str(install_dir[APPLET_INSTALL_LOC(i)] + 1);
# endif
full_write2_str(a);
full_write2_str("\n");
i++;
while (*a++ != '\0')
continue;
}
return 0;
}

if (ENABLE_FEATURE_INSTALLER && strcmp(argv[1], "--install") == 0) {
int use_symbolic_links;
const char *busybox;

busybox = xmalloc_readlink(bb_busybox_exec_path);
if (!busybox) {
/* bb_busybox_exec_path is usually "/proc/self/exe".
* In chroot, readlink("/proc/self/exe") usually fails.
* In such case, better use argv[0] as symlink target
* if it is a full path name.
*/
if (argv[0][0] != '/')
bb_error_msg_and_die("'%s' is not an absolute path", argv[0]);
busybox = argv[0];
}
/* busybox --install [-s] [DIR]:
* -s: make symlinks
* DIR: directory to install links to
*/
use_symbolic_links = (argv[2] && strcmp(argv[2], "-s") == 0 && ++argv);
install_links(busybox, use_symbolic_links, argv[2]);
return 0;
}

if (strcmp(argv[1], "--help") == 0) {
/* "busybox --help [<applet>]" */
if (!argv[2])
goto help;
/* convert to "<applet> --help" */
argv[0] = argv[2];
argv[2] = NULL;
} else {
/* "busybox <applet> arg1 arg2 ..." */
argv++;
}
/* We support "busybox /a/path/to/applet args..." too. Allows for
* "#!/bin/busybox"-style wrappers */
applet_name = bb_get_last_path_component_nostrip(argv[0]);
run_applet_and_exit(applet_name, argv);
}

前面的一堆,直到run_applet_and_exit函数,都不需要关注,仅做一些判断,我们主要关注最后将要进到的run_applet_and_exit函数

看看run_applet_and_exit这个函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static NORETURN void run_applet_and_exit(const char *name, char **argv)
{
# if ENABLE_BUSYBOX
if (is_prefixed_with(name, "busybox"))
exit(busybox_main(argv));
# endif
# if NUM_APPLETS > 0
/* find_applet_by_name() search is more expensive, so goes second */
{
int applet = find_applet_by_name(name); // 根据第一个参数值找到对应的applet
if (applet >= 0)
run_applet_no_and_exit(applet, argv); // 进入这里
}
# endif

/*bb_error_msg_and_die("applet not found"); - links in printf */
full_write2_str(applet_name);
full_write2_str(": applet not found\n");
/* POSIX: "If a command is not found, the exit status shall be 127" */
exit(127);
}

继续看run_applet_no_and_exit的实现

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
void FAST_FUNC run_applet_no_and_exit(int applet_no, char **argv)
{
int argc = 1;

while (argv[argc])
argc++;

/* Reinit some shared global data */
xfunc_error_retval = EXIT_FAILURE;
applet_name = bb_get_last_path_component_nostrip(argv[0]);

/* Special case. POSIX says "test --help"
* should be no different from e.g. "test --foo".
* Thus for "test", we skip --help check.
* "true" and "false" are also special.
*/
if (1
# if defined APPLET_NO_test
&& applet_no != APPLET_NO_test
# endif
# if defined APPLET_NO_true
&& applet_no != APPLET_NO_true
# endif
# if defined APPLET_NO_false
&& applet_no != APPLET_NO_false
# endif
) {
if (argc == 2 && strcmp(argv[1], "--help") == 0) {
/* Make "foo --help" exit with 0: */
xfunc_error_retval = 0;
bb_show_usage();
}
}
if (ENABLE_FEATURE_SUID) // 默认开启
check_suid(applet_no); // 关键
xfunc_error_retval = applet_main[applet_no](argc, argv); // 启动对应的applet,我们这里对应的是ash_main
/* Note: applet_main() may also not return (die on a xfunc or such) */
xfunc_die();
}

我们可以看到,在执行对应的applet之前,busybox执行了check_suid函数

来看看这个函数的实现

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
static void check_suid(int applet_no)
{
gid_t rgid; /* real gid */

if (ruid == 0) /* set by parse_config_file() */
return; /* run by root - no need to check more */ // 斜眼笑.jpg
rgid = getgid();

# if ENABLE_FEATURE_SUID_CONFIG
if (suid_cfg_readable) {
uid_t uid;
struct suid_config_t *sct;
mode_t m;

for (sct = suid_config; sct; sct = sct->m_next) {
if (sct->m_applet == applet_no)
goto found;
}
goto check_need_suid;
found:
/* Is this user allowed to run this applet? */
m = sct->m_mode;
if (sct->m_ugid.uid == ruid)
/* same uid */
m >>= 6;
else if ((sct->m_ugid.gid == rgid) || ingroup(ruid, sct->m_ugid.gid))
/* same group / in group */
m >>= 3;
if (!(m & S_IXOTH)) /* is x bit not set? */
bb_error_msg_and_die("you have no permission to run this applet");

/* We set effective AND saved ids. If saved-id is not set
* like we do below, seteuid(0) can still later succeed! */

/* Are we directed to change gid
* (APPLET = *s* USER.GROUP or APPLET = *S* USER.GROUP)?
*/
if (sct->m_mode & S_ISGID)
rgid = sct->m_ugid.gid;
/* else: we will set egid = rgid, thus dropping sgid effect */
if (setresgid(-1, rgid, rgid))
bb_perror_msg_and_die("setresgid");

/* Are we directed to change uid
* (APPLET = s** USER.GROUP or APPLET = S** USER.GROUP)?
*/
uid = ruid;
if (sct->m_mode & S_ISUID)
uid = sct->m_ugid.uid;
/* else: we will set euid = ruid, thus dropping suid effect */
if (setresuid(-1, uid, uid))
bb_perror_msg_and_die("setresuid");

goto ret;
}
# if !ENABLE_FEATURE_SUID_CONFIG_QUIET
{
static bool onetime = 0;

if (!onetime) {
onetime = 1;
bb_error_msg("using fallback suid method");
}
}
# endif
check_need_suid:
# endif
if (APPLET_SUID(applet_no) == BB_SUID_REQUIRE) {
/* Real uid is not 0. If euid isn't 0 too, suid bit
* is most probably not set on our executable */
if (geteuid())
bb_error_msg_and_die("must be suid to work properly");
} else if (APPLET_SUID(applet_no) == BB_SUID_DROP) {
xsetgid(rgid); /* drop all privileges */ // 如果我们只修改euid&&egid为0,不修改uid&&gid,就会执行这个分支语句,将所有id置为rxID
xsetuid(ruid);
}
# if ENABLE_FEATURE_SUID_CONFIG
ret: ;
llist_free((llist_t*)suid_config, NULL);
# endif
}

这个函数刚开始,我们就看到了关键,busybox判断了当前进程的uid是否是root,如果是,那么就不修改任何的xID,直接返回并执行对应的applet

这也就解释了为什么提权payload会失效的原因,当我们将uid&&gid设置为0后,busybox直接不做任何修改就执行了/bin/sh,导致本该被赋值成与uid&&gid一样的euid&&egid并没有被赋值,从而提权失败

这里插一个别的情况,如果我们只修改euid&&egid为0,不修改uid&&gid,那么会如何呢?

经过我的调试,如果只修改euid&&egid为0,那么xID将会经过如下的变迁过程

exp进程 -> fork(uid=gid=1000,suid=sgid=1000,euid=egid=0,fsuid=fsgid=1000) -> execve(execve将会修改suid/sgid,fsuid/fsgid代码见下方,修改后,uid=gid=1000,suid=sgid=euid=egid=fsuid=fsgid=0) -> sh(由于setsid,setgid,此时uid=gid=suid=sgid=euid=egid=fsuid=fsgid=1000)

可以看到所有的id都将会被修改为uid&&gid对应的值,同样无法实现提权

这个情况倒是与bash是一致的

附上execve中修改xID部分的代码

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
124
125
126
127
128
129
130
131
132
133
134
/**
* cap_bprm_set_creds - Set up the proposed credentials for execve().
* @bprm: The execution parameters, including the proposed creds
*
* Set up the proposed credentials for a new execution context being
* constructed by execve(). The proposed creds in @bprm->cred is altered,
* which won't take effect immediately. Returns 0 if successful, -ve on error.
*/
int cap_bprm_set_creds(struct linux_binprm *bprm)
{
const struct cred *old = current_cred();
struct cred *new = bprm->cred;
bool effective, has_cap = false, is_setid;
int ret;
kuid_t root_uid;

if (WARN_ON(!cap_ambient_invariant_ok(old)))
return -EPERM;

effective = false;
ret = get_file_caps(bprm, &effective, &has_cap);
if (ret < 0)
return ret;

root_uid = make_kuid(new->user_ns, 0);

if (!issecure(SECURE_NOROOT)) {
/*
* If the legacy file capability is set, then don't set privs
* for a setuid root binary run by a non-root user. Do set it
* for a root user just to cause least surprise to an admin.
*/
if (has_cap && !uid_eq(new->uid, root_uid) && uid_eq(new->euid, root_uid)) {
warn_setuid_and_fcaps_mixed(bprm->filename);
goto skip;
}
/*
* To support inheritance of root-permissions and suid-root
* executables under compatibility mode, we override the
* capability sets for the file.
*
* If only the real uid is 0, we do not set the effective bit.
*/
if (uid_eq(new->euid, root_uid) || uid_eq(new->uid, root_uid)) {
/* pP' = (cap_bset & ~0) | (pI & ~0) */
new->cap_permitted = cap_combine(old->cap_bset,
old->cap_inheritable);
}
if (uid_eq(new->euid, root_uid))
effective = true;
}
skip:

/* if we have fs caps, clear dangerous personality flags */
if (!cap_issubset(new->cap_permitted, old->cap_permitted))
bprm->per_clear |= PER_CLEAR_ON_SETID;


/* Don't let someone trace a set[ug]id/setpcap binary with the revised
* credentials unless they have the appropriate permit.
*
* In addition, if NO_NEW_PRIVS, then ensure we get no new privs.
*/
is_setid = !uid_eq(new->euid, old->uid) || !gid_eq(new->egid, old->gid);

if ((is_setid ||
!cap_issubset(new->cap_permitted, old->cap_permitted)) &&
bprm->unsafe & ~LSM_UNSAFE_PTRACE_CAP) { // 如果只修改euid和egid为0,保持uid以及suid为0x3,就不会进到这个分支
/* downgrade; they get no more than they had, and maybe less */
if (!capable(CAP_SETUID) ||
(bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
new->euid = new->uid;
new->egid = new->gid;
}
new->cap_permitted = cap_intersect(new->cap_permitted,
old->cap_permitted);
}

new->suid = new->fsuid = new->euid; // 修改了sID以及fsID
new->sgid = new->fsgid = new->egid;

/* File caps or setid cancels ambient. */
if (has_cap || is_setid)
cap_clear(new->cap_ambient);

/*
* Now that we've computed pA', update pP' to give:
* pP' = (X & fP) | (pI & fI) | pA'
*/
new->cap_permitted = cap_combine(new->cap_permitted, new->cap_ambient);

/*
* Set pE' = (fE ? pP' : pA'). Because pA' is zero if fE is set,
* this is the same as pE' = (fE ? pP' : 0) | pA'.
*/
if (effective)
new->cap_effective = new->cap_permitted;
else
new->cap_effective = new->cap_ambient;

if (WARN_ON(!cap_ambient_invariant_ok(new)))
return -EPERM;

bprm->cap_effective = effective;

/*
* Audit candidate if current->cap_effective is set
*
* We do not bother to audit if 3 things are true:
* 1) cap_effective has all caps
* 2) we are root
* 3) root is supposed to have all caps (SECURE_NOROOT)
* Since this is just a normal root execing a process.
*
* Number 1 above might fail if you don't have a full bset, but I think
* that is interesting information to audit.
*/
if (!cap_issubset(new->cap_effective, new->cap_ambient)) {
if (!cap_issubset(CAP_FULL_SET, new->cap_effective) ||
!uid_eq(new->euid, root_uid) || !uid_eq(new->uid, root_uid) ||
issecure(SECURE_NOROOT)) {
ret = audit_log_bprm_fcaps(bprm, new, old);
if (ret < 0)
return ret;
}
}

new->securebits &= ~issecure_mask(SECURE_KEEP_CAPS);

if (WARN_ON(!cap_ambient_invariant_ok(new)))
return -EPERM;

return 0;
}

既然都看了busybox的实现,那就顺便看看bash的实现,看看是不是符合我们的预期

bash中设置权限代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
int
main (argc, argv, env)
int argc;
char **argv, **env;
#endif /* !NO_MAIN_ENV_ARG */
{
......
running_setuid = uidget (); // 这里由于uid&&gid与euid&&egid不同,所以running_setuid为1
......
if (running_setuid && privileged_mode == 0) // privileged_mode默认为0,当bash以-p启动时,此值为1
disable_priv_mode (); //执行此函数
......
}

其中关键函数及变量的实现如下

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

/* Non-zero means that this shell is running in `privileged' mode. This
is required if the shell is to run setuid. If the `-p' option is
not supplied at startup, and the real and effective uids or gids
differ, disable_priv_mode is called to relinquish setuid status. */
int privileged_mode = 0;
......
/* Fetch the current set of uids and gids and return 1 if we're running
setuid or setgid. */
static int
uidget ()
{
uid_t u;

u = getuid ();
if (current_user.uid != u)
{
FREE (current_user.user_name);
FREE (current_user.shell);
FREE (current_user.home_dir);
current_user.user_name = current_user.shell = current_user.home_dir = (char *)NULL;
}
current_user.uid = u;
current_user.gid = getgid ();
current_user.euid = geteuid ();
current_user.egid = getegid ();

/* See whether or not we are running setuid or setgid. */
return (current_user.uid != current_user.euid) ||
(current_user.gid != current_user.egid);
}

void
disable_priv_mode ()
{
setuid (current_user.uid);
setgid (current_user.gid);
current_user.euid = current_user.uid;
current_user.egid = current_user.gid;
}

可以看出,如我们之前所推断的,bash与busybox在初始化的过程中确实有着一定的差别

另外提一嘴,要注意在我们的exp中调用了system("/bin/bash"),但实际上并不是exp进程->/bin/bash,而是exp进程->/bin/sh -c /bin/bash -> /bin/bash,不过呢,由于/bin/sh一般都被软链至dash或者是bash, 所以在设置权限的行为上,与我们上面说的bash行为是一致的,你也可以把它看成exp进程->/bin/bash -c /bin/bash -> /bin/bash,这并不影响权限设置的逻辑