为什么 setuid 被丢弃在高山容器中的 execve 上?
Why is setuid dropped on execve in an alpine container?
仅在 alpine 容器中:当 运行 启动另一个可执行文件 (execve(2)
) 的 setuid 二进制文件时,内核[1] BusyBox 似乎放弃了由 setuid 获得的特权。我认为这可能是出于安全考虑而设计的。
问题:我想了解为什么会发生这种情况以及造成这种情况的原因是什么?
我正在开发一个名为 kamikaze
written in rust
. kamikaze
is a very simple binary that unlink(2)
itself and then starts a new process using fork(2)
and execve(2)
.
的一次性 setuid 运行ner
主要成分是:
src/main.rs
[a47dedc]:实现 unlink(2)
和进程生成。
use std::env;
use std::fs;
use std::process::{Command, exit};
fn usage() {
println!("usage: kamikaze <command> <arguments>");
exit(1);
}
fn main() {
// Kill myself
fs::remove_file(
env::current_exe().expect("failed to get path to executable")
).expect("kamikaze failed");
let mut args: Vec<String> = env::args().collect();
match args.len() {
0 => usage(),
1 => usage(),
_ => {
args.remove(0);
let mut child = Command::new(args.remove(0))
.args(&args)
.spawn()
.expect("failed to execute process");
exit(
child
.wait()
.expect("wait failed")
.code().unwrap()
);
},
}
}
install.sh
[a47dedc]:一个简单的安装程序,它下载 kamikaze
,将所有权更改为 root
并设置 setuid 位。
#!/usr/bin/env sh
set -euo pipefail
REPO="Enteee/kamikaze"
INSTALL="install -m 755 -o root kamikaze-download kamikaze && chmod u+s kamikaze"
curl -s "https://api.github.com/repos/${REPO}/releases/latest" \
| grep "browser_download_url" \
| cut -d '"' -f 4 \
| xargs -n1 curl -s -L --output kamikaze-download
trap 'rm kamikaze-download' EXIT
if [[ $(id -u) -ne 0 ]]; then
sudo sh -c "${INSTALL}"
else
eval "${INSTALL}"
fi
当我 运行 kamikaze
在容器外时 [2]:
$ curl https://raw.githubusercontent.com/Enteee/kamikaze/master/install.sh | sh
$ ./kamikaze ps -f
UID PID PPID C STIME TTY TIME CMD
root 3223 9587 0 08:17 pts/0 00:00:00 ./kamikaze ps -f
root 3224 3223 0 08:17 pts/0 00:00:00 ps -f
我得到了预期的行为。子进程 (PID=3224
) 运行 为 root
。另一方面,在容器内[2]:
$ docker build -t kamikaze - <<EOF
FROM alpine
RUN set -exuo pipefail \
&& apk add curl \
&& curl https://raw.githubusercontent.com/Enteee/kamikaze/master/install.sh | sh
USER nobody
CMD ["/kamikaze", "ps"]
EOF
$ docker run kamikaze
PID USER TIME COMMAND
1 root 0:00 /kamikaze ps
6 nobody 0:00 ps
ps
运行s 作为 nobody
.
[1] 我首先想到这是因为 docker 和 Linux 内核实现了一些安全机制。但是在深入研究 Docker Security, NO_NEW_PRIVILEGES
and seccomp(2)
之后,我终于意识到 BusyBox 只是在放弃特权。
[2] kamikaze [1.0.0]
修复并更改了此行为。因此这个例子不再有效。要重现示例,请使用 kamikaze
[0.0.0] 版本。
在alpine中实现了ps
命令的BusyBox通过设置有效用户id为真实用户id来丢弃setuid获得的权限。
} else if (APPLET_SUID(applet_no) == BB_SUID_DROP) {
/*
* Drop all privileges.
*
* Don't check for errors: in normal use, they are impossible,
* and in special cases, exiting is harmful. Example:
* 'unshare --user' when user's shell is also from busybox.
*
* 'unshare --user' creates a new user namespace without any
* uid mappings. Thus, busybox binary is setuid nobody:nogroup
* within the namespace, as that is the only user. However,
* since no uids are mapped, calls to setgid/setuid
* fail (even though they would do nothing).
*/
setgid(rgid);
setuid(ruid);
}
procps/ps.c [b097a84]:定义BB_SUID_DROP
.
// APPLET_NOEXEC:name main location suid_type help
//applet:IF_PS( APPLET_NOEXEC(ps, ps, BB_DIR_BIN, BB_SUID_DROP, ps))
//applet:IF_MINIPS(APPLET_NOEXEC(minips, ps, BB_DIR_BIN, BB_SUID_DROP, ps))
解决这个问题很简单。 kamikaze
只需将真实用户id设置为execve(2)
之前的有效用户id即可。
extern crate exec;
extern crate users;
use std::env;
use std::fs;
use std::process::exit;
use users::{get_effective_uid, get_effective_gid};
use users::switch::{set_current_uid, set_current_gid};
fn usage() {
println!("usage: kamikaze <command> <arguments>");
}
fn main() {
// Kill myself
fs::remove_file(
env::current_exe().expect("failed to get path to executable")
).expect("kamikaze failed");
set_current_uid(
get_effective_uid()
).expect("failed setting current uid");
set_current_gid(
get_effective_gid()
).expect("failed setting current gid");
let mut args: Vec<String> = env::args().collect();
match args.len() {
0 => usage(),
1 => usage(),
_ => {
args.remove(0);
let err = exec::Command::new(args.remove(0))
.args(&args)
.exec();
println!("Error: {}", err);
},
}
// Should never get here
exit(1);
}
使用新发布的 kamikaze [1.0.0]
我们现在得到:
$ docker build -t kamikaze - <<EOF
FROM alpine
RUN set -exuo pipefail \
&& apk add curl \
&& curl https://raw.githubusercontent.com/Enteee/kamikaze/master/install.sh | sh
USER nobody
CMD ["/kamikaze", "ps"]
EOF
$ docker run kamikaze
PID USER TIME COMMAND
1 root 0:00 ps
仅在 alpine 容器中:当 运行 启动另一个可执行文件 (execve(2)
) 的 setuid 二进制文件时,内核[1] BusyBox 似乎放弃了由 setuid 获得的特权。我认为这可能是出于安全考虑而设计的。
问题:我想了解为什么会发生这种情况以及造成这种情况的原因是什么?
我正在开发一个名为 kamikaze
written in rust
. kamikaze
is a very simple binary that unlink(2)
itself and then starts a new process using fork(2)
and execve(2)
.
主要成分是:
src/main.rs
[a47dedc]:实现 unlink(2)
和进程生成。
use std::env;
use std::fs;
use std::process::{Command, exit};
fn usage() {
println!("usage: kamikaze <command> <arguments>");
exit(1);
}
fn main() {
// Kill myself
fs::remove_file(
env::current_exe().expect("failed to get path to executable")
).expect("kamikaze failed");
let mut args: Vec<String> = env::args().collect();
match args.len() {
0 => usage(),
1 => usage(),
_ => {
args.remove(0);
let mut child = Command::new(args.remove(0))
.args(&args)
.spawn()
.expect("failed to execute process");
exit(
child
.wait()
.expect("wait failed")
.code().unwrap()
);
},
}
}
install.sh
[a47dedc]:一个简单的安装程序,它下载 kamikaze
,将所有权更改为 root
并设置 setuid 位。
#!/usr/bin/env sh
set -euo pipefail
REPO="Enteee/kamikaze"
INSTALL="install -m 755 -o root kamikaze-download kamikaze && chmod u+s kamikaze"
curl -s "https://api.github.com/repos/${REPO}/releases/latest" \
| grep "browser_download_url" \
| cut -d '"' -f 4 \
| xargs -n1 curl -s -L --output kamikaze-download
trap 'rm kamikaze-download' EXIT
if [[ $(id -u) -ne 0 ]]; then
sudo sh -c "${INSTALL}"
else
eval "${INSTALL}"
fi
当我 运行 kamikaze
在容器外时 [2]:
$ curl https://raw.githubusercontent.com/Enteee/kamikaze/master/install.sh | sh
$ ./kamikaze ps -f
UID PID PPID C STIME TTY TIME CMD
root 3223 9587 0 08:17 pts/0 00:00:00 ./kamikaze ps -f
root 3224 3223 0 08:17 pts/0 00:00:00 ps -f
我得到了预期的行为。子进程 (PID=3224
) 运行 为 root
。另一方面,在容器内[2]:
$ docker build -t kamikaze - <<EOF
FROM alpine
RUN set -exuo pipefail \
&& apk add curl \
&& curl https://raw.githubusercontent.com/Enteee/kamikaze/master/install.sh | sh
USER nobody
CMD ["/kamikaze", "ps"]
EOF
$ docker run kamikaze
PID USER TIME COMMAND
1 root 0:00 /kamikaze ps
6 nobody 0:00 ps
ps
运行s 作为 nobody
.
[1] 我首先想到这是因为 docker 和 Linux 内核实现了一些安全机制。但是在深入研究 Docker Security, NO_NEW_PRIVILEGES
and seccomp(2)
之后,我终于意识到 BusyBox 只是在放弃特权。
[2] kamikaze [1.0.0]
修复并更改了此行为。因此这个例子不再有效。要重现示例,请使用 kamikaze
[0.0.0] 版本。
在alpine中实现了ps
命令的BusyBox通过设置有效用户id为真实用户id来丢弃setuid获得的权限。
} else if (APPLET_SUID(applet_no) == BB_SUID_DROP) {
/*
* Drop all privileges.
*
* Don't check for errors: in normal use, they are impossible,
* and in special cases, exiting is harmful. Example:
* 'unshare --user' when user's shell is also from busybox.
*
* 'unshare --user' creates a new user namespace without any
* uid mappings. Thus, busybox binary is setuid nobody:nogroup
* within the namespace, as that is the only user. However,
* since no uids are mapped, calls to setgid/setuid
* fail (even though they would do nothing).
*/
setgid(rgid);
setuid(ruid);
}
procps/ps.c [b097a84]:定义BB_SUID_DROP
.
// APPLET_NOEXEC:name main location suid_type help
//applet:IF_PS( APPLET_NOEXEC(ps, ps, BB_DIR_BIN, BB_SUID_DROP, ps))
//applet:IF_MINIPS(APPLET_NOEXEC(minips, ps, BB_DIR_BIN, BB_SUID_DROP, ps))
解决这个问题很简单。 kamikaze
只需将真实用户id设置为execve(2)
之前的有效用户id即可。
extern crate exec;
extern crate users;
use std::env;
use std::fs;
use std::process::exit;
use users::{get_effective_uid, get_effective_gid};
use users::switch::{set_current_uid, set_current_gid};
fn usage() {
println!("usage: kamikaze <command> <arguments>");
}
fn main() {
// Kill myself
fs::remove_file(
env::current_exe().expect("failed to get path to executable")
).expect("kamikaze failed");
set_current_uid(
get_effective_uid()
).expect("failed setting current uid");
set_current_gid(
get_effective_gid()
).expect("failed setting current gid");
let mut args: Vec<String> = env::args().collect();
match args.len() {
0 => usage(),
1 => usage(),
_ => {
args.remove(0);
let err = exec::Command::new(args.remove(0))
.args(&args)
.exec();
println!("Error: {}", err);
},
}
// Should never get here
exit(1);
}
使用新发布的 kamikaze [1.0.0]
我们现在得到:
$ docker build -t kamikaze - <<EOF
FROM alpine
RUN set -exuo pipefail \
&& apk add curl \
&& curl https://raw.githubusercontent.com/Enteee/kamikaze/master/install.sh | sh
USER nobody
CMD ["/kamikaze", "ps"]
EOF
$ docker run kamikaze
PID USER TIME COMMAND
1 root 0:00 ps