mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
userns
This commit is contained in:
parent
533b8cda1e
commit
a081ecc8c8
4 changed files with 216 additions and 105 deletions
|
|
@ -12,7 +12,7 @@ tokio = { version = "1.39.2", features = ["full"] }
|
|||
tracing.workspace = true
|
||||
eyre.workspace = true
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
|
||||
rustix = { version = "0.38.35", features = ["pty", "termios", "procfs", "process", "stdio", "net", "fs", "thread"] }
|
||||
rustix = { version = "0.38.35", features = ["pty", "termios", "procfs", "process", "stdio", "net", "fs", "thread", "pipe"] }
|
||||
users = "0.11.0"
|
||||
futures = "0.3.30"
|
||||
thiserror = "1.0.63"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use std::{
|
||||
os::fd::{FromRawFd, OwnedFd},
|
||||
path::Path,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
|
|
@ -18,20 +17,15 @@ use cluelessh_tokio::{
|
|||
Channel,
|
||||
};
|
||||
use eyre::{bail, ensure, Result, WrapErr};
|
||||
use rustix::{
|
||||
fs::UnmountFlags,
|
||||
process::WaitOptions,
|
||||
thread::{Pid, UnshareFlags},
|
||||
};
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
sync::mpsc,
|
||||
};
|
||||
use tracing::{debug, error, info, info_span, warn, Instrument};
|
||||
use tracing::{debug, error, info, info_span, warn};
|
||||
|
||||
pub async fn connection() -> Result<()> {
|
||||
pub fn connection() -> Result<()> {
|
||||
let mut memfd =
|
||||
unsafe { MemFd::<SerializedConnectionState>::from_raw_fd(PRIVSEP_CONNECTION_STATE_FD) }
|
||||
.wrap_err("failed to open memfd")?;
|
||||
|
|
@ -40,32 +34,19 @@ pub async fn connection() -> Result<()> {
|
|||
crate::setup_tracing(&state.config);
|
||||
|
||||
let span = info_span!("connection", addr = %state.peer_addr);
|
||||
let _guard = span.enter();
|
||||
|
||||
connection_inner(state).instrument(span).await
|
||||
crate::sandbox::drop_privileges(&state)?;
|
||||
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(connection_inner(state))
|
||||
}
|
||||
|
||||
async fn connection_inner(state: SerializedConnectionState) -> Result<()> {
|
||||
let config = state.config;
|
||||
|
||||
if rustix::process::getuid().is_root() {
|
||||
unshare_namespaces()?;
|
||||
}
|
||||
|
||||
if let Some(uid) = state.setgid {
|
||||
debug!(?uid, "Setting GID to drop privileges");
|
||||
let result = unsafe { libc::setgid(uid) };
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error()).wrap_err("failed to setgid");
|
||||
}
|
||||
}
|
||||
if let Some(uid) = state.setuid {
|
||||
debug!(?uid, "Setting UID to drop privileges");
|
||||
let result = unsafe { libc::setuid(uid) };
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error()).wrap_err("failed to setuid");
|
||||
}
|
||||
}
|
||||
|
||||
let stream = unsafe { std::net::TcpStream::from_raw_fd(PRIVSEP_CONNECTION_STREAM_FD) };
|
||||
let stream = TcpStream::from_std(stream)?;
|
||||
|
||||
|
|
@ -130,69 +111,6 @@ async fn connection_inner(state: SerializedConnectionState) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn unshare_namespaces() -> Result<()> {
|
||||
// The complicated incarnation to get a mount namespace working.
|
||||
rustix::thread::unshare(
|
||||
UnshareFlags::NEWNS
|
||||
| UnshareFlags::NEWNET
|
||||
| UnshareFlags::NEWIPC
|
||||
| UnshareFlags::NEWPID
|
||||
| UnshareFlags::NEWTIME,
|
||||
)
|
||||
.wrap_err("unsharing namespaces")?;
|
||||
|
||||
// After creating the PID namespace, we fork immediately so we can get PID 1.
|
||||
// We never exec, we just let the child live on happily.
|
||||
// The parent immediately waits for it, and then doesn't do anything really.
|
||||
// TODO: this is a bit sus....
|
||||
unsafe {
|
||||
let result = libc::fork();
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error()).wrap_err("setting propagation flags")?;
|
||||
}
|
||||
if result > 0 {
|
||||
// Parent
|
||||
let code = rustix::process::waitpid(
|
||||
Some(Pid::from_raw_unchecked(result)),
|
||||
WaitOptions::empty(),
|
||||
);
|
||||
match code {
|
||||
Err(_) => libc::exit(2),
|
||||
Ok(None) => libc::exit(1),
|
||||
Ok(Some(code)) => libc::exit(code.as_raw() as i32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = unsafe {
|
||||
libc::mount(
|
||||
c"none".as_ptr(),
|
||||
c"/".as_ptr(),
|
||||
std::ptr::null(),
|
||||
libc::MS_REC | libc::MS_PRIVATE,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error()).wrap_err("setting propagation flags")?;
|
||||
}
|
||||
|
||||
let new_root = Path::new("empty-new-root");
|
||||
let old_root = &new_root.join("old-root");
|
||||
|
||||
std::fs::create_dir_all(new_root)?;
|
||||
std::fs::create_dir_all(&old_root)?;
|
||||
|
||||
rustix::fs::bind_mount(new_root, new_root).wrap_err("bind mount the empty dir")?;
|
||||
|
||||
rustix::process::pivot_root(new_root, old_root).wrap_err("pivoting root")?;
|
||||
|
||||
// TODO: can we get rid of it entirely?
|
||||
rustix::fs::unmount("/old-root", UnmountFlags::DETACH).wrap_err("unmounting old root")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
mut conn: cluelessh_tokio::server::ServerConnection<TcpStream>,
|
||||
rpc_client: Arc<rpc::Client>,
|
||||
|
|
@ -480,7 +398,6 @@ impl SessionState {
|
|||
.pty_req(width_chars, height_rows, width_px, height_px, term_modes)
|
||||
.await?;
|
||||
|
||||
|
||||
self.pty_term = Some(term);
|
||||
|
||||
self.writer = Some(Box::pin(File::from_std(std::fs::File::from(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod config;
|
|||
mod connection;
|
||||
mod pty;
|
||||
mod rpc;
|
||||
mod sandbox;
|
||||
|
||||
use std::{
|
||||
io::{Read, Seek, SeekFrom},
|
||||
|
|
@ -35,12 +36,11 @@ struct Args {
|
|||
config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
fn main() -> eyre::Result<()> {
|
||||
match std::env::var("CLUELESSH_PRIVSEP_PROCESS") {
|
||||
Ok(privsep_process) => match privsep_process.as_str() {
|
||||
"connection" => {
|
||||
if let Err(err) = connection::connection().await {
|
||||
if let Err(err) = connection::connection() {
|
||||
error!(?err, "Error in connection child process");
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -59,14 +59,10 @@ async fn main() -> eyre::Result<()> {
|
|||
warn!("Daemon not started as root. This disables several security mitigations and permits logging in as any other user");
|
||||
}
|
||||
|
||||
let addr: SocketAddr = SocketAddr::new(config.net.ip, config.net.port);
|
||||
info!(%addr, "Starting server");
|
||||
|
||||
let listener = TcpListener::bind(addr)
|
||||
.await
|
||||
.wrap_err_with(|| format!("trying to listen on {addr}"))?;
|
||||
|
||||
main_process(config, listener).await
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(main_process(config))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,7 +117,7 @@ struct SerializedConnectionState {
|
|||
setgid: Option<u32>,
|
||||
}
|
||||
|
||||
async fn main_process(config: Config, listener: TcpListener) -> Result<()> {
|
||||
async fn main_process(config: Config) -> Result<()> {
|
||||
let user = match &config.security.unprivileged_user {
|
||||
Some(user) => Some(
|
||||
users::get_user_by_name(user).ok_or_else(|| eyre!("unprivileged {user} not found"))?,
|
||||
|
|
@ -159,6 +155,13 @@ async fn main_process(config: Config, listener: TcpListener) -> Result<()> {
|
|||
.map(|key| key.private_key.public_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let addr: SocketAddr = SocketAddr::new(config.net.ip, config.net.port);
|
||||
info!(%addr, "Starting server");
|
||||
|
||||
let listener = TcpListener::bind(addr)
|
||||
.await
|
||||
.wrap_err_with(|| format!("trying to listen on {addr}"))?;
|
||||
|
||||
loop {
|
||||
let (next_stream, peer_addr) = listener.accept().await?;
|
||||
|
||||
|
|
|
|||
191
bin/cluelesshd/src/sandbox.rs
Normal file
191
bin/cluelesshd/src/sandbox.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use eyre::{bail, Result, WrapErr};
|
||||
use rustix::{
|
||||
fs::UnmountFlags,
|
||||
process::WaitOptions,
|
||||
thread::{Pid, UnshareFlags},
|
||||
};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::SerializedConnectionState;
|
||||
|
||||
#[tracing::instrument(skip(state), ret)]
|
||||
pub fn drop_privileges(state: &SerializedConnectionState) -> Result<()> {
|
||||
if rustix::process::getuid().is_root() {
|
||||
crate::sandbox::unshare_namespaces()?;
|
||||
} else {
|
||||
// TODO: We can still do it if we're careful with the uid map.
|
||||
debug!("Not unsharing namespaces as the daemon was not started as root");
|
||||
}
|
||||
|
||||
if let Some(gid) = state.setgid {
|
||||
debug!(?gid, "Setting GID to drop privileges");
|
||||
let result = unsafe { libc::setgid(gid) };
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error()).wrap_err("failed to setgid");
|
||||
}
|
||||
}
|
||||
if let Some(uid) = state.setuid {
|
||||
debug!(?uid, "Setting UID to drop privileges");
|
||||
let result = unsafe { libc::setuid(uid) };
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error()).wrap_err("failed to setuid");
|
||||
}
|
||||
}
|
||||
|
||||
rustix::thread::set_no_new_privs(true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Fork {
|
||||
Child,
|
||||
Parent(rustix::process::Pid),
|
||||
}
|
||||
|
||||
unsafe fn fork() -> Result<Fork> {
|
||||
unsafe {
|
||||
let result = libc::fork();
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error()).wrap_err("setting propagation flags")?;
|
||||
}
|
||||
if result > 0 {
|
||||
Ok(Fork::Parent(Pid::from_raw_unchecked(result)))
|
||||
} else {
|
||||
Ok(Fork::Child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pipe() -> Result<(File, File)> {
|
||||
let (read, write) = rustix::pipe::pipe()?;
|
||||
|
||||
Ok((File::from(read), File::from(write)))
|
||||
}
|
||||
|
||||
/// Unshare namespaces to set up a sandbox.
|
||||
/// If this fails, there might be zombie child processes.
|
||||
/// Therefore, the caller must exit if this function fails.
|
||||
#[tracing::instrument]
|
||||
pub fn unshare_namespaces() -> Result<()> {
|
||||
let (mut child_ready_read, mut child_ready_write) = pipe()?;
|
||||
let (mut uid_map_ready_read, mut uid_map_ready_write) = pipe()?;
|
||||
|
||||
match unsafe { fork()? } {
|
||||
Fork::Parent(child) => {
|
||||
// In an error condition, we will not wait on the child.
|
||||
// But this is fine, as any error from this function will cause the caller to exit.
|
||||
let mut read = [0; 1];
|
||||
child_ready_read.read_exact(&mut read)?;
|
||||
if read[0] != 1 {
|
||||
bail!("child failed to write");
|
||||
}
|
||||
|
||||
trace!("Parent: child is ready");
|
||||
|
||||
let result1 = std::fs::write(
|
||||
format!("/proc/{}/uid_map", child.as_raw_nonzero().get()),
|
||||
"0 1000000 1000000",
|
||||
);
|
||||
let result2 = std::fs::write(
|
||||
format!("/proc/{}/gid_map", child.as_raw_nonzero().get()),
|
||||
"0 1000000 1000000",
|
||||
);
|
||||
|
||||
let result = result1.and(result2);
|
||||
|
||||
let value = if result.is_ok() { 1 } else { 0 };
|
||||
trace!(?value, "Parent: signaling uid_map write result");
|
||||
|
||||
uid_map_ready_write.write_all(&[value])?;
|
||||
|
||||
result?;
|
||||
|
||||
let code = rustix::process::waitpid(Some(child), WaitOptions::empty());
|
||||
match code {
|
||||
Err(_) => std::process::exit(2),
|
||||
Ok(None) => std::process::exit(1),
|
||||
Ok(Some(code)) => std::process::exit(code.as_raw() as i32),
|
||||
}
|
||||
}
|
||||
Fork::Child => {} // Move on
|
||||
}
|
||||
|
||||
// The complicated incarnation to get a mount namespace working.
|
||||
let result = rustix::thread::unshare(
|
||||
UnshareFlags::NEWNS
|
||||
| UnshareFlags::NEWNET
|
||||
| UnshareFlags::NEWIPC
|
||||
| UnshareFlags::NEWPID
|
||||
| UnshareFlags::NEWTIME
|
||||
| UnshareFlags::NEWUTS
|
||||
| UnshareFlags::NEWUSER,
|
||||
)
|
||||
.wrap_err("unsharing namespaces");
|
||||
|
||||
let value = if result.is_ok() { 1 } else { 0 };
|
||||
|
||||
trace!(?value, "Child: signaling unshare result");
|
||||
child_ready_write.write_all(&[value])?;
|
||||
|
||||
result?;
|
||||
|
||||
let mut read = [0; 1];
|
||||
uid_map_ready_read.read_exact(&mut read)?;
|
||||
if read[0] != 1 {
|
||||
bail!("parent failed to write");
|
||||
}
|
||||
trace!("Child: uid mappings set up, continue");
|
||||
|
||||
//std::thread::sleep(std::time::Duration::from_secs(1000));
|
||||
|
||||
// After creating the PID namespace, we fork immediately so we can get PID 1.
|
||||
// We never exec, we just let the child live on happily.
|
||||
// The parent immediately waits for it, and then doesn't do anything really.
|
||||
// TODO: this is a bit sus....
|
||||
|
||||
match unsafe { fork()? } {
|
||||
Fork::Parent(child) => {
|
||||
let code = rustix::process::waitpid(Some(child), WaitOptions::empty());
|
||||
match code {
|
||||
Err(_) => std::process::exit(2),
|
||||
Ok(None) => std::process::exit(1),
|
||||
Ok(Some(code)) => std::process::exit(code.as_raw() as i32),
|
||||
}
|
||||
}
|
||||
Fork::Child => {} // Move on
|
||||
}
|
||||
|
||||
let result = unsafe {
|
||||
libc::mount(
|
||||
c"none".as_ptr(),
|
||||
c"/".as_ptr(),
|
||||
std::ptr::null(),
|
||||
libc::MS_REC | libc::MS_PRIVATE,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if result == -1 {
|
||||
return Err(std::io::Error::last_os_error()).wrap_err("setting propagation flags")?;
|
||||
}
|
||||
|
||||
let new_root = Path::new("empty-new-root");
|
||||
let old_root = &new_root.join("old-root");
|
||||
|
||||
std::fs::create_dir_all(new_root)?;
|
||||
std::fs::create_dir_all(&old_root)?;
|
||||
|
||||
rustix::fs::bind_mount(new_root, new_root).wrap_err("bind mount the empty dir")?;
|
||||
|
||||
rustix::process::pivot_root(new_root, old_root).wrap_err("pivoting root")?;
|
||||
|
||||
// TODO: can we get rid of it entirely?
|
||||
rustix::fs::unmount("/old-root", UnmountFlags::DETACH).wrap_err("unmounting old root")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue