rootasrole_core/
util.rs

1use std::{
2    env,
3    error::Error,
4    fs::File,
5    io,
6    os::{fd::AsRawFd, unix::fs::MetadataExt},
7    path::{Path, PathBuf},
8};
9
10use capctl::{prctl, CapState};
11use capctl::{Cap, CapSet, ParseCapError};
12use libc::{FS_IOC_GETFLAGS, FS_IOC_SETFLAGS};
13use log::{debug, warn};
14use serde::Serialize;
15use strum::EnumIs;
16
17#[cfg(feature = "finder")]
18use crate::api::PluginManager;
19use crate::database::structs::SCommand;
20
21pub const RST: &str = "\x1B[0m";
22pub const BOLD: &str = "\x1B[1m";
23pub const UNDERLINE: &str = "\x1B[4m";
24pub const RED: &str = "\x1B[31m";
25
26#[macro_export]
27macro_rules! upweak {
28    ($e:expr) => {
29        $e.upgrade().unwrap()
30    };
31}
32
33#[macro_export]
34macro_rules! as_borrow {
35    ($e:expr) => {
36        $e.as_ref().borrow()
37    };
38}
39
40#[macro_export]
41macro_rules! as_borrow_mut {
42    ($e:expr) => {
43        $e.as_ref().borrow_mut()
44    };
45}
46
47#[macro_export]
48macro_rules! rc_refcell {
49    ($e:expr) => {
50        std::rc::Rc::new(std::cell::RefCell::new($e))
51    };
52}
53
54const FS_IMMUTABLE_FL: u32 = 0x00000010;
55
56#[derive(Debug, EnumIs)]
57pub enum ImmutableLock {
58    Set,
59    Unset,
60}
61
62fn immutable_required_privileges(file: &File, effective: bool) -> Result<(), capctl::Error> {
63    //get file owner
64    let metadata = file.metadata().unwrap();
65    let uid = metadata.uid();
66    let gid = metadata.gid();
67    immutable_effective(effective)?;
68    // check if the current user is the owner
69    if nix::unistd::Uid::effective() != nix::unistd::Uid::from_raw(uid)
70        && nix::unistd::Gid::effective() != nix::unistd::Gid::from_raw(gid)
71    {
72        read_or_dac_override(effective)?;
73        fowner_effective(effective)?;
74    }
75    Ok(())
76}
77
78fn read_or_dac_override(effective: bool) -> Result<(), capctl::Error> {
79    match effective {
80        false => {
81            read_effective(false).and(dac_override_effective(false))?;
82        }
83        true => {
84            read_effective(true).or(dac_override_effective(true))?;
85        }
86    }
87    Ok(())
88}
89
90/// Set or unset the immutable flag on a file
91/// # Arguments
92/// * `file` - The file to set the immutable flag on
93/// * `lock` - Whether to set or unset the immutable flag
94pub fn toggle_lock_config<P: AsRef<Path>>(file: &P, lock: ImmutableLock) -> io::Result<()> {
95    let file = open_with_privileges(file)?;
96    let mut val = 0;
97    let fd = file.as_raw_fd();
98    if unsafe { nix::libc::ioctl(fd, FS_IOC_GETFLAGS, &mut val) } < 0 {
99        return Err(std::io::Error::last_os_error());
100    }
101    if lock.is_unset() {
102        val &= !(FS_IMMUTABLE_FL);
103    } else {
104        val |= FS_IMMUTABLE_FL;
105    }
106    debug!("Setting immutable privilege");
107
108    immutable_required_privileges(&file, true)?;
109    if unsafe { nix::libc::ioctl(fd, FS_IOC_SETFLAGS, &mut val) } < 0 {
110        return Err(std::io::Error::last_os_error());
111    }
112    debug!("Resetting immutable privilege");
113    immutable_required_privileges(&file, false)?;
114    Ok(())
115}
116
117pub fn warn_if_mutable(file: &File, return_err: bool) -> Result<(), Box<dyn Error>> {
118    let mut val = 0;
119    let fd = file.as_raw_fd();
120    if unsafe { nix::libc::ioctl(fd, FS_IOC_GETFLAGS, &mut val) } < 0 {
121        return Err(std::io::Error::last_os_error().into());
122    }
123    if val & FS_IMMUTABLE_FL == 0 {
124        if return_err {
125            return Err(
126                "Config file is not immutable, ask your administrator to solve this issue".into(),
127            );
128        }
129        warn!("Config file is not immutable, think about setting the immutable flag.");
130    }
131    Ok(())
132}
133
134//parse string iterator to capset
135pub fn parse_capset_iter<'a, I>(iter: I) -> Result<CapSet, ParseCapError>
136where
137    I: Iterator<Item = &'a str>,
138{
139    let mut res = CapSet::empty();
140
141    for part in iter {
142        match part.parse() {
143            Ok(cap) => res.add(cap),
144            Err(error) => {
145                return Err(error);
146            }
147        }
148    }
149    Ok(res)
150}
151
152/// Reference every capabilities that lead to almost a direct privilege escalation
153pub fn capabilities_are_exploitable(caps: &CapSet) -> bool {
154    caps.has(Cap::SYS_ADMIN)
155        || caps.has(Cap::SYS_PTRACE)
156        || caps.has(Cap::SYS_MODULE)
157        || caps.has(Cap::DAC_READ_SEARCH)
158        || caps.has(Cap::DAC_OVERRIDE)
159        || caps.has(Cap::FOWNER)
160        || caps.has(Cap::CHOWN)
161        || caps.has(Cap::SETUID)
162        || caps.has(Cap::SETGID)
163        || caps.has(Cap::SETFCAP)
164        || caps.has(Cap::SYS_RAWIO)
165        || caps.has(Cap::LINUX_IMMUTABLE)
166        || caps.has(Cap::SYS_CHROOT)
167        || caps.has(Cap::SYS_BOOT)
168        || caps.has(Cap::MKNOD)
169}
170
171pub fn escape_parser_string<S>(s: S) -> String
172where
173    S: AsRef<str>,
174{
175    remove_outer_quotes(s.as_ref())
176}
177
178fn remove_outer_quotes(input: &str) -> String {
179    if input.len() >= 2
180        && (input.starts_with('"') && input.ends_with('"')
181            || input.starts_with('\'') && input.ends_with('\''))
182    {
183        remove_outer_quotes(&input[1..input.len() - 1])
184    } else {
185        input.to_string()
186    }
187}
188
189pub fn parse_conf_command(command: &SCommand) -> Result<Vec<String>, Box<dyn Error>> {
190    match command {
191        SCommand::Simple(command) => parse_simple_command(command),
192        SCommand::Complex(command) => parse_complex_command(command),
193    }
194}
195
196fn parse_simple_command(command: &str) -> Result<Vec<String>, Box<dyn Error>> {
197    shell_words::split(command).map_err(Into::into)
198}
199
200fn parse_complex_command(command: &serde_json::Value) -> Result<Vec<String>, Box<dyn Error>> {
201    if let Some(array) = command.as_array() {
202        let result: Result<Vec<String>, _> = array
203            .iter()
204            .map(|item| {
205                item.as_str()
206                    .map(|s| s.to_string())
207                    .ok_or_else(|| "Invalid command".into())
208            })
209            .collect();
210        result
211    } else {
212        parse_complex_command_with_finder(command)
213    }
214}
215
216#[cfg(feature = "finder")]
217fn parse_complex_command_with_finder(
218    command: &serde_json::Value,
219) -> Result<Vec<String>, Box<dyn Error>> {
220    let res = PluginManager::notify_complex_command_parser(command);
221    debug!("Parsed command {:?}", res);
222    res
223}
224
225#[cfg(not(feature = "finder"))]
226fn parse_complex_command_with_finder(
227    _command: &serde_json::Value,
228) -> Result<Vec<String>, Box<dyn Error>> {
229    Err("Invalid command".into())
230}
231
232pub fn find_from_envpath<P: AsRef<Path>>(exe_name: P) -> Option<PathBuf> {
233    env::var_os("PATH").and_then(|paths| {
234        env::split_paths(&paths)
235            .filter_map(|dir| {
236                let full_path = dir.join(&exe_name);
237                if full_path.is_file() {
238                    Some(full_path)
239                } else {
240                    None
241                }
242            })
243            .next()
244    })
245}
246
247pub fn final_path(path: &str) -> PathBuf {
248    if let Some(env_path) = find_from_envpath(path) {
249        env_path
250    } else if let Ok(canon_path) = std::fs::canonicalize(path) {
251        canon_path
252    } else {
253        PathBuf::from(path)
254    }
255}
256
257#[cfg(debug_assertions)]
258pub fn subsribe(_: &str) -> Result<(), Box<dyn Error>> {
259    env_logger::Builder::from_default_env()
260        .filter_level(log::LevelFilter::Debug)
261        .format_module_path(true)
262        .init();
263    Ok(())
264}
265
266#[cfg(not(debug_assertions))]
267pub fn subsribe(tool: &str) -> Result<(), Box<dyn Error>> {
268    use env_logger::Env;
269    use log::LevelFilter;
270    use syslog::{BasicLogger, Facility, Formatter3164};
271    syslog::init(Facility::LOG_AUTH, LevelFilter::Info, Some(tool))?;
272    Ok(())
273}
274
275pub fn drop_effective() -> Result<(), capctl::Error> {
276    let mut current = CapState::get_current()?;
277    current.effective.clear();
278    current.set_current()
279}
280
281pub fn cap_effective(cap: Cap, enable: bool) -> Result<(), capctl::Error> {
282    let mut current = CapState::get_current()?;
283    current.effective.set_state(cap, enable);
284    current.set_current()
285}
286
287pub fn setpcap_effective(enable: bool) -> Result<(), capctl::Error> {
288    cap_effective(Cap::SETPCAP, enable)
289}
290
291pub fn setuid_effective(enable: bool) -> Result<(), capctl::Error> {
292    cap_effective(Cap::SETUID, enable)
293}
294
295pub fn setgid_effective(enable: bool) -> Result<(), capctl::Error> {
296    cap_effective(Cap::SETGID, enable)
297}
298
299pub fn fowner_effective(enable: bool) -> Result<(), capctl::Error> {
300    cap_effective(Cap::FOWNER, enable)
301}
302
303pub fn read_effective(enable: bool) -> Result<(), capctl::Error> {
304    cap_effective(Cap::DAC_READ_SEARCH, enable)
305}
306
307pub fn dac_override_effective(enable: bool) -> Result<(), capctl::Error> {
308    cap_effective(Cap::DAC_OVERRIDE, enable)
309}
310
311pub fn immutable_effective(enable: bool) -> Result<(), capctl::Error> {
312    cap_effective(Cap::LINUX_IMMUTABLE, enable)
313}
314
315pub fn activates_no_new_privs() -> Result<(), capctl::Error> {
316    prctl::set_no_new_privs()
317}
318
319pub fn write_json_config<T: Serialize, S>(settings: &T, path: S) -> Result<(), Box<dyn Error>>
320where
321    S: std::convert::AsRef<Path> + Clone,
322{
323    let file = create_with_privileges(path)?;
324    serde_json::to_writer_pretty(file, &settings)?;
325    Ok(())
326}
327
328pub fn create_with_privileges<P: AsRef<Path>>(p: P) -> Result<File, std::io::Error> {
329    std::fs::File::create(&p).or_else(|e| {
330        debug!(
331            "Error creating file without privilege, trying with privileges: {}",
332            e
333        );
334        dac_override_effective(true)?;
335        let res = std::fs::File::create(p).inspect_err(|e| {
336            debug!(
337                "Error creating file without privilege, trying with privileges: {}",
338                e
339            );
340        });
341        dac_override_effective(false)?;
342        res
343    })
344}
345
346pub fn open_with_privileges<P: AsRef<Path>>(p: P) -> Result<File, std::io::Error> {
347    std::fs::File::open(&p).or_else(|e| {
348        debug!(
349            "Error creating file without privilege, trying with privileges: {}",
350            e
351        );
352        read_effective(true).or(dac_override_effective(true))?;
353        let res = std::fs::File::open(p);
354        read_effective(false)?;
355        dac_override_effective(false)?;
356        res
357    })
358}
359
360pub fn remove_with_privileges<P: AsRef<Path>>(p: P) -> Result<(), std::io::Error> {
361    std::fs::remove_file(&p).or_else(|e| {
362        debug!(
363            "Error creating file without privilege, trying with privileges: {}",
364            e
365        );
366        dac_override_effective(true)?;
367        let res = std::fs::remove_file(p);
368        dac_override_effective(false)?;
369        res
370    })
371}
372
373pub fn create_dir_all_with_privileges<P: AsRef<Path>>(p: P) -> Result<(), std::io::Error> {
374    std::fs::create_dir_all(&p).or_else(|e| {
375        debug!(
376            "Error creating file without privilege, trying with privileges: {}",
377            e
378        );
379        dac_override_effective(true)?;
380        let res = std::fs::create_dir_all(p);
381        read_effective(false)?;
382        dac_override_effective(false)?;
383        res
384    })
385}
386
387#[cfg(test)]
388mod test {
389    use std::fs;
390
391    use super::*;
392
393    #[test]
394    fn test_remove_outer_quotes() {
395        assert_eq!(remove_outer_quotes("'test'"), "test");
396        assert_eq!(remove_outer_quotes("\"test\""), "test");
397        assert_eq!(remove_outer_quotes("test"), "test");
398        assert_eq!(remove_outer_quotes("t'est"), "t'est");
399        assert_eq!(remove_outer_quotes("t\"est"), "t\"est");
400    }
401
402    #[test]
403    fn test_parse_capset_iter() {
404        let capset = parse_capset_iter(
405            vec!["CAP_SYS_ADMIN", "CAP_SYS_PTRACE", "CAP_DAC_READ_SEARCH"].into_iter(),
406        )
407        .expect("Failed to parse capset");
408        assert!(capset.has(Cap::SYS_ADMIN));
409        assert!(capset.has(Cap::SYS_PTRACE));
410        assert!(capset.has(Cap::DAC_READ_SEARCH));
411    }
412
413    #[test]
414    fn test_capabilities_are_exploitable() {
415        let mut capset = CapSet::empty();
416        capset.add(Cap::SYS_ADMIN);
417        assert!(capabilities_are_exploitable(&capset));
418        capset.clear();
419        capset.add(Cap::SYS_PTRACE);
420        assert!(capabilities_are_exploitable(&capset));
421        capset.clear();
422        capset.add(Cap::SYS_MODULE);
423        assert!(capabilities_are_exploitable(&capset));
424        capset.clear();
425        capset.add(Cap::DAC_READ_SEARCH);
426        assert!(capabilities_are_exploitable(&capset));
427        capset.clear();
428        capset.add(Cap::DAC_OVERRIDE);
429        assert!(capabilities_are_exploitable(&capset));
430        capset.clear();
431        capset.add(Cap::FOWNER);
432        assert!(capabilities_are_exploitable(&capset));
433        capset.clear();
434        capset.add(Cap::CHOWN);
435        assert!(capabilities_are_exploitable(&capset));
436        capset.clear();
437        capset.add(Cap::SETUID);
438        assert!(capabilities_are_exploitable(&capset));
439        capset.clear();
440        capset.add(Cap::SETGID);
441        assert!(capabilities_are_exploitable(&capset));
442        capset.clear();
443        capset.add(Cap::SETFCAP);
444        assert!(capabilities_are_exploitable(&capset));
445        capset.clear();
446        capset.add(Cap::SYS_RAWIO);
447        assert!(capabilities_are_exploitable(&capset));
448        capset.clear();
449        capset.add(Cap::LINUX_IMMUTABLE);
450        assert!(capabilities_are_exploitable(&capset));
451        capset.clear();
452        capset.add(Cap::SYS_CHROOT);
453        assert!(capabilities_are_exploitable(&capset));
454        capset.clear();
455        capset.add(Cap::SYS_BOOT);
456        assert!(capabilities_are_exploitable(&capset));
457        capset.clear();
458        capset.add(Cap::MKNOD);
459        assert!(capabilities_are_exploitable(&capset));
460        capset.clear();
461        capset.add(Cap::WAKE_ALARM);
462        assert!(!capabilities_are_exploitable(&capset));
463    }
464
465    #[test]
466    fn test_toggle_lock_config() {
467        let path = PathBuf::from("/tmp/test");
468        let file = File::create(&path).expect("Failed to create file");
469        let res = toggle_lock_config(&path, ImmutableLock::Set);
470        let status = fs::read_to_string("/proc/self/status").unwrap();
471        let capeff = status
472            .lines()
473            .find(|line| line.starts_with("CapEff:"))
474            .expect("Failed to find CapEff line");
475        let effhex = capeff
476            .split(':')
477            .last()
478            .expect("Failed to get effective capabilities")
479            .trim();
480        let eff = u64::from_str_radix(effhex, 16).expect("Failed to parse effective capabilities");
481        if eff & ((1 << Cap::LINUX_IMMUTABLE as u8) as u64) != 0 {
482            assert!(res.is_ok());
483        } else {
484            assert!(res.is_err());
485            // stop test
486            return;
487        }
488        let mut val = 0;
489        let fd = file.as_raw_fd();
490        if unsafe { nix::libc::ioctl(fd, FS_IOC_GETFLAGS, &mut val) } < 0 {
491            panic!("Failed to get flags");
492        }
493        assert_eq!(val & FS_IMMUTABLE_FL, FS_IMMUTABLE_FL);
494        //test to write on file
495        let file = File::create(&path);
496        assert!(file.is_err());
497        let res = toggle_lock_config(&path, ImmutableLock::Unset);
498        assert!(res.is_ok());
499        let file = File::create(&path);
500        assert!(file.is_ok());
501        let res = fs::remove_file(&path);
502        assert!(res.is_ok());
503    }
504}