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 let metadata = file.metadata().unwrap();
65 let uid = metadata.uid();
66 let gid = metadata.gid();
67 immutable_effective(effective)?;
68 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
90pub 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
134pub 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
152pub 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 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 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}