├── .github └── workflows │ └── cargo_publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── doc └── images │ ├── ntdsextract2.jpeg │ └── screenshot_deleted.png ├── misc └── attids.h ├── src ├── c_database.rs ├── cache │ ├── column │ │ └── mod.rs │ ├── column_index.rs │ ├── columns_of_table.rs │ ├── esedb_row_id.rs │ ├── meta_data_cache.rs │ ├── mod.rs │ ├── record │ │ ├── mod.rs │ │ └── with_value.rs │ ├── record_id.rs │ ├── record_pointer.rs │ ├── table │ │ ├── data_table.rs │ │ ├── link_table.rs │ │ ├── mod.rs │ │ ├── sd_table.rs │ │ └── special_records.rs │ └── value.rs ├── cli │ ├── args.rs │ ├── commands.rs │ ├── entry_format.rs │ ├── member_of_attribute.rs │ ├── mod.rs │ ├── output │ │ ├── csv_writer.rs │ │ ├── json_writer.rs │ │ ├── jsonlines_writer.rs │ │ ├── mod.rs │ │ └── writer.rs │ ├── output_format.rs │ └── output_options.rs ├── column_info_mapping.rs ├── column_information.rs ├── data_table_ext.rs ├── entry_id.rs ├── esedb_mitigation.rs ├── esedbinfo.rs ├── formatted_value.rs ├── group.rs ├── lib.rs ├── main.rs ├── membership_serialization │ ├── csv_serialization.rs │ ├── json_serialization.rs │ ├── membership_set.rs │ ├── mod.rs │ └── serialization_type.rs ├── ntds │ ├── attribute_id.rs │ ├── attribute_id_impl.rs │ ├── attribute_name.rs │ ├── attribute_value.rs │ ├── data_table.rs │ ├── data_table_record.rs │ ├── error.rs │ ├── from_data_table.rs │ ├── is_member_of.rs │ ├── link_table.rs │ ├── link_table_builder.rs │ ├── mod.rs │ ├── object │ │ ├── has_serializable_fields.rs │ │ ├── mod.rs │ │ ├── no_specific_attributes.rs │ │ ├── object_base.rs │ │ ├── object_computer.rs │ │ ├── object_group.rs │ │ ├── object_person.rs │ │ └── specific_object_attribute.rs │ ├── object_type.rs │ ├── schema.rs │ └── sd_table.rs ├── object_tree.rs ├── object_tree_entry.rs ├── progress_bar.rs ├── record_pointer.rs ├── record_predicate.rs ├── value │ ├── bool.rs │ ├── from_value.rs │ ├── i32.rs │ ├── i64.rs │ ├── mod.rs │ ├── sam_account_type.rs │ ├── sid.rs │ ├── string.rs │ ├── to_string.rs │ ├── u32.rs │ └── user_acount_control.rs └── win32_types │ ├── guid.rs │ ├── mod.rs │ ├── rdn.rs │ ├── sam_account_type.rs │ ├── security_descriptor.rs │ ├── sid │ ├── mod.rs │ └── sid_visitor.rs │ ├── timestamp │ ├── mod.rs │ ├── timeline_entry.rs │ ├── truncated_windows_file_time.rs │ ├── unix_timestamp.rs │ └── windows_file_time.rs │ └── user_account_control │ └── mod.rs └── tests ├── data ├── .gitattributes └── ntds_plain.dit └── test_plain.rs /.github/workflows/cargo_publish.yml: -------------------------------------------------------------------------------- 1 | name: publish at crates.io 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | override: true 15 | - uses: katyo/publish-crates@v1 16 | with: 17 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | 9 | # Added by cargo 10 | 11 | target 12 | **/*.csv 13 | **/*.json 14 | **/*.bodyfile 15 | 16 | .idea 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ntdsextract2" 3 | version = "1.4.9" 4 | edition = "2021" 5 | description = "Display contents of Active Directory database files (ntds.dit)" 6 | repository = "https://github.com/janstarke/ntdsextract2" 7 | authors = ["Jan Starke "] 8 | license = "GPL-3.0" 9 | readme = "README.md" 10 | categories = ["command-line-utilities"] 11 | keywords = ["cli", "forensics", "security"] 12 | rust-version = "1.81" 13 | 14 | [lib] 15 | name="libntdsextract2" 16 | path="src/lib.rs" 17 | 18 | [[bin]] 19 | name="ntdsextract2" 20 | path="src/main.rs" 21 | 22 | [dependencies] 23 | log = {version = "0.4", features = [ "release_max_level_info" ]} 24 | concat-idents = "1" 25 | simplelog = "0.12" 26 | anyhow = "1.0" 27 | clap = "4" 28 | clap-verbosity-flag = "3" 29 | maplit = "1.0.2" 30 | byteorder = "1.4.3" 31 | hex = "0.4" 32 | chrono = "0.4" 33 | bitflags = {version="2", features=["serde"] } 34 | strum = { version = "0", features = ["derive", "phf"] } 35 | num-traits = "0.2.15" 36 | num-derive = "0.4.0" 37 | thiserror = "2" 38 | bodyfile = "0.1.4" 39 | hashbrown = "0" 40 | libesedb = "0.2.5" 41 | #libesedb = {path="../rust-libesedb"} 42 | 43 | serde = { version = "1.0", features = ["derive"] } 44 | serde_json = "1.0" 45 | csv = "1.1" 46 | term-table = "1.3.2" 47 | termsize = "0.1" 48 | termtree = "0" 49 | regex = "1.6" 50 | 51 | lazy_static = "1.4" 52 | lazy-regex = "3" 53 | getset = "0" 54 | 55 | cap = "0.1.2" 56 | indicatif = "0.17" 57 | uuid = {version="1.6", features=["serde", "v4"]} 58 | flow-record = "0.4.9" 59 | sddl = ">=0.0.16" 60 | base64 = "0.22.1" 61 | #sddl = {path="../sddl"} 62 | 63 | [dev-dependencies] 64 | assert_cmd = "2" 65 | serde_test = "1" 66 | 67 | [build-dependencies] 68 | lazy-regex = "3" 69 | hex = ">=0.4" 70 | byteorder = "1" 71 | convert_case = "0" 72 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::io; 4 | use std::io::BufRead; 5 | use std::io::Write; 6 | use std::path::Path; 7 | use lazy_regex::regex_captures; 8 | use byteorder::BigEndian; 9 | use byteorder::ByteOrder; 10 | use convert_case::Casing; 11 | use convert_case::Case; 12 | use std::fmt::Display; 13 | use core::fmt::Formatter; 14 | 15 | struct AppId { 16 | id_name: String, 17 | numeric_id: u32, 18 | ntds_name: String 19 | } 20 | impl AppId { 21 | fn new ( 22 | id_name: String, 23 | numeric_id: u32, 24 | ntds_name: String) -> Self { 25 | Self { 26 | id_name, 27 | numeric_id, 28 | ntds_name 29 | } 30 | } 31 | fn try_from(line: &str) -> Option { 32 | if let Some((_, id_name, numeric_id, ntds_name)) = regex_captures!(r#"#define (ATT_[A-Z0-9_]+)\s+0x([0-9a-fA-F]+)\s+//\s*(\w+)"#, line) { 33 | let numeric_id = if numeric_id.len() % 2 == 1 { 34 | format!("0{numeric_id}") 35 | } else { 36 | numeric_id.to_string() 37 | }; 38 | 39 | match hex::decode(&numeric_id) { 40 | Ok(mut numeric_id) => { 41 | while numeric_id.len() < 4 { 42 | numeric_id.insert(0, 0); 43 | } 44 | Some(Self{ 45 | id_name: id_name.to_case(Case::UpperCamel), 46 | numeric_id: BigEndian::read_u32(&numeric_id), 47 | ntds_name: ntds_name.to_string(), 48 | }) 49 | } 50 | Err(why) => panic!("invalid numeric id: '{numeric_id}': {why}") 51 | } 52 | } else { 53 | None 54 | } 55 | } 56 | } 57 | 58 | impl Display for AppId { 59 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 60 | writeln!(f, r#" #[strum(serialize = "{}", to_string = "{}")]"#, self.ntds_name, self.id_name)?; 61 | writeln!(f, r#" {} = 0x{:x},"#, self.id_name, self.numeric_id)?; 62 | Ok(()) 63 | } 64 | } 65 | 66 | fn main() { 67 | 68 | let out_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); 69 | let reader = io::BufReader::new(fs::File::open("misc/attids.h").unwrap()); 70 | let mut out_file = io::BufWriter::new(fs::File::create(Path::new(&out_dir).join("src").join("ntds").join("attribute_id.rs")).unwrap()); 71 | 72 | writeln!(out_file, "use strum::{{EnumString, IntoStaticStr}};\n").unwrap(); 73 | writeln!(out_file, "use serde::{{Serialize, Deserialize}};\n").unwrap(); 74 | writeln!(out_file, "#[derive(IntoStaticStr, EnumString, Debug, Eq, PartialEq, Hash, Clone, Copy, Ord, PartialOrd, Serialize, Deserialize)]").unwrap(); 75 | writeln!(out_file, "#[strum(use_phf)]").unwrap(); 76 | writeln!(out_file, "pub enum NtdsAttributeId {{").unwrap(); 77 | 78 | for line in reader.lines() { 79 | if let Some(app_id) = AppId::try_from(&line.unwrap()) { 80 | write!(out_file, "{app_id}").unwrap(); 81 | } 82 | } 83 | 84 | write!(out_file, "{}", AppId::new("DsRecordId".to_owned(), 0x7fffff01, "DNT_col".to_owned())).unwrap(); 85 | write!(out_file, "{}", AppId::new("DsParentRecordId".to_owned(), 0x7fffff02, "PDNT_col".to_owned())).unwrap(); 86 | write!(out_file, "{}", AppId::new("DsRecordTime".to_owned(), 0x7fffff03, "time_col".to_owned())).unwrap(); 87 | write!(out_file, "{}", AppId::new("DsAncestors".to_owned(), 0x7fffff04, "Ancestors_col".to_owned())).unwrap(); 88 | 89 | writeln!(out_file, "}}").unwrap(); 90 | 91 | println!("cargo:rerun-if-changed=build.rs"); 92 | println!("cargo:rerun-if-changed=misc/attids.h"); 93 | } -------------------------------------------------------------------------------- /doc/images/ntdsextract2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janstarke/ntdsextract2/3794134dbd5874a26c0c792961b7c66553bd139f/doc/images/ntdsextract2.jpeg -------------------------------------------------------------------------------- /doc/images/screenshot_deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janstarke/ntdsextract2/3794134dbd5874a26c0c792961b7c66553bd139f/doc/images/screenshot_deleted.png -------------------------------------------------------------------------------- /src/c_database.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::{ 4 | cache::{self, MetaDataCache}, 5 | cli::{EntryFormat, OutputOptions, TimelineFormat}, 6 | ntds::{self, Computer, DataTable, Group, LinkTable, ObjectType, Person, Schema, SdTable}, 7 | object_tree::ObjectTree, 8 | EntryId, EsedbInfo, SerializationType, 9 | }; 10 | 11 | pub struct CDatabase<'info, 'db> { 12 | _esedbinfo: &'info EsedbInfo<'db>, 13 | data_table: DataTable<'info, 'db>, 14 | link_table: Rc, 15 | _sd_table: Option>, 16 | } 17 | 18 | impl<'info, 'db> CDatabase<'info, 'db> { 19 | pub fn new(esedbinfo: &'info EsedbInfo<'db>, load_sd_table: bool) -> anyhow::Result { 20 | let cached_sd_table = cache::SdTable::try_from("sd_table", esedbinfo)?; 21 | let sd_table = if load_sd_table { 22 | match SdTable::new(&cached_sd_table) { 23 | Ok(sd_table) => Some(Rc::new(sd_table)), 24 | Err(why) => { 25 | log::warn!("Error while reading table 'sd_table': {why}. Security descriptors will not be available"); 26 | None 27 | } 28 | } 29 | } else { 30 | None 31 | }; 32 | 33 | let metadata_cache = MetaDataCache::try_from(esedbinfo)?; 34 | 35 | let object_tree = Rc::new(ObjectTree::new(&metadata_cache, sd_table.clone())); 36 | 37 | let special_records = object_tree.get_special_records()?; 38 | let schema_record_id = special_records.schema().record_ptr(); 39 | log::debug!("found the schema record id is '{}'", schema_record_id); 40 | 41 | let schema = Schema::new(&metadata_cache, &special_records); 42 | 43 | let cached_data_table = cache::DataTable::new( 44 | esedbinfo.data_table(), 45 | "datatable", 46 | esedbinfo, 47 | metadata_cache, 48 | )?; 49 | 50 | let cached_link_table = 51 | cache::LinkTable::try_from(esedbinfo.link_table(), "link_table", esedbinfo)?; 52 | 53 | let link_table = Rc::new(LinkTable::new( 54 | cached_link_table, 55 | &cached_data_table, 56 | *schema_record_id, 57 | )?); 58 | 59 | let data_table = DataTable::new( 60 | cached_data_table, 61 | object_tree, 62 | *schema_record_id, 63 | Rc::clone(&link_table), 64 | sd_table.clone(), 65 | schema, 66 | special_records, 67 | )?; 68 | 69 | Ok(Self { 70 | _esedbinfo: esedbinfo, 71 | link_table, 72 | data_table, 73 | _sd_table: sd_table, 74 | }) 75 | } 76 | 77 | pub fn show_users(&self, options: &OutputOptions) -> anyhow::Result<()> { 78 | self.show_typed_objects::>(options, ObjectType::Person) 79 | } 80 | 81 | pub fn show_groups(&self, options: &OutputOptions) -> anyhow::Result<()> { 82 | self.show_typed_objects::>(options, ObjectType::Group) 83 | } 84 | 85 | pub fn show_computers( 86 | &self, 87 | options: &OutputOptions, 88 | ) -> anyhow::Result<()> { 89 | self.show_typed_objects::>(options, ObjectType::Computer) 90 | } 91 | 92 | pub fn show_typed_objects( 93 | &self, 94 | options: &OutputOptions, 95 | object_type: ObjectType, 96 | ) -> anyhow::Result<()> { 97 | self.data_table 98 | .show_typed_objects::(options, object_type) 99 | } 100 | 101 | pub fn show_type_names(&self, options: &OutputOptions) -> anyhow::Result<()> 102 | where 103 | T: SerializationType, 104 | { 105 | self.data_table.show_type_names::(options) 106 | } 107 | 108 | pub fn show_timeline( 109 | &self, 110 | options: &OutputOptions, 111 | include_deleted: bool, 112 | format: &TimelineFormat, 113 | ) -> anyhow::Result<()> { 114 | self.data_table 115 | .show_timeline(options, &self.link_table, include_deleted, format) 116 | } 117 | 118 | pub fn show_entry( 119 | &self, 120 | entry_id: EntryId, 121 | entry_format: EntryFormat, 122 | ) -> crate::ntds::Result<()> { 123 | self.data_table.show_entry(entry_id, entry_format) 124 | } 125 | 126 | pub fn show_tree(&self, max_depth: u8) -> crate::ntds::Result<()> { 127 | self.data_table.show_tree(max_depth) 128 | } 129 | 130 | pub fn search_entries(&self, regex: &str) -> anyhow::Result<()> { 131 | self.data_table.search_entries(regex) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/cache/column/mod.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use getset::Getters; 4 | 5 | use crate::ntds::{AttributeName, NtdsAttributeId}; 6 | 7 | use super::ColumnIndex; 8 | 9 | #[derive(Getters)] 10 | #[getset(get = "pub")] 11 | pub struct Column { 12 | index: ColumnIndex, 13 | name: String, 14 | attribute_id: Option, 15 | attribute_name: Option, 16 | } 17 | 18 | impl<'a> Column { 19 | pub fn new( 20 | col: libesedb::Column<'a>, 21 | index: ColumnIndex, 22 | ) -> Result { 23 | // log::warn!("caching column {name}", name=col.name()?); 24 | let name = col.name()?; 25 | if let Ok(attribute_id) = NtdsAttributeId::from_str(&name) { 26 | let attribute_name: &str = attribute_id.into(); 27 | Ok(Self { 28 | name, 29 | index, 30 | attribute_id: Some(attribute_id), 31 | attribute_name: Some(attribute_name.to_string().into()), 32 | }) 33 | } else { 34 | Ok(Self { 35 | name, 36 | index, 37 | attribute_id: None, 38 | attribute_name: None, 39 | }) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/cache/column_index.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | #[derive(Clone, Copy, Hash, Eq, PartialEq)] 4 | pub struct ColumnIndex(i32); 5 | 6 | impl From for ColumnIndex { 7 | fn from(v: i32) -> Self { 8 | Self(v) 9 | } 10 | } 11 | 12 | impl From<&i32> for ColumnIndex { 13 | fn from(v: &i32) -> Self { 14 | Self(*v) 15 | } 16 | } 17 | 18 | impl From<&ColumnIndex> for ColumnIndex { 19 | fn from(v: &ColumnIndex) -> Self { 20 | Self(v.0) 21 | } 22 | } 23 | 24 | impl Deref for ColumnIndex { 25 | type Target=i32; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.0 29 | } 30 | } -------------------------------------------------------------------------------- /src/cache/columns_of_table.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | ops::{Deref, Index}, 4 | }; 5 | 6 | use libesedb::Table; 7 | 8 | use super::{Column, ColumnIndex}; 9 | 10 | pub struct ColumnsOfTable { 11 | ids: Vec, 12 | names: HashMap, 13 | } 14 | 15 | impl TryFrom<&Table<'_>> for ColumnsOfTable { 16 | type Error = std::io::Error; 17 | 18 | fn try_from(table: &Table) -> Result { 19 | let mut ids = Vec::new(); 20 | let mut names = HashMap::new(); 21 | for (column, index) in table.iter_columns()?.zip(0..) { 22 | let column = Column::new(column?, index.into())?; 23 | names.insert(column.name().to_owned(), index.into()); 24 | ids.push(column); 25 | } 26 | Ok(Self { ids, names }) 27 | } 28 | } 29 | 30 | impl ColumnsOfTable { 31 | pub fn iter(&self) -> impl Iterator { 32 | self.ids.iter() 33 | } 34 | } 35 | 36 | impl Index for ColumnsOfTable { 37 | type Output = Column; 38 | 39 | fn index(&self, index: ColumnIndex) -> &Self::Output { 40 | self.ids.index(*index as usize) 41 | } 42 | } 43 | 44 | impl Index<&ColumnIndex> for ColumnsOfTable { 45 | type Output = Column; 46 | 47 | fn index(&self, index: &ColumnIndex) -> &Self::Output { 48 | self.ids.index(*(index.deref()) as usize) 49 | } 50 | } 51 | 52 | impl Index<&str> for ColumnsOfTable { 53 | type Output = Column; 54 | 55 | fn index(&self, name: &str) -> &Self::Output { 56 | let index = self.names[name]; 57 | self.ids.index(*(index.deref()) as usize) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/cache/esedb_row_id.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use super::RecordPointer; 4 | 5 | #[derive(Eq, PartialEq, Hash, Copy, Clone, Default, Debug)] 6 | pub struct EsedbRowId(i32); 7 | 8 | impl From for EsedbRowId { 9 | fn from(value: i32) -> Self { 10 | Self(value) 11 | } 12 | } 13 | 14 | impl Display for EsedbRowId { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | self.0.fmt(f) 17 | } 18 | } 19 | 20 | impl EsedbRowId { 21 | pub fn inner(&self) -> i32 { 22 | self.0 23 | } 24 | 25 | pub fn step(&mut self) { 26 | self.0 += 1; 27 | } 28 | } 29 | 30 | impl From for EsedbRowId { 31 | fn from(value: RecordPointer) -> Self { 32 | *value.esedb_row() 33 | } 34 | } -------------------------------------------------------------------------------- /src/cache/meta_data_cache.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::{HashMap, HashSet}; 3 | use std::fmt::Display; 4 | use std::ops::Index; 5 | 6 | use anyhow::bail; 7 | use getset::Getters; 8 | use lazy_static::lazy_static; 9 | 10 | use crate::esedb_mitigation::libesedb_count; 11 | use crate::value::FromValue; 12 | use crate::win32_types::{Guid, Rdn, Sid}; 13 | use crate::{ntds::NtdsAttributeId, EsedbInfo}; 14 | 15 | use super::{EsedbRowId, RecordId, RecordPointer}; 16 | 17 | #[derive(Getters)] 18 | #[getset(get = "pub")] 19 | pub struct DataEntryCore { 20 | record_ptr: RecordPointer, 21 | parent: RecordId, 22 | object_category: Option, 23 | cn: Option, 24 | rdn: Rdn, 25 | sid: Option, 26 | rdn_typ_col: Option, 27 | 28 | relative_distinguished_name: Option, 29 | sam_account_name: Option, 30 | 31 | #[getset(skip)] 32 | distinguished_name: RefCell>, 33 | 34 | sd_id: Option 35 | } 36 | 37 | impl Display for DataEntryCore { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | write!(f, "{} ({})", self.rdn.name(), self.record_ptr) 40 | } 41 | } 42 | 43 | lazy_static! { 44 | static ref EMPTY_HASHSET: HashSet = HashSet::new(); 45 | } 46 | 47 | #[derive(Getters)] 48 | pub struct MetaDataCache { 49 | records: Vec, 50 | record_rows: HashMap, 51 | children_of: HashMap>, 52 | 53 | #[getset(skip)] 54 | record_by_guid: HashMap, 55 | attributes: HashMap, 56 | 57 | #[getset(get = "pub")] 58 | root: RecordPointer, 59 | } 60 | 61 | impl TryFrom<&EsedbInfo<'_>> for MetaDataCache { 62 | type Error = anyhow::Error; 63 | fn try_from(info: &EsedbInfo<'_>) -> Result { 64 | let record_id_column = NtdsAttributeId::DsRecordId.id(info); 65 | let parent_column = NtdsAttributeId::DsParentRecordId.id(info); 66 | let rdn_column = NtdsAttributeId::AttRdn.id(info); 67 | let cn_column = NtdsAttributeId::AttCommonName.id(info); 68 | let object_category_column = NtdsAttributeId::AttObjectCategory.id(info); 69 | let sid_column = NtdsAttributeId::AttObjectSid.id(info); 70 | let guid_column = NtdsAttributeId::AttObjectGuid.id(info); 71 | let rdn_att_id = NtdsAttributeId::AttRdnAttId.id(info); 72 | let attribute_id_column = NtdsAttributeId::AttAttributeId.id(info); 73 | let ldap_display_name_column = NtdsAttributeId::AttLdapDisplayName.id(info); 74 | let sam_account_name_column = NtdsAttributeId::AttSamAccountName.id(info); 75 | let sd_id_column = NtdsAttributeId::AttNtSecurityDescriptor.id(info); 76 | 77 | let mut records = Vec::new(); 78 | let mut record_rows = HashMap::new(); 79 | let mut children_of: HashMap> = HashMap::new(); 80 | let mut attributes = HashMap::new(); 81 | let mut record_by_guid = HashMap::new(); 82 | let mut root = None; 83 | //let mut root_dse = None; 84 | let count = libesedb_count(|| info.data_table().count_records())?; 85 | let bar = crate::create_progressbar( 86 | "Creating cache for record IDs".to_string(), 87 | count.try_into()?, 88 | )?; 89 | 90 | for esedb_row_id in 0..count { 91 | let record = info.data_table().record(esedb_row_id)?; 92 | 93 | if let Some(parent) = RecordId::from_record_opt(&record, parent_column)? { 94 | if let Some(record_id) = RecordId::from_record_opt(&record, record_id_column)? { 95 | if let Some(rdn) = Rdn::from_record_opt(&record, rdn_column)? { 96 | let cn = Rdn::from_record_opt(&record, cn_column)?; 97 | let object_category = 98 | RecordId::from_record_opt(&record, object_category_column)?; 99 | let sid = Sid::from_record_opt(&record, sid_column).unwrap_or(None); 100 | let guid = Guid::from_record_opt(&record, guid_column)?; 101 | 102 | let sd_id = i64::from_record_opt(&record, sd_id_column)?; 103 | 104 | let sam_account_name = match String::from_record_opt( 105 | &record, 106 | sam_account_name_column, 107 | ) { 108 | Ok(v) => v, 109 | Err(why) => { 110 | let id: &'static str = NtdsAttributeId::AttSamAccountName.into(); 111 | log::error!( 112 | "error while reading samAccountName from column {id}: {why}" 113 | ); 114 | None 115 | } 116 | }; 117 | 118 | if let Some(attribute_id) = 119 | i32::from_record_opt(&record, attribute_id_column)? 120 | { 121 | if let Some(ldap_display_name) = 122 | String::from_record_opt(&record, ldap_display_name_column)? 123 | { 124 | if let std::collections::hash_map::Entry::Vacant(e) = 125 | attributes.entry(attribute_id) 126 | { 127 | e.insert(ldap_display_name); 128 | } else { 129 | bail!("unambigious attribute id: {attribute_id} in {record_id}") 130 | } 131 | } 132 | } 133 | 134 | let rdn_typ_col = i32::from_record_opt(&record, rdn_att_id)?; 135 | let rdn_val_col = match rdn_typ_col { 136 | Some(id) => { 137 | let column_name = format!("ATTm{id}"); 138 | match info.mapping().info_by_name(&column_name[..]) { 139 | Some(id) => *id.id(), 140 | None => { 141 | log::error!("invalid column name: '{column_name}', using 'cn' instead"); 142 | *cn_column 143 | } 144 | } 145 | } 146 | None => *cn_column, 147 | }; 148 | let relative_distinguished_name = 149 | Rdn::from_record_opt(&record, &rdn_val_col)?; 150 | 151 | let record_ptr = RecordPointer::new(record_id, esedb_row_id.into()); 152 | 153 | if parent.inner() != 0 { 154 | children_of.entry(parent).or_default().insert(record_ptr); 155 | } else if root.is_some() { 156 | panic!("object without parent: '{rdn}' at '{record_ptr}"); 157 | } else { 158 | // check if this really is the root entry 159 | if rdn.name() == "$ROOT_OBJECT$" { 160 | root = Some(record_ptr); 161 | } else { 162 | log::warn!("object without parent: '{rdn}' at '{record_ptr}"); 163 | } 164 | } 165 | 166 | records.push(DataEntryCore { 167 | record_ptr, 168 | parent, 169 | rdn, 170 | cn, 171 | object_category, 172 | sid, 173 | rdn_typ_col, 174 | relative_distinguished_name, 175 | sam_account_name, 176 | sd_id, 177 | distinguished_name: RefCell::new(None), 178 | }); 179 | 180 | record_rows.insert( 181 | record_id, 182 | RecordPointer::new(record_id, esedb_row_id.into()), 183 | ); 184 | 185 | if let Some(guid) = guid { 186 | record_by_guid 187 | .insert(guid, RecordPointer::new(record_id, esedb_row_id.into())); 188 | } 189 | } else { 190 | log::warn!( 191 | "ignoring entry in row {esedb_row_id}: attribute {} (RDN) has no value", 192 | Into::<&str>::into(NtdsAttributeId::AttRdn) 193 | ) 194 | } 195 | } else { 196 | log::warn!( 197 | "ignoring entry in row {esedb_row_id}: attribute {} (RecordID) has no value", 198 | Into::<&str>::into(NtdsAttributeId::DsRecordId) 199 | ) 200 | } 201 | } else { 202 | log::warn!( 203 | "ignoring entry in row {esedb_row_id}: attribute {} (ParentRecordId) has no value", 204 | Into::<&str>::into(NtdsAttributeId::DsParentRecordId) 205 | ) 206 | } 207 | 208 | bar.inc(1); 209 | } 210 | bar.finish_and_clear(); 211 | 212 | Ok(Self { 213 | records, 214 | record_rows, 215 | children_of, 216 | attributes, 217 | record_by_guid, 218 | root: root.expect("no root object found"), 219 | }) 220 | } 221 | } 222 | 223 | impl Index<&EsedbRowId> for MetaDataCache { 224 | type Output = DataEntryCore; 225 | 226 | fn index(&self, index: &EsedbRowId) -> &Self::Output { 227 | &self.records[index.inner() as usize] 228 | } 229 | } 230 | 231 | impl Index<&RecordPointer> for MetaDataCache { 232 | type Output = DataEntryCore; 233 | 234 | fn index(&self, index: &RecordPointer) -> &Self::Output { 235 | &self[index.esedb_row()] 236 | } 237 | } 238 | 239 | impl MetaDataCache { 240 | pub fn iter(&self) -> impl Iterator { 241 | self.records.iter() 242 | } 243 | 244 | pub fn children_of(&self, parent: &RecordPointer) -> impl Iterator { 245 | self.children_ptr_of(parent) 246 | .map(|ptr| &self[ptr.esedb_row()]) 247 | } 248 | 249 | pub fn children_ptr_of(&self, parent: &RecordPointer) -> impl Iterator { 250 | self.children_of 251 | .get(parent.ds_record_id()) 252 | .unwrap_or(&EMPTY_HASHSET) 253 | .iter() 254 | } 255 | 256 | pub fn entries_with_rid(&self, rid: u32) -> impl Iterator + '_ { 257 | self.records.iter().filter(move |r| match r.sid() { 258 | Some(sid) => sid.get_rid() == &rid, 259 | _ => false, 260 | }) 261 | } 262 | 263 | pub fn entries_of_type(&self, ot: &RecordId) -> impl Iterator + '_ { 264 | let ot = *ot; 265 | self.records 266 | .iter() 267 | .filter(move |r| match r.object_category() { 268 | Some(oc) => *oc == ot, 269 | _ => false, 270 | }) 271 | } 272 | 273 | pub fn entries_of_types( 274 | &self, 275 | ot: HashSet, 276 | ) -> impl Iterator + '_ { 277 | self.records 278 | .iter() 279 | .filter(move |r| match r.object_category() { 280 | Some(oc) => ot.contains(oc), 281 | _ => false, 282 | }) 283 | } 284 | 285 | pub fn entries_with_deleted_from_container_guid(&self) -> impl Iterator { 286 | self.records 287 | .iter() 288 | .filter(|r| r.rdn().deleted_from_container().is_some()) 289 | .map(|d| &d.record_ptr) 290 | } 291 | 292 | pub fn ptr_from_row(&self, row: &EsedbRowId) -> &RecordPointer { 293 | self[row].record_ptr() 294 | } 295 | 296 | pub fn ptr_from_id(&self, id: &RecordId) -> Option<&RecordPointer> { 297 | self.record_rows.get(id) 298 | } 299 | 300 | pub fn record(&self, index: &RecordId) -> Option<&DataEntryCore> { 301 | match self.record_rows.get(index) { 302 | Some(ptr) => self.records.get(ptr.esedb_row().inner() as usize), 303 | None => None, 304 | } 305 | } 306 | 307 | pub fn ptr_from_guid(&self, guid: &Guid) -> Option<&RecordPointer> { 308 | self.record_by_guid.get(guid) 309 | } 310 | 311 | pub fn rdn(&self, entry: &DataEntryCore) -> String { 312 | if let Some(type_entry_id) = entry.object_category() { 313 | if let Some(type_entry) = self.record(type_entry_id) { 314 | if let Some(rdn_att_id) = type_entry.rdn_typ_col() { 315 | if let Some(ldap_display_name) = self.attributes.get(rdn_att_id) { 316 | return format!("{ldap_display_name}={}", entry.rdn().name()); 317 | } else { 318 | log::warn!("no record entry found for attribute id {rdn_att_id}; using 'cn' as rdn attribute"); 319 | } 320 | } else { 321 | log::warn!( 322 | "no attribute id found for {entry} (object category is {type_entry}); using 'cn' as rdn attribute" 323 | ); 324 | } 325 | } else { 326 | log::warn!("invalid object category for {entry}: {type_entry_id}; using 'cn' as rdn attribute"); 327 | } 328 | } else { 329 | log::warn!("no object category for {entry}; using 'cn' as rdn attribute"); 330 | } 331 | 332 | format!("cn={}", entry.cn().as_ref().unwrap_or(entry.rdn()).name()) 333 | } 334 | 335 | pub fn dn(&self, entry: &DataEntryCore) -> Option { 336 | if entry.parent.inner() == 0 { 337 | None 338 | } else if let Some(dn) = entry.distinguished_name.borrow().as_ref() { 339 | Some(dn.to_string()) 340 | } else { 341 | let rdn = self.rdn(entry); 342 | match self.dn(self 343 | .record(&entry.parent) 344 | .expect("invalid parent reference")) 345 | { 346 | Some(parent_dn) => Some(format!("{rdn},{parent_dn}")), 347 | None => Some(rdn), 348 | } 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | mod record; 2 | mod table; 3 | mod column; 4 | mod value; 5 | mod column_index; 6 | mod record_pointer; 7 | mod record_id; 8 | mod esedb_row_id; 9 | mod meta_data_cache; 10 | mod columns_of_table; 11 | 12 | pub use record::*; 13 | pub use table::*; 14 | pub use column::*; 15 | pub use value::*; 16 | pub use column_index::*; 17 | pub use record_pointer::*; 18 | pub use record_id::*; 19 | pub use esedb_row_id::*; 20 | pub use meta_data_cache::*; 21 | pub use columns_of_table::*; -------------------------------------------------------------------------------- /src/cache/record/mod.rs: -------------------------------------------------------------------------------- 1 | mod with_value; 2 | 3 | pub use with_value::*; 4 | 5 | use std::cell::RefCell; 6 | use std::collections::hash_map::Entry; 7 | use std::collections::HashMap; 8 | use std::hash::Hash; 9 | use std::ops::Index; 10 | use std::rc::Rc; 11 | 12 | use getset::Getters; 13 | 14 | use crate::cache::{ColumnIndex, Value}; 15 | use crate::esedb_mitigation::libesedb_count; 16 | use crate::ntds::NtdsAttributeId; 17 | use crate::EsedbInfo; 18 | 19 | use super::{ColumnsOfTable, EsedbRowId}; 20 | 21 | #[derive(Getters)] 22 | #[getset(get = "pub")] 23 | pub struct Record<'info, 'db> { 24 | table_id: &'static str, 25 | row_id: EsedbRowId, 26 | 27 | #[getset(skip)] 28 | values: RefCell>>, 29 | 30 | count: i32, 31 | record: libesedb::Record<'db>, 32 | esedbinfo: &'info EsedbInfo<'db>, 33 | 34 | // this is needed for `::all_attributes` 35 | columns: Rc, 36 | } 37 | 38 | impl Eq for Record<'_, '_> {} 39 | 40 | impl PartialEq for Record<'_, '_> { 41 | fn eq(&self, other: &Self) -> bool { 42 | self.row_id == other.row_id && self.table_id == other.table_id 43 | } 44 | } 45 | 46 | impl Hash for Record<'_, '_> { 47 | fn hash(&self, state: &mut H) { 48 | self.table_id.hash(state); 49 | self.row_id.hash(state); 50 | } 51 | } 52 | 53 | impl<'info, 'db> WithValue for Record<'info, 'db> { 54 | fn with_value( 55 | &self, 56 | attribute_id: NtdsAttributeId, 57 | function: impl FnMut(Option<&Value>) -> crate::ntds::Result, 58 | ) -> crate::ntds::Result { 59 | let column_id = *self.esedbinfo().mapping().index(attribute_id).id(); 60 | self.with_value(column_id, function) 61 | } 62 | } 63 | 64 | impl<'info, 'db> WithValue for Record<'info, 'db> { 65 | fn with_value( 66 | &self, 67 | index: ColumnIndex, 68 | mut function: impl FnMut(Option<&Value>) -> crate::ntds::Result, 69 | ) -> crate::ntds::Result { 70 | match self.values.borrow_mut().entry(index) { 71 | Entry::Occupied(e) => function(e.get().as_ref()), 72 | Entry::Vacant(e) => match self.record.value(*index) { 73 | Ok(v) => function( 74 | e.insert(match v { 75 | libesedb::Value::Null(()) => None, 76 | libesedb::Value::Long => { 77 | let x = self.record.long(*index)?; 78 | Some(Value::Long(Box::new(x.vec()?))) 79 | } 80 | libesedb::Value::Multi => { 81 | let v = self.record.multi(*index)?.variant(); 82 | Some(v.into()) 83 | } 84 | v => Some(v.into()), 85 | }) 86 | .as_ref(), 87 | ), 88 | Err(why) => Err(why.into()), 89 | }, 90 | } 91 | } 92 | } 93 | 94 | impl<'info, 'db> Record<'info, 'db> { 95 | pub fn try_from( 96 | record: libesedb::Record<'db>, 97 | table_id: &'static str, 98 | row_id: EsedbRowId, 99 | esedbinfo: &'info EsedbInfo<'db>, 100 | columns: Rc, 101 | ) -> std::io::Result { 102 | Ok(Self { 103 | values: Default::default(), 104 | count: libesedb_count(|| record.count_values())?, 105 | record, 106 | esedbinfo, 107 | table_id, 108 | row_id, 109 | columns, 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/cache/record/with_value.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | pub trait WithValue { 4 | fn with_value( 5 | &self, 6 | index: I, 7 | function: impl FnMut(Option<&Value>) -> crate::ntds::Result, 8 | ) -> crate::ntds::Result; 9 | } 10 | -------------------------------------------------------------------------------- /src/cache/record_id.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::value::FromValue; 4 | 5 | use super::RecordPointer; 6 | 7 | #[derive(Eq, PartialEq, Hash, Copy, Clone, Debug, Default)] 8 | pub struct RecordId(i32); 9 | 10 | impl From for RecordId { 11 | fn from(value: i32) -> Self { 12 | Self(value) 13 | } 14 | } 15 | 16 | impl Display for RecordId { 17 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | self.0.fmt(f) 19 | } 20 | } 21 | 22 | impl FromValue for RecordId { 23 | fn from_value_opt(value: &super::Value) -> crate::ntds::Result> 24 | where 25 | Self: Sized { 26 | Ok(i32::from_value_opt(value)?.map(Self::from)) 27 | } 28 | } 29 | 30 | impl RecordId { 31 | pub fn inner(&self) -> i32 { 32 | self.0 33 | } 34 | } 35 | 36 | impl From for RecordId { 37 | fn from(value: RecordPointer) -> Self { 38 | *value.ds_record_id() 39 | } 40 | } -------------------------------------------------------------------------------- /src/cache/record_pointer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use core::hash::Hash; 3 | 4 | use getset::Getters; 5 | 6 | use super::{EsedbRowId, RecordId}; 7 | 8 | #[derive(Getters, Debug, Clone, Copy, Default)] 9 | #[getset(get = "pub", set = "pub")] 10 | pub struct RecordPointer { 11 | ds_record_id: RecordId, 12 | esedb_row: EsedbRowId, 13 | } 14 | 15 | impl RecordPointer { 16 | pub fn new(ds_record_id: RecordId, esedb_row: EsedbRowId) -> Self { 17 | Self { 18 | ds_record_id, 19 | esedb_row, 20 | } 21 | } 22 | } 23 | 24 | impl Display for RecordPointer { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | write!(f, "id={}/row={}", self.ds_record_id, self.esedb_row) 27 | } 28 | } 29 | 30 | impl PartialEq for RecordPointer { 31 | fn eq(&self, other: &Self) -> bool { 32 | if self.ds_record_id == other.ds_record_id { 33 | true 34 | } else { 35 | self.esedb_row == other.esedb_row 36 | } 37 | } 38 | } 39 | 40 | impl Eq for RecordPointer {} 41 | 42 | impl Hash for RecordPointer { 43 | fn hash(&self, state: &mut H) { 44 | self.ds_record_id.hash(state); 45 | self.esedb_row.hash(state); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/cache/table/data_table.rs: -------------------------------------------------------------------------------- 1 | use getset::Getters; 2 | 3 | use std::rc::Rc; 4 | 5 | use crate::{ 6 | cache::{self, ColumnsOfTable, MetaDataCache}, 7 | esedb_mitigation::libesedb_count, 8 | ntds::DataTableRecord, 9 | object_tree_entry::ObjectTreeEntry, 10 | EsedbInfo, 11 | }; 12 | 13 | use super::RecordPointer; 14 | 15 | #[derive(Getters)] 16 | #[getset(get = "pub")] 17 | pub struct DataTable<'info, 'db> 18 | where 19 | 'info: 'db, 20 | { 21 | table_id: &'static str, 22 | 23 | table: &'info libesedb::Table<'db>, 24 | 25 | esedbinfo: &'info EsedbInfo<'db>, 26 | metadata: MetaDataCache, 27 | 28 | number_of_records: i32, 29 | 30 | // this is needed for `::all_attributes` 31 | columns: Rc, 32 | } 33 | 34 | impl<'info, 'db> DataTable<'info, 'db> 35 | where 36 | 'info: 'db, 37 | { 38 | pub fn new( 39 | table: &'info libesedb::Table<'db>, 40 | table_id: &'static str, 41 | esedbinfo: &'info EsedbInfo<'db>, 42 | metadata: MetaDataCache, 43 | ) -> std::io::Result { 44 | Ok(Self { 45 | table, 46 | table_id, 47 | esedbinfo, 48 | metadata, 49 | number_of_records: libesedb_count(|| table.count_records())?, 50 | columns: Rc::new(ColumnsOfTable::try_from(table)?), 51 | }) 52 | } 53 | } 54 | 55 | impl<'info, 'db> DataTable<'info, 'db> { 56 | pub fn iter(&'db self) -> impl Iterator> { 57 | self.metadata 58 | .iter() 59 | .map(|e| self.data_table_record_from(*e.record_ptr())) 60 | .filter_map(Result::ok) 61 | } 62 | 63 | pub fn data_table_record_from( 64 | &self, 65 | ptr: RecordPointer, 66 | ) -> std::io::Result> { 67 | Ok(DataTableRecord::new( 68 | cache::Record::try_from( 69 | self.table.record(ptr.esedb_row().inner())?, 70 | self.table_id, 71 | *ptr.esedb_row(), 72 | self.esedbinfo, 73 | Rc::clone(&self.columns), 74 | )?, 75 | ptr, 76 | )) 77 | } 78 | pub fn path_to_str(&self, path: &[Rc]) -> String { 79 | let v: Vec<_> = path.iter().map(|e| e.name().to_string()).collect(); 80 | v.join(",") 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/cache/table/link_table.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use getset::Getters; 4 | 5 | use crate::{ 6 | cache::{self, ColumnIndex, ColumnsOfTable}, 7 | EsedbInfo, 8 | }; 9 | 10 | #[derive(Getters)] 11 | pub struct LinkTable<'info, 'db> 12 | where 13 | 'info: 'db, 14 | { 15 | _table_id: &'static str, 16 | _table: &'info libesedb::Table<'db>, 17 | esedbinfo: &'info EsedbInfo<'db>, 18 | 19 | #[getset(get = "pub")] 20 | link_dnt_id: ColumnIndex, 21 | 22 | #[getset(get = "pub")] 23 | backlink_dnt_id: ColumnIndex, 24 | 25 | #[getset(get = "pub")] 26 | link_base_id: ColumnIndex, 27 | 28 | // this is needed for `::all_atributes` 29 | columns: Rc, 30 | } 31 | 32 | impl<'info, 'db> LinkTable<'info, 'db> 33 | where 34 | 'info: 'db, 35 | { 36 | pub fn try_from( 37 | table: &'info libesedb::Table<'db>, 38 | table_id: &'static str, 39 | esedbinfo: &'info EsedbInfo<'db>, 40 | ) -> std::io::Result { 41 | let columns = ColumnsOfTable::try_from(table)?; 42 | 43 | Ok(Self { 44 | _table: table, 45 | _table_id: table_id, 46 | esedbinfo, 47 | link_dnt_id: *columns["link_DNT"].index(), 48 | backlink_dnt_id: *columns["backlink_DNT"].index(), 49 | link_base_id: *columns["link_base"].index(), 50 | columns: Rc::new(ColumnsOfTable::try_from(table)?) 51 | }) 52 | } 53 | } 54 | 55 | impl<'info, 'db> LinkTable<'info, 'db> { 56 | pub fn iter(&'db self) -> impl Iterator> { 57 | self._table 58 | .iter_records() 59 | .unwrap() 60 | .map(|r| r.unwrap()) 61 | .zip(0..) 62 | .map(|(r, row)| { 63 | cache::Record::try_from( 64 | r, 65 | self._table_id, 66 | row.into(), 67 | self.esedbinfo, 68 | Rc::clone(&self.columns), 69 | ) 70 | .unwrap() 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/cache/table/mod.rs: -------------------------------------------------------------------------------- 1 | mod special_records; 2 | mod link_table; 3 | mod data_table; 4 | mod sd_table; 5 | 6 | pub use special_records::*; 7 | pub use link_table::*; 8 | pub use data_table::*; 9 | pub use sd_table::*; 10 | 11 | use super::RecordPointer; 12 | -------------------------------------------------------------------------------- /src/cache/table/sd_table.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use getset::Getters; 4 | 5 | use crate::{ 6 | cache::{self, ColumnIndex, ColumnsOfTable}, 7 | EsedbInfo, 8 | }; 9 | 10 | #[derive(Getters)] 11 | pub struct SdTable<'info, 'db> 12 | where 13 | 'info: 'db, 14 | { 15 | _table_id: &'static str, 16 | _table: &'info libesedb::Table<'db>, 17 | esedbinfo: &'info EsedbInfo<'db>, 18 | 19 | #[getset(get = "pub")] 20 | sd_id_column: ColumnIndex, 21 | 22 | #[getset(get = "pub")] 23 | sd_hash_column: ColumnIndex, 24 | 25 | #[getset(get = "pub")] 26 | sd_refcount_column: ColumnIndex, 27 | 28 | #[getset(get = "pub")] 29 | sd_value_column: ColumnIndex, 30 | 31 | // this is needed for `::all_atributes` 32 | columns: Rc, 33 | } 34 | 35 | impl<'info, 'db> SdTable<'info, 'db> 36 | where 37 | 'info: 'db, 38 | { 39 | pub fn try_from( 40 | table_id: &'static str, 41 | esedbinfo: &'info EsedbInfo<'db>, 42 | ) -> std::io::Result { 43 | let table = esedbinfo.sd_table(); 44 | let columns = ColumnsOfTable::try_from(table)?; 45 | 46 | Ok(Self { 47 | _table: table, 48 | _table_id: table_id, 49 | esedbinfo, 50 | sd_id_column: *columns["sd_id"].index(), 51 | sd_hash_column: *columns["sd_hash"].index(), 52 | sd_refcount_column: *columns["sd_refcount"].index(), 53 | sd_value_column: *columns["sd_value"].index(), 54 | columns: Rc::new(ColumnsOfTable::try_from(table)?) 55 | }) 56 | } 57 | } 58 | 59 | impl<'info, 'db> SdTable<'info, 'db> { 60 | pub fn iter(&'db self) -> impl Iterator> { 61 | self._table 62 | .iter_records() 63 | .unwrap() 64 | .map(|r| r.unwrap()) 65 | .zip(0..) 66 | .map(|(r, row)| { 67 | cache::Record::try_from( 68 | r, 69 | self._table_id, 70 | row.into(), 71 | self.esedbinfo, 72 | Rc::clone(&self.columns), 73 | ) 74 | .unwrap() 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/cache/table/special_records.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use getset::Getters; 4 | 5 | use crate::object_tree_entry::ObjectTreeEntry; 6 | 7 | #[derive(Getters)] 8 | #[getset(get = "pub", set = "pub")] 9 | pub struct SpecialRecords { 10 | schema: Rc, 11 | deleted_objects: Rc, 12 | } 13 | 14 | impl SpecialRecords { 15 | pub fn new( 16 | schema: Rc, 17 | deleted_objects: Rc, 18 | ) -> Self { 19 | Self { 20 | schema, 21 | deleted_objects, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/cache/value.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | #[derive(PartialEq)] 4 | pub enum Value { 5 | Null(()), 6 | Bool(bool), 7 | U8(u8), 8 | I16(i16), 9 | I32(i32), 10 | Currency(i64), 11 | F32(f32), 12 | F64(f64), 13 | DateTime(u64), 14 | Binary(Box>), 15 | Text(Box), 16 | LargeBinary(Box>), 17 | LargeText(Box), 18 | SuperLarge(Box>), 19 | U32(u32), 20 | I64(i64), 21 | Guid(Box>), 22 | U16(u16), 23 | Long(Box>), 24 | Multi(Vec) 25 | } 26 | 27 | impl Eq for Value { 28 | 29 | } 30 | 31 | impl From for Value { 32 | fn from(value: libesedb::Value) -> Self { 33 | match value { 34 | libesedb::Value::Null(_) => Self::Null(()), 35 | libesedb::Value::Bool(v) => Self::Bool(v), 36 | libesedb::Value::U8(v) => Self::U8(v), 37 | libesedb::Value::I16(v) => Self::I16(v), 38 | libesedb::Value::I32(v) => Self::I32(v), 39 | libesedb::Value::Currency(v) => Self::Currency(v), 40 | libesedb::Value::F32(v) => Self::F32(v), 41 | libesedb::Value::F64(v) => Self::F64(v), 42 | libesedb::Value::DateTime(v) => Self::DateTime(v), 43 | libesedb::Value::Binary(v) => Self::Binary(Box::new(Vec::from(&v[..]))), 44 | libesedb::Value::Text(v) => Self::Text(Box::new(v)), 45 | libesedb::Value::LargeBinary(v) => Self::LargeBinary(Box::new(Vec::from(&v[..]))), 46 | libesedb::Value::LargeText(v) => Self::LargeText(Box::new(v)), 47 | libesedb::Value::SuperLarge(v) => Self::SuperLarge(Box::new(Vec::from(&v[..]))), 48 | libesedb::Value::U32(v) => Self::U32(v), 49 | libesedb::Value::I64(v) => Self::I64(v), 50 | libesedb::Value::Guid(v) => Self::Guid(Box::new(Vec::from(&v[..]))), 51 | libesedb::Value::U16(v) => Self::U16(v), 52 | v => unimplemented!("unable to convert {v:?}") 53 | } 54 | } 55 | } 56 | 57 | impl Display for Value { 58 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 59 | match self { 60 | Value::Null(_) => write!(f, "Null(())"), 61 | Value::Bool(v) => write!(f, "Bool({v})"), 62 | Value::U8(v) => write!(f, "U8({v})"), 63 | Value::I16(v) => write!(f, "I16({v})"), 64 | Value::I32(v) => write!(f, "I32({v})"), 65 | Value::Currency(v) => write!(f, "Currency({v})"), 66 | Value::F32(v) => write!(f, "F32({v})"), 67 | Value::F64(v) => write!(f, "F64({v})"), 68 | Value::DateTime(v) => write!(f, "DateTime({v})"), 69 | Value::Binary(v) => write!(f, "Binary({v:?})"), 70 | Value::Text(v) => write!(f, "Text({v})"), 71 | Value::LargeBinary(v) => write!(f, "LargeBinary({v:?})"), 72 | Value::LargeText(v) => write!(f, "LargeText({v})"), 73 | Value::SuperLarge(v) => write!(f, "SuperLarge({v:?})"), 74 | Value::U32(v) => write!(f, "U32({v})"), 75 | Value::I64(v) => write!(f, "I64({v})"), 76 | Value::Guid(v) => write!(f, "Guid({v:?})"), 77 | Value::U16(v) => write!(f, "U16({v})"), 78 | Value::Long(v) => write!(f, "Long({v:?})"), 79 | Value::Multi(_) => write!(f, "Multi"), 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/cli/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use getset::Getters; 3 | 4 | use super::Commands; 5 | 6 | 7 | #[derive(Parser, Getters)] 8 | #[getset(get="pub")] 9 | #[clap(name="ntdsextract2", author, version, about, long_about = None)] 10 | pub struct Args { 11 | #[clap(subcommand)] 12 | pub(crate) command: Commands, 13 | 14 | /// name of the file to analyze 15 | pub(crate) ntds_file: String, 16 | 17 | #[clap(flatten)] 18 | pub(crate) verbose: clap_verbosity_flag::Verbosity, 19 | } 20 | -------------------------------------------------------------------------------- /src/cli/commands.rs: -------------------------------------------------------------------------------- 1 | use clap::{Subcommand, ValueEnum}; 2 | use strum::Display; 3 | 4 | use super::{EntryFormat, MemberOfAttribute, OutputFormat}; 5 | 6 | #[derive(Subcommand)] 7 | pub enum Commands { 8 | /// Display user accounts 9 | User { 10 | /// Output format 11 | #[clap(value_enum, short('F'), long("format"), default_value_t = OutputFormat::Csv)] 12 | format: OutputFormat, 13 | 14 | /// show all non-empty values. This option is ignored when CSV-Output is selected 15 | #[clap(short('A'), long("show-all"))] 16 | show_all: bool, 17 | 18 | /// include the distinguished name (DN) in the output. 19 | /// 20 | /// Note that this 21 | /// property is not an attribute of the AD entry iself; instead it is 22 | /// constructed from the relative DN (RDN) of the entry and 23 | /// all of its parents. That's why this property is normally not shown. 24 | #[clap(short('D'), long("include-dn"))] 25 | include_dn: bool, 26 | 27 | /// include the security descriptor in hte output 28 | /// 29 | /// Note the not the raw value is show. Instead, an SDDL string is shown. 30 | #[clap(short('S'), long("include-sd"))] 31 | include_sd: bool, 32 | 33 | /// specify which attribute shall be used to display group memberships 34 | #[clap(long("member-of"), default_value_t=MemberOfAttribute::Rdn)] 35 | member_of_attribute: MemberOfAttribute, 36 | }, 37 | 38 | /// Display groups 39 | Group { 40 | /// Output format 41 | #[clap(value_enum, short('F'), long("format"), default_value_t = OutputFormat::Csv)] 42 | format: OutputFormat, 43 | 44 | /// show all non-empty values. This option is ignored when CSV-Output is selected 45 | #[clap(short('A'), long("show-all"))] 46 | show_all: bool, 47 | 48 | /// include the distinguished name (DN) in the output. 49 | /// 50 | /// Note that this 51 | /// property is not an attribute of the AD entry iself; instead it is 52 | /// constructed from the relative DN (RDN) of the entry and 53 | /// all of its parents. That's why this property is normally not shown. 54 | #[clap(short('D'), long("include-dn"))] 55 | include_dn: bool, 56 | 57 | /// include the security descriptor in hte output 58 | /// 59 | /// Note the not the raw value is show. Instead, an SDDL string is shown. 60 | #[clap(short('S'), long("include-sd"))] 61 | include_sd: bool, 62 | 63 | /// specify which attribute shall be used to display group memberships 64 | #[clap(long("member-of"), default_value_t=MemberOfAttribute::Rdn)] 65 | member_of_attribute: MemberOfAttribute, 66 | }, 67 | 68 | /// display computer accounts 69 | Computer { 70 | /// Output format 71 | #[clap(value_enum, short('F'), long("format"), default_value_t = OutputFormat::Csv)] 72 | format: OutputFormat, 73 | 74 | /// show all non-empty values. This option is ignored when CSV-Output is selected 75 | #[clap(short('A'), long("show-all"))] 76 | show_all: bool, 77 | 78 | /// include the distinguished name (DN) in the output. 79 | /// 80 | /// Note that this 81 | /// property is not an attribute of the AD entry iself; instead it is 82 | /// constructed from the relative DN (RDN) of the entry and 83 | /// all of its parents. That's why this property is normally not shown. 84 | #[clap(short('D'), long("include-dn"))] 85 | include_dn: bool, 86 | 87 | /// include the security descriptor in hte output 88 | /// 89 | /// Note the not the raw value is show. Instead, an SDDL string is shown. 90 | #[clap(short('S'), long("include-sd"))] 91 | include_sd: bool, 92 | 93 | /// specify which attribute shall be used to display group memberships 94 | #[clap(long("member-of"), default_value_t=MemberOfAttribute::Rdn)] 95 | member_of_attribute: MemberOfAttribute, 96 | }, 97 | 98 | /// create a timeline (in flow-record format) 99 | Timeline { 100 | /// show objects of any type (this might be a lot) 101 | #[clap(long("all-objects"))] 102 | all_objects: bool, 103 | 104 | /// include also deleted objects (which don't have an AttObjectCategory attribute) 105 | #[clap(long("include-deleted"))] 106 | include_deleted: bool, 107 | 108 | /// output format 109 | #[clap(short('F'), long("format"), default_value_t=TimelineFormat::Record)] 110 | format: TimelineFormat, 111 | }, 112 | 113 | /// list all defined types 114 | Types { 115 | /// Output format 116 | #[clap(value_enum, short('F'), long("format"), default_value_t = OutputFormat::Csv)] 117 | format: OutputFormat, 118 | }, 119 | 120 | /// display the directory information tree 121 | Tree { 122 | /// maximum recursion depth 123 | #[clap(long("max-depth"), default_value_t = 4)] 124 | max_depth: u8, 125 | }, 126 | 127 | /// display one single entry from the directory information tree 128 | Entry { 129 | /// id of the entry to show 130 | entry_id: i32, 131 | 132 | /// search for SID instead for NTDS.DIT entry id. 133 | /// will be interpreted as RID, wich is the last part of the SID; 134 | /// e.g. 500 will return the Administrator account 135 | #[clap(long("sid"))] 136 | use_sid: bool, 137 | 138 | #[clap(short('F'), long("format"), default_value_t = EntryFormat::Simple)] 139 | entry_format: EntryFormat, 140 | }, 141 | 142 | /// search for entries whose values match to some regular expression 143 | Search { 144 | /// regular expression to match against 145 | regex: String, 146 | 147 | /// case-insensitive search (ignore case) 148 | #[clap(short('i'), long("ignore-case"))] 149 | ignore_case: bool, 150 | }, 151 | } 152 | 153 | impl Commands { 154 | pub fn display_all_attributes(&self) -> bool { 155 | match self { 156 | Commands::User { 157 | format: OutputFormat::Json, 158 | show_all, 159 | include_dn: _, 160 | include_sd: _, 161 | member_of_attribute: _, 162 | } 163 | | Commands::User { 164 | format: OutputFormat::JsonLines, 165 | show_all, 166 | include_dn: _, 167 | include_sd: _, 168 | member_of_attribute: _, 169 | } 170 | | Commands::Computer { 171 | format: OutputFormat::Json, 172 | show_all, 173 | include_dn: _, 174 | include_sd: _, 175 | member_of_attribute: _, 176 | } 177 | | Commands::Computer { 178 | format: OutputFormat::JsonLines, 179 | show_all, 180 | include_dn: _, 181 | include_sd: _, 182 | member_of_attribute: _, 183 | } => *show_all, 184 | _ => false, 185 | } 186 | } 187 | 188 | pub fn include_dn(&self) -> bool { 189 | match self { 190 | Commands::User { 191 | format: _, 192 | show_all: _, 193 | include_dn, 194 | include_sd: _, 195 | member_of_attribute: _, 196 | } 197 | | Commands::Group { 198 | format: _, 199 | show_all: _, 200 | include_dn, 201 | include_sd: _, 202 | member_of_attribute: _, 203 | } => *include_dn, 204 | Commands::Computer { 205 | format: _, 206 | show_all: _, 207 | include_dn, 208 | include_sd: _, 209 | member_of_attribute: _, 210 | } => *include_dn, 211 | _ => false, 212 | } 213 | } 214 | 215 | pub fn include_security_descriptor(&self) -> bool { 216 | match self { 217 | Commands::User { 218 | format: _, 219 | show_all: _, 220 | include_dn: _, 221 | include_sd, 222 | member_of_attribute: _, 223 | } 224 | | Commands::Group { 225 | format: _, 226 | show_all: _, 227 | include_dn: _, 228 | include_sd, 229 | member_of_attribute: _, 230 | } => *include_sd, 231 | Commands::Computer { 232 | format: _, 233 | show_all: _, 234 | include_dn: _, 235 | include_sd, 236 | member_of_attribute: _, 237 | } => *include_sd, 238 | _ => false, 239 | } 240 | } 241 | 242 | pub fn member_of_attribute(&self) -> MemberOfAttribute { 243 | match self { 244 | Commands::User { 245 | format: _, 246 | show_all: _, 247 | include_dn: _, 248 | include_sd: _, 249 | member_of_attribute, 250 | } => *member_of_attribute, 251 | Commands::Group { 252 | format: _, 253 | show_all: _, 254 | include_dn: _, 255 | include_sd: _, 256 | member_of_attribute, 257 | } => *member_of_attribute, 258 | Commands::Computer { 259 | format: _, 260 | show_all: _, 261 | include_dn: _, 262 | include_sd: _, 263 | member_of_attribute, 264 | } => *member_of_attribute, 265 | _ => MemberOfAttribute::Rdn, 266 | } 267 | } 268 | 269 | pub fn flat_serialization(&self) -> bool { 270 | matches!( 271 | &self, 272 | Commands::User { 273 | format: OutputFormat::Csv, 274 | .. 275 | } | Commands::Computer { 276 | format: OutputFormat::Csv, 277 | .. 278 | } | Commands::Group { 279 | format: OutputFormat::Csv, 280 | .. 281 | } | Commands::Timeline { .. } 282 | ) 283 | } 284 | 285 | pub fn format(&self) -> Option { 286 | match self { 287 | Commands::User { format, .. } => Some(*format), 288 | Commands::Group { format, .. } => Some(*format), 289 | Commands::Computer { format, .. } => Some(*format), 290 | Commands::Types { format } => Some(*format), 291 | _ => None, 292 | } 293 | } 294 | } 295 | 296 | #[derive(ValueEnum, Clone, Display)] 297 | pub enum TimelineFormat { 298 | /// bodyfile format 299 | #[strum(serialize = "bodyfile")] 300 | Bodyfile, 301 | 302 | /// flow record format () 303 | #[strum(serialize = "record")] 304 | Record, 305 | } 306 | -------------------------------------------------------------------------------- /src/cli/entry_format.rs: -------------------------------------------------------------------------------- 1 | use strum::Display; 2 | 3 | #[derive(clap::ValueEnum, Clone, Copy, Display)] 4 | pub enum EntryFormat { 5 | /// use JSON format 6 | #[strum(serialize = "json")] 7 | Json, 8 | 9 | /// display a formatted table 10 | #[strum(serialize = "table")] 11 | Table, 12 | 13 | /// use a simple key-values based format 14 | #[strum(serialize = "simple")] 15 | Simple, 16 | } 17 | -------------------------------------------------------------------------------- /src/cli/member_of_attribute.rs: -------------------------------------------------------------------------------- 1 | use strum::Display; 2 | 3 | #[derive(clap::ValueEnum, Clone, Copy, Display, Hash, Eq, PartialEq)] 4 | pub enum MemberOfAttribute { 5 | /// show the Security ID (SID) 6 | #[strum(serialize = "sid")] 7 | Sid, 8 | 9 | /// show the relative distinguished name (RDN) value 10 | #[strum(serialize = "rdn")] 11 | Rdn, 12 | 13 | /// show the distinguished name (DN) 14 | #[strum(serialize = "dn")] 15 | Dn, 16 | 17 | /// show the samAccountName attribute 18 | #[strum(serialize = "sam")] 19 | #[clap(name="sam")] 20 | SamAccountName, 21 | } -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | mod commands; 2 | mod args; 3 | mod output_format; 4 | mod output_options; 5 | mod entry_format; 6 | pub mod output; 7 | mod member_of_attribute; 8 | 9 | pub use commands::*; 10 | pub use args::*; 11 | pub use output_format::*; 12 | pub use output_options::*; 13 | pub use entry_format::*; 14 | pub use member_of_attribute::*; -------------------------------------------------------------------------------- /src/cli/output/csv_writer.rs: -------------------------------------------------------------------------------- 1 | use super::Writer; 2 | 3 | #[derive(Default)] 4 | pub struct CsvWriter; 5 | 6 | impl Writer for CsvWriter { 7 | fn write_typenames(&self, names: I) -> anyhow::Result<()> 8 | where 9 | I: Iterator, 10 | { 11 | let mut csv_wtr = csv::Writer::from_writer(std::io::stdout()); 12 | anyhow::Result::from_iter( 13 | names.map(|name| csv_wtr.serialize(name).map_err(|why| anyhow::anyhow!(why))), 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/cli/output/json_writer.rs: -------------------------------------------------------------------------------- 1 | use super::Writer; 2 | 3 | #[derive(Default)] 4 | pub struct JsonWriter; 5 | 6 | impl Writer for JsonWriter { 7 | fn write_typenames(&self, names: I) -> anyhow::Result<()> 8 | where 9 | I: Iterator, 10 | { 11 | anyhow::Result::from_iter(names.map(|name| { 12 | serde_json::to_string_pretty(&name) 13 | .map_err(|why| anyhow::anyhow!(why)) 14 | .map(|json_string| { 15 | println!("{json_string}"); 16 | }) 17 | })) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cli/output/jsonlines_writer.rs: -------------------------------------------------------------------------------- 1 | use super::Writer; 2 | 3 | #[derive(Default)] 4 | pub struct JsonLinesWriter; 5 | 6 | impl Writer for JsonLinesWriter { 7 | fn write_typenames(&self, names: I) -> anyhow::Result<()> 8 | where 9 | I: Iterator, 10 | { 11 | anyhow::Result::from_iter(names.map(|name| { 12 | serde_json::to_string(&name) 13 | .map_err(|why| anyhow::anyhow!(why)) 14 | .map(|json_string| { 15 | println!("{json_string}"); 16 | }) 17 | })) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cli/output/mod.rs: -------------------------------------------------------------------------------- 1 | mod writer; 2 | mod csv_writer; 3 | mod json_writer; 4 | mod jsonlines_writer; 5 | 6 | pub use writer::*; 7 | pub use csv_writer::*; 8 | pub use json_writer::*; 9 | pub use jsonlines_writer::*; -------------------------------------------------------------------------------- /src/cli/output/writer.rs: -------------------------------------------------------------------------------- 1 | pub trait Writer { 2 | fn write_typenames(&self, names: I) -> anyhow::Result<()> 3 | where 4 | I: Iterator; 5 | } 6 | -------------------------------------------------------------------------------- /src/cli/output_format.rs: -------------------------------------------------------------------------------- 1 | use strum::Display; 2 | 3 | use crate::cli::output::{CsvWriter, JsonLinesWriter, JsonWriter, Writer}; 4 | 5 | 6 | #[derive(clap::ValueEnum, Clone, Copy, Display, Eq, PartialEq)] 7 | pub enum OutputFormat { 8 | #[strum(serialize = "csv")] 9 | Csv, 10 | 11 | #[strum(serialize = "json")] 12 | Json, 13 | 14 | #[strum(serialize = "json-lines")] 15 | JsonLines, 16 | } 17 | 18 | impl Writer for OutputFormat { 19 | fn write_typenames(&self, names: I) -> anyhow::Result<()> 20 | where 21 | I: Iterator, 22 | { 23 | match self { 24 | OutputFormat::Csv => CsvWriter.write_typenames(names), 25 | OutputFormat::Json => JsonWriter.write_typenames(names), 26 | OutputFormat::JsonLines => JsonLinesWriter.write_typenames(names), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/cli/output_options.rs: -------------------------------------------------------------------------------- 1 | use getset::{Getters, Setters}; 2 | 3 | use super::OutputFormat; 4 | 5 | #[derive(Getters, Setters, Default)] 6 | #[getset(get="pub", set="pub")] 7 | pub struct OutputOptions { 8 | flat_serialization: bool, 9 | display_all_attributes: bool, 10 | show_all_objects: bool, 11 | include_dn: bool, 12 | format: Option 13 | } -------------------------------------------------------------------------------- /src/column_info_mapping.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, ops::Index}; 2 | 3 | use crate::{ 4 | column_information::ColumnInformation, esedb_mitigation::libesedb_count, ntds::NtdsAttributeId, 5 | }; 6 | use anyhow::Result; 7 | use libesedb::Table; 8 | 9 | pub struct ColumnInfoMapping { 10 | mapping: HashMap, 11 | str_mapping: HashMap, 12 | } 13 | 14 | impl Index for ColumnInfoMapping { 15 | type Output = ColumnInformation; 16 | 17 | fn index(&self, index: NtdsAttributeId) -> &Self::Output { 18 | self.mapping.index(&index) 19 | } 20 | } 21 | 22 | impl ColumnInfoMapping { 23 | pub fn info_by_name(&self, index: &str) -> Option<&ColumnInformation> { 24 | self.str_mapping.get(index) 25 | } 26 | } 27 | 28 | impl TryFrom<&Table<'_>> for ColumnInfoMapping { 29 | type Error = anyhow::Error; 30 | fn try_from(data_table: &Table) -> Result { 31 | let mut mapping = HashMap::new(); 32 | let mut str_mapping = HashMap::new(); 33 | 34 | for index in 0..libesedb_count(|| data_table.count_columns())? { 35 | let column = data_table.column(index)?; 36 | let col_info = ColumnInformation::new(index); 37 | if let Ok(column_id) = NtdsAttributeId::try_from(&column.name()?[..]) { 38 | mapping.insert(column_id, col_info); 39 | } 40 | 41 | str_mapping.insert(column.name()?.to_string(), col_info); 42 | } 43 | 44 | Ok(Self { 45 | mapping, 46 | str_mapping, 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/column_information.rs: -------------------------------------------------------------------------------- 1 | // use libesedb::ColumnVariant; 2 | 3 | use crate::cache::ColumnIndex; 4 | 5 | #[derive(Copy, Clone, Eq, PartialEq)] 6 | pub struct ColumnInformation { 7 | id: ColumnIndex, 8 | // name: String, 9 | // variant: ColumnVariant, 10 | } 11 | 12 | impl ColumnInformation { 13 | pub fn new(id: i32, 14 | // name: String, 15 | // variant: ColumnVariant 16 | ) -> Self { 17 | Self { 18 | id: ColumnIndex::from(id), 19 | // name, 20 | // variant, 21 | } 22 | } 23 | 24 | pub fn id(&self) -> &ColumnIndex { 25 | &self.id 26 | } 27 | 28 | // pub fn name(&self) -> &str { 29 | // &self.name 30 | // } 31 | 32 | // pub fn variant(&self) -> &ColumnVariant { 33 | // &self.variant 34 | // } 35 | } -------------------------------------------------------------------------------- /src/data_table_ext.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::rc::Rc; 3 | 4 | use crate::column_info_mapping::{FormatDbRecordForCli, RecordToBodyfile}; 5 | use crate::computer::Computer; 6 | use crate::constants::*; 7 | use crate::entry_id::EntryId; 8 | use crate::esedb_utils::*; 9 | use crate::group::Group; 10 | use crate::link_table_ext::LinkTableExt; 11 | use crate::object_tree_entry::ObjectTreeEntry; 12 | use crate::{DbRecord, FromDbRecord}; 13 | use anyhow::{bail, Result}; 14 | use bodyfile::Bodyfile3Line; 15 | use libesedb::Table; 16 | use maplit::hashset; 17 | use regex::Regex; 18 | use serde::Serialize; 19 | 20 | use crate::{ 21 | column_info_mapping::ColumnInfoMapping, constants::TYPENAME_COMPUTER, person::Person, 22 | OutputFormat, 23 | }; 24 | 25 | /// wraps a ESEDB Table. 26 | /// This class assumes the a NTDS datatable is being wrapped 27 | pub(crate) struct DataTableExt<'a> { 28 | data_table: Table<'a>, 29 | link_table: LinkTableExt, 30 | mapping: ColumnInfoMapping, 31 | schema_record_id: i32, 32 | object_tree: Rc, 33 | } 34 | 35 | impl<'a> DataTableExt<'a> { 36 | /// create a new datatable wrapper 37 | pub fn from(data_table: Table<'a>, link_table: Table<'_>) -> Result { 38 | 39 | 40 | log::info!("reading schema information and creating record cache"); 41 | let mapping = ColumnInfoMapping::from(&data_table)?; 42 | 43 | let mut x = 0; 44 | log::error!("AAA"); 45 | let record_cache = 46 | iter_records(&data_table).map(|record| 47 | (record.ds_record_id(&mapping).unwrap().unwrap(), record) 48 | ).collect::>(); 49 | log::error!("BBB"); 50 | 51 | let schema_record_id = Self::get_schema_record_id(&data_table, &mapping)?; 52 | let object_tree = ObjectTreeEntry::from(&data_table, &mapping)?; 53 | 54 | 55 | let link_table: LinkTableExt = LinkTableExt::from(link_table, &data_table, &mapping, schema_record_id)?; 56 | 57 | log::debug!("found the schema record id is '{}'", schema_record_id); 58 | Ok(Self { 59 | data_table, 60 | link_table, 61 | mapping, 62 | schema_record_id, 63 | object_tree, 64 | }) 65 | } 66 | 67 | pub(crate) fn mapping(&self) -> &ColumnInfoMapping { 68 | &self.mapping 69 | } 70 | 71 | pub(crate) fn link_table(&self) -> &LinkTableExt { 72 | &self.link_table 73 | } 74 | 75 | pub(crate) fn data_table(&self) -> &Table<'a> { 76 | &self.data_table 77 | } 78 | 79 | pub(crate) fn find_children_of<'b>(data_table: &'a Table<'_>, mapping: &'a ColumnInfoMapping, parent_id: i32) -> impl Iterator> + 'b 80 | where 81 | 'a: 'b, 82 | { 83 | log::debug!("searching for children of record '{}'", parent_id); 84 | 85 | filter_records_from( 86 | data_table, 87 | move |dbrecord: &DbRecord| { 88 | dbrecord.ds_parent_record_id(mapping).unwrap().unwrap() == parent_id 89 | }, 90 | ) 91 | } 92 | 93 | fn find_children_of_int<'b>(&'a self, parent_id: i32) -> impl Iterator> + 'b 94 | where 95 | 'a: 'b, 96 | { 97 | let data_table = &self.data_table; 98 | let mapping = &self.mapping; 99 | Self::find_children_of(data_table, mapping, parent_id) 100 | } 101 | 102 | /// returns the record id of the record which contains the Schema object 103 | /// (which is identified by its name "Schema" in the object_name2 attribute) 104 | fn get_schema_record_id<'b>( 105 | data_table: &'b Table<'a>, 106 | mapping: &ColumnInfoMapping, 107 | ) -> Result 108 | where 109 | 'a: 'b, 110 | { 111 | for record in filter_records_from(data_table, |dbrecord| { 112 | "Schema" 113 | == dbrecord 114 | .ds_object_name2(mapping) 115 | .expect("unable to read object_name2 attribute") 116 | .expect("missing object_name2 attribute") 117 | }) { 118 | if let Some(schema_parent_id) = record.ds_parent_record_id(mapping)? { 119 | if let Some(schema_parent) = find_by_id(data_table, mapping, schema_parent_id) { 120 | if let Some(parent_name) = schema_parent.ds_object_name2(mapping)? { 121 | if parent_name == "Configuration" { 122 | return Ok(record 123 | .ds_record_id(mapping)? 124 | .expect("Schema record has no record ID")); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | bail!("no schema record found"); 132 | } 133 | 134 | fn find_type_record(&self, type_name: &str) -> Result> { 135 | let mut records = self.find_type_records(hashset! {type_name})?; 136 | Ok(records.remove(type_name)) 137 | } 138 | 139 | pub fn find_all_type_names(&self) -> Result> { 140 | let mut type_records = HashMap::new(); 141 | for dbrecord in self.find_children_of_int(self.schema_record_id) { 142 | let object_name2 = dbrecord 143 | .ds_object_name2(&self.mapping)? 144 | .expect("missing object_name2 attribute"); 145 | 146 | type_records.insert(dbrecord.ds_record_id(&self.mapping)?.unwrap(), object_name2); 147 | } 148 | log::info!("found all required type definitions"); 149 | Ok(type_records) 150 | } 151 | 152 | pub fn find_type_records( 153 | &self, 154 | mut type_names: HashSet<&str>, 155 | ) -> Result> { 156 | let mut type_records = HashMap::new(); 157 | let children = self.find_children_of_int(self.schema_record_id); 158 | anyhow::ensure!(children.count() > 0, "The schema record has no children"); 159 | 160 | for dbrecord in self.find_children_of_int(self.schema_record_id) { 161 | let object_name2 = dbrecord 162 | .ds_object_name2(&self.mapping)? 163 | .expect("missing object_name2 attribute"); 164 | 165 | log::trace!("found a new type definition: '{}'", object_name2); 166 | 167 | if type_names.remove(&object_name2[..]) { 168 | log::debug!("found requested type definition for '{object_name2}'"); 169 | type_records.insert(object_name2, dbrecord); 170 | } 171 | 172 | if type_names.is_empty() { 173 | break; 174 | } 175 | } 176 | log::info!("found {} type definitions", type_records.len()); 177 | Ok(type_records) 178 | } 179 | 180 | pub fn show_users(&self, format: &OutputFormat) -> Result<()> { 181 | self.show_typed_objects::(format, TYPENAME_PERSON) 182 | } 183 | 184 | pub fn show_groups(&self, format: &OutputFormat) -> Result<()> { 185 | self.show_typed_objects::(format, TYPENAME_GROUP) 186 | } 187 | 188 | pub fn show_computers(&self, format: &OutputFormat) -> Result<()> { 189 | self.show_typed_objects::(format, TYPENAME_COMPUTER) 190 | } 191 | 192 | pub fn show_type_names(&self, format: &OutputFormat) -> Result<()> { 193 | let mut type_names = HashSet::new(); 194 | for dbrecord in self.find_children_of_int(self.schema_record_id) { 195 | let object_name2 = dbrecord 196 | .ds_object_name2(&self.mapping)? 197 | .expect("missing object_name2 attribute"); 198 | 199 | type_names.insert(object_name2); 200 | 201 | if type_names.is_empty() { 202 | break; 203 | } 204 | } 205 | 206 | match format { 207 | OutputFormat::Csv => { 208 | let mut csv_wtr = csv::Writer::from_writer(std::io::stdout()); 209 | csv_wtr.serialize(type_names)? 210 | } 211 | OutputFormat::Json => { 212 | println!("{}", serde_json::to_string_pretty(&type_names)?); 213 | } 214 | OutputFormat::JsonLines => { 215 | println!("{}", serde_json::to_string(&type_names)?); 216 | } 217 | } 218 | 219 | Ok(()) 220 | } 221 | 222 | pub fn show_tree(&self, max_depth: u8) -> Result<()> { 223 | let tree = ObjectTreeEntry::to_tree(&self.object_tree, max_depth); 224 | println!("{}", tree); 225 | Ok(()) 226 | } 227 | 228 | pub fn show_entry(&self, entry_id: EntryId) -> Result<()> { 229 | let mapping = &self.mapping; 230 | 231 | let record = match entry_id { 232 | EntryId::Id(id) => find_by_id(&self.data_table, mapping, id), 233 | EntryId::Rid(rid) => find_by_rid(&self.data_table, mapping, rid) 234 | }; 235 | 236 | match record { 237 | None => println!("no matching object found"), 238 | Some(entry) => { 239 | let mut table = entry.to_table(&self.mapping); 240 | 241 | if let Some(size) = termsize::get() { 242 | let attrib_size = 20; 243 | let value_size = if size.cols > (attrib_size + 2) { 244 | size.cols - (attrib_size + 2) 245 | } else { 246 | 0 247 | }; 248 | table.set_max_column_widths(vec![ 249 | (0, attrib_size.into()), 250 | (1, value_size.into()), 251 | ]) 252 | } 253 | println!("{}", table.render()) 254 | } 255 | } 256 | Ok(()) 257 | } 258 | 259 | pub(crate) fn search_entries(&self, regex: &str) -> Result<()> { 260 | let mapping = &self.mapping; 261 | let re = Regex::new(regex)?; 262 | let mut table_columns = vec![ 263 | "DNT_col".to_owned(), 264 | "PDNT_col".to_owned(), 265 | "ATTm3".to_owned(), 266 | "ATTm589825".to_owned(), 267 | "ATTb590606".to_owned(), 268 | ]; 269 | 270 | let mut records = Vec::new(); 271 | 272 | for record in iter_records(&self.data_table) { 273 | let matching_columns = record 274 | .all_attributes(mapping) 275 | .iter() 276 | .filter(|(_, v)| re.is_match(v)) 277 | .map(|(a, v)| (a.to_owned(), v.to_owned())) 278 | .collect::>(); 279 | if !matching_columns.is_empty() { 280 | for (a, _) in matching_columns { 281 | if !table_columns.contains(&a) { 282 | table_columns.push(a); 283 | } 284 | } 285 | records.push(record); 286 | } 287 | } 288 | 289 | let mut csv_wtr = csv::Writer::from_writer(std::io::stdout()); 290 | let empty_string = "".to_owned(); 291 | csv_wtr.write_record(&table_columns)?; 292 | for record in records.into_iter() { 293 | let all_attributes = record.all_attributes(mapping); 294 | csv_wtr.write_record(table_columns.iter().map(|a| { 295 | all_attributes 296 | .get(a) 297 | .unwrap_or(&empty_string) 298 | .replace('\n', "\\n") 299 | .replace('\r', "\\r") 300 | }))?; 301 | } 302 | Ok(()) 303 | } 304 | 305 | fn show_typed_objects( 306 | &self, 307 | format: &OutputFormat, 308 | type_name: &str, 309 | ) -> Result<()> { 310 | let type_record = self 311 | .find_type_record(type_name)? 312 | .unwrap_or_else(|| panic!("missing record for type '{}'", type_name)); 313 | let type_record_id = type_record.ds_record_id(&self.mapping)?; 314 | 315 | let mut csv_wtr = csv::Writer::from_writer(std::io::stdout()); 316 | 317 | for record in iter_records(&self.data_table) 318 | .filter(|dbrecord| dbrecord.ds_object_type_id(&self.mapping).is_ok()) 319 | .filter(|dbrecord| dbrecord.ds_object_type_id(&self.mapping).unwrap() == type_record_id) 320 | .map(|dbrecord| T::from(dbrecord, self).unwrap()) 321 | { 322 | match format { 323 | OutputFormat::Csv => { 324 | csv_wtr.serialize(record)?; 325 | } 326 | OutputFormat::Json => { 327 | println!("{}", serde_json::to_string_pretty(&record)?); 328 | } 329 | OutputFormat::JsonLines => { 330 | println!("{}", serde_json::to_string(&record)?); 331 | } 332 | } 333 | } 334 | drop(csv_wtr); 335 | 336 | Ok(()) 337 | } 338 | 339 | pub fn show_timeline(&self, all_objects: bool) -> Result<()> { 340 | let type_records = self.find_type_records(hashset! { 341 | TYPENAME_PERSON, 342 | TYPENAME_COMPUTER})?; 343 | 344 | let all_type_records = self.find_all_type_names()?; 345 | 346 | let type_record_ids = if all_objects { 347 | None 348 | } else { 349 | Some( 350 | type_records 351 | .iter() 352 | .map(|(type_name, dbrecord)| { 353 | ( 354 | dbrecord 355 | .ds_record_id(&self.mapping) 356 | .expect("unable to read record id") 357 | .expect("missing record id"), 358 | type_name, 359 | ) 360 | }) 361 | .collect::>(), 362 | ) 363 | }; 364 | 365 | for bf_lines in iter_records(&self.data_table) 366 | .filter(|dbrecord| dbrecord.has_valid_ds_object_type_id(&self.mapping)) 367 | .filter_map(|dbrecord| { 368 | let current_type_id = dbrecord 369 | .ds_object_type_id(&self.mapping) 370 | .unwrap() 371 | .expect("missing object type id"); 372 | 373 | // `type_record_ids` is None if `all_objects` is True 374 | if let Some(record_ids) = type_record_ids.as_ref() { 375 | match record_ids.get(¤t_type_id) { 376 | Some(type_name) => { 377 | if *type_name == TYPENAME_PERSON { 378 | Some(Vec::::from( 379 | ::from(dbrecord, self).unwrap(), 380 | )) 381 | } else if *type_name == TYPENAME_COMPUTER { 382 | Some(Vec::::from( 383 | ::from(dbrecord, self).unwrap(), 384 | )) 385 | } else { 386 | None 387 | } 388 | } 389 | _ => None, 390 | } 391 | } else { 392 | Some( 393 | dbrecord 394 | .to_bodyfile(&self.mapping, &all_type_records[¤t_type_id][..]) 395 | .expect("unable to create timeline from DbRecord"), 396 | ) 397 | } 398 | }) 399 | .flatten() 400 | { 401 | println!("{}", bf_lines) 402 | } 403 | Ok(()) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/entry_id.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::RecordId; 2 | 3 | pub enum EntryId { 4 | Id(RecordId), 5 | Rid(u32), 6 | } -------------------------------------------------------------------------------- /src/esedb_mitigation.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | //#[deprecated(note="this function is a mitigation for some strange behaviour. see https://github.com/janstarke/ntdsextract2/pull/16")] 4 | pub(crate) fn libesedb_count(count_fn: impl Fn() -> io::Result) -> io::Result { 5 | match count_fn() { 6 | Ok(val) => Ok(val), 7 | Err(_) => count_fn() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/esedbinfo.rs: -------------------------------------------------------------------------------- 1 | use getset::Getters; 2 | use libesedb::{EseDb, Table}; 3 | use std::ops::Index; 4 | 5 | use crate::{ntds::NtdsAttributeId, ColumnInfoMapping, ColumnInformation}; 6 | 7 | #[derive(Getters)] 8 | #[getset(get="pub")] 9 | pub struct EsedbInfo<'db> { 10 | data_table: Table<'db>, 11 | link_table: Table<'db>, 12 | sd_table: Table<'db>, 13 | mapping: ColumnInfoMapping, 14 | } 15 | 16 | impl<'db> TryFrom<&'db EseDb> for EsedbInfo<'db> { 17 | type Error = anyhow::Error; 18 | 19 | fn try_from(esedb: &'db EseDb) -> Result { 20 | let data_table = esedb.table_by_name("datatable")?; 21 | let link_table = esedb.table_by_name("link_table")?; 22 | let sd_table = esedb.table_by_name("sd_table")?; 23 | let mapping = ColumnInfoMapping::try_from(&data_table)?; 24 | 25 | Ok(Self { 26 | data_table, 27 | link_table, 28 | sd_table, 29 | mapping, 30 | }) 31 | } 32 | } 33 | 34 | impl<'db> EsedbInfo<'db> { 35 | pub fn column(&self, att_id: NtdsAttributeId) -> &ColumnInformation { 36 | self.mapping().index(att_id) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/formatted_value.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | #[serde(untagged)] 7 | pub enum FormattedValue { 8 | NoValue, 9 | Hide, 10 | Value(T) 11 | } 12 | -------------------------------------------------------------------------------- /src/group.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use bodyfile::Bodyfile3Line; 4 | use serde::Serialize; 5 | 6 | use crate::column_info_mapping::{DbRecord, FromDbRecord}; 7 | use crate::{ 8 | skip_all_attributes, 9 | win32_types::{ 10 | SamAccountType, Sid, TruncatedWindowsFileTime, UserAccountControl, WindowsFileTime, 11 | }, 12 | }; 13 | use crate::{serde_flat_serialization, serialization::*, CDatabase}; 14 | use anyhow::{bail, Result}; 15 | 16 | #[derive(Serialize)] 17 | pub(crate) struct Group { 18 | sid: Option, 19 | user_principal_name: Option, 20 | sam_account_name: Option, 21 | sam_account_type: Option, 22 | user_account_control: Option, 23 | 24 | #[serde( 25 | skip_serializing_if = "serde_flat_serialization", 26 | serialize_with = "serialize_object_list" 27 | )] 28 | members: Vec, 29 | logon_count: Option, 30 | bad_pwd_count: Option, 31 | primary_group_id: Option, 32 | aduser_objects: Option, 33 | 34 | #[serde(serialize_with = "serialize_object_list")] 35 | member_of: Vec, 36 | 37 | comment: Option, 38 | 39 | #[serde(serialize_with = "to_ts")] 40 | record_time: Option, 41 | 42 | #[serde(serialize_with = "to_ts")] 43 | when_created: Option, 44 | 45 | #[serde(serialize_with = "to_ts")] 46 | when_changed: Option, 47 | 48 | #[serde(serialize_with = "to_ts")] 49 | last_logon: Option, 50 | 51 | #[serde(serialize_with = "to_ts")] 52 | last_logon_time_stamp: Option, 53 | 54 | #[serde(serialize_with = "to_ts")] 55 | account_expires: Option, 56 | 57 | #[serde(serialize_with = "to_ts")] 58 | password_last_set: Option, 59 | 60 | #[serde(serialize_with = "to_ts")] 61 | bad_pwd_time: Option, 62 | 63 | #[serde(skip_serializing_if = "skip_all_attributes")] 64 | all_attributes: HashMap, 65 | } 66 | 67 | impl FromDbRecord for Group { 68 | fn from(dbrecord: &DbRecord, database: &CDatabase) -> Result { 69 | let data_table = database.data_table(); 70 | 71 | let mapping = data_table.mapping(); 72 | let object_id = match dbrecord.ds_record_id(mapping)? { 73 | Some(id) => id, 74 | None => bail!("object has no record id"), 75 | }; 76 | let members = if let Some(children) = database.link_table().members(&object_id) { 77 | children 78 | .iter() 79 | .filter_map(|child_id| { 80 | data_table 81 | .data_table() 82 | .find_by_id(data_table.mapping(), *child_id) 83 | }) 84 | .map(|record| { 85 | record 86 | .ds_object_name2(mapping) 87 | .expect("error while reading object name") 88 | .expect("missing object name") 89 | }) 90 | .collect() 91 | } else { 92 | vec![] 93 | }; 94 | 95 | let member_of = if let Some(children) = database.link_table().member_of(&object_id) { 96 | children 97 | .iter() 98 | .filter_map(|child_id| { 99 | data_table 100 | .data_table() 101 | .find_by_id(data_table.mapping(), *child_id) 102 | }) 103 | .map(|record| { 104 | record 105 | .ds_object_name2(mapping) 106 | .expect("error while reading object name") 107 | .expect("missing object name") 108 | }) 109 | .collect() 110 | } else { 111 | vec![] 112 | }; 113 | 114 | Ok(Self { 115 | record_time: dbrecord.ds_record_time(mapping)?, 116 | when_created: dbrecord.ds_when_created(mapping)?, 117 | when_changed: dbrecord.ds_when_changed(mapping)?, 118 | sid: dbrecord.ds_sid(mapping)?, 119 | sam_account_name: dbrecord.ds_sam_account_name(mapping)?, 120 | user_principal_name: dbrecord.ds_user_principal_name(mapping)?, 121 | sam_account_type: dbrecord.ds_sam_account_type(mapping)?, 122 | members, 123 | user_account_control: dbrecord.ds_user_account_control(mapping)?, 124 | last_logon: dbrecord.ds_last_logon(mapping)?, 125 | last_logon_time_stamp: dbrecord.ds_last_logon_time_stamp(mapping)?, 126 | account_expires: dbrecord.ds_account_expires(mapping)?, 127 | password_last_set: dbrecord.ds_password_last_set(mapping)?, 128 | bad_pwd_time: dbrecord.ds_bad_pwd_time(mapping)?, 129 | logon_count: dbrecord.ds_logon_count(mapping)?, 130 | bad_pwd_count: dbrecord.ds_bad_pwd_count(mapping)?, 131 | primary_group_id: dbrecord.ds_primary_group_id(mapping)?, 132 | comment: dbrecord.ds_att_comment(mapping)?, 133 | aduser_objects: dbrecord.ds_aduser_objects(mapping)?, 134 | all_attributes: dbrecord.all_attributes(mapping), 135 | member_of, 136 | }) 137 | } 138 | } 139 | 140 | impl From for Vec { 141 | fn from(person: Group) -> Self { 142 | let mut res = Vec::new(); 143 | if let Some(upn) = person.sam_account_name { 144 | if let Some(record_time) = person.record_time { 145 | res.push( 146 | Bodyfile3Line::new() 147 | .with_owned_name(format!("{} (Person, record creation time)", upn)) 148 | .with_crtime(i64::max(0, record_time.timestamp())), 149 | ); 150 | } 151 | 152 | if let Some(when_created) = person.when_created { 153 | res.push( 154 | Bodyfile3Line::new() 155 | .with_owned_name(format!("{} (Person, object created)", upn)) 156 | .with_crtime(i64::max(0, when_created.timestamp())), 157 | ); 158 | } 159 | 160 | if let Some(when_changed) = person.when_changed { 161 | res.push( 162 | Bodyfile3Line::new() 163 | .with_owned_name(format!("{} (Person, object changed)", upn)) 164 | .with_crtime(i64::max(0, when_changed.timestamp())), 165 | ); 166 | } 167 | 168 | if let Some(last_logon) = person.last_logon { 169 | res.push( 170 | Bodyfile3Line::new() 171 | .with_owned_name(format!("{} (Person, last logon on this DC)", upn)) 172 | .with_ctime(i64::max(0, last_logon.timestamp())), 173 | ); 174 | } 175 | 176 | if let Some(last_logon_time_stamp) = person.last_logon_time_stamp { 177 | res.push( 178 | Bodyfile3Line::new() 179 | .with_owned_name(format!("{} (Person, last logon on any DC)", upn)) 180 | .with_ctime(i64::max(0, last_logon_time_stamp.timestamp())), 181 | ); 182 | } 183 | 184 | if let Some(bad_pwd_time) = person.bad_pwd_time { 185 | res.push( 186 | Bodyfile3Line::new() 187 | .with_owned_name(format!("{} (Person, bad pwd time)", upn)) 188 | .with_ctime(i64::max(0, bad_pwd_time.timestamp())), 189 | ); 190 | } 191 | 192 | if let Some(password_last_set) = person.password_last_set { 193 | res.push( 194 | Bodyfile3Line::new() 195 | .with_owned_name(format!("{} (Person, password last set)", upn)) 196 | .with_ctime(i64::max(0, password_last_set.timestamp())), 197 | ); 198 | } 199 | } 200 | res 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod c_database; 2 | mod win32_types; 3 | mod object_tree; 4 | mod object_tree_entry; 5 | mod column_info_mapping; 6 | mod column_information; 7 | mod membership_serialization; 8 | mod entry_id; 9 | mod record_predicate; 10 | mod esedbinfo; 11 | mod esedb_mitigation; 12 | mod formatted_value; 13 | pub mod cli; 14 | pub mod ntds; 15 | pub mod value; 16 | pub mod cache; 17 | mod progress_bar; 18 | pub use c_database::*; 19 | pub use column_information::*; 20 | pub use entry_id::*; 21 | pub use column_info_mapping::*; 22 | pub use record_predicate::*; 23 | pub use esedbinfo::*; 24 | pub use membership_serialization::*; 25 | use progress_bar::*; 26 | pub use formatted_value::*; -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use clap::Parser; 5 | use libesedb::EseDb; 6 | use libntdsextract2::cli::{Args, Commands, OutputOptions}; 7 | use libntdsextract2::{use_member_of_attribute, CDatabase, CsvSerialization, EntryId, EsedbInfo, JsonSerialization}; 8 | use simplelog::{Config, TermLogger}; 9 | 10 | mod progress_bar; 11 | 12 | use cap::Cap; 13 | use std::alloc; 14 | 15 | macro_rules! do_with_serialization { 16 | ($cmd: expr, $db: expr, $function: ident, $options: expr) => { 17 | if $cmd.flat_serialization() { 18 | $db.$function::($options) 19 | } else { 20 | $db.$function::($options) 21 | } 22 | }; 23 | } 24 | 25 | #[global_allocator] 26 | static ALLOCATOR: Cap = Cap::new(alloc::System, usize::MAX); 27 | 28 | fn main() -> Result<()> { 29 | ALLOCATOR.set_limit(4096 * 1024 * 1024).unwrap(); 30 | 31 | let cli = Args::parse(); 32 | let _ = TermLogger::init( 33 | cli.verbose().log_level_filter(), 34 | Config::default(), 35 | simplelog::TerminalMode::Stderr, 36 | simplelog::ColorChoice::Auto, 37 | ); 38 | 39 | let ntds_path = Path::new(cli.ntds_file()); 40 | if !(ntds_path.exists() && ntds_path.is_file()) { 41 | eprintln!("unable to open '{}'", cli.ntds_file()); 42 | std::process::exit(-1); 43 | } 44 | 45 | let esedb = EseDb::open(cli.ntds_file())?; 46 | let info = EsedbInfo::try_from(&esedb)?; 47 | let database = CDatabase::new(&info, cli.command().include_security_descriptor())?; 48 | 49 | let mut options = OutputOptions::default(); 50 | options.set_display_all_attributes(cli.command().display_all_attributes()); 51 | options.set_flat_serialization(cli.command().flat_serialization()); 52 | options.set_format(cli.command().format()); 53 | options.set_include_dn(cli.command().include_dn()); 54 | 55 | use_member_of_attribute(cli.command().member_of_attribute()); 56 | 57 | match cli.command() { 58 | Commands::Group { .. } => { 59 | do_with_serialization!(cli.command(), database, show_groups, &options) 60 | } 61 | Commands::User { .. } => { 62 | do_with_serialization!(cli.command(), database, show_users, &options) 63 | } 64 | Commands::Computer { .. } => { 65 | do_with_serialization!(cli.command(), database, show_computers, &options) 66 | } 67 | Commands::Types { .. } => { 68 | do_with_serialization!(cli.command(), database, show_type_names, &options) 69 | } 70 | Commands::Timeline { 71 | all_objects, 72 | include_deleted, 73 | format 74 | } => { 75 | options.set_show_all_objects(*all_objects); 76 | database.show_timeline(&options, *include_deleted, format) 77 | } 78 | Commands::Tree { max_depth } => Ok(database.show_tree(*max_depth)?), 79 | Commands::Entry { 80 | entry_id, 81 | use_sid, 82 | entry_format, 83 | } => { 84 | let id = if *use_sid { 85 | EntryId::Rid((*entry_id).try_into().unwrap()) 86 | } else { 87 | EntryId::Id((*entry_id).into()) 88 | }; 89 | Ok(database.show_entry(id, *entry_format)?) 90 | } 91 | Commands::Search { regex, ignore_case } => { 92 | let regex = if *ignore_case { 93 | format!("(?i:{regex})") 94 | } else { 95 | regex.to_owned() 96 | }; 97 | database.search_entries(®ex) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/membership_serialization/csv_serialization.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::{win32_types::Rdn, MembershipSet, SerializationType}; 4 | 5 | use super::Membership; 6 | 7 | pub struct CsvSerialization; 8 | 9 | impl SerializationType for CsvSerialization { 10 | fn serialize_list( 11 | items: impl Iterator>, 12 | serializer: S, 13 | ) -> Result 14 | where 15 | S: serde::Serializer, 16 | { 17 | let v = Vec::from_iter(items.map(|i| match i { 18 | Some(i) => i, 19 | None => "".to_owned(), 20 | })) 21 | .join(","); 22 | serializer.serialize_str(&v) 23 | } 24 | 25 | fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 26 | where 27 | D: serde::Deserializer<'de>, 28 | { 29 | let s = String::deserialize(deserializer)?; 30 | let mut parts = Vec::new(); 31 | for s in s.split(',') { 32 | parts.push(Membership::::from(Rdn::try_from(s).unwrap())) 33 | } 34 | Ok(MembershipSet::::from(parts.into_iter())) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/membership_serialization/json_serialization.rs: -------------------------------------------------------------------------------- 1 | use serde::{ser::SerializeSeq, Deserialize}; 2 | 3 | use crate::{win32_types::Rdn, MembershipSet, SerializationType}; 4 | 5 | use super::Membership; 6 | pub struct JsonSerialization; 7 | 8 | impl SerializationType for JsonSerialization { 9 | fn serialize_list( 10 | items: impl Iterator>, 11 | serializer: S, 12 | ) -> Result 13 | where 14 | S: serde::Serializer, 15 | { 16 | let mut ser = serializer.serialize_seq(None)?; 17 | for item in items { 18 | ser.serialize_element(&item)?; 19 | } 20 | ser.end() 21 | } 22 | 23 | fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 24 | where 25 | D: serde::Deserializer<'de>, 26 | { 27 | let v = serde_json::Value::deserialize(deserializer)?; 28 | 29 | match v { 30 | serde_json::Value::Null => Ok(vec![].into_iter().into()), 31 | serde_json::Value::Bool(b) => Ok(vec![Membership::::from( 32 | Rdn::try_from(format!("{b}")).unwrap(), 33 | )] 34 | .into_iter() 35 | .into()), 36 | serde_json::Value::Number(n) => Ok(vec![Membership::::from( 37 | Rdn::try_from(format!("{n}")).unwrap(), 38 | )] 39 | .into_iter() 40 | .into()), 41 | serde_json::Value::String(s) => { 42 | Ok(vec![Membership::::from(Rdn::try_from(s).unwrap())] 43 | .into_iter() 44 | .into()) 45 | } 46 | serde_json::Value::Array(a) => { 47 | let mut values = Vec::new(); 48 | for v in a.into_iter() { 49 | values.push(Membership::::from( 50 | Rdn::try_from(v.to_string()).unwrap(), 51 | )); 52 | } 53 | Ok(values.into_iter().into()) 54 | } 55 | serde_json::Value::Object(_) => panic!("unexpected type: object"), 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/membership_serialization/membership_set.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, sync::Mutex}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{ 6 | cache::RecordPointer, 7 | cli::MemberOfAttribute, 8 | object_tree::ObjectTree, 9 | win32_types::{Rdn, Sid}, 10 | SerializationType, 11 | }; 12 | 13 | static MEMBER_OF_ATTRIBUTE: Mutex = Mutex::new(MemberOfAttribute::Rdn); 14 | 15 | pub fn use_member_of_attribute(att: MemberOfAttribute) { 16 | *MEMBER_OF_ATTRIBUTE.lock().unwrap() = att; 17 | } 18 | 19 | pub fn member_of_attribute() -> MemberOfAttribute { 20 | *MEMBER_OF_ATTRIBUTE.lock().unwrap() 21 | } 22 | 23 | enum PointerOrString { 24 | Pointer(RecordPointer), 25 | String(String), 26 | 27 | // this can only be set when deserializing a value 28 | None, 29 | } 30 | 31 | pub struct Membership { 32 | rdn: Rdn, 33 | sid: Option, 34 | sam_account_name: Option, 35 | dn: PointerOrString, 36 | phantom: PhantomData, 37 | } 38 | 39 | impl From<(RecordPointer, Rdn, Option, Option)> for Membership 40 | where 41 | T: SerializationType, 42 | { 43 | fn from(value: (RecordPointer, Rdn, Option, Option)) -> Self { 44 | Self { 45 | rdn: value.1, 46 | sid: value.2, 47 | dn: PointerOrString::Pointer(value.0), 48 | sam_account_name: value.3, 49 | phantom: PhantomData, 50 | } 51 | } 52 | } 53 | 54 | impl From<(String, Rdn, Option, Option)> for Membership 55 | where 56 | T: SerializationType, 57 | { 58 | fn from(value: (String, Rdn, Option, Option)) -> Self { 59 | Self { 60 | rdn: value.1, 61 | sid: value.2, 62 | dn: PointerOrString::String(value.0), 63 | sam_account_name: value.3, 64 | phantom: PhantomData, 65 | } 66 | } 67 | } 68 | 69 | impl From for Membership 70 | where 71 | T: SerializationType, 72 | { 73 | fn from(rdn: Rdn) -> Self { 74 | Self { 75 | rdn, 76 | sid: None, 77 | dn: PointerOrString::None, 78 | sam_account_name: None, 79 | phantom: PhantomData, 80 | } 81 | } 82 | } 83 | 84 | impl<'de, T> Deserialize<'de> for Membership 85 | where 86 | T: SerializationType, 87 | { 88 | fn deserialize(deserializer: D) -> Result 89 | where 90 | D: serde::Deserializer<'de>, 91 | { 92 | match Rdn::deserialize(deserializer) { 93 | Ok(rdn) => Ok(Self::from(rdn)), 94 | Err(why) => Err(why), 95 | } 96 | } 97 | } 98 | 99 | impl Serialize for Membership 100 | where 101 | T: SerializationType, 102 | { 103 | fn serialize(&self, serializer: S) -> Result 104 | where 105 | S: serde::Serializer, 106 | { 107 | match member_of_attribute() { 108 | MemberOfAttribute::Sid => { 109 | T::serialize(self.sid.as_ref().map(|s| s.to_string()), serializer) 110 | } 111 | MemberOfAttribute::Rdn => T::serialize(Some(self.rdn.to_string()), serializer), 112 | MemberOfAttribute::Dn => T::serialize( 113 | match &self.dn { 114 | PointerOrString::Pointer(_ptr) => None, 115 | PointerOrString::String(dn) => Some(dn.clone()), 116 | PointerOrString::None => { 117 | panic!("it is not expected to serialize a previously deserialized value") 118 | } 119 | }, 120 | serializer, 121 | ), 122 | MemberOfAttribute::SamAccountName => { 123 | T::serialize(self.sam_account_name.clone(), serializer) 124 | } 125 | } 126 | } 127 | } 128 | 129 | pub struct MembershipSet(Vec>); 130 | 131 | impl MembershipSet { 132 | pub fn update_dn(&mut self, tree: &ObjectTree) { 133 | for m in self.0.iter_mut() { 134 | if let PointerOrString::Pointer(ptr) = m.dn { 135 | if let Some(dn) = tree.dn_of(&ptr) { 136 | m.dn = PointerOrString::String(dn); 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | impl From for MembershipSet 144 | where 145 | I: Iterator>, 146 | T: SerializationType, 147 | { 148 | fn from(iter: I) -> Self { 149 | Self(iter.collect()) 150 | } 151 | } 152 | 153 | impl Serialize for MembershipSet 154 | where 155 | T: SerializationType, 156 | { 157 | fn serialize(&self, serializer: S) -> Result 158 | where 159 | S: serde::Serializer, 160 | { 161 | match member_of_attribute() { 162 | MemberOfAttribute::Sid => T::serialize_list( 163 | self.0.iter().map(|m| m.sid.as_ref().map(|s| s.to_string())), 164 | serializer, 165 | ), 166 | MemberOfAttribute::Rdn => { 167 | T::serialize_list(self.0.iter().map(|m| Some(m.rdn.to_string())), serializer) 168 | } 169 | MemberOfAttribute::Dn => T::serialize_list( 170 | self.0.iter().map(|m| match &m.dn { 171 | PointerOrString::Pointer(ptr) => Some(format!("MISSING ENTRY FOR REFERENCE {ptr}")), 172 | PointerOrString::String(dn) => Some(dn.clone()), 173 | PointerOrString::None => { 174 | panic!("it is not expected to serialize a previously deserialized value") 175 | } 176 | }), 177 | serializer, 178 | ), 179 | MemberOfAttribute::SamAccountName => T::serialize_list( 180 | self.0.iter().map(|m| m.sam_account_name.clone()), 181 | serializer, 182 | ), 183 | } 184 | } 185 | } 186 | 187 | impl<'de, T> Deserialize<'de> for MembershipSet 188 | where 189 | T: SerializationType, 190 | { 191 | fn deserialize(deserializer: D) -> Result 192 | where 193 | D: serde::Deserializer<'de>, 194 | { 195 | T::deserialize(deserializer) 196 | } 197 | } 198 | 199 | #[cfg(test)] 200 | mod tests { 201 | use serde::Serialize; 202 | 203 | use crate::{ 204 | cache::RecordPointer, cli::MemberOfAttribute, use_member_of_attribute, win32_types::Rdn, 205 | CsvSerialization, JsonSerialization, 206 | }; 207 | 208 | use super::{Membership, MembershipSet, SerializationType}; 209 | 210 | #[derive(Serialize)] 211 | #[serde(bound = "T: SerializationType")] 212 | struct SampleRecord { 213 | data: MembershipSet, 214 | } 215 | 216 | fn test_data() -> SampleRecord 217 | where 218 | T: SerializationType, 219 | { 220 | SampleRecord { 221 | data: MembershipSet::::from( 222 | vec![ 223 | Membership::::from(( 224 | RecordPointer::new(1.into(), 1.into()), 225 | Rdn::try_from("a").unwrap(), 226 | None, 227 | None, 228 | )), 229 | Membership::::from(( 230 | RecordPointer::new(2.into(), 2.into()), 231 | Rdn::try_from("b").unwrap(), 232 | None, 233 | None, 234 | )), 235 | Membership::::from(( 236 | RecordPointer::new(3.into(), 3.into()), 237 | Rdn::try_from("c").unwrap(), 238 | None, 239 | None, 240 | )), 241 | ] 242 | .into_iter(), 243 | ), 244 | } 245 | } 246 | 247 | #[test] 248 | fn test_serialize_csv() { 249 | use_member_of_attribute(MemberOfAttribute::Rdn); 250 | 251 | let mut wtr = csv::Writer::from_writer(vec![]); 252 | wtr.serialize(test_data::()).unwrap(); 253 | 254 | let result = String::from_utf8(wtr.into_inner().unwrap()).unwrap(); 255 | 256 | assert_eq!( 257 | result, 258 | r#"data 259 | "a,b,c" 260 | "# 261 | ); 262 | } 263 | 264 | #[test] 265 | fn test_serialize_json() { 266 | use_member_of_attribute(MemberOfAttribute::Rdn); 267 | let result = serde_json::to_string(&test_data::()).unwrap(); 268 | assert_eq!(result, r#"{"data":["a","b","c"]}"#); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/membership_serialization/mod.rs: -------------------------------------------------------------------------------- 1 | mod membership_set; 2 | pub use membership_set::*; 3 | 4 | mod serialization_type; 5 | pub use serialization_type::*; 6 | 7 | mod csv_serialization; 8 | pub use csv_serialization::*; 9 | 10 | mod json_serialization; 11 | pub use json_serialization::*; -------------------------------------------------------------------------------- /src/membership_serialization/serialization_type.rs: -------------------------------------------------------------------------------- 1 | use crate::MembershipSet; 2 | 3 | pub trait SerializationType { 4 | fn serialize_list( 5 | items: impl Iterator>, 6 | serializer: S, 7 | ) -> Result 8 | where 9 | S: serde::Serializer; 10 | 11 | fn serialize(item: Option, serializer: S) -> Result 12 | where 13 | S: serde::Serializer, 14 | { 15 | match item { 16 | Some(v) => serializer.serialize_str(&v), 17 | None => serializer.serialize_none(), 18 | } 19 | } 20 | 21 | fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 22 | where 23 | Self: Sized, 24 | D: serde::Deserializer<'de>; 25 | } 26 | -------------------------------------------------------------------------------- /src/ntds/attribute_id_impl.rs: -------------------------------------------------------------------------------- 1 | use crate::{cache::ColumnIndex, EsedbInfo}; 2 | 3 | use super::NtdsAttributeId; 4 | 5 | impl NtdsAttributeId { 6 | pub fn id<'info>(&self, info: &'info EsedbInfo) -> &'info ColumnIndex { 7 | info.column(*self).id() 8 | } 9 | } -------------------------------------------------------------------------------- /src/ntds/attribute_name.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Serialize, Deserialize}; 4 | 5 | 6 | #[derive(Serialize, Deserialize)] 7 | #[derive(Clone)] 8 | pub struct AttributeName(String); 9 | 10 | impl From for AttributeName { 11 | fn from(value: String) -> Self { 12 | Self(value) 13 | } 14 | } 15 | 16 | impl Display for AttributeName { 17 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | self.0.fmt(f) 19 | } 20 | } -------------------------------------------------------------------------------- /src/ntds/attribute_value.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Serialize, Deserialize}; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct AttributeValue(String); 7 | 8 | impl From for AttributeValue { 9 | fn from(value: String) -> Self { 10 | Self(value) 11 | } 12 | } 13 | 14 | impl Display for AttributeValue { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | self.0.fmt(f) 17 | } 18 | } 19 | 20 | impl AttributeValue { 21 | pub fn value(&self) -> &str { 22 | &self.0[..] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ntds/data_table_record.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::{self, MetaDataCache, RecordId, RecordPointer}; 2 | use crate::cache::{ColumnIndex, WithValue}; 3 | use crate::ntds::{Error, NtdsAttributeId}; 4 | use crate::value::FromValue; 5 | use crate::win32_types::TimelineEntry; 6 | use crate::win32_types::{ 7 | Rdn, SamAccountType, Sid, TruncatedWindowsFileTime, UserAccountControl, WindowsFileTime, 8 | }; 9 | use crate::ColumnInfoMapping; 10 | use bodyfile::Bodyfile3Line; 11 | use chrono::{DateTime, Utc}; 12 | use concat_idents::concat_idents; 13 | use flow_record::derive::*; 14 | use flow_record::prelude::*; 15 | use getset::Getters; 16 | use serde::ser::SerializeStruct; 17 | use serde::Serialize; 18 | use std::collections::HashMap; 19 | use term_table::row::Row; 20 | use term_table::table_cell::{Alignment, TableCell}; 21 | 22 | use super::{AttributeName, AttributeValue}; 23 | 24 | #[derive(Getters, Serialize)] 25 | #[getset(get = "pub")] 26 | pub struct EntryAttribute { 27 | column: String, 28 | attribute: AttributeName, 29 | value: AttributeValue, 30 | } 31 | 32 | #[derive(Getters)] 33 | pub struct DataTableRecord<'info, 'db> { 34 | inner: cache::Record<'info, 'db>, 35 | 36 | #[getset(get = "pub")] 37 | ptr: RecordPointer, 38 | } 39 | 40 | macro_rules! record_attribute { 41 | ($name: ident, $id: ident, $type: ty) => { 42 | pub fn $name(&self) -> crate::ntds::Result<$type> { 43 | self.get_value(NtdsAttributeId::$id) 44 | } 45 | 46 | concat_idents!(fn_name=$name, _opt { 47 | pub fn fn_name(&self) -> crate::ntds::Result> { 48 | self.get_value_opt(NtdsAttributeId::$id) 49 | } 50 | }); 51 | 52 | concat_idents!(fn_name=has_, $name { 53 | pub fn fn_name(&self, other: &$type) -> crate::ntds::Result { 54 | self.has_value(NtdsAttributeId::$id, other) 55 | } 56 | }); 57 | }; 58 | } 59 | 60 | impl<'info, 'db> DataTableRecord<'info, 'db> { 61 | pub fn new(inner: cache::Record<'info, 'db>, ptr: RecordPointer) -> Self { 62 | Self { inner, ptr } 63 | } 64 | 65 | fn get_value(&self, column: NtdsAttributeId) -> crate::ntds::Result 66 | where 67 | T: FromValue, 68 | { 69 | self.inner.with_value(column, |v| match v { 70 | None => Err(Error::ValueIsMissing), 71 | Some(v) => Ok(::from_value(v)?), 72 | }) 73 | } 74 | fn get_value_opt(&self, column: NtdsAttributeId) -> crate::ntds::Result> 75 | where 76 | T: FromValue, 77 | { 78 | self.inner.with_value(column, |v| match v { 79 | None => Ok(None), 80 | Some(v) => Ok(Some(::from_value(v)?)), 81 | }) 82 | } 83 | fn has_value(&self, column: NtdsAttributeId, other: &T) -> crate::ntds::Result 84 | where 85 | T: FromValue + Eq, 86 | { 87 | self.inner.with_value(column, |v| match v { 88 | None => Ok(false), 89 | Some(v) => Ok(&(::from_value(v)?) == other), 90 | }) 91 | } 92 | 93 | record_attribute!(ds_record_id, DsRecordId, RecordId); 94 | record_attribute!(object_category, AttObjectCategory, RecordId); 95 | record_attribute!(ds_parent_record_id, DsParentRecordId, RecordId); 96 | record_attribute!(ds_record_time, DsRecordTime, TruncatedWindowsFileTime); 97 | record_attribute!(ds_ancestors, DsAncestors, i32); 98 | record_attribute!(att_object_sid, AttObjectSid, Sid); 99 | record_attribute!(att_when_created, AttWhenCreated, TruncatedWindowsFileTime); 100 | record_attribute!(att_when_changed, AttWhenChanged, TruncatedWindowsFileTime); 101 | record_attribute!(att_object_type_id, AttObjectCategory, RecordId); 102 | record_attribute!(att_object_name, AttCommonName, Rdn); 103 | record_attribute!(att_object_name2, AttRdn, Rdn); 104 | record_attribute!(att_sam_account_name, AttSamAccountName, String); 105 | record_attribute!(att_sam_account_type, AttSamAccountType, SamAccountType); 106 | record_attribute!(att_user_principal_name, AttUserPrincipalName, String); 107 | record_attribute!(att_service_principal_name, AttServicePrincipalName, String); 108 | record_attribute!( 109 | att_user_account_control, 110 | AttUserAccountControl, 111 | UserAccountControl 112 | ); 113 | record_attribute!(att_last_logon, AttLastLogon, WindowsFileTime); 114 | record_attribute!( 115 | att_last_logon_time_stamp, 116 | AttLastLogonTimestamp, 117 | WindowsFileTime 118 | ); 119 | record_attribute!(att_account_expires, AttAccountExpires, WindowsFileTime); 120 | record_attribute!(att_password_last_set, AttPwdLastSet, WindowsFileTime); 121 | record_attribute!(att_bad_pwd_time, AttBadPasswordTime, WindowsFileTime); 122 | record_attribute!(att_logon_count, AttLogonCount, i32); 123 | record_attribute!(att_bad_pwd_count, AttBadPwdCount, i32); 124 | record_attribute!(att_primary_group_id, AttPrimaryGroupId, i32); 125 | //record_attribute!(att_aduser_objects, AttX509Cert, Vec); 126 | record_attribute!(att_comment, AttComment, String); 127 | record_attribute!(att_dns_host_name, AttDnsHostName, String); 128 | record_attribute!(att_os_name, AttOperatingSystem, String); 129 | record_attribute!(att_os_version, AttOperatingSystemVersion, String); 130 | record_attribute!(att_link_id, AttLinkId, u32); 131 | record_attribute!(att_ldap_display_name, AttLdapDisplayName, String); 132 | record_attribute!(att_creator_sid, AttMsDsCreatorSid, Sid); 133 | record_attribute!(att_admin_count, AttAdminCount, i32); 134 | record_attribute!(att_is_deleted, AttIsDeleted, bool); 135 | record_attribute!(att_last_known_parent, AttLastKnownParent, RecordId); 136 | record_attribute!(att_nt_security_descriptor, AttNtSecurityDescriptor, i64); 137 | 138 | pub fn mapping(&self) -> &ColumnInfoMapping { 139 | self.inner.esedbinfo().mapping() 140 | } 141 | pub fn all_attributes(&self) -> HashMap { 142 | (0..*self.inner.count()) 143 | .map(ColumnIndex::from) 144 | .filter_map(|idx| { 145 | let column = &self.inner.columns()[idx]; 146 | if column.attribute_id().is_some() { 147 | Some(column) 148 | } else { 149 | None 150 | } 151 | }) 152 | .map(|column| { 153 | self.inner.with_value(*column.index(), |v| { 154 | Ok(v.map(|x| { 155 | ( 156 | column.attribute_id().unwrap(), 157 | EntryAttribute { 158 | column: column.name().to_string(), 159 | attribute: column 160 | .attribute_name() 161 | .as_ref() 162 | .cloned() 163 | .unwrap_or(AttributeName::from(column.name().to_string())), 164 | value: AttributeValue::from(x.to_string()), 165 | }, 166 | ) 167 | })) 168 | }) 169 | }) 170 | .filter_map(Result::ok) 171 | .flatten() 172 | .collect() 173 | } 174 | 175 | pub fn object_type_name(&self, metadata: &MetaDataCache) -> anyhow::Result { 176 | Ok(if let Some(type_id) = self.att_object_type_id_opt()? { 177 | metadata 178 | .record(&type_id) 179 | .map(|entry| entry.rdn().name().to_string()) 180 | .unwrap_or("Object".to_string()) 181 | } else { 182 | "Object".to_string() 183 | }) 184 | } 185 | 186 | pub fn to_bodyfile(&self, metadata: &MetaDataCache) -> anyhow::Result> { 187 | let my_name = self 188 | .att_sam_account_name() 189 | .or(self.att_object_name().map(|s| s.name().to_string())); 190 | 191 | let object_type_name = self.object_type_name(metadata)?; 192 | let object_type_caption = 193 | if let Some(last_known_parent) = self.att_last_known_parent_opt()? { 194 | metadata 195 | .record(&last_known_parent) 196 | .and_then(|entry| metadata.dn(entry)) 197 | .map(|e| format!("{object_type_name}, deleted from {e}")) 198 | .unwrap_or(format!("deleted {object_type_name}")) 199 | } else if self.att_is_deleted_opt()?.unwrap_or(false) { 200 | format!("deleted {object_type_name}") 201 | } else { 202 | object_type_name 203 | }; 204 | 205 | let inode = self.ptr.ds_record_id().to_string(); 206 | if let Ok(upn) = &my_name { 207 | Ok(vec![ 208 | self.ds_record_time().map(|ts| { 209 | ts.cr_entry(upn, "record creation time", &object_type_caption) 210 | .with_inode(&inode) 211 | }), 212 | self.att_when_created().map(|ts| { 213 | ts.cr_entry(upn, "object created", &object_type_caption) 214 | .with_inode(&inode) 215 | }), 216 | self.att_when_changed().map(|ts| { 217 | ts.cr_entry(upn, "object changed", &object_type_caption) 218 | .with_inode(&inode) 219 | }), 220 | self.att_last_logon().map(|ts| { 221 | ts.c_entry(upn, "last logon on this DC", &object_type_caption) 222 | .with_inode(&inode) 223 | }), 224 | self.att_last_logon_time_stamp().map(|ts| { 225 | ts.c_entry(upn, "last logon on any DC", &object_type_caption) 226 | .with_inode(&inode) 227 | }), 228 | self.att_bad_pwd_time().map(|ts| { 229 | ts.c_entry(upn, "bad pwd time", &object_type_caption) 230 | .with_inode(&inode) 231 | }), 232 | self.att_password_last_set().map(|ts| { 233 | ts.c_entry(upn, "password last set", object_type_caption) 234 | .with_inode(&inode) 235 | }), 236 | ] 237 | .into_iter() 238 | .flatten() 239 | .collect()) 240 | } else { 241 | Ok(Vec::new()) 242 | } 243 | } 244 | pub fn to_flow_record(&self, metadata: &MetaDataCache) -> anyhow::Result { 245 | let name = self 246 | .att_sam_account_name() 247 | .or(self.att_object_name().map(|s| s.name().to_string()))?; 248 | 249 | let object_type = self.object_type_name(metadata)?; 250 | let deleted_from = self 251 | .att_last_known_parent_opt()? 252 | .and_then(|last_known_parent| { 253 | metadata 254 | .record(&last_known_parent) 255 | .and_then(|entry| metadata.dn(entry)) 256 | }); 257 | 258 | Ok(NtdsEntry { 259 | name, 260 | object_type, 261 | record_id: self.ptr.ds_record_id().inner(), 262 | is_deleted: self.att_is_deleted_opt()?.unwrap_or(false), 263 | deleted_from, 264 | record_time: self.ds_record_time_opt()?.map(|ts| ts.into()), 265 | when_created: self.att_when_created_opt()?.map(|ts| ts.into()), 266 | when_changed: self.att_when_changed_opt()?.map(|ts| ts.into()), 267 | last_logon: self.att_last_logon_opt()?.map(|ts| ts.into()), 268 | last_logon_timestamp: self.att_last_logon_time_stamp_opt()?.map(|ts| ts.into()), 269 | bad_pwd_time: self.att_bad_pwd_time_opt()?.map(|ts| ts.into()), 270 | password_last_set: self.att_password_last_set_opt()?.map(|ts| ts.into()), 271 | }) 272 | } 273 | } 274 | 275 | impl<'info, 'db> WithValue for DataTableRecord<'info, 'db> { 276 | fn with_value( 277 | &self, 278 | index: NtdsAttributeId, 279 | function: impl FnMut(Option<&cache::Value>) -> crate::ntds::Result, 280 | ) -> crate::ntds::Result { 281 | self.inner.with_value(index, function) 282 | } 283 | } 284 | 285 | impl<'info, 'db> WithValue for DataTableRecord<'info, 'db> { 286 | fn with_value( 287 | &self, 288 | index: ColumnIndex, 289 | function: impl FnMut(Option<&cache::Value>) -> crate::ntds::Result, 290 | ) -> crate::ntds::Result { 291 | self.inner.with_value(index, function) 292 | } 293 | } 294 | 295 | impl<'info, 'db> From<&DataTableRecord<'info, 'db>> for term_table::Table { 296 | fn from(value: &DataTableRecord<'info, 'db>) -> Self { 297 | let mut table = term_table::Table::new(); 298 | let all_attributes = value.all_attributes(); 299 | let mut keys: Vec<_> = all_attributes.keys().collect(); 300 | keys.sort(); 301 | 302 | table.add_row(Row::new(vec![ 303 | TableCell::builder("Attribute") 304 | .alignment(Alignment::Center) 305 | .build(), 306 | TableCell::builder("Value") 307 | .alignment(Alignment::Center) 308 | .build(), 309 | ])); 310 | 311 | for id in keys { 312 | let attribute = &all_attributes[id]; 313 | table.add_row(Row::new(vec![ 314 | TableCell::new(attribute.attribute()), 315 | TableCell::new(attribute.column()), 316 | TableCell::new(attribute.value()), 317 | ])); 318 | } 319 | 320 | table 321 | } 322 | } 323 | 324 | impl<'info, 'db> Serialize for DataTableRecord<'info, 'db> { 325 | fn serialize(&self, serializer: S) -> Result 326 | where 327 | S: serde::Serializer, 328 | { 329 | let all_attributes = self.all_attributes(); 330 | let mut ser = serializer.serialize_struct("record", all_attributes.len())?; 331 | for (id, att) in all_attributes { 332 | let key: &'static str = id.into(); 333 | ser.serialize_field(key, att.value())?; 334 | } 335 | ser.end() 336 | } 337 | } 338 | 339 | #[derive(FlowRecord)] 340 | #[flow_record(version = 1, source = "ntdsextract2", classification = "ntds")] 341 | pub struct NtdsEntry { 342 | name: String, 343 | object_type: String, 344 | record_id: i32, 345 | record_time: Option>, 346 | when_created: Option>, 347 | when_changed: Option>, 348 | last_logon: Option>, 349 | last_logon_timestamp: Option>, 350 | bad_pwd_time: Option>, 351 | password_last_set: Option>, 352 | is_deleted: bool, 353 | deleted_from: Option, 354 | } 355 | -------------------------------------------------------------------------------- /src/ntds/error.rs: -------------------------------------------------------------------------------- 1 | use std::num::TryFromIntError; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum Error { 5 | #[error("this value has no data")] 6 | ValueIsMissing, 7 | 8 | #[error("invalid value detected: '{0:?}'; expected type was {1}")] 9 | InvalidValueDetected(String, &'static str), 10 | 11 | #[error("unable to convert integer '{value:?}' to {intended_type}: {why}")] 12 | IntegerConversionError { 13 | value: String, 14 | intended_type: &'static str, 15 | why: TryFromIntError, 16 | }, 17 | 18 | #[error("unable to convert '{value:?}' to {intended_type}: {why}")] 19 | MiscConversionError { 20 | value: String, 21 | intended_type: &'static str, 22 | why: anyhow::Error, 23 | }, 24 | 25 | #[error("no schema record found")] 26 | MissingSchemaRecord, 27 | 28 | #[error("The schema record has no children")] 29 | SchemaRecordHasNoChildren, 30 | 31 | #[error("IO Error: {0}")] 32 | IoError(#[from] std::io::Error), 33 | 34 | #[error("Invalid UUID: {0}")] 35 | UuidError(#[from] uuid::Error), 36 | 37 | #[error("Invalid SDDL: {0}")] 38 | SddlError(#[from] sddl::Error), 39 | 40 | #[error("Invalid forward LinkID: {0}, the forward LinkID must be a even number")] 41 | InvalidForwardLinkId(u32), 42 | 43 | #[error("invalid LinkID values: {member_link_id} and {member_of_link_id}")] 44 | InvalidLinkIdValues{member_link_id: u32, member_of_link_id: u32} 45 | } 46 | 47 | pub type Result = core::result::Result; -------------------------------------------------------------------------------- /src/ntds/from_data_table.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::{cli::OutputOptions, win32_types::SecurityDescriptor, FormattedValue}; 4 | 5 | use super::{DataTable, DataTableRecord, LinkTable}; 6 | 7 | pub trait FromDataTable: Sized + Serialize { 8 | fn new( 9 | dbrecord: DataTableRecord, 10 | options: &OutputOptions, 11 | data_table: &DataTable, 12 | link_table: &LinkTable, 13 | distinguished_name: FormattedValue, 14 | sd: Option<&SecurityDescriptor> 15 | ) -> Result; 16 | } 17 | -------------------------------------------------------------------------------- /src/ntds/is_member_of.rs: -------------------------------------------------------------------------------- 1 | use crate::object_tree::ObjectTree; 2 | 3 | pub trait IsMemberOf { 4 | fn update_membership_dn(&mut self, tree: &ObjectTree); 5 | } -------------------------------------------------------------------------------- /src/ntds/link_table.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use crate::cache::RecordPointer; 4 | use crate::cache::{self, RecordId}; 5 | use crate::ntds::link_table_builder::LinkTableBuilder; 6 | use crate::win32_types::Rdn; 7 | use crate::{Membership, MembershipSet, SerializationType}; 8 | 9 | use super::DataTable; 10 | 11 | /// wraps a ESEDB Table. 12 | /// This class assumes the a NTDS link_table is being wrapped 13 | pub struct LinkTable { 14 | pub(crate) _forward_map: HashMap>, 15 | pub(crate) backward_map: HashMap>, 16 | } 17 | 18 | impl LinkTable { 19 | /// create a new datatable wrapper 20 | pub fn new<'info, 'db>( 21 | link_table: cache::LinkTable<'info, 'db>, 22 | data_table: &cache::DataTable<'info, 'db>, 23 | schema_record_id: RecordPointer, 24 | ) -> crate::ntds::Result { 25 | log::info!("reading link information and creating link_table cache"); 26 | 27 | let builder = LinkTableBuilder::from(link_table, data_table, schema_record_id)?; 28 | builder.build(data_table.metadata()) 29 | } 30 | 31 | pub(crate) fn member_of(&self, dnt: &RecordId) -> Option<&HashSet> { 32 | self.backward_map.get(dnt) 33 | } 34 | 35 | pub fn member_names_of(&self, object_id: RecordId, data_table: &DataTable<'_, '_>) -> Vec { 36 | let member_of = if let Some(children) = self.member_of(&object_id) { 37 | children 38 | .iter() 39 | .map(|child_id| &data_table.data_table().metadata()[child_id]) 40 | .map(|record| record.rdn().clone()) 41 | .collect() 42 | } else { 43 | vec![] 44 | }; 45 | member_of 46 | } 47 | 48 | pub fn member_refs_of( 49 | &self, 50 | object_id: RecordId, 51 | data_table: &DataTable<'_, '_>, 52 | ) -> MembershipSet { 53 | let member_of = if let Some(children) = self.member_of(&object_id) { 54 | children 55 | .iter() 56 | .map(|child_id| &data_table.data_table().metadata()[child_id]) 57 | .map(|record| { 58 | ( 59 | *record.record_ptr(), 60 | record.rdn().clone(), 61 | record.sid().clone(), 62 | record.sam_account_name().clone(), 63 | ) 64 | }) 65 | .collect() 66 | } else { 67 | vec![] 68 | }; 69 | MembershipSet::::from(member_of.into_iter().map(Membership::from)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ntds/link_table_builder.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use crate::cache::{self, MetaDataCache, RecordId, RecordPointer, Value, WithValue}; 4 | use crate::ntds::Error; 5 | use crate::value::FromValue; 6 | 7 | use super::{LinkTable, NtdsAttributeId}; 8 | 9 | pub(crate) struct LinkTableBuilder<'info, 'db> { 10 | link_table: cache::LinkTable<'info, 'db>, 11 | data_table: &'db cache::DataTable<'info, 'db>, 12 | schema_record_id: RecordPointer, 13 | } 14 | 15 | impl<'info, 'db> LinkTableBuilder<'info, 'db> { 16 | pub fn from( 17 | link_table: cache::LinkTable<'info, 'db>, 18 | data_table: &'db cache::DataTable<'info, 'db>, 19 | schema_record_id: RecordPointer, 20 | ) -> crate::ntds::Result { 21 | Ok(Self { 22 | link_table, 23 | data_table, 24 | schema_record_id, 25 | }) 26 | } 27 | 28 | pub fn build(self, metadata: &MetaDataCache) -> crate::ntds::Result { 29 | log::info!("building link table associations"); 30 | 31 | let (member_link_id, _member_of_link_id) = self.find_member_link_id_pair()?; 32 | let link_base = member_link_id / 2; 33 | let link_dnt_id = self.link_table.link_dnt_id(); 34 | let backlink_dnt_id = self.link_table.backlink_dnt_id(); 35 | let link_base_id = self.link_table.link_base_id(); 36 | 37 | let mut forward_map = HashMap::new(); 38 | let mut backward_map = HashMap::new(); 39 | 40 | for record in self.link_table.iter().filter(|r| { 41 | r.with_value(*link_base_id, |value| match value { 42 | Some(Value::U32(v)) => Ok(*v == member_link_id), 43 | Some(Value::I32(v)) => Ok(u32::try_from(*v).map_or(false, |v| v == link_base)), 44 | _ => Ok(false), 45 | }) 46 | .unwrap_or(false) 47 | }) { 48 | if let Ok(Some(forward_link)) = record.with_value(*link_dnt_id, |v| { 49 | RecordId::from_value(v.unwrap()) 50 | .map(|id| { 51 | metadata.ptr_from_id(&id).or_else(|| { 52 | log::warn!("I expected to find an entry for forward link {id}; but there was none. I'll ignore that entry."); 53 | None 54 | }) 55 | }) 56 | }) { 57 | if let Ok(Some(backward_link)) = record.with_value(*backlink_dnt_id, |v| { 58 | RecordId::from_value(v.unwrap()) 59 | .map(|id| { 60 | metadata.ptr_from_id(&id).or_else(|| { 61 | log::warn!( 62 | "I expected to find an entry for backward link {id}; but there was none. I'll ignore that entry." 63 | ); 64 | None 65 | }) 66 | }) 67 | }) { 68 | forward_map 69 | .entry(*forward_link.ds_record_id()) 70 | .or_insert_with(HashSet::new) 71 | .insert(*backward_link); 72 | backward_map 73 | .entry(*backward_link.ds_record_id()) 74 | .or_insert_with(HashSet::new) 75 | .insert(*forward_link); 76 | } 77 | } 78 | } 79 | 80 | for (key, value) in forward_map.iter() { 81 | log::info!("found link {key} --> {value:?}"); 82 | } 83 | 84 | for (key, value) in backward_map.iter() { 85 | log::info!("found backlink {key} --> {value:?}"); 86 | } 87 | 88 | log::debug!( 89 | "found {} forward links and {} backward links", 90 | forward_map.len(), 91 | backward_map.len() 92 | ); 93 | 94 | Ok(LinkTable { 95 | _forward_map: forward_map, 96 | backward_map, 97 | }) 98 | } 99 | 100 | fn find_member_link_id_pair(&self) -> crate::ntds::Result<(u32, u32)> { 101 | log::info!("searching for link attributes 'Member' and 'Is-Member-Of-DL'"); 102 | 103 | let member_link_id = self.find_link_id(&String::from("Member"))?; 104 | log::info!("'Member' has Link-ID '{member_link_id}'"); 105 | 106 | let member_of_link_id = self.find_link_id(&String::from("Is-Member-Of-DL"))?; 107 | log::info!("'Is-Member-Of-DL' has Link-ID '{member_of_link_id}'"); 108 | 109 | if member_link_id & 1 != 0 { 110 | return Err(Error::InvalidForwardLinkId(member_link_id)); 111 | } 112 | 113 | if member_link_id + 1 != member_of_link_id { 114 | return Err(Error::InvalidLinkIdValues { 115 | member_link_id, member_of_link_id 116 | }); 117 | } 118 | 119 | Ok((member_link_id, member_of_link_id)) 120 | } 121 | 122 | fn find_link_id(&self, attribute_name: &String) -> crate::ntds::Result { 123 | let entry = self 124 | .data_table 125 | .metadata() 126 | .children_of(&self.schema_record_id) 127 | .find(|r| r.rdn().name() == attribute_name) 128 | .unwrap_or_else(|| panic!("found no record by that name: '{attribute_name}'")); 129 | 130 | let link_id_column = NtdsAttributeId::AttLinkId.id(self.data_table.esedbinfo()); 131 | 132 | let record = self 133 | .data_table 134 | .table() 135 | .record(entry.record_ptr().esedb_row().inner())?; 136 | Ok(u32::from_record_opt(&record, link_id_column)? 137 | .unwrap_or_else(|| panic!("missing link-id attribute in {attribute_name}"))) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/ntds/mod.rs: -------------------------------------------------------------------------------- 1 | mod data_table; 2 | mod link_table; 3 | mod sd_table; 4 | mod attribute_id; 5 | mod link_table_builder; 6 | mod object_type; 7 | mod data_table_record; 8 | mod error; 9 | mod from_data_table; 10 | mod object; 11 | mod schema; 12 | mod attribute_name; 13 | mod attribute_value; 14 | mod attribute_id_impl; 15 | mod is_member_of; 16 | 17 | pub use data_table::*; 18 | pub use link_table::*; 19 | pub use sd_table::*; 20 | pub use attribute_id::*; 21 | pub use object_type::*; 22 | pub use data_table_record::*; 23 | pub use error::*; 24 | pub use from_data_table::*; 25 | pub use object::*; 26 | pub use schema::*; 27 | pub use attribute_name::*; 28 | pub use attribute_value::*; 29 | pub use is_member_of::*; 30 | -------------------------------------------------------------------------------- /src/ntds/object/has_serializable_fields.rs: -------------------------------------------------------------------------------- 1 | pub trait HasSerializableFields { 2 | fn field_count() -> usize { 3 | Self::fields().len() 4 | } 5 | fn fields() -> &'static Vec<&'static str>; 6 | } -------------------------------------------------------------------------------- /src/ntds/object/mod.rs: -------------------------------------------------------------------------------- 1 | mod object_base; 2 | mod specific_object_attribute; 3 | mod no_specific_attributes; 4 | mod has_serializable_fields; 5 | 6 | mod object_computer; 7 | mod object_group; 8 | mod object_person; 9 | 10 | pub use object_base::*; 11 | pub use specific_object_attribute::*; 12 | pub use no_specific_attributes::*; 13 | pub use has_serializable_fields::*; 14 | 15 | pub use object_computer::*; 16 | pub use object_group::*; 17 | pub use object_person::*; -------------------------------------------------------------------------------- /src/ntds/object/no_specific_attributes.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::ntds::DataTableRecord; 4 | 5 | use super::{HasSerializableFields, SpecificObjectAttributes}; 6 | 7 | pub struct NoSpecificAttributes; 8 | 9 | impl HasSerializableFields for NoSpecificAttributes { 10 | fn fields() -> &'static Vec<&'static str> { 11 | static NO_HEADER: Vec<&'static str> = vec![]; 12 | &NO_HEADER 13 | } 14 | } 15 | 16 | impl SpecificObjectAttributes for NoSpecificAttributes { 17 | fn from(_record: &DataTableRecord) -> anyhow::Result { 18 | Ok(Self) 19 | } 20 | 21 | fn serialize_to(&self, _s: &mut S::SerializeStruct) -> Result<(), S::Error> where S: serde::Serializer { 22 | Ok(()) 23 | } 24 | } 25 | 26 | impl Serialize for NoSpecificAttributes { 27 | fn serialize(&self, serializer: S) -> Result 28 | where 29 | S: serde::Serializer, 30 | { 31 | serializer.serialize_none() 32 | } 33 | } 34 | 35 | impl<'de> Deserialize<'de> for NoSpecificAttributes { 36 | fn deserialize(_deserializer: D) -> Result 37 | where 38 | D: serde::Deserializer<'de> { 39 | Ok(Self) 40 | } 41 | } -------------------------------------------------------------------------------- /src/ntds/object/object_base.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::RecordPointer; 2 | use crate::cli::OutputOptions; 3 | use crate::win32_types::{Rdn, SecurityDescriptor, TimelineEntry, TruncatedWindowsFileTime, WindowsFileTime}; 4 | use crate::win32_types::{SamAccountType, Sid, UserAccountControl}; 5 | use crate::{FormattedValue, Membership, MembershipSet, SerializationType}; 6 | use bodyfile::Bodyfile3Line; 7 | use getset::Getters; 8 | use serde::ser::SerializeStruct; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::ntds::{ 12 | DataTable, DataTableRecord, FromDataTable, HasObjectType, IsMemberOf, LinkTable, 13 | }; 14 | use std::marker::PhantomData; 15 | 16 | use super::{HasSerializableFields, SpecificObjectAttributes}; 17 | 18 | #[derive(Getters, Deserialize)] 19 | #[getset(get = "pub")] 20 | #[serde(bound = "T: SerializationType")] 21 | pub struct Object 22 | where 23 | O: HasObjectType, 24 | T: SerializationType, 25 | A: SpecificObjectAttributes, 26 | { 27 | distinguished_name: FormattedValue, 28 | 29 | sid: Option, 30 | user_principal_name: Option, 31 | service_principal_name: Option, 32 | rdn: Option, 33 | sam_account_name: Option, 34 | sam_account_type: Option, 35 | user_account_control: Option, 36 | logon_count: Option, 37 | bad_pwd_count: Option, 38 | admin_count: Option, 39 | is_deleted: bool, 40 | 41 | //#[serde(skip_serializing)] 42 | #[allow(dead_code)] 43 | primary_group_id: Option, 44 | 45 | primary_group: Option>, 46 | 47 | //aduser_objects: Option, 48 | member_of: MembershipSet, 49 | 50 | comment: Option, 51 | 52 | record_time: Option, 53 | 54 | when_created: Option, 55 | when_changed: Option, 56 | last_logon: Option, 57 | last_logon_time_stamp: Option, 58 | account_expires: Option, 59 | password_last_set: Option, 60 | bad_pwd_time: Option, 61 | 62 | sddl: Option, 63 | //#[serde(flatten)] 64 | specific_attributes: A, 65 | 66 | 67 | #[serde(skip)] 68 | _marker: PhantomData, 69 | 70 | #[serde(skip)] 71 | ptr: RecordPointer, 72 | } 73 | 74 | impl HasSerializableFields for Object 75 | where 76 | O: HasObjectType, 77 | T: SerializationType, 78 | A: SpecificObjectAttributes, 79 | { 80 | fn fields() -> &'static Vec<&'static str> { 81 | lazy_static::lazy_static! { 82 | static ref FIELDS: Vec<&'static str> = vec![ 83 | "sid", 84 | "user_principal_name", 85 | "service_principal_name", 86 | "rdn", 87 | "sam_account_name", 88 | "sam_account_type", 89 | "user_account_control", 90 | "logon_count", 91 | "bad_pwd_count", 92 | "admin_count", 93 | "is_deleted", 94 | "primary_group_id", 95 | "primary_group", 96 | "member_of", 97 | "comment", 98 | "record_time", 99 | "when_created", 100 | "when_changed", 101 | "last_logon", 102 | "last_logon_time_stamp", 103 | "account_expires", 104 | "password_last_set", 105 | "bad_pwd_time", 106 | "sddl" 107 | ]; 108 | } 109 | &FIELDS 110 | } 111 | } 112 | 113 | impl Serialize for Object 114 | where 115 | O: HasObjectType, 116 | T: SerializationType, 117 | A: SpecificObjectAttributes, 118 | { 119 | fn serialize(&self, serializer: S) -> Result 120 | where 121 | S: serde::Serializer, 122 | { 123 | let mut s = 124 | serializer.serialize_struct("Object", Self::field_count() + A::field_count())?; 125 | 126 | s.serialize_field("sid", self.sid())?; 127 | 128 | match &self.distinguished_name { 129 | FormattedValue::NoValue => s.serialize_field("distinguished_name", &None::)?, 130 | FormattedValue::Hide => (), 131 | FormattedValue::Value(dn) => s.serialize_field("distinguished_name", dn)?, 132 | } 133 | 134 | s.serialize_field("user_principal_name", self.user_principal_name())?; 135 | s.serialize_field("service_principal_name", self.service_principal_name())?; 136 | s.serialize_field("rdn", self.rdn())?; 137 | s.serialize_field("sam_account_name", self.sam_account_name())?; 138 | s.serialize_field("sam_account_type", self.sam_account_type())?; 139 | s.serialize_field("user_account_control", self.user_account_control())?; 140 | s.serialize_field("logon_count", self.logon_count())?; 141 | s.serialize_field("bad_pwd_count", self.bad_pwd_count())?; 142 | s.serialize_field("admin_count", self.admin_count())?; 143 | s.serialize_field("is_deleted", self.is_deleted())?; 144 | s.serialize_field("primary_group_id", self.primary_group_id())?; 145 | s.serialize_field("primary_group", self.primary_group())?; 146 | s.serialize_field("member_of", self.member_of())?; 147 | s.serialize_field("comment", self.comment())?; 148 | s.serialize_field("record_time", self.record_time())?; 149 | s.serialize_field("when_created", self.when_created())?; 150 | s.serialize_field("when_changed", self.when_changed())?; 151 | s.serialize_field("last_logon", self.last_logon())?; 152 | s.serialize_field("last_logon_time_stamp", self.last_logon_time_stamp())?; 153 | s.serialize_field("account_expires", self.account_expires())?; 154 | s.serialize_field("password_last_set", self.password_last_set())?; 155 | s.serialize_field("bad_pwd_time", self.bad_pwd_time())?; 156 | s.serialize_field("sddl", self.sddl())?; 157 | 158 | self.specific_attributes().serialize_to::(&mut s)?; 159 | s.end() 160 | } 161 | } 162 | 163 | impl FromDataTable for Object 164 | where 165 | O: HasObjectType, 166 | T: SerializationType, 167 | A: SpecificObjectAttributes, 168 | { 169 | fn new( 170 | dbrecord: DataTableRecord, 171 | _options: &OutputOptions, 172 | data_table: &DataTable, 173 | link_table: &LinkTable, 174 | distinguished_name: FormattedValue, 175 | sd: Option<&SecurityDescriptor> 176 | ) -> Result { 177 | let object_id = dbrecord.ds_record_id()?; 178 | 179 | let primary_group_id = dbrecord.att_primary_group_id().ok(); 180 | let primary_group = primary_group_id.and_then(|group_id| { 181 | match data_table 182 | .data_table() 183 | .metadata() 184 | .entries_with_rid(group_id.try_into().unwrap()) 185 | .next() // there should be at most one entry with this rid 186 | .map(|e| { 187 | data_table 188 | .data_table() 189 | .data_table_record_from(*e.record_ptr()) 190 | .unwrap() 191 | }) { 192 | Some(group) => { 193 | let rdn = group.att_object_name2().unwrap(); 194 | let sid = group.att_object_sid_opt().unwrap(); 195 | let dn = data_table.object_tree().dn_of(group.ptr()); 196 | let sam_account_name = group.att_sam_account_name_opt().unwrap(); 197 | if let Some(dn) = dn { 198 | Some(Membership::::from((dn, rdn, sid, sam_account_name))) 199 | } else { 200 | Some(Membership::::from(( 201 | *group.ptr(), 202 | rdn, 203 | sid, 204 | sam_account_name, 205 | ))) 206 | } 207 | } 208 | None => None, 209 | } 210 | }); 211 | 212 | let member_refs = link_table.member_refs_of::(object_id, data_table); 213 | let specific_attributes = A::from(&dbrecord)?; 214 | 215 | Ok(Self { 216 | distinguished_name, 217 | record_time: dbrecord.ds_record_time().ok(), 218 | when_created: dbrecord.att_when_created().ok(), 219 | when_changed: dbrecord.att_when_changed().ok(), 220 | sid: dbrecord.att_object_sid().ok(), 221 | sam_account_name: dbrecord.att_sam_account_name().ok(), 222 | rdn: dbrecord.att_object_name2().ok(), 223 | user_principal_name: dbrecord.att_user_principal_name().ok(), 224 | service_principal_name: dbrecord.att_service_principal_name().ok(), 225 | sam_account_type: dbrecord.att_sam_account_type().ok(), 226 | user_account_control: dbrecord.att_user_account_control().ok(), 227 | last_logon: dbrecord.att_last_logon().ok(), 228 | last_logon_time_stamp: dbrecord.att_last_logon_time_stamp().ok(), 229 | account_expires: dbrecord.att_account_expires().ok(), 230 | password_last_set: dbrecord.att_password_last_set().ok(), 231 | bad_pwd_time: dbrecord.att_bad_pwd_time().ok(), 232 | logon_count: dbrecord.att_logon_count().ok(), 233 | bad_pwd_count: dbrecord.att_bad_pwd_count().ok(), 234 | admin_count: dbrecord.att_admin_count().ok(), 235 | is_deleted: dbrecord.att_is_deleted().unwrap_or(false), 236 | primary_group_id, 237 | primary_group, 238 | comment: dbrecord.att_comment().ok(), 239 | //aduser_objects: dbrecord.att_u()?, 240 | member_of: member_refs, 241 | specific_attributes, 242 | sddl: sd.map(|sd| sd.to_string()), 243 | _marker: PhantomData, 244 | ptr: *dbrecord.ptr(), 245 | }) 246 | } 247 | } 248 | 249 | impl From> for Vec 250 | where 251 | O: HasObjectType, 252 | T: SerializationType, 253 | A: SpecificObjectAttributes, 254 | { 255 | fn from(obj: Object) -> Self { 256 | let object_type = O::object_type(); 257 | let upn = match obj.sam_account_name() { 258 | Some(n) => Some(n.to_string()), 259 | None => obj 260 | .rdn() 261 | .as_ref() 262 | .map(|n| n.name().to_string()) 263 | .or(Some("UNNAMED_OBJECT".to_string())), 264 | }; 265 | let inode = obj.ptr().ds_record_id().to_string(); 266 | 267 | if let Some(upn) = upn { 268 | vec![ 269 | obj.record_time().as_ref().map(|ts| { 270 | ts.cr_entry(&upn, "record creation time", object_type) 271 | .with_inode(&inode) 272 | }), 273 | obj.when_created().as_ref().map(|ts| { 274 | ts.cr_entry(&upn, "object created", object_type) 275 | .with_inode(&inode) 276 | }), 277 | obj.when_changed().as_ref().map(|ts| { 278 | ts.c_entry(&upn, "object changed", object_type) 279 | .with_inode(&inode) 280 | }), 281 | obj.last_logon().as_ref().map(|ts| { 282 | ts.a_entry(&upn, "last logon on this DC", object_type) 283 | .with_inode(&inode) 284 | }), 285 | obj.last_logon_time_stamp().as_ref().map(|ts| { 286 | ts.c_entry(&upn, "last logon on any DC", object_type) 287 | .with_inode(&inode) 288 | }), 289 | obj.bad_pwd_time().as_ref().map(|ts| { 290 | ts.c_entry(&upn, "bad pwd time", object_type) 291 | .with_inode(&inode) 292 | }), 293 | obj.password_last_set().as_ref().map(|ts| { 294 | ts.m_entry(&upn, "password last set", object_type) 295 | .with_inode(&inode) 296 | }), 297 | ] 298 | .into_iter() 299 | .flatten() 300 | .collect() 301 | } else { 302 | Vec::new() 303 | } 304 | } 305 | } 306 | 307 | impl IsMemberOf for Object 308 | where 309 | O: HasObjectType, 310 | T: SerializationType, 311 | A: SpecificObjectAttributes, 312 | { 313 | fn update_membership_dn(&mut self, tree: &crate::object_tree::ObjectTree) { 314 | self.member_of.update_dn(tree) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/ntds/object/object_computer.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use serde::{ser::SerializeStruct, Deserialize, Serialize}; 3 | 4 | use crate::win32_types::Sid; 5 | 6 | use crate::ntds::{types, HasSerializableFields, Object}; 7 | 8 | use super::SpecificObjectAttributes; 9 | 10 | #[derive(Deserialize, Serialize)] 11 | pub struct SpecificComputerAttributes { 12 | creator_sid: Option, 13 | } 14 | 15 | impl HasSerializableFields for SpecificComputerAttributes { 16 | fn fields() -> &'static Vec<&'static str> { 17 | lazy_static! { 18 | static ref COMPUTER_HEADER: Vec<&'static str> = vec!["creator_sid"]; 19 | } 20 | &COMPUTER_HEADER 21 | } 22 | } 23 | 24 | impl SpecificObjectAttributes for SpecificComputerAttributes { 25 | fn from(record: &crate::ntds::DataTableRecord) -> anyhow::Result { 26 | let creator_sid = record.att_creator_sid_opt()?; 27 | Ok(Self { creator_sid }) 28 | } 29 | 30 | fn serialize_to(&self, s: &mut S::SerializeStruct) -> Result<(), S::Error> 31 | where 32 | S: serde::Serializer, 33 | { 34 | s.serialize_field("creator_sid", &self.creator_sid)?; 35 | Ok(()) 36 | } 37 | } 38 | 39 | pub type Computer = Object; 40 | -------------------------------------------------------------------------------- /src/ntds/object/object_group.rs: -------------------------------------------------------------------------------- 1 | use crate::ntds::{types, NoSpecificAttributes, Object}; 2 | 3 | 4 | pub type Group = Object; -------------------------------------------------------------------------------- /src/ntds/object/object_person.rs: -------------------------------------------------------------------------------- 1 | use crate::ntds::{types, NoSpecificAttributes, Object}; 2 | 3 | 4 | pub type Person = Object; -------------------------------------------------------------------------------- /src/ntds/object/specific_object_attribute.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::ntds::DataTableRecord; 4 | 5 | use super::HasSerializableFields; 6 | 7 | 8 | pub trait SpecificObjectAttributes: for<'de> Deserialize<'de> + Serialize + HasSerializableFields { 9 | fn from(record: &DataTableRecord) -> anyhow::Result; 10 | fn serialize_to(&self, s: &mut S::SerializeStruct) -> Result<(), S::Error> where S: serde::Serializer; 11 | } 12 | -------------------------------------------------------------------------------- /src/ntds/object_type.rs: -------------------------------------------------------------------------------- 1 | use strum::{Display, EnumString, IntoStaticStr}; 2 | 3 | #[derive(IntoStaticStr, EnumString, Debug, Display, Eq, PartialEq, Hash, Clone, Copy)] 4 | pub enum ObjectType { 5 | Person, 6 | Group, 7 | Computer, 8 | } 9 | 10 | pub trait HasObjectType { 11 | fn object_type() -> ObjectType; 12 | } 13 | 14 | pub mod types { 15 | use super::{HasObjectType, ObjectType}; 16 | 17 | pub struct Person; 18 | pub struct Group; 19 | pub struct Computer; 20 | 21 | impl HasObjectType for Person { 22 | fn object_type() -> ObjectType { 23 | ObjectType::Person 24 | } 25 | } 26 | 27 | impl HasObjectType for Group { 28 | fn object_type() -> ObjectType { 29 | ObjectType::Group 30 | } 31 | } 32 | 33 | impl HasObjectType for Computer { 34 | fn object_type() -> ObjectType { 35 | ObjectType::Computer 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ntds/schema.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Index, collections::HashSet}; 2 | 3 | use getset::Getters; 4 | use hashbrown::HashMap; 5 | 6 | use crate::cache::{MetaDataCache, RecordPointer, SpecialRecords}; 7 | 8 | use super::ObjectType; 9 | 10 | #[derive(Getters)] 11 | #[getset(get="pub")] 12 | pub struct Schema { 13 | supported_type_entries: HashMap, 14 | all_type_entries: HashSet, 15 | } 16 | 17 | impl Schema { 18 | pub fn new(metadata: &MetaDataCache, special_records: &SpecialRecords) -> Self { 19 | let mut supported_type_entries = HashMap::new(); 20 | let mut all_type_entries = HashSet::new(); 21 | for record in metadata.children_of(special_records.schema().record_ptr()) { 22 | if let Ok(object_type) = ObjectType::try_from(&record.rdn().name()[..]) { 23 | supported_type_entries.insert(object_type, *record.record_ptr()); 24 | } 25 | all_type_entries.insert(*record.record_ptr()); 26 | } 27 | Self { supported_type_entries, all_type_entries } 28 | } 29 | } 30 | 31 | impl Index<&ObjectType> for Schema { 32 | type Output = RecordPointer; 33 | 34 | fn index(&self, index: &ObjectType) -> &Self::Output { 35 | &self.supported_type_entries[index] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ntds/sd_table.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use base64::prelude::*; 4 | 5 | use crate::{ 6 | cache::{self, ColumnIndex, Record, Value, WithValue}, 7 | win32_types::SecurityDescriptor, 8 | }; 9 | 10 | pub struct SdTable { 11 | descriptors: HashMap>, 12 | } 13 | 14 | impl SdTable { 15 | pub fn new(sd_table: &cache::SdTable) -> crate::ntds::Result { 16 | let sd_id_column = sd_table.sd_id_column(); 17 | let sd_value_column = sd_table.sd_value_column(); 18 | 19 | let descriptors: crate::ntds::Result>> = sd_table 20 | .iter() 21 | .map(|record| Self::descriptor_from_record(&record, sd_id_column, sd_value_column)) 22 | .collect(); 23 | Ok(Self { 24 | descriptors: descriptors?, 25 | }) 26 | } 27 | 28 | fn descriptor_from_record( 29 | record: &Record<'_, '_>, 30 | sd_id_column: &ColumnIndex, 31 | sd_value_column: &ColumnIndex, 32 | ) -> crate::ntds::Result<(i64, Vec)> { 33 | Ok(( 34 | Self::sd_id_from_record(record, sd_id_column)?, 35 | Self::sd_value_from_record(record, sd_value_column)?, 36 | )) 37 | } 38 | 39 | fn sd_id_from_record( 40 | record: &Record<'_, '_>, 41 | sd_id_column: &ColumnIndex, 42 | ) -> crate::ntds::Result { 43 | record.with_value(*sd_id_column, |v| match v.unwrap() { 44 | Value::I16(v) => Ok(i64::from(*v)), 45 | Value::I32(v) => Ok(i64::from(*v)), 46 | Value::I64(v) => Ok(*v), 47 | Value::Currency(v) => Ok(*v), 48 | v => unimplemented!("no support for {v} as sd_id"), 49 | }) 50 | } 51 | 52 | fn sd_value_from_record( 53 | record: &Record<'_, '_>, 54 | sd_value_column: &ColumnIndex, 55 | ) -> crate::ntds::Result> { 56 | record.with_value(*sd_value_column, |v| match v.unwrap() { 57 | Value::Long(v) | Value::Binary(v) | Value::LargeBinary(v) => Ok(v.as_ref().clone()), 58 | v => unimplemented!("no support for {v} as sd_value"), 59 | }) 60 | } 61 | 62 | pub fn descriptor( 63 | &self, 64 | sd_id: &i64, 65 | ) -> Option> { 66 | self.descriptors 67 | .get(sd_id) 68 | .map(|v| match SecurityDescriptor::try_from(&v[..]) { 69 | Ok(sd) => Ok(sd), 70 | Err(why) => { 71 | log::error!("failed descriptor was: {}", BASE64_STANDARD.encode(v)); 72 | log::error!("{why}"); 73 | Err(why) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/object_tree.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use std::{ 3 | collections::HashMap, 4 | rc::{Rc, Weak}, 5 | }; 6 | use termtree::Tree; 7 | 8 | use crate::{ 9 | cache::{MetaDataCache, RecordPointer, SpecialRecords}, 10 | ntds::SdTable, 11 | object_tree_entry::ObjectTreeEntry, 12 | }; 13 | 14 | pub struct ObjectTree { 15 | root: Rc, 16 | record_index: HashMap>, 17 | } 18 | 19 | impl ObjectTree { 20 | pub fn new(metadata: &MetaDataCache, sd_table: Option>) -> Self { 21 | let mut record_index = HashMap::new(); 22 | let root = ObjectTreeEntry::populate_object_tree(metadata, sd_table, &mut record_index); 23 | Self { 24 | root, 25 | record_index, 26 | } 27 | } 28 | 29 | pub fn get_special_records(&self) -> anyhow::Result { 30 | log::info!("obtaining special record ids"); 31 | 32 | let domain_root = ObjectTreeEntry::find_domain_root(&self.root) 33 | .ok_or(anyhow!("db has no domain root"))?; 34 | 35 | log::info!("found domain root '{}'", domain_root[0].name()); 36 | 37 | let configuration = domain_root[0] 38 | .find_child_by_name("Configuration") 39 | .ok_or(anyhow!("db has no `Configuration` entry"))?; 40 | 41 | let schema_subpath = configuration 42 | .find_child_by_name("Schema") 43 | .ok_or(anyhow!("db has no `Schema` entry"))?; 44 | 45 | let deleted_objects = domain_root[0] 46 | .find_child_by_name("Deleted Objects") 47 | .ok_or(anyhow!("db has no `Deleted Objects` entry"))?; 48 | 49 | Ok(SpecialRecords::new(schema_subpath, deleted_objects)) 50 | } 51 | 52 | pub(crate) fn to_termtree(&self, max_depth: u8) -> Tree> { 53 | Self::__to_termtree(&self.root, max_depth) 54 | } 55 | 56 | pub fn __to_termtree(me: &Rc, max_depth: u8) -> Tree> { 57 | let tree = Tree::new(Rc::clone(me)); 58 | if max_depth > 0 { 59 | let leaves: Vec>> = me 60 | .children() 61 | .borrow() 62 | .iter() 63 | .map(|c| Self::__to_termtree(c, max_depth - 1)) 64 | .collect(); 65 | tree.with_leaves(leaves) 66 | } else { 67 | tree 68 | } 69 | } 70 | 71 | pub fn dn_of(&self, ptr: &RecordPointer) -> Option { 72 | match self.record_index.get(ptr) { 73 | Some(record) => Some( 74 | record 75 | .upgrade() 76 | .unwrap_or_else(|| { 77 | panic!("record pointer {ptr} points to already deleted object") 78 | }) 79 | .distinguished_name() 80 | .clone(), 81 | ), 82 | None => { 83 | log::error!("Missing entry {ptr} in the data_table. This might happen if there is an inconsistency in the link_table. I'll ignore this reference"); 84 | None 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/object_tree_entry.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use std::{ 3 | cell::RefCell, 4 | collections::{HashMap, HashSet}, 5 | fmt::Display, 6 | hash::Hash, 7 | rc::{Rc, Weak}, 8 | }; 9 | 10 | use getset::Getters; 11 | use lazy_static::lazy_static; 12 | 13 | use crate::{ 14 | cache::{MetaDataCache, RecordPointer, SpecialRecords}, 15 | ntds::SdTable, 16 | win32_types::{Rdn, SecurityDescriptor}, 17 | }; 18 | lazy_static! { 19 | static ref DOMAINROOT_CHILDREN: HashSet = HashSet::from_iter(vec![ 20 | "Deleted Objects".to_string(), 21 | "Configuration".to_string(), 22 | "Builtin".to_string(), 23 | //"DomainDnsZones".to_string(), 24 | "NTDS Quotas".to_string() 25 | ].into_iter()); 26 | } 27 | 28 | /// represents an object in the DIT 29 | #[derive(Getters)] 30 | #[getset(get = "pub")] 31 | pub struct ObjectTreeEntry { 32 | name: Rdn, 33 | relative_distinguished_name: String, 34 | distinguished_name: String, 35 | record_ptr: RecordPointer, 36 | _sddl: Option>, 37 | //parent: Option>, 38 | children: RefCell>>, 39 | parent: Option>, 40 | } 41 | 42 | impl Eq for ObjectTreeEntry {} 43 | 44 | impl PartialEq for ObjectTreeEntry { 45 | fn eq(&self, other: &Self) -> bool { 46 | self.name == other.name && self.record_ptr == other.record_ptr 47 | } 48 | } 49 | 50 | impl Hash for ObjectTreeEntry { 51 | fn hash(&self, state: &mut H) { 52 | self.name.hash(state); 53 | self.record_ptr.hash(state); 54 | } 55 | } 56 | 57 | impl Display for ObjectTreeEntry { 58 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 59 | let is_deleted = self.name().deleted_from_container().is_some(); 60 | let display_name = self.relative_distinguished_name(); 61 | /* 62 | let sddl = self 63 | .sddl() 64 | .as_ref() 65 | .map(|sddl| match sddl { 66 | Ok(sddl) => format!(";{sddl}"), 67 | Err(_why) => format!(";sddl-error: {_why}") 68 | } 69 | ) 70 | .unwrap_or_default(); 71 | */ 72 | let sddl = ""; 73 | let flags = if is_deleted { "DELETED; " } else { "" }; 74 | 75 | write!(f, "{display_name} ({flags}{}{sddl})", self.record_ptr) 76 | } 77 | } 78 | 79 | impl ObjectTreeEntry { 80 | pub fn get(&self, rdn: &str) -> Option> { 81 | log::debug!("searching for {rdn}"); 82 | for child in self.children.borrow().iter() { 83 | log::debug!(" candidate is {}", child.name); 84 | if child.name.name() == rdn { 85 | return Some(Rc::clone(child)); 86 | } 87 | } 88 | None 89 | } 90 | 91 | pub(crate) fn populate_object_tree( 92 | metadata: &MetaDataCache, 93 | sd_table: Option>, 94 | record_index: &mut HashMap>, 95 | ) -> Rc { 96 | log::info!("populating the object tree"); 97 | Self::create_tree_node( 98 | metadata.root(), 99 | metadata, 100 | sd_table.as_deref(), 101 | None, 102 | record_index, 103 | ) 104 | } 105 | 106 | fn create_tree_node( 107 | record_ptr: &RecordPointer, 108 | metadata: &MetaDataCache, 109 | sd_table: Option<&SdTable>, 110 | parent: Option>, 111 | record_index: &mut HashMap>, 112 | ) -> Rc { 113 | let entry = &metadata[record_ptr]; 114 | let name = entry.rdn().to_owned(); 115 | let relative_distinguished_name = metadata.rdn(entry); 116 | 117 | let distinguished_name = match &parent { 118 | Some(parent) => match parent.upgrade() { 119 | Some(parent) => { 120 | if parent.parent.is_none() { 121 | log::debug!("hiding the $ROOT_OBJECT$ item"); 122 | relative_distinguished_name.clone() 123 | } else { 124 | format!( 125 | "{relative_distinguished_name},{}", 126 | parent.distinguished_name() 127 | ) 128 | } 129 | } 130 | None => { 131 | panic!( 132 | "unable to upgrade weak link to parent object; there \ 133 | seems to be an inconsistency in the object tree" 134 | ); 135 | } 136 | }, 137 | None => { 138 | log::debug!("found the object tree root"); 139 | relative_distinguished_name.clone() 140 | } 141 | }; 142 | 143 | let _sddl = sd_table.and_then(|sd_table| { 144 | entry 145 | .sd_id() 146 | .as_ref() 147 | .and_then(|sd_id| sd_table.descriptor(sd_id)) 148 | }); 149 | 150 | let me = Rc::new(ObjectTreeEntry { 151 | name, 152 | relative_distinguished_name, 153 | distinguished_name, 154 | record_ptr: *record_ptr, 155 | children: RefCell::new(HashSet::new()), 156 | parent, 157 | _sddl, 158 | }); 159 | 160 | record_index.insert(*record_ptr, Rc::downgrade(&me)); 161 | 162 | // [`ObjectTreeEntry`] uses interior mutability, but its hash()-Implementation 163 | // don't use the mutable parts, so this is not a problem 164 | #[allow(clippy::mutable_key_type)] 165 | let children: HashSet<_> = metadata 166 | .children_of(record_ptr) 167 | .map(|c| { 168 | Self::create_tree_node( 169 | c.record_ptr(), 170 | metadata, 171 | sd_table, 172 | Some(Rc::downgrade(&me)), 173 | record_index, 174 | ) 175 | }) 176 | .collect(); 177 | me.children.replace_with(|_| children); 178 | me 179 | } 180 | 181 | pub fn get_special_records(root: Rc) -> anyhow::Result { 182 | log::info!("obtaining special record ids"); 183 | 184 | let domain_root = 185 | ObjectTreeEntry::find_domain_root(&root).ok_or(anyhow!("db has no domain root"))?; 186 | 187 | log::info!("found domain root '{}'", domain_root[0].name()); 188 | 189 | let configuration = domain_root[0] 190 | .find_child_by_name("Configuration") 191 | .ok_or(anyhow!("db has no `Configuration` entry"))?; 192 | 193 | let schema_subpath = configuration 194 | .find_child_by_name("Schema") 195 | .ok_or(anyhow!("db has no `Schema` entry"))?; 196 | 197 | let deleted_objects = domain_root[0] 198 | .find_child_by_name("Deleted Objects") 199 | .ok_or(anyhow!("db has no `Deleted Objects` entry"))?; 200 | 201 | Ok(SpecialRecords::new(schema_subpath, deleted_objects)) 202 | } 203 | 204 | /// returns the path to the domain root object, where the first entry in the list is the domain root object, 205 | /// and the last object is the root of the tree 206 | pub fn find_domain_root(root: &Rc) -> Option>> { 207 | let my_children: HashSet<_> = root 208 | .children() 209 | .borrow() 210 | .iter() 211 | .map(|o| o.name().to_string()) 212 | .collect(); 213 | 214 | if my_children.is_superset(&DOMAINROOT_CHILDREN) { 215 | return Some(vec![Rc::clone(root)]); 216 | } else { 217 | for child in root.children().borrow().iter() { 218 | if let Some(mut path) = Self::find_domain_root(child) { 219 | path.push(Rc::clone(root)); 220 | return Some(path); 221 | } 222 | } 223 | } 224 | 225 | None 226 | } 227 | 228 | pub fn find_child_by_name(&self, name: &str) -> Option> { 229 | self.children() 230 | .borrow() 231 | .iter() 232 | .find(|e| e.name().name() == name) 233 | .cloned() 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/progress_bar.rs: -------------------------------------------------------------------------------- 1 | use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; 2 | 3 | #[allow(dead_code)] 4 | pub(crate) fn create_progressbar(message: String, count: u64) -> anyhow::Result { 5 | let bar = indicatif::ProgressBar::new(count); 6 | let target = ProgressDrawTarget::stderr_with_hz(10); 7 | bar.set_draw_target(target); 8 | 9 | let progress_style = ProgressStyle::default_bar() 10 | .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>9}/{len:9}({percent}%) {msg}")? 11 | .progress_chars("##-"); 12 | bar.set_style(progress_style); 13 | bar.set_message(message); 14 | Ok(bar) 15 | } 16 | -------------------------------------------------------------------------------- /src/record_pointer.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use libesedb::Record; 4 | 5 | #[derive(Clone)] 6 | pub struct RecordPointer { 7 | pointer: i32 8 | } 9 | 10 | impl<'r> From for Record<'r> { 11 | 12 | } -------------------------------------------------------------------------------- /src/record_predicate.rs: -------------------------------------------------------------------------------- 1 | use crate::{cache::RecordId, ntds::DataTableRecord, win32_types::Rdn}; 2 | 3 | pub trait RecordPredicate<'info, 'db> { 4 | fn matches(&self, record: &DataTableRecord<'info, 'db>) -> bool; 5 | } 6 | 7 | pub struct RecordHasRid(pub u32); 8 | 9 | impl<'info, 'db> RecordPredicate<'info, 'db> for RecordHasRid { 10 | fn matches(&self, record: &DataTableRecord<'info, 'db>) -> bool { 11 | match record.att_object_sid() { 12 | Ok(sid) => sid.get_rid() == &self.0, 13 | _ => false, 14 | } 15 | } 16 | } 17 | 18 | pub struct RecordHasParent(pub RecordId); 19 | 20 | impl<'info, 'db> RecordPredicate<'info, 'db> for RecordHasParent { 21 | fn matches(&self, record: &DataTableRecord<'info, 'db>) -> bool { 22 | log::debug!( 23 | "searching children of {}; current is {:?}", 24 | self.0, 25 | record.ds_parent_record_id() 26 | ); 27 | match record.ds_parent_record_id() { 28 | Ok(r) => r == self.0, 29 | _ => false, 30 | } 31 | } 32 | } 33 | 34 | pub struct RecordHasAttRdn(pub Rdn); 35 | 36 | impl<'info, 'db> RecordPredicate<'info, 'db> for RecordHasAttRdn { 37 | fn matches(&self, record: &DataTableRecord<'info, 'db>) -> bool { 38 | match record.att_object_name2() { 39 | Ok(r) => r == self.0, 40 | _ => false, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/value/bool.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | use crate::ntds::Error; 4 | 5 | use super::FromValue; 6 | 7 | impl FromValue for bool { 8 | fn from_value_opt(value: &Value) -> Result, Error> 9 | where 10 | Self: Sized, 11 | { 12 | match value { 13 | Value::Null(_) => Ok(None), 14 | Value::U8(val) => Ok(Some(*val == 1)), 15 | Value::U16(val) => Ok(Some(*val == 1)), 16 | Value::U32(val) => Ok(Some(*val == 1)), 17 | Value::I16(val) => Ok(Some(*val == 1)), 18 | Value::I32(val) => Ok(Some(*val == 1)), 19 | _ => Err(Error::InvalidValueDetected(value.to_string(), "bool (one of u8, u16, u32, i16 or i32)")), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/value/from_value.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::{ColumnIndex, Value}; 2 | use crate::ntds::Error; 3 | 4 | pub trait FromValue { 5 | /// does the same as [`from_value_opt`], but returns an Error instead of `None`, if no value was found 6 | fn from_value(value: &Value) -> crate::ntds::Result 7 | where 8 | Self: Sized, 9 | { 10 | Self::from_value_opt(value)?.ok_or(Error::ValueIsMissing) 11 | } 12 | 13 | /// converts the value into the requested type, if possible 14 | fn from_value_opt(value: &Value) -> crate::ntds::Result> 15 | where 16 | Self: Sized; 17 | 18 | fn from_record_opt( 19 | record: &libesedb::Record, 20 | record_id: &ColumnIndex, 21 | ) -> crate::ntds::Result> 22 | where 23 | Self: Sized, 24 | { 25 | if record.is_long(**record_id)? { 26 | let v = record.long(**record_id)?; 27 | let value = match v.variant() { 28 | libesedb::Value::Null(_) => Value::Null(()), 29 | libesedb::Value::Bool(_) => panic!("A Bool should not be stored as Long"), 30 | libesedb::Value::U8(_) => panic!("A U8 should not be stored as Long"), 31 | libesedb::Value::I16(_) => panic!("An I16 should not be stored as Long"), 32 | libesedb::Value::I32(_) => panic!("An I32 should not be stored as Long"), 33 | libesedb::Value::Currency(_) => panic!("A Currency should not be stored as Long"), 34 | libesedb::Value::F32(_) => panic!("A F32 should not be stored as Long"), 35 | libesedb::Value::F64(_) => panic!("A F64 should not be stored as Long"), 36 | libesedb::Value::DateTime(_) => panic!("A DateTime should not be stored as Long"), 37 | libesedb::Value::Binary(_) => Value::Binary(Box::new(v.vec()?)), 38 | libesedb::Value::Text(_) => Value::Text(Box::new(v.utf8()?)), 39 | libesedb::Value::LargeBinary(_) => Value::LargeBinary(Box::new(v.vec()?)), 40 | libesedb::Value::LargeText(_) => Value::LargeText(Box::new(v.utf8()?)), 41 | libesedb::Value::SuperLarge(_) => Value::SuperLarge(Box::new(v.vec()?)), 42 | libesedb::Value::U32(_) => panic!("A U32 should not be stored as Long"), 43 | libesedb::Value::I64(_) => panic!("An I64 should not be stored as Long"), 44 | libesedb::Value::Guid(_) => Value::Guid(Box::new(v.vec()?)), 45 | libesedb::Value::U16(_) => panic!("A U16 should not be stored as Long"), 46 | libesedb::Value::Long => panic!("A Long inside of a Long? This should not happen!"), 47 | libesedb::Value::Multi => { 48 | panic!("A Multi inside of a Long? This should not happen!") 49 | } 50 | }; 51 | Ok(Self::from_value_opt(&value)?) 52 | } else if record.is_multi(**record_id)? { 53 | let v = record.multi(**record_id)?; 54 | let mut values = Vec::new(); 55 | for value in v.iter_values()? { 56 | values.push(Value::from(value?)); 57 | } 58 | Ok(Self::from_value_opt(&Value::Multi(values))?) 59 | } else { 60 | Ok(Self::from_value_opt(&Value::from( 61 | record.value(**record_id)?, 62 | ))?) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/value/i32.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | use crate::ntds::Error; 4 | 5 | use super::FromValue; 6 | 7 | impl FromValue for i32 { 8 | fn from_value_opt(value: &Value) -> Result, Error> 9 | where 10 | Self: Sized, 11 | { 12 | match value { 13 | Value::I32(val) => Ok(Some(*val)), 14 | Value::Null(()) => Ok(None), 15 | _ => Err(Error::InvalidValueDetected(value.to_string(), "i32")), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/value/i64.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | use crate::ntds::Error; 4 | 5 | use super::FromValue; 6 | 7 | impl FromValue for i64 { 8 | fn from_value_opt(value: &Value) -> Result, Error> 9 | where 10 | Self: Sized, 11 | { 12 | match value { 13 | Value::I16(val) => Ok(Some(i64::from(*val))), 14 | Value::I32(val) => Ok(Some(i64::from(*val))), 15 | Value::I64(val) => Ok(Some(*val)), 16 | Value::Currency(val) => Ok(Some(*val)), 17 | Value::Binary(v) | Value::LargeBinary(v) if v.len() == 8 => { 18 | Ok(Some(i64::from_le_bytes([ 19 | v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], 20 | ]))) 21 | } 22 | Value::Null(()) => Ok(None), 23 | _ => panic!("test"), //Err(Error::InvalidValueDetected(value.to_string(), "i64")), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/value/mod.rs: -------------------------------------------------------------------------------- 1 | mod bool; 2 | mod from_value; 3 | mod i32; 4 | mod i64; 5 | mod sam_account_type; 6 | mod sid; 7 | mod string; 8 | mod u32; 9 | mod user_acount_control; 10 | mod to_string; 11 | 12 | pub use from_value::*; 13 | //pub use bool::*; 14 | //pub use i32::*; 15 | //pub use sam_account_type::*; 16 | //pub use sid::*; 17 | //pub use string::*; 18 | //pub use u32::*; 19 | //pub use user_acount_control::*; 20 | pub use to_string::*; 21 | -------------------------------------------------------------------------------- /src/value/sam_account_type.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | use num_traits::FromPrimitive; 3 | 4 | use crate::{ntds::Error, win32_types::SamAccountType}; 5 | 6 | use super::FromValue; 7 | 8 | impl FromValue for SamAccountType { 9 | fn from_value_opt(value: &Value) -> Result, Error> { 10 | match value { 11 | Value::I32(val) => Ok(FromPrimitive::from_u32(u32::from_ne_bytes( 12 | val.to_ne_bytes(), 13 | ))), 14 | Value::Null(()) => Ok(None), 15 | _ => Err(Error::InvalidValueDetected(value.to_string(), "SamAccountType (i32)")), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/value/sid.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | use crate::{ntds::Error, win32_types::Sid}; 4 | 5 | use super::FromValue; 6 | 7 | impl FromValue for Sid { 8 | fn from_value_opt(value: &Value) -> Result, Error> 9 | where 10 | Self: Sized, 11 | { 12 | match value { 13 | Value::Binary(val) | Value::LargeBinary(val) => { 14 | Ok(Some(Sid::try_from(val.as_ref()).map_err(|why| Error::MiscConversionError { 15 | value: value.to_string(), 16 | intended_type: "Sid", 17 | why, 18 | })?)) 19 | } 20 | Value::Null(()) => Ok(None), 21 | _ => Err(Error::InvalidValueDetected(value.to_string(), "Sid (binary)")), 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/value/string.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | use crate::ntds::Error; 4 | 5 | use super::FromValue; 6 | 7 | impl FromValue for String { 8 | fn from_value_opt(value: &Value) -> Result, Error> 9 | where 10 | Self: Sized, 11 | { 12 | match value { 13 | Value::Text(val) => Ok(Some(val.as_ref().to_owned())), 14 | Value::LargeText(val) => Ok(Some(val.as_ref().to_owned())), 15 | Value::Binary(val) | Value::LargeBinary(val) => Ok(Some(hex::encode(val.as_ref()))), 16 | Value::Null(()) => Ok(None), 17 | _ => Err(Error::InvalidValueDetected(value.to_string(), "String (one of (text, largetext or binary)")), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/value/to_string.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | pub trait ToString { 4 | fn to_string(&self) -> String; 5 | } 6 | 7 | impl ToString for Value { 8 | fn to_string(&self) -> String { 9 | match self { 10 | Value::Null(()) => panic!("unreachable code executed"), 11 | Value::Bool(v) => format!("{v}"), 12 | Value::U8(v) => format!("{v}"), 13 | Value::U16(v) => format!("{v}"), 14 | Value::U32(v) => format!("{v}"), 15 | Value::I16(v) => format!("{v}"), 16 | Value::I32(v) => format!("{v}"), 17 | Value::I64(v) => format!("{v}"), 18 | Value::F32(v) => format!("{v}"), 19 | Value::F64(v) => format!("{v}"), 20 | Value::Currency(v) => format!("{v}"), 21 | Value::DateTime(v) => format!("{v}"), 22 | Value::Binary(v) => hex::encode(v.as_ref()).to_string(), 23 | Value::Text(v) => v.as_ref().to_owned(), 24 | Value::LargeBinary(v) => hex::encode(v.as_ref()).to_string(), 25 | Value::LargeText(v) => v.as_ref().to_owned(), 26 | Value::SuperLarge(v) => hex::encode(v.as_ref()).to_string(), 27 | Value::Guid(v) => hex::encode(v.as_ref()).to_string(), 28 | Value::Long(_) => "Long".to_string(), 29 | Value::Multi(_) => "Multi".to_string(), 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/value/u32.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | use crate::ntds::Error; 4 | 5 | use super::FromValue; 6 | 7 | impl FromValue for u32 { 8 | fn from_value_opt(value: &Value) -> Result, Error> 9 | where 10 | Self: Sized, 11 | { 12 | match value { 13 | Value::U8(val) => Ok(Some((*val).into())), 14 | Value::U16(val) => Ok(Some((*val).into())), 15 | Value::U32(val) => Ok(Some(*val)), 16 | Value::I16(val) => Ok(Some((*val).try_into().map_err(|why| Error::IntegerConversionError { 17 | value: value.to_string(), 18 | intended_type: "i16", 19 | why, 20 | })?)), 21 | Value::I32(val) => Ok(Some((*val).try_into().map_err(|why| Error::IntegerConversionError { 22 | value: value.to_string(), 23 | intended_type: "i16", 24 | why, 25 | })?)), 26 | Value::Null(()) => Ok(None), 27 | _ => Err(Error::InvalidValueDetected(value.to_string(), "u32 (or one of u8, u16, i16 or i32)")), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/value/user_acount_control.rs: -------------------------------------------------------------------------------- 1 | use crate::cache::Value; 2 | 3 | use crate::{ntds::Error, win32_types::UserAccountControl}; 4 | 5 | use super::FromValue; 6 | 7 | impl FromValue for UserAccountControl { 8 | fn from_value_opt(value: &Value) -> Result, Error> 9 | where 10 | Self: Sized, 11 | { 12 | match value { 13 | Value::I32(val) => Ok(Some(::from_bits_truncate( 14 | u32::from_ne_bytes(val.to_ne_bytes()), 15 | ))), 16 | Value::Null(()) => Ok(None), 17 | _ => Err(Error::InvalidValueDetected(value.to_string(), "UserAccountControl (i32)")), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/win32_types/guid.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, fmt::Display}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | use crate::{cache::Value, value::FromValue}; 7 | 8 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Hash)] 9 | pub struct Guid(Uuid); 10 | 11 | impl FromValue for Guid { 12 | fn from_value_opt(value: &crate::cache::Value) -> crate::ntds::Result> 13 | where 14 | Self: Sized, 15 | { 16 | match value { 17 | Value::Null(_) => Ok(None), 18 | Value::Binary(v) | Value::LargeBinary(v) | Value::Guid(v) => { 19 | Ok(Some(Self(Uuid::from_slice_le(&v[..])?))) 20 | } 21 | v => { 22 | log::error!("I don't know how to extract GUIDs from {v}"); 23 | Ok(None) 24 | } 25 | } 26 | } 27 | } 28 | 29 | impl FromStr for Guid { 30 | type Err = uuid::Error; 31 | 32 | fn from_str(s: &str) -> Result { 33 | Ok(Self(Uuid::from_str(s)?)) 34 | } 35 | } 36 | 37 | impl Display for Guid { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | self.0.fmt(f) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/win32_types/mod.rs: -------------------------------------------------------------------------------- 1 | mod sam_account_type; 2 | mod user_account_control; 3 | mod sid; 4 | mod timestamp; 5 | mod rdn; 6 | mod guid; 7 | mod security_descriptor; 8 | 9 | pub use sam_account_type::*; 10 | pub use user_account_control::*; 11 | pub use sid::*; 12 | pub use timestamp::*; 13 | pub use rdn::*; 14 | pub use guid::*; 15 | pub use security_descriptor::*; -------------------------------------------------------------------------------- /src/win32_types/rdn.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use std::str::FromStr; 3 | 4 | use anyhow::bail; 5 | use getset::Getters; 6 | use lazy_regex::regex_captures; 7 | use serde::de::{Error, Visitor}; 8 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 9 | 10 | use crate::cache::Value; 11 | use crate::ntds; 12 | use crate::value::FromValue; 13 | 14 | use super::Guid; 15 | 16 | #[derive(Getters, Eq, PartialEq, Clone, Hash)] 17 | #[getset(get = "pub")] 18 | pub struct Rdn { 19 | name: String, 20 | deleted_from_container: Option, //TODO: should by a UUID 21 | conflicting_objects: Vec, //TODO: should be UUIDs 22 | } 23 | 24 | impl Display for Rdn { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 26 | match &self.deleted_from_container { 27 | Some(_guid) => write!(f, "{} (DELETED)", self.name), 28 | None => self.name.fmt(f), 29 | } 30 | } 31 | } 32 | 33 | impl FromValue for Rdn { 34 | fn from_value_opt(value: &Value) -> ntds::Result> 35 | where 36 | Self: Sized, 37 | { 38 | match value { 39 | Value::Text(s) | Value::LargeText(s) => { 40 | let mut lines = s.lines(); 41 | let name = lines.next().unwrap().to_string(); 42 | let mut deleted_from_container = None; 43 | let mut conflicting_objects = Vec::new(); 44 | 45 | for line in lines { 46 | if let Some(guid) = line.strip_prefix("DEL:") { 47 | if deleted_from_container.is_some() { 48 | return Err(ntds::Error::InvalidValueDetected(value.to_string(), "Rdn (text)")); 49 | } 50 | deleted_from_container = Some(Guid::from_str(guid)?); 51 | } else if let Some(guid) = line.strip_prefix("CNF:") { 52 | conflicting_objects.push(Guid::from_str(guid)?); 53 | } else { 54 | log::warn!("unexpected value in Rdn field: '{line}'"); 55 | } 56 | } 57 | 58 | Ok(Some(Self { 59 | name, 60 | deleted_from_container, 61 | conflicting_objects, 62 | })) 63 | } 64 | Value::Null(()) => Ok(None), 65 | Value::Long(_) => { 66 | log::warn!("no support for LONG columns yet, generating a random value"); 67 | Ok(Some(Self{ 68 | name: uuid::Uuid::new_v4().to_string(), 69 | deleted_from_container: None, 70 | conflicting_objects: Vec::new(), 71 | })) 72 | } 73 | _ => Err(ntds::Error::InvalidValueDetected(value.to_string(), "Rdn (text)")), 74 | } 75 | } 76 | } 77 | 78 | impl TryFrom<&str> for Rdn { 79 | type Error = anyhow::Error; 80 | 81 | fn try_from(v: &str) -> Result { 82 | match regex_captures!(r#"^(?P.*)( -- DEL:(?P\d+))?$"#, v) { 83 | None => bail!("invalid object name: '{v}'"), 84 | Some((_, name, _, deleted_from_container)) => { 85 | if deleted_from_container.is_empty() { 86 | Ok(Rdn { 87 | name: name.to_owned(), 88 | deleted_from_container: None, 89 | conflicting_objects: Vec::new(), 90 | }) 91 | } else { 92 | Ok(Rdn { 93 | name: name.to_owned(), 94 | deleted_from_container: Some(Guid::from_str(deleted_from_container)?), 95 | conflicting_objects: Vec::new(), 96 | }) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | impl TryFrom for Rdn { 104 | type Error = anyhow::Error; 105 | 106 | fn try_from(v: String) -> Result { 107 | Self::try_from(&v[..]) 108 | } 109 | } 110 | 111 | impl Serialize for Rdn { 112 | fn serialize(&self, serializer: S) -> Result 113 | where 114 | S: Serializer, 115 | { 116 | if let Some(guid) = &self.deleted_from_container { 117 | serializer.serialize_str(&format!("{} -- DEL:{guid}", self.name)) 118 | } else { 119 | serializer.serialize_str(&self.name) 120 | } 121 | } 122 | } 123 | 124 | impl<'de> Deserialize<'de> for Rdn { 125 | fn deserialize(deserializer: D) -> Result 126 | where 127 | D: Deserializer<'de>, 128 | { 129 | deserializer.deserialize_string(NameWithGuidVisitor::default()) 130 | } 131 | } 132 | 133 | #[derive(Default)] 134 | pub struct NameWithGuidVisitor {} 135 | 136 | impl<'de> Visitor<'de> for NameWithGuidVisitor { 137 | type Value = Rdn; 138 | 139 | fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { 140 | write!( 141 | formatter, 142 | "a string, possibly containing information about the original container" 143 | ) 144 | } 145 | fn visit_str(self, v: &str) -> Result 146 | where 147 | E: Error, 148 | { 149 | Rdn::try_from(v).or(Err(E::custom(format!("invalid object name: '{v}'")))) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/win32_types/sam_account_type.rs: -------------------------------------------------------------------------------- 1 | use num_derive::FromPrimitive; 2 | use serde::{Deserialize, Serialize}; 3 | use strum::EnumString; 4 | 5 | #[derive(EnumString, FromPrimitive, Deserialize, Serialize, PartialEq, Eq)] 6 | #[allow(non_camel_case_types)] 7 | pub enum SamAccountType { 8 | SAM_GROUP_OBJECT = 0x10000000, 9 | SAM_NON_SECURITY_GROUP_OBJECT = 0x10000001, 10 | SAM_ALIAS_OBJECT = 0x20000000, 11 | SAM_NON_SECURITY_ALIAS_OBJECT = 0x20000001, 12 | SAM_USER_OBJECT = 0x30000000, 13 | SAM_MACHINE_ACCOUNT = 0x30000001, 14 | SAM_TRUST_ACCOUNT = 0x30000002, 15 | SAM_APP_BASIC_GROUP = 0x40000000, 16 | SAM_APP_QUERY_GROUP = 0x40000001, 17 | } 18 | -------------------------------------------------------------------------------- /src/win32_types/security_descriptor.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::value::FromValue; 4 | 5 | pub struct SecurityDescriptor(sddl::SecurityDescriptor); 6 | 7 | impl FromValue for SecurityDescriptor { 8 | fn from_value_opt(value: &crate::cache::Value) -> crate::ntds::Result> 9 | where 10 | Self: Sized { 11 | match value { 12 | crate::cache::Value::Null(_) => Ok(None), 13 | crate::cache::Value::Binary(vec) | crate::cache::Value::LargeBinary(vec) => 14 | { 15 | Ok(Some(Self(sddl::SecurityDescriptor::from_bytes(&vec[..])?))) 16 | } 17 | v => { 18 | log::error!("I don't know how to extract a security descriptor from {v}"); 19 | Ok(None) 20 | } 21 | } 22 | } 23 | } 24 | 25 | impl Display for SecurityDescriptor { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | self.0.fmt(f) 28 | } 29 | } 30 | 31 | impl TryFrom<&[u8]> for SecurityDescriptor { 32 | type Error = crate::ntds::Error; 33 | 34 | fn try_from(value: &[u8]) -> Result { 35 | Ok(Self(sddl::SecurityDescriptor::try_from(value)?)) 36 | } 37 | } 38 | 39 | impl AsRef for SecurityDescriptor { 40 | fn as_ref(&self) -> &sddl::SecurityDescriptor { 41 | &self.0 42 | } 43 | } -------------------------------------------------------------------------------- /src/win32_types/sid/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, io::Cursor}; 2 | 3 | use anyhow::{ensure, Result}; 4 | use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | mod sid_visitor; 8 | 9 | /// 10 | /// https://devblogs.microsoft.com/oldnewthing/20040315-00/?p=40253 11 | #[derive(PartialEq, Eq, Clone)] 12 | pub struct Sid { 13 | revision: u8, 14 | authority: u64, 15 | numbers: Vec, 16 | } 17 | 18 | impl Sid { 19 | pub fn get_rid(&self) -> &u32 { 20 | self.numbers.last().unwrap() 21 | } 22 | pub fn new(revision: u8, authority: u64, numbers: Vec) -> Self { 23 | Self { 24 | revision, 25 | authority, 26 | numbers, 27 | } 28 | } 29 | } 30 | 31 | impl TryFrom<&Vec> for Sid { 32 | type Error = anyhow::Error; 33 | 34 | fn try_from(val: &Vec) -> Result { 35 | let mut rdr = Cursor::new(val); 36 | let revision = rdr.read_u8()?; 37 | let number_of_dashes = rdr.read_u8()?; 38 | let authority = rdr.read_u48::()?; 39 | 40 | //log::debug!("authority: {:012x}", authority); 41 | 42 | let mut numbers = vec![]; 43 | for _i in 0..number_of_dashes - 1 { 44 | numbers.push(rdr.read_u32::()?); 45 | } 46 | numbers.push(rdr.read_u32::()?); 47 | ensure!(!numbers.is_empty(), "invalid SID format"); 48 | 49 | Ok(Self { 50 | revision, 51 | authority, 52 | numbers, 53 | }) 54 | } 55 | } 56 | 57 | impl Display for Sid { 58 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 59 | let numbers = self 60 | .numbers 61 | .iter() 62 | .map(|n| format!("{n}")) 63 | .collect::>() 64 | .join("-"); 65 | 66 | let revision = &self.revision; 67 | let authority = &self.authority; 68 | write!(f, "S-{revision}-{authority}-{numbers}") 69 | } 70 | } 71 | 72 | impl Serialize for Sid { 73 | fn serialize(&self, serializer: S) -> Result 74 | where 75 | S: serde::Serializer, 76 | { 77 | serializer.serialize_str(&format!("{self}")) 78 | } 79 | } 80 | 81 | impl<'de> Deserialize<'de> for Sid { 82 | fn deserialize(deserializer: D) -> std::prelude::v1::Result 83 | where 84 | D: serde::Deserializer<'de>, 85 | { 86 | deserializer.deserialize_str(sid_visitor::SIDVisitor::default()) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::Sid; 93 | 94 | #[test] 95 | fn test_deserialization() { 96 | let sample = r#""S-1-5-21-2623811015-3361044348-030300820-1013""#; 97 | let sid: Sid = serde_json::from_str(sample).unwrap(); 98 | assert_eq!(sid.revision, 1); 99 | assert_eq!(sid.authority, 5); 100 | assert_eq!( 101 | sid.numbers, 102 | vec![21, 2_623_811_015, 3_361_044_348, 30_300_820, 1013] 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/win32_types/sid/sid_visitor.rs: -------------------------------------------------------------------------------- 1 | use lazy_regex::regex_captures; 2 | use serde::de::Visitor; 3 | 4 | use super::Sid; 5 | 6 | #[derive(Default)] 7 | pub struct SIDVisitor {} 8 | 9 | impl<'de> Visitor<'de> for SIDVisitor { 10 | type Value = Sid; 11 | 12 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 13 | write!(formatter, "a string looking like a Windows Security ID") 14 | } 15 | 16 | fn visit_str(self, v: &str) -> Result 17 | where 18 | E: serde::de::Error, 19 | { 20 | match regex_captures!( 21 | r#"^S-(?P\d+)-(?P\d+)-(?P(?:-|\d+)+)$"#, 22 | v 23 | ) { 24 | None => Err(E::custom(format!("invalid SID: '{v}'"))), 25 | Some((_, revision, authority, numbers)) => { 26 | let revision = revision.parse::().map_err(|why| { 27 | E::custom(format!("unable to parse revision in '{revision}': {why}")) 28 | })?; 29 | let authority = authority.parse::().map_err(|why| { 30 | E::custom(format!("unable to parse authority in '{authority}': {why}")) 31 | })?; 32 | let mut vec_numbers = Vec::new(); 33 | for r in numbers.split('-').map(|n| (n, n.parse::())) { 34 | match r.1 { 35 | Err(why) => { 36 | return Err(E::custom(format!( 37 | "unable to parse number '{}' in '{numbers}': {why}", r.0 38 | ))) 39 | } 40 | Ok(n) => vec_numbers.push(n), 41 | } 42 | } 43 | Ok(Sid::new(revision, authority, vec_numbers)) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/win32_types/timestamp/mod.rs: -------------------------------------------------------------------------------- 1 | use chrono::{format::StrftimeItems, DateTime, FixedOffset}; 2 | use lazy_static::lazy_static; 3 | 4 | mod timeline_entry; 5 | mod truncated_windows_file_time; 6 | mod unix_timestamp; 7 | mod windows_file_time; 8 | 9 | pub use timeline_entry::*; 10 | pub use truncated_windows_file_time::TruncatedWindowsFileTime; 11 | pub use unix_timestamp::*; 12 | pub use windows_file_time::WindowsFileTime; 13 | 14 | lazy_static! { 15 | pub static ref TIMESTAMP_FORMAT: String = { 16 | if let Ok(format) = std::env::var("DFIR_DATE") { 17 | if StrftimeItems::new(&format).any(|i| i == chrono::format::Item::Error) { 18 | eprintln!(); 19 | eprintln!("ERROR: invalid date format: '{format}' stored in environment variable $DFIR_DATE!"); 20 | eprintln!(); 21 | eprintln!("Please take a look at"); 22 | eprintln!(); 23 | eprintln!( 24 | " " 25 | ); 26 | eprintln!(); 27 | eprintln!("to see which format strings are accepted."); 28 | eprintln!(); 29 | std::process::exit(-1); 30 | } else { 31 | format 32 | } 33 | } else { 34 | // use this format, because to_rfc3339 creates values like '+30828-09-14T02:48:05.477580+00:00', 35 | // which cannot be parsed with parse_from_rfc3339() 36 | "%Y-%m-%dT%H:%M:%S%z".to_string() 37 | } 38 | }; 39 | pub static ref ZERO: DateTime = 40 | DateTime::::parse_from_rfc3339("0000-00-00T00:00:00+00:00") 41 | .expect("unable to parse literal timestamp"); 42 | } 43 | 44 | #[macro_export] 45 | macro_rules! impl_timestamp { 46 | ($type: ident) => { 47 | impl $crate::value::FromValue for $type { 48 | fn from_value_opt( 49 | value: &$crate::cache::Value, 50 | ) -> Result, $crate::ntds::Error> { 51 | match value { 52 | $crate::cache::Value::Currency(val) => { 53 | let val = *val as u64; 54 | Ok(Some($type::from(val))) 55 | }, 56 | $crate::cache::Value::Null(()) => Ok(None), 57 | _ => Err($crate::ntds::Error::InvalidValueDetected( 58 | value.to_string(), 59 | stringify!($type), 60 | )), 61 | } 62 | } 63 | } 64 | 65 | impl serde::Serialize for $type { 66 | fn serialize(&self, serializer: S) -> Result 67 | where 68 | S: serde::Serializer, 69 | { 70 | serializer.serialize_str( 71 | &self 72 | .0 73 | .format(&$crate::win32_types::timestamp::TIMESTAMP_FORMAT) 74 | .to_string(), 75 | ) 76 | } 77 | } 78 | 79 | impl<'de> serde::Deserialize<'de> for $type { 80 | fn deserialize(deserializer: D) -> Result 81 | where 82 | D: serde::Deserializer<'de>, 83 | { 84 | use serde::de; 85 | let buf = String::deserialize(deserializer)?; 86 | match DateTime::parse_from_str( 87 | &buf[..], 88 | &$crate::win32_types::timestamp::TIMESTAMP_FORMAT, 89 | ) { 90 | Ok(dt) => Ok(Self(dt.with_timezone(&Utc))), 91 | Err(why) => Err(de::Error::custom(format!( 92 | "unable to parse timestamp '{buf}': {why}" 93 | ))), 94 | } 95 | } 96 | } 97 | 98 | impl $crate::win32_types::UnixTimestamp for $type { 99 | #[allow(dead_code)] 100 | fn timestamp(&self) -> i64 { 101 | self.0.timestamp() 102 | } 103 | } 104 | impl $crate::win32_types::TimelineEntry for $type {} 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /src/win32_types/timestamp/timeline_entry.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use bodyfile::Bodyfile3Line; 4 | 5 | use super::UnixTimestamp; 6 | 7 | pub trait TimelineEntry: UnixTimestamp { 8 | fn cr_entry(&self, upn: &str, caption: &str, object_type: impl Display) -> Bodyfile3Line { 9 | Bodyfile3Line::new() 10 | .with_owned_name(format!("{upn} ({object_type}, {caption})")) 11 | .with_crtime(i64::max(0, self.timestamp())) 12 | } 13 | 14 | fn c_entry(&self, upn: &str, caption: &str, object_type: impl Display) -> Bodyfile3Line { 15 | Bodyfile3Line::new() 16 | .with_owned_name(format!("{upn} ({object_type}, {caption})")) 17 | .with_ctime(i64::max(0, self.timestamp())) 18 | } 19 | 20 | fn a_entry(&self, upn: &str, caption: &str, object_type: impl Display) -> Bodyfile3Line { 21 | Bodyfile3Line::new() 22 | .with_owned_name(format!("{upn} ({object_type}, {caption})")) 23 | .with_atime(i64::max(0, self.timestamp())) 24 | } 25 | 26 | fn m_entry(&self, upn: &str, caption: &str, object_type: impl Display) -> Bodyfile3Line { 27 | Bodyfile3Line::new() 28 | .with_owned_name(format!("{upn} ({object_type}, {caption})")) 29 | .with_mtime(i64::max(0, self.timestamp())) 30 | } 31 | } -------------------------------------------------------------------------------- /src/win32_types/timestamp/truncated_windows_file_time.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Duration, Utc}; 2 | use lazy_static::lazy_static; 3 | use libesedb::systemtime_from_filetime; 4 | 5 | use crate::impl_timestamp; 6 | 7 | #[derive(Eq, PartialEq)] 8 | pub struct TruncatedWindowsFileTime(DateTime); 9 | 10 | impl_timestamp!(TruncatedWindowsFileTime); 11 | 12 | lazy_static!{ 13 | static ref BASE_TIME: DateTime = systemtime_from_filetime(0).into(); 14 | } 15 | 16 | impl From for TruncatedWindowsFileTime { 17 | fn from(value: u64) -> Self { 18 | Self(*BASE_TIME + Duration::seconds(value.try_into().unwrap())) 19 | } 20 | } 21 | impl From> for TruncatedWindowsFileTime { 22 | fn from(value: DateTime) -> Self { 23 | Self(value) 24 | } 25 | } 26 | impl From for DateTime { 27 | fn from(value: TruncatedWindowsFileTime) -> Self { 28 | value.0 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use crate::win32_types::TruncatedWindowsFileTime; 35 | 36 | #[test] 37 | fn convert_from_ntds() { 38 | let ds_record_time = 13390472401u64; 39 | let ts = TruncatedWindowsFileTime::from(ds_record_time); 40 | assert_eq!(ts.0.to_rfc3339(), "2025-04-30T07:40:01+00:00"); 41 | } 42 | } -------------------------------------------------------------------------------- /src/win32_types/timestamp/unix_timestamp.rs: -------------------------------------------------------------------------------- 1 | pub trait UnixTimestamp { 2 | fn timestamp(&self) -> i64; 3 | } -------------------------------------------------------------------------------- /src/win32_types/timestamp/windows_file_time.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use libesedb::systemtime_from_filetime; 3 | 4 | use crate::impl_timestamp; 5 | 6 | #[derive(Eq, PartialEq)] 7 | pub struct WindowsFileTime(DateTime); 8 | 9 | impl_timestamp!(WindowsFileTime); 10 | 11 | impl From for WindowsFileTime { 12 | fn from(value: u64) -> Self { 13 | Self(systemtime_from_filetime(value).into()) 14 | } 15 | } 16 | impl From> for WindowsFileTime { 17 | fn from(value: DateTime) -> Self { 18 | Self(value) 19 | } 20 | } 21 | impl From for DateTime { 22 | fn from(value: WindowsFileTime) -> Self { 23 | value.0 24 | } 25 | } -------------------------------------------------------------------------------- /src/win32_types/user_account_control/mod.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | bitflags! { 5 | 6 | /// Source: https://docs.microsoft.com/en-us/windows/win32/adschema/a-useraccountcontrol 7 | #[derive(PartialEq, Eq, Serialize, Deserialize)] 8 | pub struct UserAccountControl : u32 { 9 | 10 | /// The logon script is executed. 11 | const ADS_UF_SCRIPT = 0x0000_0001; 12 | 13 | /// The user account is disabled. 14 | const ADS_UF_ACCOUNTDISABLE = 0x0000_0002; 15 | 16 | /// The home directory is required. 17 | const ADS_UF_HOMEDIR_REQUIRED = 0x0000_0008; 18 | 19 | /// The account is currently locked out. 20 | const ADS_UF_LOCKOUT = 0x0000_0010; 21 | 22 | /// No password is required. 23 | const ADS_UF_PASSWD_NOTREQD = 0x0000_0020; 24 | 25 | /// The user cannot change the password. 26 | const ADS_UF_PASSWD_CANT_CHANGE = 0x0000_0040; 27 | 28 | /// The user can send an encrypted password. 29 | const ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x0000_0080; 30 | 31 | /// This is an account for users whose primary account is in another 32 | /// domain. This account provides user access to this domain, but not 33 | /// to any domain that trusts this domain. Also known as a local user 34 | /// account. 35 | const ADS_UF_TEMP_DUPLICATE_ACCOUNT = 0x0000_0100; 36 | 37 | /// This is a default account type that represents a typical user. 38 | const ADS_UF_NORMAL_ACCOUNT = 0x0000_0200; 39 | 40 | /// This is a permit to trust account for a system domain that trusts 41 | /// other domains. 42 | const ADS_UF_INTERDOMAIN_TRUST_ACCOUNT = 0x0000_0800; 43 | 44 | /// This is a computer account for a computer that is a member of this 45 | /// domain. 46 | const ADS_UF_WORKSTATION_TRUST_ACCOUNT = 0x0000_1000; 47 | 48 | /// This is a computer account for a system backup domain controller 49 | /// that is a member of this domain. 50 | const ADS_UF_SERVER_TRUST_ACCOUNT = 0x0000_2000; 51 | 52 | /// The password for this account will never expire. 53 | const ADS_UF_DONT_EXPIRE_PASSWD = 0x0001_0000; 54 | 55 | /// This is an MNS logon account. 56 | const ADS_UF_MNS_LOGON_ACCOUNT = 0x0002_0000; 57 | 58 | /// The user must log on using a smart card. 59 | const ADS_UF_SMARTCARD_REQUIRED = 0x0004_0000; 60 | 61 | /// The service account (user or computer account), under which a 62 | /// service runs, is trusted for Kerberos delegation. Any such service 63 | /// can impersonate a client requesting the service. 64 | const ADS_UF_TRUSTED_FOR_DELEGATION = 0x0008_0000; 65 | 66 | /// The security context of the user will not be delegated to a service 67 | /// even if the service account is set as trusted for Kerberos 68 | /// delegation. 69 | const ADS_UF_NOT_DELEGATED = 0x0010_0000; 70 | 71 | /// Restrict this principal to use only Data Encryption Standard (DES) 72 | /// encryption types for keys. 73 | const ADS_UF_USE_DES_KEY_ONLY = 0x0020_0000; 74 | 75 | /// This account does not require Kerberos pre-authentication for 76 | /// logon. 77 | const ADS_UF_DONT_REQUIRE_PREAUTH = 0x0040_0000; 78 | 79 | /// The user password has expired. This flag is created by the system 80 | /// using data from the Pwd-Last-Set attribute and the domain policy. 81 | const ADS_UF_PASSWORD_EXPIRED = 0x0080_0000; 82 | 83 | /// The account is enabled for delegation. This is a security-sensitive 84 | /// setting; accounts with this option enabled should be strictly 85 | /// controlled. This setting enables a service running under the 86 | /// account to assume a client identity and authenticate as that user 87 | /// to other remote servers on the network. 88 | const ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x0100_0000; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/data/.gitattributes: -------------------------------------------------------------------------------- 1 | ntds_plain.dit filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /tests/data/ntds_plain.dit: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:37f417be4cbb82a2bdd358df02d5b8bc0960e062526325e92dda5b393a5132e7 3 | size 18874368 4 | -------------------------------------------------------------------------------- /tests/test_plain.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | io::{BufReader, Cursor}, 4 | path::PathBuf, 5 | }; 6 | 7 | use assert_cmd::Command; 8 | use libntdsextract2::{ntds::Person, CsvSerialization}; 9 | 10 | #[test] 11 | fn test_plain() { 12 | let dstfile = get_test_data("ntds_plain.dit"); 13 | 14 | let mut cmd = Command::cargo_bin("ntdsextract2").unwrap(); 15 | let result = cmd.arg(dstfile).arg("user").arg("-D").ok(); 16 | 17 | match &result { 18 | Ok(out) => { 19 | let mut users = HashMap::new(); 20 | let reader = BufReader::new(Cursor::new(&out.stdout)); 21 | let mut rdr = csv::Reader::from_reader(reader); 22 | 23 | for result in rdr.deserialize() { 24 | let record: Person = result.unwrap(); 25 | users.insert(record.sam_account_name().as_ref().unwrap().clone(), record); 26 | } 27 | 28 | assert!(users.contains_key("Administrator")); 29 | assert!(!users.contains_key("InvalidUser")); 30 | 31 | let admin = users.get("Administrator").unwrap(); 32 | assert!(!admin.is_deleted()) 33 | } 34 | Err(why) => { 35 | println!("{why}"); 36 | } 37 | } 38 | assert!(result.is_ok()); 39 | } 40 | 41 | fn get_test_data(filename: &str) -> PathBuf { 42 | let mut data_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 43 | data_path.push("tests"); 44 | data_path.push("data"); 45 | data_path.push(filename); 46 | 47 | data_path 48 | } 49 | --------------------------------------------------------------------------------