├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── Justfile ├── LICENCE ├── README.md ├── files ├── etc_minisudo-rules.toml ├── etc_pam.d_minisudo_linux └── etc_pam.d_minisudo_macos └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | os: 7 | - linux 8 | - osx 9 | jobs: 10 | allow_failures: 11 | - rust: nightly 12 | fast_finish: true 13 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "libc" 5 | version = "0.2.69" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 8 | 9 | [[package]] 10 | name = "minisudo" 11 | version = "0.1.0" 12 | dependencies = [ 13 | "pam", 14 | "rpassword", 15 | "serde", 16 | "toml", 17 | "users", 18 | ] 19 | 20 | [[package]] 21 | name = "pam" 22 | version = "0.7.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "fa2bdc959c201c047004a1420a92aaa1dd1a6b64d5ef333aa3a4ac764fb93097" 25 | dependencies = [ 26 | "libc", 27 | "pam-sys", 28 | "users", 29 | ] 30 | 31 | [[package]] 32 | name = "pam-sys" 33 | version = "0.5.6" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "cd4858311a097f01a0006ef7d0cd50bca81ec430c949d7bf95cbefd202282434" 36 | dependencies = [ 37 | "libc", 38 | ] 39 | 40 | [[package]] 41 | name = "proc-macro2" 42 | version = "1.0.12" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "8872cf6f48eee44265156c111456a700ab3483686b3f96df4cf5481c89157319" 45 | dependencies = [ 46 | "unicode-xid", 47 | ] 48 | 49 | [[package]] 50 | name = "quote" 51 | version = "1.0.4" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "4c1f4b0efa5fc5e8ceb705136bfee52cfdb6a4e3509f770b478cd6ed434232a7" 54 | dependencies = [ 55 | "proc-macro2", 56 | ] 57 | 58 | [[package]] 59 | name = "rpassword" 60 | version = "4.0.5" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "99371657d3c8e4d816fb6221db98fa408242b0b53bac08f8676a41f8554fe99f" 63 | dependencies = [ 64 | "libc", 65 | "winapi", 66 | ] 67 | 68 | [[package]] 69 | name = "serde" 70 | version = "1.0.106" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" 73 | dependencies = [ 74 | "serde_derive", 75 | ] 76 | 77 | [[package]] 78 | name = "serde_derive" 79 | version = "1.0.106" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" 82 | dependencies = [ 83 | "proc-macro2", 84 | "quote", 85 | "syn", 86 | ] 87 | 88 | [[package]] 89 | name = "syn" 90 | version = "1.0.18" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "410a7488c0a728c7ceb4ad59b9567eb4053d02e8cc7f5c0e0eeeb39518369213" 93 | dependencies = [ 94 | "proc-macro2", 95 | "quote", 96 | "unicode-xid", 97 | ] 98 | 99 | [[package]] 100 | name = "toml" 101 | version = "0.5.6" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" 104 | dependencies = [ 105 | "serde", 106 | ] 107 | 108 | [[package]] 109 | name = "unicode-xid" 110 | version = "0.2.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 113 | 114 | [[package]] 115 | name = "users" 116 | version = "0.8.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "7fed7d0912567d35f88010c23dbaf865e9da8b5227295e8dc0f2fdd109155ab7" 119 | dependencies = [ 120 | "libc", 121 | ] 122 | 123 | [[package]] 124 | name = "winapi" 125 | version = "0.3.8" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 128 | dependencies = [ 129 | "winapi-i686-pc-windows-gnu", 130 | "winapi-x86_64-pc-windows-gnu", 131 | ] 132 | 133 | [[package]] 134 | name = "winapi-i686-pc-windows-gnu" 135 | version = "0.4.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 138 | 139 | [[package]] 140 | name = "winapi-x86_64-pc-windows-gnu" 141 | version = "0.4.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 144 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minisudo" 3 | description = "A little privilege escalator, for learning" 4 | 5 | authors = ["Benjamin Sago "] 6 | edition = "2018" 7 | exclude = ["/files/*", "Justfile", "LICENCE", "/README.md"] 8 | license = "MIT" 9 | readme = "README.md" 10 | repository = "https://github.com/ogham/minisudo" 11 | version = "0.1.0" 12 | 13 | [dependencies] 14 | pam = "0.7" 15 | rpassword = "4.0" 16 | users = "0.8" 17 | 18 | serde = { version = "1.0", features = [ "derive" ] } 19 | toml = "0.5" 20 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | # builds the project 2 | build: 3 | cargo build --release 4 | 5 | # removes build artifacts 6 | clean: 7 | cargo clean 8 | 9 | # installs the binary and config files to privileged locations 10 | install: 11 | install -o0 -g0 -m4755 -- target/release/minisudo /usr/local/bin/minisudo 12 | install -o0 -g0 -m640 -- files/etc_minisudo-rules.toml /etc/minisudo-rules.toml 13 | test -f /usr/lib/pam/pam_opendirectory.so.2 \ 14 | && install -o0 -g0 -m444 -- files/etc_pam.d_minisudo_macos /etc/pam.d/minisudo \ 15 | || install -o0 -g0 -m444 -- files/etc_pam.d_minisudo_linux /etc/pam.d/minisudo 16 | 17 | # removes binary and config files from privileged locations 18 | uninstall: 19 | rm -f /usr/local/bin/minisudo 20 | rm -f /etc/pam.d/minisudo 21 | rm -f /etc/minisudo-rules.toml 22 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Benjamin Sago 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minisudo [![Build status](https://travis-ci.org/ogham/minisudo.svg)](https://travis-ci.org/ogham/minisudo) 2 | 3 | This is a small sudo-like privilege escalator for Unix-like operating systems. 4 | 5 | It was written for learning, not to replace sudo, and has been tested on macOS and Linux. 6 | 7 | ``` 8 | $ minisudo whoami 9 | Password for ben: [password hidden] 10 | root 11 | ``` 12 | 13 | This project was inspired by [kibi](https://github.com/ilai-deutel/kibi), a text editor written in ≤1024 lines of code. I’ve seen many people with the false impression that `sudo` is “magical” or special in some way, but it doesn’t do anything that’s not available to any other program. minisudo tries to implement its most popular use case — running programs as root after checking against a list of rules — with a couple pages of code and minimal dependencies. 14 | 15 | 16 | Installation 17 | ------------ 18 | 19 | minisudo is written in [Rust](https://www.rust-lang.org/), and uses [just](https://github.com/casey/just) as its build script runner. To build and install: 20 | 21 | $ just build 22 | $ sudo just install 23 | 24 | To uninstall: 25 | 26 | $ sudo just uninstall 27 | 28 | Debian users will need to have the `libpam0g-dev` package installed. 29 | 30 | 31 | How it works 32 | ------------ 33 | 34 | minisudo uses [PAM](https://en.wikipedia.org/wiki/Pluggable_authentication_module) as its authentication mechanism, which is how it knows what your password is. It installs a file into `/etc/pam.d` to allow it to do this. 35 | 36 | The binary is installed with the [setuid bit](https://en.wikipedia.org/wiki/Setuid) set, which is how it’s able to run programs as root. 37 | 38 | 39 | Rules file 40 | ---------- 41 | 42 | The rules for which users can run which programs are specified in a TOML file, `/etc/minisudo-rules.toml`. Here’s an example: 43 | 44 | ```toml 45 | # The user ‘ben’ can run ‘ls’, but nothing else. 46 | [[rule]] 47 | user = "ben" 48 | program = "/bin/ls" 49 | 50 | # All members of the ‘staff’ group can run ‘whoami’, but nothing else. 51 | [[rule]] 52 | group = "staff" 53 | program = "/usr/bin/whoami" 54 | ``` 55 | 56 | Binaries must be specified by their _full path_, not just their basename. Specify `*` to allow any program to be run. 57 | 58 | 59 | Safety 60 | ------ 61 | 62 | Although no unsafe Rust code is present in the `minisudo` crate itself, its dependencies call functions in PAM and libc, so the project can never be _entirely_ free of unsafe code. 63 | 64 | 65 | Security vulnerabilities 66 | ------------------------ 67 | 68 | Probably. 69 | 70 | 71 | Licence 72 | ------- 73 | 74 | minisudo’s source code is under the [MIT Licence](LICENCE). 75 | -------------------------------------------------------------------------------- /files/etc_minisudo-rules.toml: -------------------------------------------------------------------------------- 1 | # The user ‘ben’ can run any program. 2 | [[rule]] 3 | user = "ben" 4 | program = "*" 5 | -------------------------------------------------------------------------------- /files/etc_pam.d_minisudo_linux: -------------------------------------------------------------------------------- 1 | # minisudo: account password session 2 | account required pam_permit.so 3 | password required pam_deny.so 4 | session required pam_permit.so 5 | -------------------------------------------------------------------------------- /files/etc_pam.d_minisudo_macos: -------------------------------------------------------------------------------- 1 | # minisudo: auth account password session 2 | auth required pam_opendirectory.so 3 | account required pam_permit.so 4 | password required pam_deny.so 5 | session required pam_permit.so 6 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(future_incompatible)] 2 | #![warn(nonstandard_style)] 3 | #![warn(rust_2018_compatibility)] 4 | #![warn(rust_2018_idioms)] 5 | #![warn(unused)] 6 | 7 | #![deny(unsafe_code)] 8 | 9 | use std::env::{args_os, current_dir, var}; 10 | use std::ffi::OsStr; 11 | use std::fs; 12 | use std::os::unix::{fs::PermissionsExt, process::CommandExt}; 13 | use std::path::{Path, PathBuf}; 14 | use std::process::{Command, exit}; 15 | 16 | use serde::Deserialize; 17 | use users::User; 18 | 19 | 20 | /// Name of the PAM config file specified in `/etc/pam.d`. 21 | static PAM_NAME: &str = "minisudo"; 22 | 23 | /// Path to the configuration file. 24 | static CONFIG_PATH: &str = "/etc/minisudo-rules.toml"; 25 | 26 | 27 | fn main() { 28 | 29 | // Load rules from the config file 30 | let config = Config::load_from_file(); 31 | 32 | // Put together the command to run 33 | let args = args_os().skip(1).collect::>(); 34 | if args.is_empty() { 35 | eprintln!("Usage: minisudo "); 36 | exit(2); 37 | } 38 | 39 | // Look up the full path of the program in the first argument. 40 | // We need this to check against the path given in the config file, 41 | // and also because exec requires the full path, not just the name. 42 | let binary = lookup_binary(&args[0]); 43 | 44 | // Make sure the rules say it’s OK for this user to run this program 45 | let user = current_user().expect("No current user"); 46 | let username = user.name().to_str().expect("Non-UTF8 username"); 47 | if ! config.test(&user, &binary) { 48 | eprintln!("minisudo: User {:?} is not allowed to run {}.", username, binary.display()); 49 | eprintln!("This incident will be reported."); // not really 50 | exit(1); 51 | } 52 | 53 | // Have the user enter a password 54 | let message = format!("Password for {}: ", username); 55 | let password = rpassword::read_password_from_tty(Some(&message)).expect("No password"); 56 | 57 | // Authenticate them using PAM 58 | let mut authenticator = pam::Authenticator::with_password(PAM_NAME).expect("No authenticator"); 59 | authenticator.get_handler().set_credentials(username, password); 60 | if let Err(e) = authenticator.authenticate() { 61 | eprintln!("minisudo: Authentication failed: {}", e); 62 | exit(1); 63 | } 64 | 65 | // Run the command, stopping the current process if the new one 66 | // starts successfully, and continuing if there’s an error. 67 | authenticator.open_session().expect("No session"); 68 | let error = Command::new(binary) 69 | .args(&args[1..]) 70 | .exec(); 71 | 72 | // If you get here the command didn’t work, so print the error. 73 | eprintln!("minisudo: Error running program: {}", error); 74 | exit(1); 75 | } 76 | 77 | 78 | /// Looks up the current user by their ID. 79 | fn current_user() -> Option { 80 | users::get_user_by_uid(users::get_current_uid()) 81 | } 82 | 83 | 84 | /// Turns the binary the user is trying to run, which could be a 85 | /// basename like `ls`, an absolute path such as `/usr/bin/ls`, or a 86 | /// relative path such as `./ls`, into an absolute path. 87 | fn lookup_binary(binary_basename: &OsStr) -> PathBuf { 88 | let input_path = PathBuf::from(binary_basename); 89 | 90 | // If the path is absolute, we already have the full path. 91 | if input_path.is_absolute() { 92 | return input_path; 93 | } 94 | 95 | // If the path is relative and has more than one component, then 96 | // create the full path relative to where we are now. 97 | if input_path.components().count() > 1 { 98 | let mut path = current_dir().expect("No current directory"); 99 | path.push(input_path); 100 | return path; 101 | } 102 | 103 | // Otherwise, search through every directory in the `PATH` 104 | // environment variable to find the absolute path of the binary. 105 | // This variable is user-controlled, so be very careful about which 106 | // files are deemed acceptable. 107 | for pathlet in var("PATH").expect("no $PATH").split(':') { 108 | let mut potential_path = PathBuf::from(pathlet); 109 | potential_path.push(binary_basename); 110 | 111 | if potential_path.exists() 112 | && potential_path.metadata().map_or(false, |m| m.permissions().mode() & 0o111 != 0) 113 | { 114 | return potential_path; 115 | } 116 | } 117 | 118 | // If the code above can’t find one, then there is no such program. 119 | eprintln!("minisudo: No such command {}", input_path.display()); 120 | exit(1); 121 | } 122 | 123 | 124 | /// The root type for the config file. 125 | #[derive(PartialEq, Debug, Deserialize)] 126 | struct Config { 127 | #[serde(rename = "rule")] 128 | rules: Vec, 129 | } 130 | 131 | /// One of the rules specified in the config file. 132 | #[derive(PartialEq, Debug, Deserialize)] 133 | struct Rule { 134 | #[serde(flatten)] 135 | matcher: Matcher, 136 | program: String, 137 | } 138 | 139 | /// The specification for which user or group this rule applies to. 140 | #[derive(PartialEq, Debug, Deserialize)] 141 | #[serde(untagged)] 142 | enum Matcher { 143 | UserByName { user: String }, 144 | GroupByName { group: String }, 145 | } 146 | 147 | impl Config { 148 | 149 | /// Load all the rules from the config file. 150 | fn load_from_file() -> Self { 151 | let file = fs::read_to_string(CONFIG_PATH).expect("No rules"); 152 | toml::from_str(&file).expect("Bad parse") 153 | } 154 | 155 | /// Tests whether the rule lets the user run the program. 156 | fn test(&self, user: &User, program: &Path) -> bool { 157 | self.rules.iter() 158 | .any(|r| r.matcher.test(user) && (r.program == "*" || &*r.program == program.as_os_str())) 159 | } 160 | } 161 | 162 | impl Matcher { 163 | 164 | /// Tests whether this matcher is for the given user. 165 | fn test(&self, user: &User) -> bool { 166 | match self { 167 | Self::UserByName { user: username } => { 168 | user.name() == &**username 169 | } 170 | Self::GroupByName { group: groupname } => { 171 | user.groups().iter().flatten().any(|g| g.name() == &**groupname) 172 | } 173 | } 174 | } 175 | } 176 | --------------------------------------------------------------------------------