diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6c3b496 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +data/** filter=lfs diff=lfs merge=lfs -text +data/ filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..31bae2a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: "Version number" + required: true + +env: + CARGO_TERM_COLOR: always + +permissions: + contents: write + +jobs: + build: + name: Build and Create Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Build Release + run: cargo build --release + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ github.event.inputs.version }} + draft: false + prerelease: false + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./target/release/convertsnapshot + asset_name: convertsnapshot-${{ github.event.inputs.version }} + asset_content_type: application/octet-stream diff --git a/.github/workflows/rust_tests.yml b/.github/workflows/rust_tests.yml new file mode 100644 index 0000000..ffb25d2 --- /dev/null +++ b/.github/workflows/rust_tests.yml @@ -0,0 +1,25 @@ +name: Rust Tests + +on: + push: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c4e01d7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1152 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ADExplorerSnapshot-rs" +version = "0.1.0" +dependencies = [ + "byteorder", + "chrono", + "clap", + "flate2", + "memmap2", + "nom", + "rand", + "rayon", + "serde", + "serde_json", + "tar", + "zip", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deflate64" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "thiserror" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "rand", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.12+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..585e834 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "ADExplorerSnapshot-rs" +version = "0.1.0" +edition = "2021" + + +[lib] +name = "adexplorersnapshot" +path = "src/lib.rs" + +[[bin]] +name = "convertsnapshot" +path = "src/main.rs" + +[[bench]] +name = "parser" + + +[dependencies] +byteorder = "1.4.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = "0.4" +nom = "7.1.3" +memmap2 = "0.9.4" +zip = "2.1.3" +rand = "0.8" +clap = { version = "4.3", features = ["derive"] } +rayon = "1.5.1" +tar = "0.4" +flate2 = "1.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11a8f32 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +Idk yet \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4142baf --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# ADExplorerSnapshot-rs + +Rewrite of [ADExplorerSnapshot.py](https://github.com/c3c/ADExplorerSnapshot.py). Output is BloodHound CE + +# TODO + +- Add Links to domains.rs +- Add ChildObjects to domains.rs +- Add [ReadLAPSPassword](https://github.com/BloodHoundAD/SharpHoundCommon/blob/ea6b097927c5bb795adb8589e9a843293d36ae37/src/CommonLib/Processors/ACLProcessor.cs#L350) +- Add [ADCS](https://github.com/BloodHoundAD/SharpHoundCommon/blob/v3/src/CommonLib/Processors/LDAPPropertyProcessor.cs#L428) + +# Fun Links + +- https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/1522b774-6464-41a3-87a5-1e5633c3fbbb diff --git a/benches/parser.rs b/benches/parser.rs new file mode 100644 index 0000000..811ec25 --- /dev/null +++ b/benches/parser.rs @@ -0,0 +1,23 @@ +#![feature(test)] + +extern crate test; +use std::fs::File; + +use adexplorersnapshot::parser::ADExplorerSnapshot; +use memmap2::Mmap; +use test::Bencher; + +const SNAPSHOT_PATH: &str = "data/snapshot.bak"; + +#[bench] +fn snapshot(b: &mut Bencher) { + let file = File::open(SNAPSHOT_PATH).expect("Failed to open snapshot"); + let mapped = unsafe { Mmap::map(&file) }.expect("Failed to map in snapshot"); + + b.iter(|| { + test::black_box( + ADExplorerSnapshot::snapshot_from_memory(&mapped[..]) + .expect("Failed to parse snapshot"), + ); + }); +} diff --git a/data/snapshot.bak b/data/snapshot.bak new file mode 100644 index 0000000..f8f40f4 --- /dev/null +++ b/data/snapshot.bak @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e6cdd52fbaa496ee1d0ac262f52af104f3ccc462176edac48a80cd6b44caf47 +size 3379644 diff --git a/src/guid/guid.rs b/src/guid/guid.rs new file mode 100644 index 0000000..19cb0f6 --- /dev/null +++ b/src/guid/guid.rs @@ -0,0 +1,92 @@ +use nom::{ + bytes::complete::take, + combinator::map, + number::complete::{le_u16, le_u32}, + sequence::tuple, + IResult, +}; +use serde::Serialize; + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct GUID { + data1: u32, + data2: u16, + data3: u16, + data4: [u8; 8], +} + +impl GUID { + pub fn from_bytes(input: &[u8]) -> Result>> { + let (_, guid) = parse_guid(input)?; + Ok(guid) + } + + pub fn from_next_bytes(input: &[u8]) -> IResult<&[u8], Self> { + parse_guid(input) + } + + pub fn to_string(&self) -> String { + format!( + "{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}", + self.data1, + self.data2, + self.data3, + self.data4[0], + self.data4[1], + self.data4[2], + self.data4[3], + self.data4[4], + self.data4[5], + self.data4[6], + self.data4[7] + ) + } +} + +fn parse_guid(input: &[u8]) -> IResult<&[u8], GUID> { + let (input, (data1, data2, data3, data4)) = tuple(( + le_u32, + le_u16, + le_u16, + map(take(8usize), |slice: &[u8]| { + let mut arr = [0u8; 8]; + arr.copy_from_slice(slice); + arr + }), + ))(input)?; + + Ok(( + input, + GUID { + data1, + data2, + data3, + data4, + }, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_guid_parsing() { + let bytes = [ + 166, 109, 2, 155, 60, 13, 92, 70, 139, 238, 81, 153, 215, 22, 92, 186, + ]; + let guid = GUID::from_bytes(&bytes).unwrap(); + assert_eq!(guid.to_string(), "9B026DA6-0D3C-465C-8BEE-5199D7165CBA"); + } + + #[test] + fn test_from_next_bytes() { + let bytes = [ + 166, 109, 2, 155, 60, 13, 92, 70, 139, 238, 81, 153, 215, 22, 92, 186, 0xFF, + 0xFF, // Additional data + ]; + let (remaining, guid) = GUID::from_next_bytes(&bytes).unwrap(); + assert_eq!(guid.to_string(), "9B026DA6-0D3C-465C-8BEE-5199D7165CBA"); + assert_eq!(remaining, &[0xFF, 0xFF]); + } +} diff --git a/src/guid/mod.rs b/src/guid/mod.rs new file mode 100644 index 0000000..9eacad9 --- /dev/null +++ b/src/guid/mod.rs @@ -0,0 +1,3 @@ +mod guid; + +pub use guid::*; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9c45a86 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +mod guid; +mod security_descriptor; +mod sid; + +pub mod output; +pub mod parser; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..da8cc7e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,161 @@ +use clap::Parser; +use flate2::write::GzEncoder; +use flate2::Compression; +use rand::Rng; +use std::fs::File; +use std::io::BufWriter; +use std::io::{Error, ErrorKind}; +use std::path::PathBuf; +use std::sync::Mutex; +use std::time::Instant; +use tar::Builder; + +use adexplorersnapshot::output::bloodhound::{ + ComputersOutput, ContainersOutput, DomainsOutput, GPOsOutput, GroupsOutput, OUsOutput, + UsersOutput, +}; +use adexplorersnapshot::parser::ADExplorerSnapshot; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + #[clap(help = "Input .dat file path")] + input: String, + + #[clap(short, long, help = "Output .tar.gz file path")] + output: Option, + + #[clap(short, long, help = "Compression level (0-9, default 6)")] + compression: Option, + + #[clap(short, long, help = "Verbose output")] + verbose: bool, +} + +trait Output: Send { + fn to_json(&self) -> serde_json::Result>; +} + +impl Output for T { + fn to_json(&self) -> serde_json::Result> { + serde_json::to_vec(self) + } +} + +fn main() -> std::io::Result<()> { + let start_time = Instant::now(); + let args = Args::parse(); + + let verbose = args.verbose; + + if verbose { + println!("Parsing"); + } + let parsing_start = Instant::now(); + let snapshot = ADExplorerSnapshot::snapshot_from_file(&args.input)?; + if verbose { + println!("Parsing took: {:?}", parsing_start.elapsed()); + } + + let output_path = args.output.map(PathBuf::from).unwrap_or_else(|| { + let random_name: String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(10) + .map(char::from) + .collect(); + PathBuf::from(format!("{}.tar.gz", random_name)) + }); + + let file = File::create(&output_path)?; + let buf_writer = BufWriter::with_capacity(8 * 1024 * 1024, file); + let compression_level = args.compression.unwrap_or(6); + let gzip_encoder = GzEncoder::new(buf_writer, Compression::new(compression_level)); + let archive = Mutex::new(Builder::new(gzip_encoder)); + + process_outputs(&archive, &snapshot, verbose)?; + + let write_start = Instant::now(); + archive.into_inner().unwrap().into_inner()?.finish()?; + if verbose { + println!("Writing zip took: {:?}", write_start.elapsed()); + } + + println!("Output written to: {}", output_path.display()); + println!("Total elapsed time: {:?}", start_time.elapsed()); + + Ok(()) +} + +fn process_outputs( + archive: &Mutex>>>, + snapshot: &ADExplorerSnapshot, + verbose: bool, +) -> std::io::Result<()> { + let output_types: Vec<(&str, Box Box>)> = vec![ + ( + "domains.json", + Box::new(|| Box::new(DomainsOutput::new(snapshot))), + ), + ( + "users.json", + Box::new(|| Box::new(UsersOutput::new(snapshot))), + ), + ( + "computers.json", + Box::new(|| Box::new(ComputersOutput::new(snapshot))), + ), + ( + "groups.json", + Box::new(|| Box::new(GroupsOutput::new(snapshot))), + ), + ("ous.json", Box::new(|| Box::new(OUsOutput::new(snapshot)))), + ( + "containers.json", + Box::new(|| Box::new(ContainersOutput::new(snapshot))), + ), + ( + "gpos.json", + Box::new(|| Box::new(GPOsOutput::new(snapshot))), + ), + ]; + + for (filename, output_fn) in output_types { + if verbose { + println!("Generating {}", filename); + } + let start = Instant::now(); + let output = output_fn(); + if verbose { + println!("Generating {} took: {:?}", filename, start.elapsed()); + } + + add_output(archive, filename, &*output, verbose)?; + } + + Ok(()) +} + +fn add_output( + archive: &Mutex>>>, + filename: &str, + output: &dyn Output, + verbose: bool, +) -> std::io::Result<()> { + if verbose { + println!("Processing {}", filename); + } + let start = Instant::now(); + let mut header = tar::Header::new_ustar(); + let json = output + .to_json() + .map_err(|e| Error::new(ErrorKind::Other, e))?; + header.set_size(json.len() as u64); + header.set_cksum(); + + let mut archive = archive.lock().unwrap(); + archive.append_data(&mut header, filename, json.as_slice())?; + if verbose { + println!("Processing {} took: {:?}", filename, start.elapsed()); + } + Ok(()) +} diff --git a/src/output/bloodhound/common.rs b/src/output/bloodhound/common.rs new file mode 100644 index 0000000..dc7aa66 --- /dev/null +++ b/src/output/bloodhound/common.rs @@ -0,0 +1,47 @@ +use super::utils::Aces; +use crate::parser::{ADExplorerSnapshot, ObjectType}; +use crate::parser::{AttributeValue, Object}; +use crate::security_descriptor::ControlFlag; + +pub fn get_sid(obj: &Object) -> String { + obj.get_object_identifier() + .unwrap_or("ERR_UNKNOWN".to_string()) +} + +pub fn is_acl_protected(obj: &Object) -> bool { + obj.get_first("nTSecurityDescriptor") + .and_then(AttributeValue::as_nt_security_descriptor) + .map(|sd| sd.control_flags.is_set(ControlFlag::DP)) + .unwrap_or(false) +} + +pub fn get_aces(obj: &Object, snapshot: &ADExplorerSnapshot) -> Vec { + let has_laps = obj.get("ms-Mcs-AdmPwdExpirationTime").is_some(); + let object_type = obj.get_type(); + obj.get_first("nTSecurityDescriptor") + .and_then(AttributeValue::as_nt_security_descriptor) + .map(|sd| Aces::from_security_descriptor(&sd, snapshot, &object_type, has_laps)) + .unwrap_or_default() +} + +pub fn ldap2domain(ldap: &str) -> String { + ldap.split(',') + .filter(|&part| part.to_lowercase().starts_with("dc=")) + .map(|part| &part[3..]) + .collect::>() + .join(".") +} + +pub fn type_string(obj: &Object) -> String { + match obj.get_type() { + ObjectType::Computer => "Computer".to_string(), + ObjectType::Domain => "Domain".to_string(), + ObjectType::Group => "Group".to_string(), + ObjectType::User => "User".to_string(), + ObjectType::UserDisabled => "User".to_string(), + ObjectType::OU => "OU".to_string(), + ObjectType::GPO => "GPO".to_string(), + ObjectType::Container => "Container".to_string(), + ObjectType::Unknown => "Unknown".to_string(), + } +} diff --git a/src/output/bloodhound/computers.rs b/src/output/bloodhound/computers.rs new file mode 100644 index 0000000..5159dad --- /dev/null +++ b/src/output/bloodhound/computers.rs @@ -0,0 +1,328 @@ +use super::common::{get_aces, get_sid, is_acl_protected, ldap2domain}; +use super::utils::{Aces, Meta}; +use crate::output::bloodhound::common::type_string; +use crate::parser::{ADExplorerSnapshot, AttributeValue, Object}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ComputersOutput { + meta: Meta, + #[serde(rename = "data")] + computers: Vec, +} + +impl ComputersOutput { + pub fn new(snapshot: &ADExplorerSnapshot) -> Self { + let computers: Vec = snapshot + .snapshot + .objects + .iter() + .filter(|obj| { + obj.get_first("sAMAccountType") + .and_then(AttributeValue::as_integer) + .map(|account_type| account_type == 805306369) + .unwrap_or(false) + }) + .map(|obj| Computer::new(obj, snapshot)) + .collect(); + + Self { + meta: Meta { + methods: 46067, + r#type: "computers".to_string(), + count: computers.len() as u64, + version: 5, + }, + computers, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Computer { + #[serde(rename = "Properties")] + pub properties: ComputerProperties, + + #[serde(rename = "AllowedToDelegate")] + pub allowed_to_delegate: Vec, + + #[serde(rename = "AllowedToAct")] + pub allowed_to_act: Vec, + + #[serde(rename = "PrimaryGroupSID")] + pub primary_group_sid: String, + + #[serde(rename = "HasSIDHistory")] + pub has_sid_history: Vec, + + #[serde(rename = "Sessions")] + pub sessions: SessionsInfo, + + #[serde(rename = "PrivilegedSessions")] + pub privileged_sessions: SessionsInfo, + + #[serde(rename = "RegistrySessions")] + pub registry_sessions: SessionsInfo, + + #[serde(rename = "LocalGroups")] + pub local_groups: Vec, + + #[serde(rename = "Aces")] + aces: Vec, + + #[serde(rename = "ObjectIdentifier")] + object_identifier: String, + + #[serde(rename = "IsDeleted")] + is_deleted: bool, + + #[serde(rename = "IsACLProtected")] + is_acl_protected: bool, +} + +impl Computer { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + Computer { + properties: ComputerProperties::new(obj, snapshot), + allowed_to_delegate: process_allowed_to_delegate(obj, snapshot), + allowed_to_act: process_allowed_to_act(obj), + primary_group_sid: get_primary_group_sid(obj, snapshot), + has_sid_history: process_sid_history(obj), + sessions: SessionsInfo::default(), + privileged_sessions: SessionsInfo::default(), + registry_sessions: SessionsInfo::default(), + local_groups: Vec::new(), // This would need to be populated if the data is available + aces: get_aces(obj, snapshot), + object_identifier: get_sid(obj), + is_deleted: false, // Assuming this information is not available in the snapshot + is_acl_protected: is_acl_protected(obj), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ComputerProperties { + pub domain: String, + pub name: String, + pub distinguishedname: String, + pub domainsid: String, + pub haslaps: bool, + pub description: Option, + pub whencreated: i64, + pub enabled: bool, + pub unconstraineddelegation: bool, + pub trustedtoauth: bool, + pub lastlogon: i64, + pub lastlogontimestamp: i64, + pub pwdlastset: i64, + pub serviceprincipalnames: Vec, + pub operatingsystem: Option, + pub sidhistory: Vec, + pub samaccountname: Option, +} + +impl ComputerProperties { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + let distinguished_name = obj + .get_first("distinguishedName") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + + let domain = ldap2domain(&distinguished_name).to_uppercase(); + let name = obj + .get_first("name") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + + let uac = obj + .get_first("userAccountControl") + .and_then(AttributeValue::as_integer) + .unwrap_or(0); + + ComputerProperties { + domain: domain.clone(), + name: format!("{}@{}", name.to_uppercase(), domain), + distinguishedname: distinguished_name, + domainsid: snapshot.caches.domain_sid.as_ref().unwrap().to_string(), + haslaps: obj + .get_first("ms-mcs-admpwdexpirationtime") + .and_then(AttributeValue::as_integer) + .map(|v| v != 0) + .unwrap_or(false), + description: obj + .get_first("description") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()), + whencreated: obj + .get_first("whenCreated") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + enabled: uac & 2 == 0, + unconstraineddelegation: uac & 0x00080000 == 0x00080000, + trustedtoauth: uac & 0x01000000 == 0x01000000, + lastlogon: obj + .get_first("lastLogon") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + lastlogontimestamp: obj + .get_first("lastLogonTimestamp") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(-1), + pwdlastset: obj + .get_first("pwdLastSet") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + serviceprincipalnames: obj + .get("servicePrincipalName") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_string) + .cloned() + .collect() + }) + .unwrap_or_default(), + operatingsystem: obj + .get_first("operatingSystem") + .and_then(AttributeValue::as_string) + .map(|os| { + obj.get_first("operatingSystemServicePack") + .and_then(AttributeValue::as_string) + .map(|sp| format!("{} {}", os, sp)) + .unwrap_or_else(|| os.to_string()) + }), + sidhistory: obj + .get("sIDHistory") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_sid) + .map(|sid| sid.to_string()) + .collect() + }) + .unwrap_or_default(), + samaccountname: obj + .get_first("sAMAccountName") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DelegationTarget { + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, + #[serde(rename = "ObjectType")] + pub object_type: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SIDHistoryItem { + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, + #[serde(rename = "ObjectType")] + pub object_type: String, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct SessionsInfo { + pub results: Vec, + pub collected: bool, + pub failure_reason: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SessionResult { + #[serde(rename = "UserSID")] + pub user_sid: String, + #[serde(rename = "ComputerSID")] + pub computer_sid: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LocalGroup { + pub collected: bool, + pub failure_reason: String, + pub results: Vec, + #[serde(rename = "LocalName")] + pub local_name: Vec, + pub name: String, + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LocalGroupMember { + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, + #[serde(rename = "ObjectType")] + pub object_type: String, +} + +fn process_allowed_to_delegate( + obj: &Object, + snapshot: &ADExplorerSnapshot, +) -> Vec { + obj.get("msDS-AllowedToDelegateTo") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_string) + .flat_map(|host| { + let target = host.split('/').nth(1).unwrap_or(host); + if let Some(target_obj) = snapshot.get_computer(target) { + vec![DelegationTarget { + object_identifier: target_obj + .get_object_identifier() + .unwrap_or("ERR_UNKNOWN".to_string()), + object_type: type_string(target_obj), + }] + } else if target.contains('.') { + vec![DelegationTarget { + object_identifier: target.to_uppercase(), + object_type: "Computer".to_string(), + }] + } else { + eprintln!("Invalid delegation target: {}", host); + vec![] + } + }) + .collect() + }) + .unwrap_or_default() +} + +fn process_allowed_to_act(_obj: &Object) -> Vec { + // TODO: Property msDS-AllowedToActOnBehalfOfOtherIdentity? + + Vec::new() +} + +fn get_primary_group_sid(obj: &Object, snapshot: &ADExplorerSnapshot) -> String { + let group_id = obj + .get_first("primaryGroupID") + .and_then(AttributeValue::as_integer) + .unwrap_or(513); // Default to 513 (Domain Users) if not found + + let domain_sid = snapshot.caches.domain_sid.as_ref().unwrap(); + + format!("{}-{}", domain_sid.to_string(), group_id) +} + +fn process_sid_history(obj: &Object) -> Vec { + obj.get("sIDHistory") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_sid) + .map(|sid| SIDHistoryItem { + object_identifier: sid.to_string(), + object_type: "Computer".to_string(), + }) + .collect() + }) + .unwrap_or_default() +} diff --git a/src/output/bloodhound/containers.rs b/src/output/bloodhound/containers.rs new file mode 100644 index 0000000..1ff785e --- /dev/null +++ b/src/output/bloodhound/containers.rs @@ -0,0 +1,110 @@ +use super::common::{get_aces, is_acl_protected, ldap2domain}; +use super::utils::{Aces, Meta}; +use crate::parser::{ADExplorerSnapshot, AttributeValue, Object, ObjectType}; +use serde::{Deserialize, Serialize}; +#[derive(Debug, Serialize, Deserialize)] +pub struct ContainersOutput { + meta: Meta, + #[serde(rename = "data")] + containers: Vec, +} + +impl ContainersOutput { + pub fn new(snapshot: &ADExplorerSnapshot) -> Self { + let containers: Vec = snapshot + .snapshot + .objects + .iter() + .filter(|obj| obj.get_type() == ObjectType::Container) + .map(|obj| Container::new(obj, snapshot)) + .collect(); + + Self { + meta: Meta { + methods: 46067, + r#type: "containers".to_string(), + count: containers.len() as u64, + version: 5, + }, + containers, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Container { + #[serde(rename = "Properties")] + pub properties: ContainerProperties, + + #[serde(rename = "ChildObjects")] + pub child_objects: Vec, + + #[serde(rename = "Aces")] + aces: Vec, + + #[serde(rename = "ObjectIdentifier")] + object_identifier: String, + + #[serde(rename = "IsDeleted")] + is_deleted: bool, + + #[serde(rename = "IsACLProtected")] + is_acl_protected: bool, +} + +impl Container { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + Container { + properties: ContainerProperties::new(obj, snapshot), + // TODO: How do you get child objects of a container? + child_objects: Vec::new(), + aces: get_aces(obj, snapshot), + object_identifier: obj + .get_first("objectGUID") + .and_then(AttributeValue::as_guid) + .map(|v| v.to_string()) + .unwrap_or_default(), + is_deleted: false, // Assuming this information is not available in the snapshot + is_acl_protected: is_acl_protected(obj), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ContainerProperties { + pub domain: String, + pub name: String, + pub distinguishedname: String, + pub domainsid: String, +} + +impl ContainerProperties { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + let distinguished_name = obj + .get_first("distinguishedName") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + let domain = ldap2domain(&distinguished_name); + let name = obj + .get_first("name") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + + ContainerProperties { + domain: domain.clone(), + name: format!("{}@{}", name, domain), + distinguishedname: distinguished_name, + domainsid: snapshot.caches.domain_sid.as_ref().unwrap().to_string(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChildObject { + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, + #[serde(rename = "ObjectType")] + pub object_type: String, +} diff --git a/src/output/bloodhound/domains.rs b/src/output/bloodhound/domains.rs new file mode 100644 index 0000000..395504a --- /dev/null +++ b/src/output/bloodhound/domains.rs @@ -0,0 +1,268 @@ +use crate::parser::{ADExplorerSnapshot, AttributeValue, Object}; +use crate::security_descriptor::ControlFlag; +use serde::{Deserialize, Serialize}; + +use super::common::get_aces; +use super::utils::Aces; + +#[derive(Debug, Serialize, Deserialize)] +pub struct DomainsOutput { + meta: Meta, + #[serde(rename = "data")] + domains: Vec, +} + +impl DomainsOutput { + pub fn new(snapshot: &ADExplorerSnapshot) -> Self { + Self { + meta: Meta { + methods: 46067, + r#type: "domains".to_string(), + count: 5, + }, + domains: snapshot + .get_root_domain() + .map(|root| Domain::new(root, snapshot)) + .into_iter() + .collect(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Meta { + methods: u64, + r#type: String, + count: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Domain { + #[serde(rename = "Properties")] + pub properties: DomainProperties, + + #[serde(rename = "ChildObjects")] + pub child_objects: Vec, + + #[serde(rename = "Trusts")] + pub trusts: Vec, + + #[serde(rename = "Links")] + links: Vec, + + #[serde(rename = "Aces")] + aces: Vec, + + #[serde(rename = "ObjectIdentifier")] + object_identifier: String, + + #[serde(rename = "IsDeleted")] + is_deleted: bool, + + #[serde(rename = "IsACLProtected")] + is_acl_protected: bool, +} + +impl Domain { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + // TODO: Error checking + // TODO: Make get_guid a method on Object + let guid = obj + .get_first("objectGUID") + .and_then(AttributeValue::as_guid) + .unwrap(); + + let sddls = obj + .get_first("nTSecurityDescriptor") + .and_then(AttributeValue::as_nt_security_descriptor); + + let is_acl_protected = sddls + .iter() + .any(|sd| sd.control_flags.is_set(ControlFlag::DP)); + + Domain { + properties: DomainProperties::new(obj, snapshot), + child_objects: Vec::new(), + trusts: process_trusts(snapshot), + links: Vec::new(), + aces: get_aces(obj, snapshot), + object_identifier: guid.to_string(), + is_deleted: false, + is_acl_protected, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Links { + #[serde(rename = "IsEnforced")] + is_enforced: bool, + + #[serde(rename = "GUID")] + guid: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChildObject { + #[serde(rename = "ObjectIdentifier")] + object_identifier: String, + + #[serde(rename = "ObjectType")] + object_type: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DomainProperties { + pub name: String, + pub domain: String, + pub distinguishedname: String, + pub domainsid: String, + pub description: Option, + pub functionallevel: String, + pub whencreated: i64, + pub highvalue: bool, +} + +impl DomainProperties { + pub fn new(obj: &Object, _snapshot: &ADExplorerSnapshot) -> Self { + DomainProperties { + name: obj + .get_first("name") + .and_then(AttributeValue::as_string) + .unwrap() + .clone(), + domain: obj + .get_first("name") + .and_then(AttributeValue::as_string) + .unwrap() + .to_uppercase(), + distinguishedname: obj + .get_first("distinguishedName") + .and_then(AttributeValue::as_string) + .unwrap() + .to_string(), + domainsid: Self::get_domain_sid(obj), + description: obj + .get_first("description") + .and_then(AttributeValue::as_string) + .map(String::to_string), + functionallevel: Self::get_functional_level(obj), + whencreated: Self::get_when_created(obj), + highvalue: true, + } + } + + pub fn get_domain_sid(obj: &Object) -> String { + obj.get_object_identifier() + .unwrap_or("ERR_UNKNOWN".to_string()) + } + + pub fn get_functional_level(obj: &Object) -> String { + obj.get_first("msDS-Behavior-Version") + .and_then(AttributeValue::as_integer) + .map(|level| match level { + 0 => "2000 Mixed/Native", + 1 => "2003 Interim", + 2 => "2003", + 3 => "2008", + 4 => "2008 R2", + 5 => "2012", + 6 => "2012 R2", + 7 => "2016", + _ => "Unknown", + }) + .unwrap_or("Unknown") + .to_string() + } + + pub fn get_when_created(obj: &Object) -> i64 { + obj.get_first("creationTime") + .and_then(AttributeValue::as_large_integer) + .unwrap_or(0) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Trust { + #[serde(rename = "TargetDomainSid")] + target_domain_sid: String, + + #[serde(rename = "TargetDomainName")] + target_domain_name: String, + + #[serde(rename = "IsTransitive")] + is_transitive: bool, + + #[serde(rename = "SidFilteringEnabled")] + sid_filtering_enabled: bool, + + #[serde(rename = "TrustDirection")] + trust_direction: String, + + #[serde(rename = "TrustType")] + trust_type: String, +} + +pub fn process_trusts(snapshot: &ADExplorerSnapshot) -> Vec { + snapshot + .snapshot + .objects + .iter() + .filter_map(|obj| process_trust(obj)) + .collect() +} + +fn process_trust(obj: &Object) -> Option { + // Check if 'trustedDomain' is in the object classes + if !obj + .get_attribute_classes()? + .iter() + .any(|class| class == "trustedDomain") + { + return None; + } + + Some(Trust { + target_domain_sid: obj + .get_first("securityIdentifier") + .and_then(AttributeValue::as_sid) + .map(|s| s.to_string()) + .unwrap_or("Unknown".to_string()), + target_domain_name: obj + .get_first("name") + .and_then(AttributeValue::as_string) + .map(String::to_string) + .unwrap(), + is_transitive: obj + .get_first("trustTransitive") + .and_then(AttributeValue::as_boolean) + .unwrap_or_default(), + sid_filtering_enabled: (obj + .get_first("trustAttributes") + .and_then(AttributeValue::as_integer) + .unwrap_or_default() + & 0x00000040) + != 0, + trust_direction: match obj + .get_first("trustDirection") + .and_then(AttributeValue::as_integer) + .unwrap_or_default() + { + 0 => "Disabled".to_string(), + 1 => "Inbound".to_string(), + 2 => "Outbound".to_string(), + 3 => "Bidirectional".to_string(), + _ => "Unknown".to_string(), + }, + trust_type: match obj + .get_first("trustType") + .and_then(AttributeValue::as_integer) + .unwrap_or_default() + { + 1 => "WINDOWS_NON_ACTIVE_DIRECTORY".to_string(), + 2 => "WINDOWS_ACTIVE_DIRECTORY".to_string(), + 3 => "MIT".to_string(), + _ => "Unknown".to_string(), + }, + }) +} diff --git a/src/output/bloodhound/gpos.rs b/src/output/bloodhound/gpos.rs new file mode 100644 index 0000000..89c318d --- /dev/null +++ b/src/output/bloodhound/gpos.rs @@ -0,0 +1,109 @@ +use super::common::{get_aces, is_acl_protected, ldap2domain}; +use super::utils::{Aces, Meta}; +use crate::parser::{ADExplorerSnapshot, AttributeValue, Object, ObjectType}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct GPOsOutput { + meta: Meta, + #[serde(rename = "data")] + gpos: Vec, +} + +impl GPOsOutput { + pub fn new(snapshot: &ADExplorerSnapshot) -> Self { + let gpos: Vec = snapshot + .snapshot + .objects + .iter() + .filter(|v| v.get_type() == ObjectType::GPO) + .map(|obj| GPO::new(obj, snapshot)) + .collect(); + + Self { + meta: Meta { + methods: 46067, + r#type: "gpos".to_string(), + count: gpos.len() as u64, + version: 6, + }, + gpos, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GPO { + #[serde(rename = "Properties")] + pub properties: GPOProperties, + + #[serde(rename = "Aces")] + aces: Vec, + + #[serde(rename = "ObjectIdentifier")] + object_identifier: String, + + #[serde(rename = "IsDeleted")] + is_deleted: bool, + + #[serde(rename = "IsACLProtected")] + is_acl_protected: bool, +} + +impl GPO { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + GPO { + properties: GPOProperties::new(obj, snapshot), + aces: get_aces(obj, snapshot), + object_identifier: obj + .get_first("objectGUID") + .and_then(AttributeValue::as_guid) + .map(|v| v.to_string()) + .unwrap_or_default(), + is_deleted: false, // Assuming this information is not available in the snapshot + is_acl_protected: is_acl_protected(obj), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GPOProperties { + pub domain: String, + pub name: String, + pub distinguishedname: String, + pub domainsid: String, + pub whencreated: i64, + pub gpcpath: String, +} + +impl GPOProperties { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + let distinguished_name = obj + .get_first("distinguishedName") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + let domain = ldap2domain(&distinguished_name); + let name = obj + .get_first("displayName") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + + GPOProperties { + domain: domain.clone(), + name: format!("{}@{}", name.to_uppercase(), domain.to_uppercase()), + distinguishedname: distinguished_name, + domainsid: snapshot.caches.domain_sid.as_ref().unwrap().to_string(), + whencreated: obj + .get_first("whenCreated") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + gpcpath: obj + .get_first("gPCFileSysPath") + .and_then(AttributeValue::as_string) + .map(|v| v.to_string()) + .unwrap_or_default(), + } + } +} diff --git a/src/output/bloodhound/groups.rs b/src/output/bloodhound/groups.rs new file mode 100644 index 0000000..1a2ec9e --- /dev/null +++ b/src/output/bloodhound/groups.rs @@ -0,0 +1,256 @@ +use super::common::{get_aces, get_sid, is_acl_protected, ldap2domain, type_string}; +use super::utils::{Aces, Meta}; +use crate::parser::{ADExplorerSnapshot, AttributeValue, Object}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +#[derive(Debug, Serialize, Deserialize)] +pub struct GroupsOutput { + meta: Meta, + #[serde(rename = "data")] + groups: Vec, +} + +impl GroupsOutput { + pub fn new(snapshot: &ADExplorerSnapshot) -> Self { + let domain_sid = snapshot.caches.domain_sid.as_ref().unwrap().to_string(); + let highvalue_sids: HashSet<&str> = [ + "S-1-5-32-544", + "S-1-5-32-550", + "S-1-5-32-549", + "S-1-5-32-551", + "S-1-5-32-548", + ] + .iter() + .cloned() + .collect(); + + let groups: Vec = snapshot + .snapshot + .objects + .iter() + .filter(|obj| { + obj.get("objectClass") + .map(|values| { + values + .iter() + .any(|v| v.as_string() == Some(&"group".to_string())) + }) + .unwrap_or(false) + }) + .map(|obj| Group::new(obj, snapshot, &domain_sid, &highvalue_sids)) + .collect(); + + Self { + meta: Meta { + methods: 46067, + r#type: "groups".to_string(), + count: groups.len() as u64, + version: 5, + }, + groups, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Group { + #[serde(rename = "Properties")] + pub properties: GroupProperties, + + #[serde(rename = "Members")] + pub members: Vec, + + #[serde(rename = "Aces")] + aces: Vec, + + #[serde(rename = "ObjectIdentifier")] + object_identifier: String, + + #[serde(rename = "IsDeleted")] + is_deleted: bool, + + #[serde(rename = "IsACLProtected")] + is_acl_protected: bool, +} + +impl Group { + pub fn new( + obj: &Object, + snapshot: &ADExplorerSnapshot, + domain_sid: &str, + highvalue_sids: &HashSet<&str>, + ) -> Self { + let sid = get_sid(obj); + let object_identifier = if WELLKNOWN_SIDS.contains(&sid.as_str()) { + format!("{}-{}", domain_sid, sid) + } else { + sid.clone() + }; + + Group { + properties: GroupProperties::new(obj, snapshot, &sid, highvalue_sids), + members: process_members(obj, snapshot), + aces: get_aces(obj, snapshot), + object_identifier, + is_deleted: obj + .get_first("isDeleted") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + is_acl_protected: is_acl_protected(obj), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GroupProperties { + pub domain: String, + pub domainsid: String, + pub highvalue: bool, + pub name: String, + pub distinguishedname: String, + pub admincount: bool, + pub description: Option, + pub whencreated: i64, +} + +impl GroupProperties { + pub fn new( + obj: &Object, + snapshot: &ADExplorerSnapshot, + sid: &str, + highvalue_sids: &HashSet<&str>, + ) -> Self { + let distinguished_name = obj + .get_first("distinguishedName") + .and_then(AttributeValue::as_string) + .map(|s| s.to_string()) + .unwrap_or_default(); + let domain = ldap2domain(&distinguished_name).to_uppercase(); + let name = obj + .get_first("name") + .and_then(AttributeValue::as_string) + .map(|s| s.to_string()) + .unwrap_or_default(); + + GroupProperties { + domain: domain.clone(), + domainsid: snapshot.caches.domain_sid.as_ref().unwrap().to_string(), + highvalue: is_highvalue(sid, highvalue_sids), + name: format!("{}@{}", name.to_uppercase(), domain), + distinguishedname: distinguished_name.to_string(), + admincount: obj + .get_first("adminCount") + .and_then(AttributeValue::as_integer) + .map(|count| count == 1) + .unwrap_or(false), + description: obj + .get_first("description") + .and_then(AttributeValue::as_string) + .map(|v| v.to_string()), + whencreated: obj + .get_first("whenCreated") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GroupMember { + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, + #[serde(rename = "ObjectType")] + pub object_type: String, +} + +fn process_members(obj: &Object, snapshot: &ADExplorerSnapshot) -> Vec { + obj.get("member") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_string) + .filter_map(|member_dn| resolve_membership(member_dn, snapshot)) + .collect() + }) + .unwrap_or_default() +} + +fn resolve_membership(member_dn: &str, snapshot: &ADExplorerSnapshot) -> Option { + snapshot.get_dn(member_dn).map(|obj| GroupMember { + object_identifier: get_sid(obj), + object_type: type_string(obj), + }) +} + +fn is_highvalue(sid: &str, highvalue_sids: &HashSet<&str>) -> bool { + sid.ends_with("-512") + || sid.ends_with("-516") + || sid.ends_with("-519") + || sid.ends_with("-520") + || highvalue_sids.contains(sid) +} + +const WELLKNOWN_SIDS: &[&str] = &[ + "S-1-0", + "S-1-0-0", + "S-1-1", + "S-1-1-0", + "S-1-2", + "S-1-2-0", + "S-1-2-1", + "S-1-3", + "S-1-3-0", + "S-1-3-1", + "S-1-3-2", + "S-1-3-3", + "S-1-3-4", + "S-1-5-1", + "S-1-5-2", + "S-1-5-3", + "S-1-5-4", + "S-1-5-6", + "S-1-5-7", + "S-1-5-8", + "S-1-5-9", + "S-1-5-10", + "S-1-5-11", + "S-1-5-12", + "S-1-5-13", + "S-1-5-14", + "S-1-5-15", + "S-1-5-17", + "S-1-5-18", + "S-1-5-19", + "S-1-5-20", + "S-1-5-21-0-0-0-496", + "S-1-5-21-0-0-0-497", + "S-1-5-32-544", + "S-1-5-32-545", + "S-1-5-32-546", + "S-1-5-32-547", + "S-1-5-32-548", + "S-1-5-32-549", + "S-1-5-32-550", + "S-1-5-32-551", + "S-1-5-32-552", + "S-1-5-32-554", + "S-1-5-32-555", + "S-1-5-32-556", + "S-1-5-32-557", + "S-1-5-32-558", + "S-1-5-32-559", + "S-1-5-32-560", + "S-1-5-32-561", + "S-1-5-32-562", + "S-1-5-32-568", + "S-1-5-32-569", + "S-1-5-32-573", + "S-1-5-32-574", + "S-1-5-32-575", + "S-1-5-32-576", + "S-1-5-32-577", + "S-1-5-32-578", + "S-1-5-32-579", + "S-1-5-32-580", +]; diff --git a/src/output/bloodhound/mod.rs b/src/output/bloodhound/mod.rs new file mode 100644 index 0000000..ec37385 --- /dev/null +++ b/src/output/bloodhound/mod.rs @@ -0,0 +1,17 @@ +mod common; +mod computers; +mod containers; +mod domains; +mod gpos; +mod groups; +mod ous; +mod users; +mod utils; + +pub use computers::ComputersOutput; +pub use containers::ContainersOutput; +pub use domains::DomainsOutput; +pub use gpos::GPOsOutput; +pub use groups::GroupsOutput; +pub use ous::OUsOutput; +pub use users::UsersOutput; diff --git a/src/output/bloodhound/ous.rs b/src/output/bloodhound/ous.rs new file mode 100644 index 0000000..d4e77fe --- /dev/null +++ b/src/output/bloodhound/ous.rs @@ -0,0 +1,214 @@ +use super::common::{get_aces, is_acl_protected, ldap2domain, type_string}; +use super::utils::{Aces, Meta}; +use crate::parser::{ADExplorerSnapshot, AttributeValue, Object, ObjectType}; +use nom::{ + branch::alt, + bytes::complete::{is_not, tag}, + character::complete::char, + combinator::{map, opt, value}, + multi::separated_list0, + sequence::{delimited, preceded, tuple}, + IResult, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct OUsOutput { + meta: Meta, + #[serde(rename = "data")] + ous: Vec, +} + +impl OUsOutput { + pub fn new(snapshot: &ADExplorerSnapshot) -> Self { + let ous: Vec = snapshot + .snapshot + .objects + .iter() + .filter(|obj| obj.get_type() == ObjectType::OU) + .map(|obj| OU::new(obj, snapshot)) + .collect(); + + Self { + meta: Meta { + methods: 46067, + r#type: "ous".to_string(), + count: ous.len() as u64, + version: 5, + }, + ous, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OU { + #[serde(rename = "Properties")] + pub properties: OUProperties, + + #[serde(rename = "Links")] + pub links: Vec, + + #[serde(rename = "ChildObjects")] + pub child_objects: Vec, + + #[serde(rename = "Aces")] + aces: Vec, + + #[serde(rename = "ObjectIdentifier")] + object_identifier: String, + + #[serde(rename = "IsDeleted")] + is_deleted: bool, + + #[serde(rename = "IsACLProtected")] + is_acl_protected: bool, +} + +impl OU { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + OU { + properties: OUProperties::new(obj, snapshot), + links: process_links(obj), + child_objects: process_child_objects(obj, snapshot), + aces: get_aces(obj, snapshot), + object_identifier: obj + .get_first("objectGUID") + .and_then(AttributeValue::as_guid) + .map(|v| v.to_string()) + .unwrap_or_default(), + is_deleted: false, // Assuming this information is not available in the snapshot + is_acl_protected: is_acl_protected(obj), + } + } +} + +fn process_child_objects(obj: &Object, snapshot: &ADExplorerSnapshot) -> Vec { + let mut child_objects = Vec::new(); + + let ou_dn = match obj + .get_first("distinguishedName") + .and_then(AttributeValue::as_string) + { + Some(dn) => dn, + None => return child_objects, + }; + + let child_indexes = snapshot.caches.dn_cache.get_ou_children(ou_dn); + + for &index in &child_indexes { + if let Some(child_obj) = snapshot.snapshot.objects.get(index) { + child_objects.push(ChildObject { + object_identifier: child_obj + .get_object_identifier() + .unwrap_or("ERR_UNKNOWN".to_string()), + object_type: type_string(child_obj), + }); + } + } + + child_objects +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OUProperties { + pub domain: String, + pub name: String, + pub distinguishedname: String, + pub domainsid: String, + pub description: Option, + pub whencreated: i64, + pub blocksinheritance: bool, +} + +impl OUProperties { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + let distinguished_name = obj + .get_first("distinguishedName") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + let domain = ldap2domain(&distinguished_name).to_uppercase(); + let name = obj + .get_first("name") + .and_then(AttributeValue::as_string) + .map(|v| v.clone().to_uppercase()) + .unwrap_or_default(); + + OUProperties { + domain: domain.clone(), + name: format!("{}@{}", name, domain), + distinguishedname: distinguished_name, + domainsid: snapshot.caches.domain_sid.as_ref().unwrap().to_string(), + description: obj + .get_first("description") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()), + whencreated: obj + .get_first("whenCreated") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + blocksinheritance: obj + .get_first("gPOptions") + .and_then(AttributeValue::as_integer) + .map(|v| v & 1 != 0) + .unwrap_or(false), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Link { + #[serde(rename = "IsEnforced")] + pub is_enforced: bool, + #[serde(rename = "GUID")] + pub guid: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChildObject { + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, + #[serde(rename = "ObjectType")] + pub object_type: String, +} + +impl Link { + fn parse_guid(input: &str) -> IResult<&str, String> { + map(delimited(char('{'), is_not("}"), char('}')), |s: &str| { + s.to_uppercase() + })(input) + } + + fn parse_gplink_entry(input: &str) -> IResult<&str, Link> { + map( + tuple(( + preceded(tag("LDAP://cn="), Self::parse_guid), + alt((value(true, tag(";2")), value(false, opt(tag(";0"))))), + )), + |(guid, is_enforced)| Link { guid, is_enforced }, + )(input) + } + + fn parse_gplink(input: &str) -> IResult<&str, Vec> { + separated_list0( + char(']'), + delimited(char('['), Self::parse_gplink_entry, opt(char(']'))), + )(input) + } + + pub fn from_gplink(gplink: &str) -> Vec { + match Self::parse_gplink(gplink) { + Ok((_, links)) => links, + Err(_) => Vec::new(), + } + } +} + +fn process_links(obj: &Object) -> Vec { + obj.get("gPLink") + .and_then(|values| values.first()) + .and_then(AttributeValue::as_string) + .map(|gplink| Link::from_gplink(gplink)) + .unwrap_or_default() +} diff --git a/src/output/bloodhound/users.rs b/src/output/bloodhound/users.rs new file mode 100644 index 0000000..584d919 --- /dev/null +++ b/src/output/bloodhound/users.rs @@ -0,0 +1,395 @@ +use super::common::{get_aces, get_sid, is_acl_protected, ldap2domain}; +use super::utils::{Aces, Meta}; +use crate::output::bloodhound::common::type_string; +use crate::parser::Cache; +use crate::parser::{ADExplorerSnapshot, AttributeValue, Object}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Serialize, Deserialize)] +pub struct UsersOutput { + meta: Meta, + #[serde(rename = "data")] + users: Vec, +} + +impl UsersOutput { + pub fn new(snapshot: &ADExplorerSnapshot) -> Self { + let snapshot = Arc::new(snapshot); + let domain_sid = snapshot.caches.domain_sid.as_ref().unwrap().to_string(); + + let users: Vec = snapshot + .snapshot + .objects + .iter() + .filter(|obj| Self::is_valid_user(obj, &snapshot)) + .map(|obj| User::new(obj, &snapshot, &domain_sid)) + .collect(); + + Self { + meta: Meta { + methods: 46067, + r#type: "users".to_string(), + count: users.len() as u64, + version: 5, + }, + users, + } + } + + fn is_valid_user(obj: &Object, snapshot: &ADExplorerSnapshot) -> bool { + let classes = obj + .get("objectClass") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_string) + .map(|v| v.to_string()) + .collect::>() + }) + .unwrap_or_default(); + let category = Self::get_object_category(obj, snapshot).unwrap_or_default(); + + let class_condition = (classes.contains(&"user".to_string()) && category == "person") + || classes.contains(&"ms-DS-Group-Managed-Service-Account".to_string()); + + let account_type_condition = obj + .get_first("sAMAccountType") + .and_then(AttributeValue::as_integer) + .map(|account_type| account_type != 805306370) + .unwrap_or(false); + + class_condition && account_type_condition + } + + fn get_object_category(obj: &Object, snapshot: &ADExplorerSnapshot) -> Option { + obj.get_first("objectCategory") + .and_then(AttributeValue::as_string) + .and_then(|cat_dn| snapshot.caches.class_cache.get(cat_dn)) + .and_then(|cat_idx| snapshot.snapshot.classes.get(*cat_idx)) + .map(|cat_obj| cat_obj.class_name.clone()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + #[serde(rename = "Properties")] + pub properties: UserProperties, + + #[serde(rename = "AllowedToDelegate")] + pub allowed_to_delegate: Vec, + + #[serde(rename = "PrimaryGroupSID")] + pub primary_group_sid: String, + + #[serde(rename = "HasSIDHistory")] + pub has_sid_history: Vec, + + #[serde(rename = "SpnTargets")] + pub spn_targets: Vec, + + #[serde(rename = "Aces")] + aces: Vec, + + #[serde(rename = "ObjectIdentifier")] + object_identifier: String, + + #[serde(rename = "IsDeleted")] + is_deleted: bool, + + #[serde(rename = "IsACLProtected")] + is_acl_protected: bool, +} + +impl User { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot, domain_sid: &str) -> Self { + User { + properties: UserProperties::new(obj, snapshot), + allowed_to_delegate: process_allowed_to_delegate(obj, snapshot), + primary_group_sid: get_primary_group_sid(obj, domain_sid), + has_sid_history: process_sid_history(obj), + spn_targets: process_spn_targets(obj, snapshot), + aces: get_aces(obj, snapshot), + object_identifier: get_sid(obj), + is_deleted: false, // Assuming this information is not available in the snapshot + is_acl_protected: is_acl_protected(obj), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserProperties { + pub domain: String, + pub name: String, + pub distinguishedname: String, + pub domainsid: String, + pub description: Option, + pub whencreated: i64, + pub sensitive: bool, + pub dontreqpreauth: bool, + pub passwordnotreqd: bool, + pub unconstraineddelegation: bool, + pub pwdneverexpires: bool, + pub enabled: bool, + pub trustedtoauth: bool, + pub lastlogon: i64, + pub lastlogontimestamp: i64, + pub pwdlastset: i64, + pub serviceprincipalnames: Vec, + pub hasspn: bool, + pub displayname: Option, + pub admincount: bool, + pub sidhistory: Vec, +} + +impl UserProperties { + pub fn new(obj: &Object, snapshot: &ADExplorerSnapshot) -> Self { + let distinguished_name = obj + .get_first("distinguishedName") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + let domain = ldap2domain(&distinguished_name).to_uppercase(); + let name = obj + .get_first("name") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()) + .unwrap_or_default(); + + UserProperties { + domain: domain.clone(), + name: format!("{}@{}", name.to_uppercase(), domain), + distinguishedname: distinguished_name, + domainsid: snapshot.caches.domain_sid.as_ref().unwrap().to_string(), + description: obj + .get_first("description") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()), + whencreated: obj + .get_first("whenCreated") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + sensitive: obj + .get_first("isSensitiveAccount") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + dontreqpreauth: obj + .get_first("doesNotRequirePreAuth") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + passwordnotreqd: obj + .get_first("passwordNotRequired") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + unconstraineddelegation: obj + .get_first("trustToDelegateComputer") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + pwdneverexpires: obj + .get_first("passwordNeverExpires") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + enabled: !obj + .get_first("accountDisabled") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + trustedtoauth: obj + .get_first("trustedToAuthForDelegation") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + lastlogon: obj + .get_first("lastLogon") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + lastlogontimestamp: obj + .get_first("lastLogonTimestamp") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(-1), + pwdlastset: obj + .get_first("pwdLastSet") + .and_then(AttributeValue::as_unix_timestamp) + .unwrap_or(0), + serviceprincipalnames: obj + .get("servicePrincipalName") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_string) + .cloned() + .collect() + }) + .unwrap_or_default(), + hasspn: !obj + .get("servicePrincipalName") + .map(|values| values.is_empty()) + .unwrap_or(true), + displayname: obj + .get_first("displayName") + .and_then(AttributeValue::as_string) + .map(|v| v.clone()), + admincount: obj + .get_first("adminCount") + .and_then(AttributeValue::as_boolean) + .unwrap_or(false), + sidhistory: obj + .get("sIDHistory") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_sid) + .map(|sid| sid.to_string()) + .collect() + }) + .unwrap_or_default(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DelegationTarget { + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, + #[serde(rename = "ObjectType")] + pub object_type: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SIDHistoryItem { + #[serde(rename = "ObjectIdentifier")] + pub object_identifier: String, + #[serde(rename = "ObjectType")] + pub object_type: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SPNTarget { + #[serde(rename = "ComputerSID")] + pub computer_sid: String, + pub port: u16, + pub service: String, +} + +fn process_allowed_to_delegate( + obj: &Object, + snapshot: &ADExplorerSnapshot, +) -> Vec { + obj.get("msDS-AllowedToDelegateTo") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_string) + .flat_map(|host| { + let target = host.split('/').nth(1).unwrap_or(host); + + if let Some(target_obj) = snapshot.get_computer(&target) { + vec![DelegationTarget { + object_identifier: target_obj + .get_object_identifier() + .unwrap_or("ERR_UNKNOWN".to_string()), + object_type: type_string(target_obj), + }] + } else if target.contains('.') { + vec![DelegationTarget { + object_identifier: target.to_uppercase(), + object_type: "Computer".to_string(), + }] + } else { + eprintln!("Invalid delegation target: {}", host); + vec![] + } + }) + .collect() + }) + .unwrap_or_default() +} + +fn get_primary_group_sid(obj: &Object, domain_sid: &str) -> String { + let group_id = obj + .get_first("primaryGroupID") + .and_then(AttributeValue::as_integer) + .unwrap_or(513); // Default to 513 (Domain Users) if not found + + format!("{}-{}", domain_sid, group_id) +} + +fn process_sid_history(obj: &Object) -> Vec { + obj.get("sIDHistory") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_sid) + .map(|sid| SIDHistoryItem { + object_identifier: sid.to_string(), + object_type: "User".to_string(), + }) + .collect() + }) + .unwrap_or_default() +} + +// https://github.com/BloodHoundAD/SharpHoundCommon/blob/ea6b097927c5bb795adb8589e9a843293d36ae37/src/CommonLib/Processors/SPNProcessors.cs#L19 +pub fn process_spn_targets(obj: &Object, snapshot: &ADExplorerSnapshot) -> Vec { + let computer_cache = &snapshot.caches.computer_cache; + obj.get("servicePrincipalName") + .map(|values| { + values + .iter() + .filter_map(AttributeValue::as_string) + .filter_map(|spn| { + // Skip SPNs containing '@' + if spn.contains('@') { + return None; + } + + let parts: Vec<&str> = spn.split('/').collect(); + if parts.len() >= 2 { + let service = parts[0].to_lowercase(); + let target_with_port = parts[1]; + + // Extract hostname (remove port if present) + let target = target_with_port + .split(':') + .next() + .unwrap_or(target_with_port) + .to_string(); + + // Parse port, defaulting to 1433 if not specified or invalid + let port = parts + .get(2) + .and_then(|p| p.split(':').last()) + .and_then(|p| p.parse().ok()) + .or_else(|| { + target_with_port + .split(':') + .nth(1) + .and_then(|p| p.parse().ok()) + }) + .unwrap_or(1433); + + // Check if the service is MSSQL (case-insensitive) + if service.contains("MSSQLSvc") { + let computer_sid = if computer_cache.contains_key(&target) { + target.clone() + } else if target.contains('.') { + target.to_uppercase() + } else { + eprintln!("Invalid SPN target: {} - {}", spn, target); + return None; + }; + + Some(SPNTarget { + computer_sid, + port, + service: String::from("SQLAdmin"), + }) + } else { + None + } + } else { + None + } + }) + .collect() + }) + .unwrap_or_default() +} diff --git a/src/output/bloodhound/utils/aces.rs b/src/output/bloodhound/utils/aces.rs new file mode 100644 index 0000000..3e7b76f --- /dev/null +++ b/src/output/bloodhound/utils/aces.rs @@ -0,0 +1,178 @@ +use crate::{ + output::bloodhound::common::type_string, + parser::{ADExplorerSnapshot, ObjectType}, + security_descriptor::{ACEFlags, ACEGuid, AccessMask, ACE, SDDL}, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Aces { + #[serde(rename = "PrincipalSID")] + pub principal_sid: String, + + #[serde(rename = "PrincipalType")] + pub principal_type: String, + + #[serde(rename = "RightName")] + pub right_name: String, + + #[serde(rename = "IsInherited")] + pub is_inherited: bool, +} + +impl Aces { + pub fn from_security_descriptor( + sd: &SDDL, + snapshot: &ADExplorerSnapshot, + object_type: &ObjectType, + has_laps: bool, + ) -> Vec { + let mut aces = Vec::new(); + if let Some(owner) = &sd.owner_sid { + if let Some(obj) = snapshot.get_sid(owner) { + let ace = Aces { + principal_sid: owner.to_string(), + principal_type: type_string(obj), + right_name: "Owns".to_string(), + is_inherited: false, + }; + aces.push(ace); + } else { + // eprintln!("Owner SID not found in snapshot: {}", owner.to_string()); + } + } + + if let Some(dacl) = &sd.dacl { + for ace in dacl + .aces + .iter() + .filter(|ace| !matches!(ace, ACE::AccessDenied(_) | ACE::AccessDeniedObject(_))) + { + let rights = Self::rights(ace, object_type, has_laps); + if let Some(target_obj) = snapshot.get_sid(&ace.sid()) { + for right in rights { + let ace = Aces { + principal_sid: ace.sid().to_string(), + principal_type: type_string(target_obj), + right_name: right, + is_inherited: Self::is_inherited(ace), + }; + aces.push(ace); + } + } + } + } + + aces + } + + fn is_inherited(ace: &ACE) -> bool { + ace.header().ace_flags.is_set(ACEFlags::INHERITED_ACE) + } + + fn rights(ace: &ACE, object_type: &ObjectType, has_laps: bool) -> HashSet { + let mut rights = HashSet::new(); + let ace_mask = ace.mask(); + let ace_type = ace.object_type_s(); + + // GenericAll + if ace_mask.has_flag(AccessMask::GENERIC_ALL) { + if ace_type.is_none() || ace_type == Some(ACEGuid::AllGuid) { + rights.insert("GenericAll".to_string()); + } + return rights; // Early return to avoid other checks + } + + // WriteDACL and WriteOwner + if ace_mask.has_flag(AccessMask::WRITE_DACL) { + rights.insert("WriteDacl".to_string()); + } + if ace_mask.has_flag(AccessMask::WRITE_OWNER) { + rights.insert("WriteOwner".to_string()); + } + + // AddSelf + if ace_mask.has_flag(AccessMask::ADS_RIGHT_DS_SELF) + && !ace_mask.has_flag(AccessMask::ADS_RIGHT_DS_WRITE_PROP) + && !ace_mask.has_flag(AccessMask::GENERIC_WRITE) + && object_type == &ObjectType::Group + && ace_type == Some(ACEGuid::WriteMember) + { + rights.insert("AddSelf".to_string()); + } + + // ExtendedRights + if ace_mask.has_flag(AccessMask::ADS_RIGHT_DS_CONTROL_ACCESS) { + match object_type { + ObjectType::Domain => match ace_type { + Some(ACEGuid::DSReplicationGetChanges) => { + rights.insert("GetChanges".to_string()); + } + Some(ACEGuid::DSReplicationGetChangesAll) => { + rights.insert("GetChangesAll".to_string()); + } + Some(ACEGuid::DSReplicationGetChangesInFilteredSet) => { + rights.insert("GetChangesInFilteredSet".to_string()); + } + Some(ACEGuid::AllGuid) | None => { + rights.insert("AllExtendedRights".to_string()); + } + _ => {} + }, + ObjectType::User => match ace_type { + Some(ACEGuid::UserForceChangePassword) => { + rights.insert("ForceChangePassword".to_string()); + } + Some(ACEGuid::AllGuid) | None => { + rights.insert("AllExtendedRights".to_string()); + } + _ => {} + }, + ObjectType::Computer => { + if has_laps { + if ace_type.is_none() || ace_type == Some(ACEGuid::AllGuid) { + rights.insert("AllExtendedRights".to_string()); + } + } + } + _ => {} + } + } + + // GenericWrite and WriteProperty + if ace_mask.has_flag(AccessMask::GENERIC_WRITE) + || ace_mask.has_flag(AccessMask::ADS_RIGHT_DS_WRITE_PROP) + { + match object_type { + ObjectType::User | ObjectType::Group | ObjectType::Computer | ObjectType::GPO => { + if ace_type.is_none() || ace_type == Some(ACEGuid::AllGuid) { + rights.insert("GenericWrite".to_string()); + } + } + _ => {} + } + + match (object_type, ace_type) { + (ObjectType::User, Some(ACEGuid::WriteSPN)) => { + rights.insert("WriteSPN".to_string()); + } + (ObjectType::Computer, Some(ACEGuid::WriteAllowedToAct)) => { + rights.insert("AddAllowedToAct".to_string()); + } + (ObjectType::Computer, Some(ACEGuid::UserAccountRestrictions)) => { + rights.insert("WriteAccountRestrictions".to_string()); + } + (ObjectType::Group, Some(ACEGuid::WriteMember)) => { + rights.insert("AddMember".to_string()); + } + (ObjectType::User | ObjectType::Computer, Some(ACEGuid::AddKeyPrincipal)) => { + rights.insert("AddKeyCredentialLink".to_string()); + } + _ => {} + } + } + + rights + } +} diff --git a/src/output/bloodhound/utils/meta.rs b/src/output/bloodhound/utils/meta.rs new file mode 100644 index 0000000..462f868 --- /dev/null +++ b/src/output/bloodhound/utils/meta.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Meta { + pub methods: u64, + pub r#type: String, + pub count: u64, + pub version: u8, +} diff --git a/src/output/bloodhound/utils/mod.rs b/src/output/bloodhound/utils/mod.rs new file mode 100644 index 0000000..b1c17c1 --- /dev/null +++ b/src/output/bloodhound/utils/mod.rs @@ -0,0 +1,5 @@ +mod aces; +mod meta; + +pub use aces::Aces; +pub use meta::Meta; diff --git a/src/output/mod.rs b/src/output/mod.rs new file mode 100644 index 0000000..f2dcb9c --- /dev/null +++ b/src/output/mod.rs @@ -0,0 +1 @@ +pub mod bloodhound; diff --git a/src/parser/adexplorersnapshot.rs b/src/parser/adexplorersnapshot.rs new file mode 100644 index 0000000..4c18186 --- /dev/null +++ b/src/parser/adexplorersnapshot.rs @@ -0,0 +1,58 @@ +use super::Caches; +use super::Object; +use super::Snapshot; +use crate::parser::cache::Cache; +use crate::sid::SID; +use serde::Serialize; +use std::io::Result; +use std::path::Path; + +#[derive(Debug, Serialize)] +pub struct ADExplorerSnapshot { + pub snapshot: Snapshot, + #[serde(skip_serializing)] + pub caches: Caches, +} + +impl ADExplorerSnapshot { + pub fn snapshot_from_file>(path: P) -> Result { + let snapshot = Snapshot::snapshot_from_file(path)?; + let mut caches = Caches::new(); + caches.build_caches(&snapshot); + + Ok(ADExplorerSnapshot { snapshot, caches }) + } + + pub fn snapshot_from_memory(snapshot: impl AsRef<[u8]>) -> Result { + let snapshot = Snapshot::snapshot_from_memory(snapshot)?; + let mut caches = Caches::new(); + caches.build_caches(&snapshot); + + Ok(ADExplorerSnapshot { snapshot, caches }) + } + + pub fn build_caches(&mut self, caches: Caches) { + self.caches = caches; + } + + pub fn get_root_domain(&self) -> Option<&Object> { + let root_domain_dn = self.caches.root_domain.as_ref()?; + let root_domain_index = self.caches.dn_cache.get(root_domain_dn)?; + self.snapshot.objects.get(*root_domain_index) + } + + pub fn get_sid(&self, sid: &SID) -> Option<&Object> { + let sid_index = self.caches.sid_cache.get(sid)?; + self.snapshot.objects.get(*sid_index) + } + + pub fn get_computer(&self, computer: &str) -> Option<&Object> { + let computer_index = self.caches.computer_cache.get(&computer.to_string())?; + self.snapshot.objects.get(*computer_index) + } + + pub fn get_dn(&self, dn: &str) -> Option<&Object> { + let dn_index = self.caches.dn_cache.get(&dn.to_string())?; + self.snapshot.objects.get(*dn_index) + } +} diff --git a/src/parser/cache.rs b/src/parser/cache.rs new file mode 100644 index 0000000..248d022 --- /dev/null +++ b/src/parser/cache.rs @@ -0,0 +1,422 @@ +use crate::guid::GUID; +use crate::parser::{AttributeValue, Object}; +use crate::sid::SID; +use std::collections::{HashMap, HashSet}; + +use super::parser::Snapshot; + +pub trait Cache { + fn get(&self, key: &K) -> Option<&V>; + fn insert(&mut self, key: K, value: V); +} + +#[derive(Debug)] +pub struct SIDCache { + cache: HashMap, +} + +impl Cache for SIDCache { + fn get(&self, key: &SID) -> Option<&usize> { + self.cache.get(key) + } + + fn insert(&mut self, key: SID, value: usize) { + self.cache.insert(key, value); + } +} + +#[derive(Debug)] +pub struct DNCache { + cache: HashMap, +} + +impl DNCache { + pub fn get(&self, key: &String) -> Option<&usize> { + self.cache.get(&key.to_uppercase()) + } + + pub fn insert(&mut self, key: String, value: usize) { + self.cache.insert(key.to_uppercase(), value); + } + + pub fn get_ou_children(&self, ou_dn: &str) -> Vec { + let ou_dn_upper = ou_dn.to_uppercase(); + let ou_prefix = format!(",{}", ou_dn_upper); + let mut children = HashSet::new(); + + for (dn, &index) in &self.cache { + if dn != &ou_dn_upper && (dn.ends_with(&ou_prefix) || dn == &ou_dn_upper) { + let relative_dn = &dn[..dn.len() - ou_dn_upper.len()]; + if relative_dn.matches(',').count() <= 1 { + children.insert(index); + } + } + } + + children.into_iter().collect() + } +} + +#[derive(Debug)] +pub struct ComputerCache { + cache: HashMap, +} + +impl ComputerCache { + pub fn get(&self, key: &String) -> Option<&usize> { + self.cache.get(&key.to_uppercase()) + } + + pub fn insert(&mut self, key: String, value: usize) { + self.cache.insert(key.to_uppercase(), value); + } + + pub fn contains_key(&self, key: &String) -> bool { + self.cache.contains_key(&key.to_uppercase()) + } +} + +#[derive(Debug)] +pub struct ObjectTypeGUIDCache { + cache: HashMap, +} + +impl Cache for ObjectTypeGUIDCache { + fn get(&self, key: &usize) -> Option<&GUID> { + self.cache.get(key) + } + + fn insert(&mut self, key: usize, value: GUID) { + self.cache.insert(key, value); + } +} + +#[derive(Debug)] +pub struct ClassCache { + cache: HashMap, +} + +impl Cache for ClassCache { + fn get(&self, key: &String) -> Option<&usize> { + self.cache.get(key) + } + + fn insert(&mut self, key: String, value: usize) { + self.cache.insert(key, value); + } +} + +#[derive(Debug)] +pub struct DomainCache { + domains: HashMap, +} + +impl DomainCache { + fn new() -> Self { + DomainCache { + domains: HashMap::new(), + } + } + + fn insert_domain(&mut self, dn: String, idx: usize) { + self.domains.insert(dn, idx); + } + + fn insert_forest_domain(&mut self, ncname: String, idx: usize) { + if !self.domains.contains_key(&ncname) { + self.domains.insert(ncname, idx); + } + } +} + +#[derive(Debug)] +pub struct CertificateTemplateCache { + templates: HashMap>, +} + +impl CertificateTemplateCache { + fn new() -> Self { + CertificateTemplateCache { + templates: HashMap::new(), + } + } + + fn insert(&mut self, template: String, name: String) { + self.templates + .entry(template) + .or_insert_with(HashSet::new) + .insert(name); + } + + fn _get(&self, template: &str) -> Option<&HashSet> { + self.templates.get(template) + } +} + +#[derive(Debug)] +pub struct Caches { + pub root_domain: Option, + pub domain_sid: Option, + pub sid_cache: SIDCache, + pub dn_cache: DNCache, + pub computer_cache: ComputerCache, + pub object_type_guid_cache: ObjectTypeGUIDCache, + pub class_cache: ClassCache, + pub domain_cache: DomainCache, + pub domain_controllers: Vec, + pub certificate_template_cache: CertificateTemplateCache, +} + +impl Caches { + pub fn new() -> Self { + Caches { + root_domain: None, + domain_sid: None, + sid_cache: SIDCache { + cache: HashMap::new(), + }, + dn_cache: DNCache { + cache: HashMap::new(), + }, + computer_cache: ComputerCache { + cache: HashMap::new(), + }, + object_type_guid_cache: ObjectTypeGUIDCache { + cache: HashMap::new(), + }, + class_cache: ClassCache { + cache: HashMap::new(), + }, + domain_cache: DomainCache::new(), + domain_controllers: Vec::new(), + certificate_template_cache: CertificateTemplateCache::new(), + } + } + + pub fn build_caches(&mut self, snapshot: &Snapshot) { + self.build_object_type_guid_cache(snapshot); + self.build_class_cache(snapshot); + self.build_object_caches(snapshot); + } + + fn build_object_type_guid_cache(&mut self, snapshot: &Snapshot) { + // Build cache from classes + for (i, cl) in snapshot.classes.iter().enumerate() { + self.object_type_guid_cache + .insert(i, cl.schema_id_guid.clone()); + } + + // Build cache from properties + for (i, p) in snapshot.properties.iter().enumerate() { + self.object_type_guid_cache + .insert(i, p.schema_id_guid.clone()); + } + } + + fn build_class_cache(&mut self, snapshot: &Snapshot) { + for (index, class) in snapshot.classes.iter().enumerate() { + // Store by class name + self.class_cache.insert(class.class_name.clone(), index); + + // Store by DN + self.class_cache.insert(class.dn.clone(), index); + + // Store by CN (first part of DN) + if let Some(cn) = class + .dn + .split(',') + .next() + .and_then(|part| part.split('=').nth(1)) + { + self.class_cache.insert(cn.to_string(), index); + } + } + } + + fn build_object_caches(&mut self, snapshot: &Snapshot) { + for (idx, obj) in snapshot.objects.iter().enumerate() { + // Build SID cache + let sid = Self::get_object_sid(obj); + if let Some(sid) = sid.as_ref() { + self.sid_cache.insert(sid.clone(), idx); + } + + // Build DN cache + if let Some(dn) = Self::get_object_dn(obj) { + self.dn_cache.insert(dn, idx); + } + + if let Some(classes) = obj.get_attribute_classes() { + let lowercase_classes: Vec = + classes.iter().map(|s| s.to_lowercase()).collect(); + + // Build Domain cache + if lowercase_classes.contains(&"domain".to_string()) { + self.root_domain = Self::get_object_dn(obj); + self.domain_sid = sid.clone(); + if let Some(dn) = Self::get_object_dn(obj) { + self.domain_cache.insert_domain(dn, idx); + } + } + + // Build Forest Domain cache + if lowercase_classes.contains(&"crossref".to_string()) { + if let Some(system_flags) = self.get_attribute_value::(obj, "systemFlags") + { + if system_flags & 2 == 2 { + if let Some(ncname) = self.get_attribute_value::(obj, "nCName") + { + self.domain_cache.insert_forest_domain(ncname, idx); + } + } + } + } + + // Build Certificate Template cache + if lowercase_classes.contains(&"pkienrollmentservice".to_string()) { + if let Some(name) = self.get_attribute_value::(obj, "name") { + if let Some(templates) = + self.get_attribute_value::>(obj, "certificateTemplates") + { + for template in templates { + self.certificate_template_cache + .insert(template, name.clone()); + } + } + } + } + } + + if Self::is_computer(obj) { + if let Some(dnshostname) = Self::get_object_dnshostname(obj) { + self.computer_cache.insert(dnshostname, idx); + } + if let Some(name) = Self::get_object_name(obj) { + self.computer_cache.insert(name, idx); + } + } + + if let Some(uac) = self.get_attribute_value::(obj, "userAccountControl") { + if uac & 0x2000 == 0x2000 { + self.domain_controllers.push(idx); + } + } + } + } + + fn is_computer(obj: &Object) -> bool { + obj.attributes + .get("sAMAccountType") + .and_then(|attr| { + if let Some(AttributeValue::Integer(account_type)) = attr.values.first() { + Some(*account_type == 805306369) + } else { + None + } + }) + .unwrap_or(false) + } + + fn get_attribute_value( + &self, + obj: &Object, + attr_name: &str, + ) -> Option { + obj.attributes.get(attr_name).and_then(|attr| { + attr.values + .first() + .and_then(|value| T::from_attribute_value(value)) + }) + } + + fn get_object_sid(obj: &Object) -> Option { + obj.attributes.get("objectSid").and_then(|attr| { + if let Some(AttributeValue::OctetString(octet_string)) = attr.values.first() { + SID::from_bytes(octet_string).ok() + } else { + None + } + }) + } + + fn get_object_dn(obj: &Object) -> Option { + obj.attributes.get("distinguishedName").and_then(|attr| { + if let Some(AttributeValue::String(dn)) = attr.values.first() { + Some(dn.clone()) + } else { + None + } + }) + } + + fn get_object_dnshostname(obj: &Object) -> Option { + obj.attributes.get("dNSHostName").and_then(|attr| { + if let Some(AttributeValue::String(hostname)) = attr.values.first() { + Some(hostname.clone()) + } else { + None + } + }) + } + + fn get_object_name(obj: &Object) -> Option { + obj.attributes.get("name").and_then(|attr| { + if let Some(AttributeValue::String(name)) = attr.values.first() { + Some(name.clone()) + } else { + None + } + }) + } +} + +trait FromAttributeValue { + fn from_attribute_value(value: &AttributeValue) -> Option + where + Self: Sized; +} + +impl FromAttributeValue for String { + fn from_attribute_value(value: &AttributeValue) -> Option { + match value { + AttributeValue::String(s) => Some(s.clone()), + _ => None, + } + } +} + +impl FromAttributeValue for i64 { + fn from_attribute_value(value: &AttributeValue) -> Option { + match value { + AttributeValue::LargeInteger(i) => Some(*i), + _ => None, + } + } +} + +impl FromAttributeValue for u32 { + fn from_attribute_value(value: &AttributeValue) -> Option { + match value { + AttributeValue::Integer(i) => Some(*i), + _ => None, + } + } +} + +impl FromAttributeValue for bool { + fn from_attribute_value(value: &AttributeValue) -> Option { + match value { + AttributeValue::Boolean(b) => Some(*b), + _ => None, + } + } +} + +impl FromAttributeValue for Vec { + fn from_attribute_value(value: &AttributeValue) -> Option { + match value { + AttributeValue::String(s) => Some(vec![s.clone()]), + _ => None, + } + } +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs new file mode 100644 index 0000000..657fb48 --- /dev/null +++ b/src/parser/mod.rs @@ -0,0 +1,8 @@ +mod adexplorersnapshot; +mod cache; +mod parser; + +pub use adexplorersnapshot::ADExplorerSnapshot; +pub use cache::{Cache, Caches}; +use parser::Snapshot; +pub use parser::{AttributeValue, Object, ObjectType}; diff --git a/src/parser/parser.rs b/src/parser/parser.rs new file mode 100644 index 0000000..ebae850 --- /dev/null +++ b/src/parser/parser.rs @@ -0,0 +1,753 @@ +use crate::guid::GUID; +use crate::security_descriptor::SDDL; +use crate::sid::SID; +use byteorder::{LittleEndian, ReadBytesExt}; +use chrono::{TimeZone, Utc}; +use memmap2::Mmap; +use serde::Serialize; +use std::char; +use std::collections::HashMap; +use std::fs::File; +use std::io::Result; +use std::io::{Cursor, Error, ErrorKind, Read, Seek, SeekFrom}; +use std::path::Path; + +fn read_wstring_exact(reader: &mut impl Read, num_chars: usize) -> Result { + let mut buffer = vec![0u8; num_chars * 2]; + reader.read_exact(&mut buffer)?; + + let utf16_chars: Vec = buffer + .chunks_exact(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .take_while(|&c| c != 0) + .collect(); + + Ok(String::from_utf16_lossy(&utf16_chars)) +} + +fn read_next_wstring(file: &mut impl Read) -> Result { + let mut result = String::new(); + let mut word_buf = [0u8; 2]; + + loop { + file.read_exact(&mut word_buf)?; + let word = u16::from_le_bytes(word_buf); + + if word == 0 { + break; + } + + if let Some(ch) = char::from_u32(word as u32) { + result.push(ch); + } else { + result.push(char::REPLACEMENT_CHARACTER); + } + } + + Ok(result) +} + +fn read_wstring(reader: &mut T) -> Result { + let len = reader.read_u32::()? as usize; + let mut result = String::with_capacity(len / 2); + let mut word_buf = [0u8; 2]; + + for _ in 0..len / 2 { + reader.read_exact(&mut word_buf)?; + let word = u16::from_le_bytes(word_buf); + + if word == 0 { + break; + } + + if let Some(ch) = char::from_u32(word as u32) { + result.push(ch); + } else { + result.push(char::REPLACEMENT_CHARACTER); + } + } + + Ok(result) +} + +fn read_guid(reader: &mut T) -> Result { + let mut buffer = [0u8; 16]; + reader.read_exact(&mut buffer)?; + GUID::from_bytes(&buffer).map_err(|e| { + Error::new( + ErrorKind::InvalidData, + format!("Failed to parse GUID: {:?}", e), + ) + }) +} + +#[derive(Debug, Serialize)] +pub struct Header { + pub win_ad_sig: String, + pub marker: i32, + pub filetime: u64, + pub optional_description: String, + pub server: String, + pub num_objects: u32, + pub num_attributes: u32, + pub fileoffset_low: u32, + pub fileoffset_high: u32, + pub fileoffset_end: u32, + pub unk0x43a: i32, +} + +impl Header { + fn parse(reader: &mut impl Read) -> Result { + let mut win_ad_sig = [0u8; 10]; + reader.read_exact(&mut win_ad_sig)?; + + Ok(Header { + win_ad_sig: String::from_utf8_lossy(&win_ad_sig).to_string(), + marker: reader.read_i32::()?, + filetime: reader.read_u64::()?, + optional_description: read_wstring_exact(reader, 260)?, + server: read_wstring_exact(reader, 260)?, + num_objects: reader.read_u32::()?, + num_attributes: reader.read_u32::()?, + fileoffset_low: reader.read_u32::()?, + fileoffset_high: reader.read_u32::()?, + fileoffset_end: reader.read_u32::()?, + unk0x43a: reader.read_i32::()?, + }) + } +} + +#[derive(Debug, Serialize)] +pub struct Property { + pub prop_name: String, + pub unk1: i32, + pub ads_type: u32, + pub dn: String, + pub schema_id_guid: GUID, + pub attribute_security_guid: GUID, +} + +impl Property { + pub fn parse(reader: &mut T) -> Result { + let prop_name = read_wstring(reader)?; + let unk1 = reader.read_i32::()?; + let ads_type = reader.read_u32::()?; + let dn = read_wstring(reader)?; + let schema_id_guid = read_guid(reader)?; + let attribute_security_guid = read_guid(reader)?; + + // Skip the blob (4 bytes) + reader.seek(SeekFrom::Current(4))?; + + Ok(Property { + prop_name, + unk1, + ads_type, + dn, + schema_id_guid, + attribute_security_guid, + }) + } +} + +#[derive(Debug, Serialize)] +struct MappingEntry { + attr_index: u32, + attr_offset: i32, +} + +#[derive(Debug, Serialize, PartialEq, Eq)] +pub enum ObjectType { + Computer, + User, + UserDisabled, + Group, + Domain, + OU, + Container, + GPO, + Unknown, +} + +#[derive(Debug, Serialize)] +pub struct Object { + pub obj_size: u32, + pub table_size: u32, + mapping_table: Vec, + pub attributes: HashMap, +} + +impl Object { + fn parse(reader: &mut (impl Read + Seek), properties: &[Property]) -> Result { + let start_pos = reader.stream_position()?; + let obj_size = reader.read_u32::()?; + let table_size = reader.read_u32::()?; + + let mapping_table = (0..table_size) + .map(|_| { + Ok(MappingEntry { + attr_index: reader.read_u32::()?, + attr_offset: reader.read_i32::()?, + }) + }) + .collect::>>()?; + + let attributes = mapping_table + .iter() + .filter_map(|entry| { + let property = properties.get(entry.attr_index as usize)?; + let attr_pos = if entry.attr_offset >= 0 { + start_pos + entry.attr_offset as u64 + } else { + start_pos.checked_sub(entry.attr_offset.unsigned_abs() as u64)? + }; + + let current_pos = reader.stream_position().ok()?; + reader.seek(SeekFrom::Start(attr_pos)).ok()?; + let attribute = Attribute::parse(reader, property.ads_type).ok()?; + reader.seek(SeekFrom::Start(current_pos)).ok()?; + + Some((property.prop_name.clone(), attribute)) + }) + .collect(); + + reader.seek(SeekFrom::Start(start_pos + obj_size as u64))?; + + Ok(Object { + obj_size, + table_size, + mapping_table, + attributes, + }) + } + + pub fn get_attribute_names(&self) -> Vec { + self.attributes.keys().cloned().collect() + } + + pub fn get(&self, attr_name: &str) -> Option<&Vec> { + self.attributes.get(attr_name).map(|attr| &attr.values) + } + + pub fn get_first(&self, attr_name: &str) -> Option<&AttributeValue> { + self.get(attr_name).and_then(|values| values.first()) + } + + pub fn get_attribute_classes(&self) -> Option> { + let values = self.get("objectClass")?; + Some( + values + .iter() + .filter_map(|v| AttributeValue::as_string(v).cloned()) + .collect(), + ) + } + + pub fn has_attribute_class(&self, class: &str) -> bool { + self.get_attribute_classes() + .map(|classes| classes.iter().any(|c| c == class)) + .unwrap_or(false) + } + + pub fn get_object_identifier(&self) -> Option { + match self.get_type() { + ObjectType::Computer | ObjectType::User | ObjectType::Group => self + .get_first("objectSid") + .and_then(AttributeValue::as_sid) + .map(|sid| sid.to_string()), + ObjectType::OU | ObjectType::Container | ObjectType::GPO => self + .get_first("objectGUID") + .and_then(AttributeValue::as_guid) + .map(|guid| guid.to_string()), + _ => None, + } + } + + pub fn get_type(&self) -> ObjectType { + // For some reason, some GPOs have gPCFileSysPath attribute but not in the objectClass of groupPolicyContainer + if self.get_first("gPCFileSysPath").is_some() { + return ObjectType::GPO; + } + + if self.has_attribute_class("user") { + if let Some(uac) = self + .get_first("userAccountControl") + .and_then(AttributeValue::as_integer) + { + return if uac & 0x00000002 != 0 { + ObjectType::UserDisabled + } else { + ObjectType::User + }; + } + } + + if let Some(classes) = self.get_attribute_classes() { + for class in classes.iter() { + match class.as_str() { + "computer" => return ObjectType::Computer, + "user" => { + if let Some(uac) = self + .get_first("userAccountControl") + .and_then(AttributeValue::as_integer) + { + return if uac & 0x00000002 != 0 { + ObjectType::UserDisabled + } else { + ObjectType::User + }; + } + } + "group" => return ObjectType::Group, + "domain" => return ObjectType::Domain, + "organizationalUnit" => return ObjectType::OU, + "container" => return ObjectType::Container, + "groupPolicyContainer" => return ObjectType::GPO, + _ => continue, + } + } + } + ObjectType::Unknown + } +} + +#[derive(Debug, Serialize)] +pub struct Attribute { + pub num_values: u32, + pub values: Vec, +} + +impl Attribute { + fn parse(reader: &mut T, ads_type: u32) -> Result { + let attribute_start = reader.stream_position()?; + let num_values = reader.read_u32::()?; + + let values = match ads_type { + 1 | 2 | 3 | 4 | 5 | 12 => { + Self::parse_string_values(reader, num_values, attribute_start)? + } + 8 => Self::parse_octet_string_values(reader, num_values)?, + 6 => { + if num_values != 1 { + return Err(Error::new( + ErrorKind::InvalidData, + "Boolean attribute should have only one value", + )); + } + vec![AttributeValue::Boolean( + reader.read_u32::()? != 0, + )] + } + 7 => (0..num_values) + .map(|_| Ok(AttributeValue::Integer(reader.read_u32::()?))) + .collect::>>()?, + 10 => (0..num_values) + .map(|_| { + Ok(AttributeValue::LargeInteger( + reader.read_i64::()?, + )) + }) + .collect::>>()?, + 9 => Self::parse_utc_time_values(reader, num_values)?, + 25 => Self::parse_nt_security_descriptor(reader)?, + _ => { + return Err(Error::new( + ErrorKind::InvalidData, + format!("Unhandled ADSTYPE: {}", ads_type), + )) + } + }; + + Ok(Attribute { num_values, values }) + } + + fn parse_string_values( + reader: &mut T, + num_values: u32, + attribute_start: u64, + ) -> Result> { + let mut result = Vec::with_capacity(num_values as usize); + let mut offset_buf = vec![0u32; num_values as usize]; + reader.read_u32_into::(&mut offset_buf)?; + + for &offset in &offset_buf { + let current_pos = reader.stream_position()?; + reader.seek(SeekFrom::Start(attribute_start + offset as u64))?; + let value = AttributeValue::String(read_next_wstring(reader)?); + reader.seek(SeekFrom::Start(current_pos))?; + result.push(value); + } + + Ok(result) + } + + fn parse_octet_string_values( + reader: &mut T, + num_values: u32, + ) -> Result> { + let mut lengths = vec![0u32; num_values as usize]; + reader.read_u32_into::(&mut lengths)?; + + let mut result = Vec::with_capacity(num_values as usize); + + for &length in &lengths { + let mut buffer = vec![0u8; length as usize]; + reader.read_exact(&mut buffer)?; + result.push(AttributeValue::OctetString(buffer)); + } + + Ok(result) + } + + fn parse_utc_time_values( + reader: &mut T, + num_values: u32, + ) -> Result> { + let mut time_values = Vec::with_capacity(num_values as usize); + + for _ in 0..num_values { + let time = SystemTime { + year: reader.read_u16::()?, + month: reader.read_u16::()?, + day_of_week: reader.read_u16::()?, + day: reader.read_u16::()?, + hour: reader.read_u16::()?, + minute: reader.read_u16::()?, + second: reader.read_u16::()?, + milliseconds: reader.read_u16::()?, + }; + + time_values.push(AttributeValue::UTCTime( + time.to_unix_timestamp() + .ok_or_else(|| Error::new(ErrorKind::InvalidData, "Invalid UTC time"))?, + )) + } + + Ok(time_values) + } + + fn parse_nt_security_descriptor(reader: &mut T) -> Result> { + let len_descriptor_bytes = reader.read_u32::()?; + let mut buffer = vec![0u8; len_descriptor_bytes as usize]; + reader.read_exact(&mut buffer)?; + Ok(vec![AttributeValue::NTSecurityDescriptor(buffer)]) + } +} + +#[derive(Debug, Serialize, Clone)] +pub enum AttributeValue { + String(String), + OctetString(Vec), + Boolean(bool), + Integer(u32), + LargeInteger(i64), + UTCTime(i64), + NTSecurityDescriptor(Vec), +} + +impl AttributeValue { + pub fn as_string(&self) -> Option<&String> { + if let AttributeValue::String(s) = self { + Some(s) + } else { + None + } + } + + pub fn as_str(&self) -> Option<&str> { + if let AttributeValue::String(s) = self { + Some(s) + } else { + None + } + } + + pub fn as_integer(&self) -> Option { + if let AttributeValue::Integer(i) = self { + Some(*i) + } else { + None + } + } + + pub fn as_large_integer(&self) -> Option { + if let AttributeValue::LargeInteger(i) = self { + Some(*i) + } else { + None + } + } + + pub fn as_boolean(&self) -> Option { + if let AttributeValue::Boolean(b) = self { + Some(*b) + } else { + None + } + } + + pub fn as_octet_string(&self) -> Option<&Vec> { + if let AttributeValue::OctetString(o) = self { + Some(o) + } else { + None + } + } + + pub fn as_nt_security_descriptor(&self) -> Option { + if let AttributeValue::NTSecurityDescriptor(o) = self { + SDDL::from_bytes(&o).ok() + } else { + None + } + } + + pub fn as_sid(&self) -> Option { + if let AttributeValue::OctetString(o) = self { + SID::from_bytes(&o).ok() + } else { + None + } + } + + pub fn as_guid(&self) -> Option { + if let AttributeValue::OctetString(o) = self { + GUID::from_bytes(&o).ok() + } else { + None + } + } + + pub fn as_unix_timestamp(&self) -> Option { + match self { + AttributeValue::LargeInteger(t) => { + if *t == 0 { + return Some(0); + } + + Some((*t - 116444736000000000) / 10000000) + } + AttributeValue::UTCTime(t) => Some(*t), + _ => None, + } + } +} + +#[derive(Debug, Serialize, Clone)] +pub struct SystemTime { + year: u16, + month: u16, + day_of_week: u16, + day: u16, + hour: u16, + minute: u16, + second: u16, + milliseconds: u16, +} + +impl SystemTime { + pub fn to_unix_timestamp(&self) -> Option { + let datetime = Utc.with_ymd_and_hms( + self.year as i32, + self.month as u32, + self.day as u32, + self.hour as u32, + self.minute as u32, + self.second as u32, + ); + + datetime.single().map(|dt| dt.timestamp()) + } +} + +#[derive(Debug, Serialize)] +struct SystemPossSuperior { + system_poss_superior: String, +} + +#[derive(Debug, Serialize)] +struct AuxiliaryClasses { + auxiliary_class: String, +} + +#[derive(Debug, Serialize)] +struct Block { + unk1: u32, + unk2: u32, + unk3: Vec, +} + +impl Block { + pub fn parse(reader: &mut T) -> Result { + let unk1 = reader.read_u32::()?; + let unk2 = reader.read_u32::()?; + let mut unk3 = vec![0u8; unk2 as usize]; + reader.read_exact(&mut unk3)?; + Ok(Block { unk1, unk2, unk3 }) + } +} + +#[derive(Debug, Serialize)] +pub struct Class { + pub class_name: String, + pub dn: String, + pub common_class_name: String, + pub sub_class_of: String, + pub schema_id_guid: GUID, + pub unk2: Vec, + blocks: Vec, + pub unknown: Vec, + system_poss_superiors: Vec, + auxiliary_classes: Vec, +} + +impl Class { + pub fn parse(reader: &mut T) -> Result { + Ok(Class { + class_name: read_wstring(reader)?, + dn: read_wstring(reader)?, + common_class_name: read_wstring(reader)?, + sub_class_of: read_wstring(reader)?, + schema_id_guid: read_guid(reader)?, + unk2: Self::parse_unk2(reader)?, + blocks: Self::parse_blocks(reader)?, + unknown: Self::parse_unknown(reader)?, + system_poss_superiors: Self::parse_system_poss_superiors(reader)?, + auxiliary_classes: Self::parse_auxiliary_classes(reader)?, + }) + } + + fn parse_unk2(reader: &mut T) -> Result> { + let offset_to_num_blocks = reader.read_u32::()?; + let mut unk2 = vec![0u8; offset_to_num_blocks as usize]; + reader.read_exact(&mut unk2)?; + Ok(unk2) + } + + fn parse_blocks(reader: &mut T) -> Result> { + let num_blocks = reader.read_u32::()?; + (0..num_blocks).map(|_| Block::parse(reader)).collect() + } + + fn parse_unknown(reader: &mut T) -> Result> { + let num_unknown = reader.read_u32::()?; + let mut unknown = vec![0u8; (num_unknown * 0x10) as usize]; + reader.read_exact(&mut unknown)?; + Ok(unknown) + } + + fn parse_system_poss_superiors( + reader: &mut T, + ) -> Result> { + let num_items = reader.read_u32::()?; + (0..num_items) + .map(|_| { + Ok(SystemPossSuperior { + system_poss_superior: read_wstring(reader)?, + }) + }) + .collect() + } + + fn parse_auxiliary_classes(reader: &mut T) -> Result> { + let num_items = reader.read_u32::()?; + (0..num_items) + .map(|_| { + Ok(AuxiliaryClasses { + auxiliary_class: read_wstring(reader)?, + }) + }) + .collect() + } +} + +pub fn parse_classes(reader: &mut T) -> Result> { + let num_classes = reader.read_u32::()?; + (0..num_classes).map(|_| Class::parse(reader)).collect() +} + +#[derive(Debug, Serialize)] +struct Right { + name: String, + desc: String, + blob: [u8; 20], +} + +impl Right { + pub fn parse(reader: &mut T) -> Result { + Ok(Right { + name: read_wstring(reader)?, + desc: read_wstring(reader)?, + blob: Self::read_blob(reader)?, + }) + } + + fn read_blob(reader: &mut T) -> Result<[u8; 20]> { + let mut blob = [0u8; 20]; + reader.read_exact(&mut blob)?; + Ok(blob) + } +} + +fn parse_rights(reader: &mut T) -> Result> { + let num_rights = reader.read_u32::()?; + (0..num_rights).map(|_| Right::parse(reader)).collect() +} + +#[derive(Debug, Serialize)] +pub struct Snapshot { + pub header: Header, + pub properties: Vec, + pub objects: Vec, + pub classes: Vec, + rights: Vec, +} + +impl Snapshot { + pub fn snapshot_from_file>(path: P) -> Result { + let file = File::open(path)?; + let mmap = unsafe { Mmap::map(&file)? }; + Self::snapshot_from_memory(&mmap[..]) + } + + pub fn snapshot_from_memory(snapshot: impl AsRef<[u8]>) -> Result { + let mut cursor = Cursor::new(snapshot.as_ref()); + + let header = Header::parse(&mut cursor)?; + + cursor.seek(SeekFrom::Start( + (header.fileoffset_high as u64) << 32 | header.fileoffset_low as u64, + ))?; + + let num_properties = cursor.read_u32::()?; + + let mut properties = Vec::new(); + for _ in 0..num_properties { + properties.push(Property::parse(&mut cursor)?); + } + + let offset_properties = cursor.position(); + + cursor.seek(SeekFrom::Start(0x43e))?; + + let mut objects = Vec::new(); + for _ in 0..header.num_objects { + objects.push(Object::parse(&mut cursor, &properties)?); + } + + cursor.seek(SeekFrom::Start(offset_properties))?; + + let classes = parse_classes(&mut cursor)?; + let rights = parse_rights(&mut cursor)?; + + let result = Snapshot { + header, + properties, + objects, + classes, + rights, + }; + + Ok(result) + } +} diff --git a/src/security_descriptor/access_mask.rs b/src/security_descriptor/access_mask.rs new file mode 100644 index 0000000..36eba3c --- /dev/null +++ b/src/security_descriptor/access_mask.rs @@ -0,0 +1,193 @@ +use nom::{number::complete::le_u32, IResult}; +use serde::Serialize; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct AccessMask(u32); + +impl AccessMask { + // Generic rights + pub const GENERIC_READ: u32 = 0x80000000; + pub const GENERIC_WRITE: u32 = 0x40000000; + pub const GENERIC_EXECUTE: u32 = 0x20000000; + pub const GENERIC_ALL: u32 = 0x10000000; + + // Standard rights + pub const MAXIMUM_ALLOWED: u32 = 0x02000000; + pub const ACCESS_SYSTEM_SECURITY: u32 = 0x01000000; + pub const SYNCHRONIZE: u32 = 0x00100000; + pub const WRITE_OWNER: u32 = 0x00080000; + pub const WRITE_DACL: u32 = 0x00040000; + pub const READ_CONTROL: u32 = 0x00020000; + pub const DELETE: u32 = 0x00010000; + + // AD rights + pub const ADS_RIGHT_DS_CONTROL_ACCESS: u32 = 0x00000100; + pub const ADS_RIGHT_DS_CREATE_CHILD: u32 = 0x00000001; + pub const ADS_RIGHT_DS_DELETE_CHILD: u32 = 0x00000002; + pub const ADS_RIGHT_DS_READ_PROP: u32 = 0x00000010; + pub const ADS_RIGHT_DS_WRITE_PROP: u32 = 0x00000020; + pub const ADS_RIGHT_DS_SELF: u32 = 0x00000008; + + // Object-specific rights are represented by the lower 16 bits (0-15) + pub const OBJECT_SPECIFIC_RIGHTS_MASK: u32 = 0x0000FFFF; + + pub fn new(mask: u32) -> Self { + AccessMask(mask) + } + + pub fn as_u32(&self) -> u32 { + self.0 + } + + pub fn has_flag(&self, flag: u32) -> bool { + self.0 & flag == flag + } + + pub fn set_flag(&mut self, flag: u32) { + self.0 |= flag; + } + + pub fn clear_flag(&mut self, flag: u32) { + self.0 &= !flag; + } + + pub fn get_rights_generic(&self) -> Vec { + vec![ + AccessMask::GENERIC_READ, + AccessMask::GENERIC_WRITE, + AccessMask::GENERIC_EXECUTE, + AccessMask::GENERIC_ALL, + AccessMask::MAXIMUM_ALLOWED, + AccessMask::ACCESS_SYSTEM_SECURITY, + AccessMask::SYNCHRONIZE, + AccessMask::WRITE_OWNER, + AccessMask::WRITE_DACL, + AccessMask::READ_CONTROL, + AccessMask::DELETE, + ] + .into_iter() + .filter(|&flag| self.has_flag(flag)) + .collect() + } + + pub fn get_rights_ad(&self) -> Vec { + vec![ + AccessMask::ADS_RIGHT_DS_CONTROL_ACCESS, + AccessMask::ADS_RIGHT_DS_CREATE_CHILD, + AccessMask::ADS_RIGHT_DS_DELETE_CHILD, + AccessMask::ADS_RIGHT_DS_READ_PROP, + AccessMask::ADS_RIGHT_DS_WRITE_PROP, + AccessMask::ADS_RIGHT_DS_SELF, + AccessMask::GENERIC_READ, + AccessMask::GENERIC_WRITE, + AccessMask::GENERIC_EXECUTE, + AccessMask::GENERIC_ALL, + AccessMask::MAXIMUM_ALLOWED, + AccessMask::ACCESS_SYSTEM_SECURITY, + AccessMask::SYNCHRONIZE, + AccessMask::WRITE_OWNER, + AccessMask::WRITE_DACL, + AccessMask::READ_CONTROL, + AccessMask::DELETE, + ] + .into_iter() + .filter(|&flag| self.has_flag(flag)) + .collect() + } +} + +impl From for AccessMask { + fn from(mask: u32) -> Self { + AccessMask(mask) + } +} + +impl Into for AccessMask { + fn into(self) -> u32 { + self.0 + } +} + +impl IntoIterator for AccessMask { + type Item = u32; + type IntoIter = AccessMaskIter; + + fn into_iter(self) -> Self::IntoIter { + AccessMaskIter { + mask: self.0, + index: 0, + } + } +} + +pub fn parse_access_mask(input: &[u8]) -> IResult<&[u8], AccessMask> { + let (input, mask) = le_u32(input)?; + Ok((input, AccessMask::new(mask))) +} + +pub struct AccessMaskIter { + mask: u32, + index: usize, +} + +impl Iterator for AccessMaskIter { + type Item = u32; + + fn next(&mut self) -> Option { + const RIGHTS: [u32; 11] = [ + AccessMask::GENERIC_READ, + AccessMask::GENERIC_WRITE, + AccessMask::GENERIC_EXECUTE, + AccessMask::GENERIC_ALL, + AccessMask::MAXIMUM_ALLOWED, + AccessMask::ACCESS_SYSTEM_SECURITY, + AccessMask::SYNCHRONIZE, + AccessMask::WRITE_OWNER, + AccessMask::WRITE_DACL, + AccessMask::READ_CONTROL, + AccessMask::DELETE, + ]; + + while self.index < RIGHTS.len() { + let right = RIGHTS[self.index]; + self.index += 1; + if self.mask & right == right { + return Some(right); + } + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_access_mask() { + let mut mask = AccessMask::new(0); + + // Test setting and checking generic rights + mask.set_flag(AccessMask::GENERIC_READ); + assert!(mask.has_flag(AccessMask::GENERIC_READ)); + assert!(!mask.has_flag(AccessMask::GENERIC_WRITE)); + + // Test setting and checking standard rights + mask.set_flag(AccessMask::WRITE_DACL); + assert!(mask.has_flag(AccessMask::WRITE_DACL)); + assert!(!mask.has_flag(AccessMask::WRITE_OWNER)); + + // Test clearing flags + mask.clear_flag(AccessMask::GENERIC_READ); + assert!(!mask.has_flag(AccessMask::GENERIC_READ)); + + // Test object-specific rights + let object_rights = 0x1234; + mask = AccessMask::new(object_rights); + + // Test conversion + let mask_u32: u32 = mask.into(); + assert_eq!(mask_u32, object_rights); + } +} diff --git a/src/security_descriptor/ace.rs b/src/security_descriptor/ace.rs new file mode 100644 index 0000000..9dec882 --- /dev/null +++ b/src/security_descriptor/ace.rs @@ -0,0 +1,507 @@ +use super::access_mask::{parse_access_mask, AccessMask}; +use crate::guid::GUID; +use crate::sid::SID; +use nom::{ + bytes::complete::take, + number::complete::{le_u16, le_u32, le_u8}, + sequence::tuple, + IResult, +}; +use serde::Serialize; + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub enum ACE { + AccessAllowed(AccessAllowedAce), + AccessAllowedObject(AccessAllowedObjectAce), + AccessDenied(AccessDeniedAce), + SystemAuditObject(SystemAuditObjectAce), + AccessDeniedObject(AccessDeniedObjectAce), +} + +impl ACE { + pub fn header(&self) -> &ACEHeader { + match self { + ACE::AccessAllowed(ace) => &ace.header, + ACE::AccessAllowedObject(ace) => &ace.header, + ACE::AccessDenied(ace) => &ace.header, + ACE::SystemAuditObject(ace) => &ace.header, + ACE::AccessDeniedObject(ace) => &ace.header, + } + } + + pub fn sid(&self) -> &SID { + match self { + ACE::AccessAllowed(ace) => &ace.sid, + ACE::AccessAllowedObject(ace) => &ace.sid, + ACE::AccessDenied(ace) => &ace.sid, + ACE::SystemAuditObject(ace) => &ace.sid, + ACE::AccessDeniedObject(ace) => &ace.sid, + } + } + + pub fn mask(&self) -> AccessMask { + match self { + ACE::AccessAllowed(ace) => ace.mask, + ACE::AccessAllowedObject(ace) => ace.mask, + ACE::AccessDenied(ace) => ace.mask, + ACE::SystemAuditObject(ace) => ace.mask, + ACE::AccessDeniedObject(ace) => ace.mask, + } + } + + pub fn object_type(&self) -> Option<&GUID> { + match self { + ACE::AccessAllowedObject(ace) => ace.object_type.as_ref(), + ACE::SystemAuditObject(ace) => ace.object_type.as_ref(), + ACE::AccessDeniedObject(ace) => ace.object_type.as_ref(), + _ => None, + } + } + + pub fn object_type_s(&self) -> Option { + let ot = self.object_type()?; + ACEGuid::from_guid(ot) + } + + pub fn inherited_object_type(&self) -> Option<&GUID> { + match self { + ACE::AccessAllowedObject(ace) => ace.inherited_object_type.as_ref(), + ACE::SystemAuditObject(ace) => ace.inherited_object_type.as_ref(), + ACE::AccessDeniedObject(ace) => ace.inherited_object_type.as_ref(), + _ => None, + } + } +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub enum ACEGuid { + DSReplicationGetChanges, + DSReplicationGetChangesAll, + DSReplicationGetChangesInFilteredSet, + UserForceChangePassword, + AllGuid, + WriteMember, + WriteAllowedToAct, + WriteSPN, + AddKeyPrincipal, + UserAccountRestrictions, + PKINameFlag, + PKIEnrollmentFlag, + Enroll, + AutoEnroll, +} + +impl ACEGuid { + pub fn from_guid(guid: &GUID) -> Option { + // https://github.com/BloodHoundAD/SharpHoundCommon/blob/ea6b097927c5bb795adb8589e9a843293d36ae37/src/CommonLib/Processors/ACEGuids.cs#L4 + match guid.to_string().as_str() { + "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" => Some(ACEGuid::DSReplicationGetChanges), + "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" => Some(ACEGuid::DSReplicationGetChangesAll), + "89e95b76-444d-4c62-991a-0facbeda640c" => { + Some(ACEGuid::DSReplicationGetChangesInFilteredSet) + } + "00299570-246d-11d0-a768-00aa006e0529" => Some(ACEGuid::UserForceChangePassword), + "00000000-0000-0000-0000-000000000000" => Some(ACEGuid::AllGuid), + "bf9679c0-0de6-11d0-a285-00aa003049e2" => Some(ACEGuid::WriteMember), + "3f78c3e5-f79a-46bd-a0b8-9d18116ddc79" => Some(ACEGuid::WriteAllowedToAct), + "f3a64788-5306-11d1-a9c5-0000f80367c1" => Some(ACEGuid::WriteSPN), + "5b47d60f-6090-40b2-9f37-2a4de88f3063" => Some(ACEGuid::AddKeyPrincipal), + "4c164200-20c0-11d0-a768-00aa006e0529" => Some(ACEGuid::UserAccountRestrictions), + "ea1dddc4-60ff-416e-8cc0-17cee534bce7" => Some(ACEGuid::PKINameFlag), + "d15ef7d8-f226-46db-ae79-b34e560bd12c" => Some(ACEGuid::PKIEnrollmentFlag), + "0e10c968-78fb-11d2-90d4-00c04f79dc55" => Some(ACEGuid::Enroll), + "a05b8cc2-17bc-4802-a710-e7c15ab866a2" => Some(ACEGuid::AutoEnroll), + _ => None, + } + } +} + +pub fn parse_ace(input: &[u8]) -> IResult<&[u8], ACE> { + let (input, header) = parse_ace_header(input)?; + match header.ace_type { + ACEType::AccessAllowed => { + let (input, ace) = parse_access_allowed_ace(input, header)?; + Ok((input, ACE::AccessAllowed(ace))) + } + ACEType::AccessAllowedObject => { + let (input, ace) = parse_access_allowed_object_ace(input, header)?; + Ok((input, ACE::AccessAllowedObject(ace))) + } + ACEType::AccessDenied => { + let (input, ace) = parse_access_denied_ace(input, header)?; + Ok((input, ACE::AccessDenied(ace))) + } + ACEType::SystemAuditObject => { + let (input, ace) = parse_system_audit_object_ace(input, header)?; + Ok((input, ACE::SystemAuditObject(ace))) + } + ACEType::AccessDeniedObject => { + let (input, ace) = parse_access_denied_object_ace(input, header)?; + Ok((input, ACE::AccessDeniedObject(ace))) + } + _ => unimplemented!("ACE type not implemented: {:?}", header.ace_type), + } +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct AccessAllowedAce { + pub header: ACEHeader, + pub mask: AccessMask, + pub sid: SID, +} + +fn parse_access_allowed_ace(input: &[u8], header: ACEHeader) -> IResult<&[u8], AccessAllowedAce> { + let (input, mask) = parse_access_mask(input)?; + let (input, sid) = SID::from_next_bytes(input)?; + + Ok((input, AccessAllowedAce { header, mask, sid })) +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct AccessAllowedObjectAce { + pub header: ACEHeader, + pub mask: AccessMask, + pub flags: u32, + pub object_type: Option, + pub inherited_object_type: Option, + pub sid: SID, +} + +fn parse_access_allowed_object_ace( + input: &[u8], + header: ACEHeader, +) -> IResult<&[u8], AccessAllowedObjectAce> { + let (input, mask) = parse_access_mask(input)?; + let (input, flags) = le_u32(input)?; + let (input, mut object_type) = (input, None); + let (mut input, mut inherited_object_type) = (input, None); + + if flags & 1 != 0 { + let (inner_input, ot) = GUID::from_next_bytes(input)?; + input = inner_input; + object_type = Some(ot); + } + if flags & 2 != 0 { + let (inner_input, iot) = GUID::from_next_bytes(input)?; + input = inner_input; + inherited_object_type = Some(iot); + } + let (input, sid) = SID::from_next_bytes(input)?; + + Ok(( + input, + AccessAllowedObjectAce { + header, + mask, + flags, + object_type, + inherited_object_type, + sid, + }, + )) +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct AccessDeniedAce { + pub header: ACEHeader, + pub mask: AccessMask, + pub sid: SID, +} + +fn parse_access_denied_ace(input: &[u8], header: ACEHeader) -> IResult<&[u8], AccessDeniedAce> { + let (input, mask) = parse_access_mask(input)?; + let (input, sid) = SID::from_next_bytes(input)?; + + Ok((input, AccessDeniedAce { header, mask, sid })) +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct SystemAuditObjectAce { + pub header: ACEHeader, + pub mask: AccessMask, + pub flags: u32, + pub object_type: Option, + pub inherited_object_type: Option, + pub sid: SID, + pub application_data: Vec, +} + +fn parse_system_audit_object_ace( + input: &[u8], + header: ACEHeader, +) -> IResult<&[u8], SystemAuditObjectAce> { + let (input, mask) = parse_access_mask(input)?; + let (input, flags) = le_u32(input)?; + + let (input, object_type) = if flags & 0x00000001 != 0 { + let (input, guid) = GUID::from_next_bytes(input)?; + (input, Some(guid)) + } else { + (input, None) + }; + + let (input, inherited_object_type) = if flags & 0x00000002 != 0 { + let (input, guid) = GUID::from_next_bytes(input)?; + (input, Some(guid)) + } else { + (input, None) + }; + + let (input, _) = take(8usize)(input)?; + + let (input, sid) = SID::from_next_bytes(input)?; + + // Calculate the size of application data + let app_data_size = header.ace_size as usize; + let (input, application_data) = take(app_data_size)(input)?; + + Ok(( + input, + SystemAuditObjectAce { + header, + mask, + flags, + object_type, + inherited_object_type, + sid, + application_data: application_data.to_vec(), + }, + )) +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct AccessDeniedObjectAce { + pub header: ACEHeader, + pub mask: AccessMask, + pub flags: u32, + pub object_type: Option, + pub inherited_object_type: Option, + pub sid: SID, +} + +// Constants for the flags +const ACE_OBJECT_TYPE_PRESENT: u32 = 0x00000001; +const ACE_INHERITED_OBJECT_TYPE_PRESENT: u32 = 0x00000002; + +fn parse_access_denied_object_ace( + input: &[u8], + header: ACEHeader, +) -> IResult<&[u8], AccessDeniedObjectAce> { + let (input, mask) = parse_access_mask(input)?; + let (input, flags) = le_u32(input)?; + let (input, mut object_type) = (input, None); + let (mut input, mut inherited_object_type) = (input, None); + + if flags & ACE_OBJECT_TYPE_PRESENT != 0 { + let (inner_input, ot) = GUID::from_next_bytes(input)?; + input = inner_input; + object_type = Some(ot); + } + if flags & ACE_INHERITED_OBJECT_TYPE_PRESENT != 0 { + let (inner_input, iot) = GUID::from_next_bytes(input)?; + input = inner_input; + inherited_object_type = Some(iot); + } + let (input, sid) = SID::from_next_bytes(input)?; + + Ok(( + input, + AccessDeniedObjectAce { + header, + mask, + flags, + object_type, + inherited_object_type, + sid, + }, + )) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum ACEType { + AccessAllowed = 0x00, + AccessDenied = 0x01, + SystemAudit = 0x02, + SystemAlarm = 0x03, + AccessAllowedCompound = 0x04, + AccessAllowedObject = 0x05, + AccessDeniedObject = 0x06, + SystemAuditObject = 0x07, + SystemAlarmObject = 0x08, + AccessAllowedCallback = 0x09, + AccessDeniedCallback = 0x0A, + AccessAllowedCallbackObject = 0x0B, + AccessDeniedCallbackObject = 0x0C, + SystemAuditCallback = 0x0D, + SystemAlarmCallback = 0x0E, + SystemAuditCallbackObject = 0x0F, + SystemAlarmCallbackObject = 0x10, + SystemMandatoryLabel = 0x11, + SystemResourceAttribute = 0x12, + SystemScopedPolicyId = 0x13, +} + +impl From for ACEType { + fn from(value: u8) -> Self { + match value { + 0x00 => ACEType::AccessAllowed, + 0x01 => ACEType::AccessDenied, + 0x02 => ACEType::SystemAudit, + 0x03 => ACEType::SystemAlarm, + 0x04 => ACEType::AccessAllowedCompound, + 0x05 => ACEType::AccessAllowedObject, + 0x06 => ACEType::AccessDeniedObject, + 0x07 => ACEType::SystemAuditObject, + 0x08 => ACEType::SystemAlarmObject, + 0x09 => ACEType::AccessAllowedCallback, + 0x0A => ACEType::AccessDeniedCallback, + 0x0B => ACEType::AccessAllowedCallbackObject, + 0x0C => ACEType::AccessDeniedCallbackObject, + 0x0D => ACEType::SystemAuditCallback, + 0x0E => ACEType::SystemAlarmCallback, + 0x0F => ACEType::SystemAuditCallbackObject, + 0x10 => ACEType::SystemAlarmCallbackObject, + 0x11 => ACEType::SystemMandatoryLabel, + 0x12 => ACEType::SystemResourceAttribute, + 0x13 => ACEType::SystemScopedPolicyId, + _ => panic!("Invalid ACE type"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct ACEFlags(u8); + +impl ACEFlags { + pub const CONTAINER_INHERIT_ACE: u8 = 0x02; + pub const FAILED_ACCESS_ACE_FLAG: u8 = 0x80; + pub const INHERIT_ONLY_ACE: u8 = 0x08; + pub const INHERITED_ACE: u8 = 0x10; + pub const NO_PROPAGATE_INHERIT_ACE: u8 = 0x04; + pub const OBJECT_INHERIT_ACE: u8 = 0x01; + pub const SUCCESSFUL_ACCESS_ACE_FLAG: u8 = 0x40; + + pub fn new(value: u8) -> Self { + ACEFlags(value) + } + + pub fn is_set(&self, flag: u8) -> bool { + self.0 & flag != 0 + } +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct ACEHeader { + pub ace_type: ACEType, + pub ace_flags: ACEFlags, + pub ace_size: u16, +} + +pub fn parse_ace_header(input: &[u8]) -> IResult<&[u8], ACEHeader> { + let (input, (ace_type, ace_flags, ace_size)) = tuple((le_u8, le_u8, le_u16))(input)?; + + Ok(( + input, + ACEHeader { + ace_type: ACEType::from(ace_type), + ace_flags: ACEFlags::new(ace_flags), + ace_size, + }, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore] + fn test_sddl_parsing() { + let ace_bytes = vec![ + 0, 1, 0, 0, 1, 0, 0, 0, 24, 126, 15, 62, 122, 44, 16, 76, 186, 130, 77, 146, 109, 185, + 154, 62, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, + 109, 38, 58, 10, 2, 0, 0, 5, 0, 56, 0, 0, 1, 0, 0, 1, 0, 0, 0, 170, 246, 49, 17, 7, + 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, + 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, 242, 1, 0, 0, 5, 0, 56, 0, 0, 1, + 0, 0, 1, 0, 0, 0, 173, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, + 210, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, + 38, 58, 4, 2, 0, 0, 5, 2, 56, 0, 48, 0, 0, 0, 1, 0, 0, 0, 15, 214, 71, 91, 144, 96, + 178, 64, 159, 55, 42, 77, 232, 143, 48, 99, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, + 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, 14, 2, 0, 0, 5, 2, 56, 0, 48, 0, 0, 0, + 1, 0, 0, 0, 15, 214, 71, 91, 144, 96, 178, 64, 159, 55, 42, 77, 232, 143, 48, 99, 1, 5, + 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, 15, + 2, 0, 0, 5, 10, 56, 0, 8, 0, 0, 0, 3, 0, 0, 0, 166, 109, 2, 155, 60, 13, 92, 70, 139, + 238, 81, 153, 215, 22, 92, 186, 134, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, + 0, 48, 73, 226, 1, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 5, 10, 56, 0, 8, 0, 0, 0, 3, 0, 0, + 0, 166, 109, 2, 155, 60, 13, 92, 70, 139, 238, 81, 153, 215, 22, 92, 186, 134, 122, + 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, 0, 0, 0, 0, 0, 5, + 10, 0, 0, 0, 5, 10, 56, 0, 16, 0, 0, 0, 3, 0, 0, 0, 109, 158, 198, 183, 199, 44, 210, + 17, 133, 78, 0, 160, 201, 131, 246, 8, 134, 122, 150, 191, 230, 13, 208, 17, 162, 133, + 0, 170, 0, 48, 73, 226, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 10, 56, 0, 16, 0, 0, 0, + 3, 0, 0, 0, 109, 158, 198, 183, 199, 44, 210, 17, 133, 78, 0, 160, 201, 131, 246, 8, + 156, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, 0, 0, 0, + 0, 0, 5, 9, 0, 0, 0, 5, 10, 56, 0, 16, 0, 0, 0, 3, 0, 0, 0, 109, 158, 198, 183, 199, + 44, 210, 17, 133, 78, 0, 160, 201, 131, 246, 8, 186, 122, 150, 191, 230, 13, 208, 17, + 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 10, 56, 0, 32, + 0, 0, 0, 3, 0, 0, 0, 147, 123, 27, 234, 72, 94, 213, 70, 188, 108, 77, 244, 253, 167, + 138, 53, 134, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, + 0, 0, 0, 0, 0, 5, 10, 0, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 118, 91, 233, 137, + 77, 68, 98, 76, 153, 26, 15, 172, 190, 218, 100, 12, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, + 0, 32, 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 170, 246, 49, 17, 7, 156, 209, 17, + 247, 159, 0, 192, 79, 194, 220, 210, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, + 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 171, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, + 192, 79, 194, 220, 210, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 5, 0, 44, 0, + 0, 1, 0, 0, 1, 0, 0, 0, 172, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, + 220, 210, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, + 0, 0, 0, 173, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 2, + 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 174, + 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 2, 0, 0, 0, 0, 0, + 5, 32, 0, 0, 0, 32, 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 201, 109, 163, 226, + 23, 174, 195, 71, 181, 139, 190, 52, 197, 91, 166, 51, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, + 0, 0, 45, 2, 0, 0, 5, 0, 44, 0, 16, 0, 0, 0, 1, 0, 0, 0, 96, 115, 64, 199, 191, 32, + 208, 17, 167, 104, 0, 170, 0, 110, 5, 41, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, + 0, 0, 5, 0, 44, 0, 16, 0, 0, 0, 1, 0, 0, 0, 208, 159, 17, 184, 246, 4, 98, 71, 171, + 122, 73, 134, 199, 107, 63, 154, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, + 10, 44, 0, 148, 0, 2, 0, 2, 0, 0, 0, 20, 204, 40, 72, 55, 20, 188, 69, 155, 7, 173, + 111, 1, 94, 95, 40, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 44, 0, + 148, 0, 2, 0, 2, 0, 0, 0, 156, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, + 48, 73, 226, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 44, 0, 148, 0, 2, + 0, 2, 0, 0, 0, 186, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, + 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, + 94, 76, 199, 5, 235, 77, 180, 67, 189, 159, 134, 102, 76, 42, 127, 213, 1, 1, 0, 0, 0, + 0, 0, 5, 11, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 118, 91, 233, 137, 77, 68, + 98, 76, 153, 26, 15, 172, 190, 218, 100, 12, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 0, + 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 125, 220, 194, 204, 173, 166, 122, 74, 136, 70, 192, 78, + 60, 197, 53, 1, 1, 1, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, + 0, 156, 54, 15, 40, 199, 103, 142, 67, 174, 152, 29, 70, 243, 198, 245, 65, 1, 1, 0, 0, + 0, 0, 0, 5, 11, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 170, 246, 49, 17, 7, 156, + 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 0, + 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 171, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, + 194, 220, 210, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, + 172, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 1, 0, 0, 0, + 0, 0, 5, 9, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 174, 246, 49, 17, 7, 156, + 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 0, + 40, 0, 16, 0, 0, 0, 1, 0, 0, 0, 208, 159, 17, 184, 246, 4, 98, 71, 171, 122, 73, 134, + 199, 107, 63, 154, 1, 1, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0, 5, 3, 40, 0, 48, 0, 0, 0, 1, 0, + 0, 0, 229, 195, 120, 63, 154, 247, 189, 70, 160, 184, 157, 24, 17, 109, 220, 121, 1, 1, + 0, 0, 0, 0, 0, 5, 10, 0, 0, 0, 5, 10, 40, 0, 48, 1, 0, 0, 1, 0, 0, 0, 222, 71, 230, + 145, 111, 217, 112, 75, 149, 87, 214, 63, 244, 243, 204, 216, 1, 1, 0, 0, 0, 0, 0, 5, + 10, 0, 0, 0, 0, 0, 36, 0, 189, 1, 14, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, + 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, 0, 2, 0, 0, 0, 2, 36, 0, 255, 1, 15, 0, 1, + 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, + 7, 2, 0, 0, 0, 0, 24, 0, 16, 0, 2, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, + 0, 2, 24, 0, 4, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 0, 2, 24, 0, + 189, 1, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 0, 0, 20, 0, 16, 0, 0, + 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 20, 0, 148, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, + 5, 9, 0, 0, 0, 0, 0, 20, 0, 148, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0, 0, 0, + 20, 0, 255, 1, 15, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, + 0, 0, 0, 32, 2, 0, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, + ]; + let (input, _) = parse_ace(&ace_bytes).unwrap(); + + if input.len() > 0 { + println!("remaining: {:?}", input); + assert!(false, "Failed to parse ACE"); + } + } +} diff --git a/src/security_descriptor/acl.rs b/src/security_descriptor/acl.rs new file mode 100644 index 0000000..465f56a --- /dev/null +++ b/src/security_descriptor/acl.rs @@ -0,0 +1,51 @@ +use nom::{ + multi::count, + number::complete::{le_u16, le_u8}, + sequence::tuple, + IResult, +}; +use serde::Serialize; + +use super::ace::{parse_ace, ACE}; + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct ACL { + pub acl_revision: u8, + pub sbz1: u8, + pub acl_size: u16, + pub ace_count: u16, + pub sbz2: u16, + pub aces: Vec, +} + +pub fn parse_acl(input: &[u8]) -> IResult<&[u8], ACL> { + let (input, (acl_revision, sbz1, acl_size, ace_count, sbz2)) = + tuple((le_u8, le_u8, le_u16, le_u16, le_u16))(input)?; + + // TODO: Handle these errors instead of panicking + if acl_revision != 2 && acl_revision != 4 { + panic!("ACL revision must be 2 or 4. Got: {}", acl_revision); + } + + if sbz1 != 0 { + panic!("sbz1 must be 0. Got: {}", sbz1); + } + + if sbz2 != 0 { + panic!("sbz2 must be 0. Got: {}", sbz2); + } + + let (input, aces) = count(parse_ace, ace_count as usize)(input)?; + + Ok(( + input, + ACL { + acl_revision, + sbz1, + acl_size, + ace_count, + sbz2, + aces: aces, + }, + )) +} diff --git a/src/security_descriptor/control_flags.rs b/src/security_descriptor/control_flags.rs new file mode 100644 index 0000000..04bcf57 --- /dev/null +++ b/src/security_descriptor/control_flags.rs @@ -0,0 +1,71 @@ +use serde::{Deserialize, Serialize}; + +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/7d4dac05-9cef-4563-a058-f108abecce1d +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ControlFlag { + SR = 0x8000, // Self Relative + RM = 0x4000, // RM Control Valid + PS = 0x2000, // SACL Protected + PD = 0x1000, // DACL Protected + SI = 0x0800, // SACL Auto-Inherited + DI = 0x0400, // DACL Auto-Inherited + SC = 0x0200, // SACL Computed Inheritance Required + DC = 0x0100, // DACL Computed Inheritance Required + SS = 0x0080, // Server Security + DT = 0x0040, // DACL Trusted + SD = 0x0020, // SACL Defaulted + SP = 0x0010, // SACL Present + DD = 0x0008, // DACL Defaulted + DP = 0x0004, // DACL Present + GD = 0x0002, // Group Defaulted + OD = 0x0001, // Owner Defaulted +} + +impl std::ops::BitOr for ControlFlag { + type Output = ControlFlags; + + fn bitor(self, rhs: ControlFlag) -> Self::Output { + ControlFlags(self as u16 | rhs as u16) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct ControlFlags(pub u16); + +impl ControlFlags { + pub fn new(value: u16) -> Self { + ControlFlags(value) + } + + pub fn is_set(&self, flag: ControlFlag) -> bool { + self.0 & (flag as u16) != 0 + } + + pub fn as_u16(&self) -> u16 { + self.0 + } + + pub fn get_flags(&self) -> Vec { + vec![ + ControlFlag::SR, + ControlFlag::RM, + ControlFlag::PS, + ControlFlag::PD, + ControlFlag::SI, + ControlFlag::DI, + ControlFlag::SC, + ControlFlag::DC, + ControlFlag::SS, + ControlFlag::DT, + ControlFlag::SD, + ControlFlag::SP, + ControlFlag::DD, + ControlFlag::DP, + ControlFlag::GD, + ControlFlag::OD, + ] + .into_iter() + .filter(|&flag| self.is_set(flag)) + .collect() + } +} diff --git a/src/security_descriptor/mod.rs b/src/security_descriptor/mod.rs new file mode 100644 index 0000000..c889e02 --- /dev/null +++ b/src/security_descriptor/mod.rs @@ -0,0 +1,11 @@ +mod access_mask; +mod ace; +mod acl; +mod control_flags; +mod sddl; + +pub use access_mask::*; +pub use ace::*; +pub use acl::*; +pub use control_flags::*; +pub use sddl::*; diff --git a/src/security_descriptor/sddl.rs b/src/security_descriptor/sddl.rs new file mode 100644 index 0000000..3d88059 --- /dev/null +++ b/src/security_descriptor/sddl.rs @@ -0,0 +1,279 @@ +use crate::security_descriptor::ControlFlags; +use crate::security_descriptor::{parse_acl, ACL}; +use crate::sid::SID; +use serde::Serialize; + +use nom::{ + number::complete::{le_u16, le_u32, le_u8}, + sequence::tuple, + IResult, +}; + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct SDDL { + revision: u8, + sbz1: u8, + pub control_flags: ControlFlags, + offset_owner: u32, + offset_group: u32, + offset_sacl: u32, + offset_dacl: u32, + pub owner_sid: Option, + pub group_sid: Option, + pub dacl: Option, +} + +impl SDDL { + pub fn from_bytes(input: &[u8]) -> Result>> { + let (_, sddl) = parse_sddl(input)?; + Ok(sddl) + } +} + +fn parse_sddl(input: &[u8]) -> IResult<&[u8], SDDL> { + let (_, (revision, sbz1, control, offset_owner, offset_group, offset_sacl, offset_dacl)) = + tuple((le_u8, le_u8, le_u16, le_u32, le_u32, le_u32, le_u32))(input)?; + + let control_flags = ControlFlags::new(control); + + let owner_sid = if offset_owner != 0 { + Some( + SID::from_bytes(&input[offset_owner as usize..]).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Fail)) + })?, + ) + } else { + None + }; + + let group_sid = if offset_group != 0 { + Some( + SID::from_bytes(&input[offset_group as usize..]).map_err(|_| { + nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Fail)) + })?, + ) + } else { + None + }; + + let dacl = if offset_dacl != 0 { + Some(parse_acl(&input[offset_dacl as usize..])?.1) + } else { + None + }; + + Ok(( + input, + SDDL { + revision, + sbz1, + control_flags, + offset_owner, + offset_group, + offset_sacl, + offset_dacl, + owner_sid, + group_sid, + dacl, + }, + )) +} + +#[cfg(test)] +mod tests { + use crate::security_descriptor::{ACEType, ACE}; + + use super::*; + + #[test] + fn test_sddl_parsing() { + // O:S-1-5-21-3890413604-3811681533-153378300-1005G:S-1-5-21-3890413604-3811681533-153378300-513D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;S-1-5-21-3890413604-3811681533-153378300-1005) + let sddl_bytes = vec![ + 1, 0, 4, 128, 20, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 76, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, + 5, 21, 0, 0, 0, 36, 0, 227, 231, 253, 164, 49, 227, 252, 93, 36, 9, 237, 3, 0, 0, 1, 5, + 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 36, 0, 227, 231, 253, 164, 49, 227, 252, 93, 36, 9, 1, + 2, 0, 0, 2, 0, 88, 0, 3, 0, 0, 0, 0, 16, 20, 0, 255, 1, 31, 0, 1, 1, 0, 0, 0, 0, 0, 5, + 18, 0, 0, 0, 0, 16, 24, 0, 255, 1, 31, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, + 0, 0, 0, 16, 36, 0, 255, 1, 31, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 36, 0, 227, + 231, 253, 164, 49, 227, 252, 93, 36, 9, 237, 3, 0, 0, + ]; + let sddl = SDDL::from_bytes(&sddl_bytes).unwrap(); + + // We'll add assertions here to check the parsed values + assert_eq!(sddl.revision, 1); + assert_eq!(sddl.sbz1, 0); + assert_eq!(sddl.control_flags, ControlFlags(32772)); + assert_eq!(sddl.offset_owner, 20); + assert_eq!(sddl.offset_group, 48); + assert_eq!(sddl.offset_sacl, 0); + assert_eq!(sddl.offset_dacl, 76); + + if let Some(owner_sid) = &sddl.owner_sid { + assert_eq!( + owner_sid.to_string(), + "S-1-5-21-3890413604-3811681533-153378300-1005" + ); + } else { + assert!(false, "Owner SID should be present"); + } + + if let Some(group_sid) = &sddl.group_sid { + assert_eq!( + group_sid.to_string(), + "S-1-5-21-3890413604-3811681533-153378300-513" + ); + } else { + assert!(false, "Group SID should be present"); + } + + if let Some(dacl) = &sddl.dacl { + assert_eq!(dacl.acl_revision, 2, "ACL revision should be 4"); + assert_eq!(dacl.sbz1, 0, "sbz1 should be 0"); + assert_eq!(dacl.acl_size, 88, "ACL size should be 2416"); + assert_eq!(dacl.ace_count, 3, "ACE count should be 53"); + assert_eq!(dacl.sbz2, 0, "sbz2 should be 0"); + + if let Some(ACE::AccessAllowed(first_ace)) = dacl.aces.get(0) { + assert_eq!( + first_ace.header.ace_type, + ACEType::AccessAllowed, + "First ACE should be ACCESS_ALLOWED_OBJECT_ACE_TYPE" + ); + assert_eq!(first_ace.header.ace_size, 20, "First ACE size should be 60"); + } else { + assert!(false, "First ACE should be AccessAllowed"); + } + } else { + assert!(false, "DACL should be present"); + } + } + + #[test] + fn test_sddl_testing() { + /* + Used for testing SDDLs to see if they parse or not. Returns debug output. + Get ACL: + ``` + $file = ".\file.bin" + $acl = Get-Acl $file + (Get-ACL $file).sddl + (Get-ACL $file).GetSecurityDescriptorBinaryForm() -join ", " + ``` + */ + + /* + O:BAG:BAD:AI(A;;RP;;;WD)(A;;LCRPLORC;;;ED)(A;;LCRPLORC;;;AU)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;CI;CCLCSWRPWPLOCRSDRCWDWO;;;BA)(A;;RPRC;;;RU)(A;CI;LC;;;RU)(A;;CCLCSWRPWPLOCRRCWDWO;;;DA)(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-21-1935163693-1572912069-975596842-519)(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO)(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;ED)(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;ED)(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;ED)(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;ED)(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;ED)(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIO;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS)(OA;CIIO;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)(OA;OICI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)(OA;;CR;05c74c5e-4deb-43b4-bd9f-86664c2a7fd5;;AU)(OA;;CR;ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501;;AU)(OA;;CR;280f369c-67c7-438e-ae98-1d46f3c6f541;;AU)(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;AU)(OA;;CR;1131f6ae-9c07-11d1-f79f-00c04fc2dcd2;;BA)(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;BA)(OA;;CR;1131f6ac-9c07-11d1-f79f-00c04fc2dcd2;;BA)(OA;;CR;1131f6ab-9c07-11d1-f79f-00c04fc2dcd2;;BA)(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;BA)(OA;;CR;89e95b76-444d-4c62-991a-0facbeda640c;;BA)(OA;CIIO;LCRPLORC;;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIO;LCRPLORC;;bf967a9c-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIO;LCRPLORC;;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;;RP;b8119fd0-04f6-4762-ab7a-4986c76b3f9a;;RU)(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIO;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;;RP;c7407360-20bf-11d0-a768-00aa006e0529;;RU)(OA;CIIO;RP;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIO;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIO;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIO;RP;4c164200-20c0-11d0-a768-00aa006e0529;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;;CR;e2a36dc9-ae17-47c3-b58b-be34c55ba633;;S-1-5-32-557)(OA;;CR;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2;;S-1-5-21-1935163693-1572912069-975596842-498)(OA;;CR;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2;;DD)(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;S-1-5-21-1935163693-1572912069-975596842-522)(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;S-1-5-21-1935163693-1572912069-975596842-526)(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;S-1-5-21-1935163693-1572912069-975596842-527) + */ + let sddl_bytes = vec![ + 1, 0, 4, 132, 20, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, + 5, 32, 0, 0, 0, 32, 2, 0, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 4, 0, + 112, 9, 53, 0, 0, 0, 0, 0, 20, 0, 16, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 20, 0, 148, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 0, 0, 20, 0, 148, 0, 2, 0, + 1, 1, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0, 0, 0, 20, 0, 255, 1, 15, 0, 1, 1, 0, 0, 0, 0, 0, + 5, 18, 0, 0, 0, 0, 2, 24, 0, 189, 1, 15, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, + 0, 0, 0, 0, 24, 0, 16, 0, 2, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 0, 2, + 24, 0, 4, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 0, 0, 36, 0, 189, + 1, 14, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, + 109, 38, 58, 0, 2, 0, 0, 0, 2, 36, 0, 255, 1, 15, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, + 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, 7, 2, 0, 0, 5, 10, 56, 0, 8, 0, + 0, 0, 3, 0, 0, 0, 166, 109, 2, 155, 60, 13, 92, 70, 139, 238, 81, 153, 215, 22, 92, + 186, 134, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, 0, + 0, 0, 0, 0, 3, 0, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 172, 246, 49, 17, 7, + 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, + 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 171, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, + 192, 79, 194, 220, 210, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, + 0, 0, 0, 170, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 1, + 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 118, 91, 233, 137, + 77, 68, 98, 76, 153, 26, 15, 172, 190, 218, 100, 12, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, + 0, 5, 10, 56, 0, 16, 0, 0, 0, 3, 0, 0, 0, 109, 158, 198, 183, 199, 44, 210, 17, 133, + 78, 0, 160, 201, 131, 246, 8, 186, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, + 0, 48, 73, 226, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, + 0, 174, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 1, 0, 0, + 0, 0, 0, 5, 9, 0, 0, 0, 5, 10, 56, 0, 16, 0, 0, 0, 3, 0, 0, 0, 109, 158, 198, 183, 199, + 44, 210, 17, 133, 78, 0, 160, 201, 131, 246, 8, 156, 122, 150, 191, 230, 13, 208, 17, + 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 10, 56, 0, 16, + 0, 0, 0, 3, 0, 0, 0, 109, 158, 198, 183, 199, 44, 210, 17, 133, 78, 0, 160, 201, 131, + 246, 8, 134, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, + 0, 0, 0, 0, 0, 5, 9, 0, 0, 0, 5, 10, 56, 0, 8, 0, 0, 0, 3, 0, 0, 0, 166, 109, 2, 155, + 60, 13, 92, 70, 139, 238, 81, 153, 215, 22, 92, 186, 134, 122, 150, 191, 230, 13, 208, + 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, 0, 0, 0, 0, 0, 5, 10, 0, 0, 0, 5, 10, 40, + 0, 48, 1, 0, 0, 1, 0, 0, 0, 222, 71, 230, 145, 111, 217, 112, 75, 149, 87, 214, 63, + 244, 243, 204, 216, 1, 1, 0, 0, 0, 0, 0, 5, 10, 0, 0, 0, 5, 10, 56, 0, 32, 0, 0, 0, 3, + 0, 0, 0, 147, 123, 27, 234, 72, 94, 213, 70, 188, 108, 77, 244, 253, 167, 138, 53, 134, + 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 1, 0, 0, 0, 0, 0, + 5, 10, 0, 0, 0, 5, 3, 40, 0, 48, 0, 0, 0, 1, 0, 0, 0, 229, 195, 120, 63, 154, 247, 189, + 70, 160, 184, 157, 24, 17, 109, 220, 121, 1, 1, 0, 0, 0, 0, 0, 5, 10, 0, 0, 0, 5, 0, + 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 94, 76, 199, 5, 235, 77, 180, 67, 189, 159, 134, 102, + 76, 42, 127, 213, 1, 1, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, + 0, 0, 125, 220, 194, 204, 173, 166, 122, 74, 136, 70, 192, 78, 60, 197, 53, 1, 1, 1, 0, + 0, 0, 0, 0, 5, 11, 0, 0, 0, 5, 0, 40, 0, 0, 1, 0, 0, 1, 0, 0, 0, 156, 54, 15, 40, 199, + 103, 142, 67, 174, 152, 29, 70, 243, 198, 245, 65, 1, 1, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0, + 5, 0, 40, 0, 16, 0, 0, 0, 1, 0, 0, 0, 208, 159, 17, 184, 246, 4, 98, 71, 171, 122, 73, + 134, 199, 107, 63, 154, 1, 1, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, + 1, 0, 0, 0, 174, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, + 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, + 173, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 2, 0, 0, 0, + 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 172, 246, 49, + 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 2, 0, 0, 0, 0, 0, 5, 32, + 0, 0, 0, 32, 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 171, 246, 49, 17, 7, 156, + 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, + 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 170, 246, 49, 17, 7, 156, 209, 17, 247, + 159, 0, 192, 79, 194, 220, 210, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 5, 0, + 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 118, 91, 233, 137, 77, 68, 98, 76, 153, 26, 15, 172, + 190, 218, 100, 12, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, 5, 10, 44, 0, 148, + 0, 2, 0, 2, 0, 0, 0, 186, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, + 226, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 44, 0, 148, 0, 2, 0, 2, + 0, 0, 0, 156, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 2, + 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 60, 0, 16, 0, 0, 0, 3, 0, 0, 0, 66, + 47, 186, 89, 162, 121, 208, 17, 144, 32, 0, 192, 79, 194, 211, 207, 186, 122, 150, 191, + 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, + 0, 42, 2, 0, 0, 5, 10, 44, 0, 148, 0, 2, 0, 2, 0, 0, 0, 20, 204, 40, 72, 55, 20, 188, + 69, 155, 7, 173, 111, 1, 94, 95, 40, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, + 5, 0, 44, 0, 16, 0, 0, 0, 1, 0, 0, 0, 208, 159, 17, 184, 246, 4, 98, 71, 171, 122, 73, + 134, 199, 107, 63, 154, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 60, 0, + 16, 0, 0, 0, 3, 0, 0, 0, 248, 136, 112, 3, 225, 10, 210, 17, 180, 34, 0, 160, 201, 104, + 249, 57, 20, 204, 40, 72, 55, 20, 188, 69, 155, 7, 173, 111, 1, 94, 95, 40, 1, 2, 0, 0, + 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 60, 0, 16, 0, 0, 0, 3, 0, 0, 0, 0, 66, 22, + 76, 192, 32, 208, 17, 167, 104, 0, 170, 0, 110, 5, 41, 186, 122, 150, 191, 230, 13, + 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, + 0, 0, 5, 10, 60, 0, 16, 0, 0, 0, 3, 0, 0, 0, 16, 32, 32, 95, 165, 121, 208, 17, 144, + 32, 0, 192, 79, 194, 212, 207, 20, 204, 40, 72, 55, 20, 188, 69, 155, 7, 173, 111, 1, + 94, 95, 40, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 60, 0, 16, 0, 0, + 0, 3, 0, 0, 0, 16, 32, 32, 95, 165, 121, 208, 17, 144, 32, 0, 192, 79, 194, 212, 207, + 186, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 2, 0, 0, 0, + 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 0, 44, 0, 16, 0, 0, 0, 1, 0, 0, 0, 96, 115, 64, + 199, 191, 32, 208, 17, 167, 104, 0, 170, 0, 110, 5, 41, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, + 0, 0, 42, 2, 0, 0, 5, 10, 60, 0, 16, 0, 0, 0, 3, 0, 0, 0, 66, 47, 186, 89, 162, 121, + 208, 17, 144, 32, 0, 192, 79, 194, 211, 207, 20, 204, 40, 72, 55, 20, 188, 69, 155, 7, + 173, 111, 1, 94, 95, 40, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 60, + 0, 16, 0, 0, 0, 3, 0, 0, 0, 248, 136, 112, 3, 225, 10, 210, 17, 180, 34, 0, 160, 201, + 104, 249, 57, 186, 122, 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, + 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 60, 0, 16, 0, 0, 0, 3, 0, 0, + 0, 64, 194, 10, 188, 169, 121, 208, 17, 144, 32, 0, 192, 79, 194, 212, 207, 186, 122, + 150, 191, 230, 13, 208, 17, 162, 133, 0, 170, 0, 48, 73, 226, 1, 2, 0, 0, 0, 0, 0, 5, + 32, 0, 0, 0, 42, 2, 0, 0, 5, 10, 60, 0, 16, 0, 0, 0, 3, 0, 0, 0, 64, 194, 10, 188, 169, + 121, 208, 17, 144, 32, 0, 192, 79, 194, 212, 207, 20, 204, 40, 72, 55, 20, 188, 69, + 155, 7, 173, 111, 1, 94, 95, 40, 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, + 10, 60, 0, 16, 0, 0, 0, 3, 0, 0, 0, 0, 66, 22, 76, 192, 32, 208, 17, 167, 104, 0, 170, + 0, 110, 5, 41, 20, 204, 40, 72, 55, 20, 188, 69, 155, 7, 173, 111, 1, 94, 95, 40, 1, 2, + 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 42, 2, 0, 0, 5, 0, 44, 0, 0, 1, 0, 0, 1, 0, 0, 0, 201, + 109, 163, 226, 23, 174, 195, 71, 181, 139, 190, 52, 197, 91, 166, 51, 1, 2, 0, 0, 0, 0, + 0, 5, 32, 0, 0, 0, 45, 2, 0, 0, 5, 0, 56, 0, 0, 1, 0, 0, 1, 0, 0, 0, 170, 246, 49, 17, + 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, 210, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, + 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, 242, 1, 0, 0, 5, 0, 56, 0, 0, + 1, 0, 0, 1, 0, 0, 0, 173, 246, 49, 17, 7, 156, 209, 17, 247, 159, 0, 192, 79, 194, 220, + 210, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, + 38, 58, 4, 2, 0, 0, 5, 0, 56, 0, 0, 1, 0, 0, 1, 0, 0, 0, 24, 126, 15, 62, 122, 44, 16, + 76, 186, 130, 77, 146, 109, 185, 154, 62, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, + 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, 10, 2, 0, 0, 5, 2, 56, 0, 48, 0, 0, 0, 1, + 0, 0, 0, 15, 214, 71, 91, 144, 96, 178, 64, 159, 55, 42, 77, 232, 143, 48, 99, 1, 5, 0, + 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, 58, 14, 2, + 0, 0, 5, 2, 56, 0, 48, 0, 0, 0, 1, 0, 0, 0, 15, 214, 71, 91, 144, 96, 178, 64, 159, 55, + 42, 77, 232, 143, 48, 99, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, + 187, 192, 93, 42, 109, 38, 58, 15, 2, 0, 0, + ]; + let sddl = SDDL::from_bytes(&sddl_bytes).unwrap(); + assert_eq!(sddl.owner_sid.unwrap().to_string(), "S-1-5-32-544"); + assert_eq!(sddl.group_sid.unwrap().to_string(), "S-1-5-32-544"); + } +} diff --git a/src/sid/mod.rs b/src/sid/mod.rs new file mode 100644 index 0000000..5d65d25 --- /dev/null +++ b/src/sid/mod.rs @@ -0,0 +1,2 @@ +mod sid; +pub use sid::*; diff --git a/src/sid/sid.rs b/src/sid/sid.rs new file mode 100644 index 0000000..91d40f6 --- /dev/null +++ b/src/sid/sid.rs @@ -0,0 +1,186 @@ +use core::hash::{Hash, Hasher}; +use core::str::FromStr; +use nom::{ + bits::complete::take, error::Error, multi::count, number::complete::le_u32, sequence::tuple, + IResult, +}; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct SID { + revision: u8, + sub_authority_count: u8, + identifier_authority: [u8; 6], + sub_authorities: [u32; 15], // Maximum 15 sub-authorities +} + +impl SID { + pub fn from_bytes(input: &[u8]) -> Result>> { + let (_, sid) = parse_sid(input)?; + Ok(sid) + } + + pub fn from_next_bytes(input: &[u8]) -> IResult<&[u8], Self> { + parse_sid(input) + } + + pub fn to_string(&self) -> String { + let auth = u64::from_be_bytes([ + 0, + 0, + self.identifier_authority[0], + self.identifier_authority[1], + self.identifier_authority[2], + self.identifier_authority[3], + self.identifier_authority[4], + self.identifier_authority[5], + ]); + let sub_auths = self.sub_authorities[..self.sub_authority_count as usize] + .iter() + .map(|&x| x.to_string()) + .collect::>() + .join("-"); + format!("S-{}-{}-{}", self.revision, auth, sub_auths) + } +} + +fn parse_sid(input: &[u8]) -> IResult<&[u8], SID> { + let (input, ((revision, sub_authority_count), identifier_authority)) = tuple(( + nom::bits::bits::<_, _, Error<(&[u8], usize)>, _, _>(tuple((take(8usize), take(8usize)))), + nom::combinator::map(nom::bytes::complete::take(6usize), |slice: &[u8]| { + let mut arr = [0u8; 6]; + arr.copy_from_slice(slice); + arr + }), + ))(input)?; + + let (input, sub_authorities) = count(le_u32, sub_authority_count as usize)(input)?; + + let mut sid = SID { + revision, + sub_authority_count, + identifier_authority, + sub_authorities: [0; 15], + }; + sid.sub_authorities[..sub_authority_count as usize].copy_from_slice(&sub_authorities); + + Ok((input, sid)) +} + +impl FromStr for SID { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split('-').collect(); + if parts.len() < 3 || parts[0] != "S" { + return Err("Invalid SID format".to_string()); + } + + let revision = parts[1].parse::().map_err(|e| e.to_string())?; + let auth = parts[2].parse::().map_err(|e| e.to_string())?; + let identifier_authority = auth.to_be_bytes()[2..].try_into().unwrap(); + + let sub_authorities: Vec = parts[3..] + .iter() + .map(|&s| s.parse::()) + .collect::, _>>() + .map_err(|e| e.to_string())?; + + if sub_authorities.len() > 15 { + return Err("Too many sub-authorities".to_string()); + } + + let mut sid = SID { + revision, + sub_authority_count: sub_authorities.len() as u8, + identifier_authority, + sub_authorities: [0; 15], + }; + sid.sub_authorities[..sub_authorities.len()].copy_from_slice(&sub_authorities); + + Ok(sid) + } +} + +impl Hash for SID { + fn hash(&self, state: &mut H) { + self.revision.hash(state); + self.sub_authority_count.hash(state); + self.identifier_authority.hash(state); + for &sub_auth in &self.sub_authorities[..self.sub_authority_count as usize] { + sub_auth.hash(state); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + #[test] + fn test_sid_creation_and_to_string() { + let octet_string = vec![1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0]; + let sid = SID::from_bytes(&octet_string).unwrap(); + assert_eq!(sid.to_string(), "S-1-5-32-544"); + } + + #[test] + fn test_sid_equality() { + let octet_string1 = vec![ + 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, + 58, 80, 4, 0, 0, + ]; + let octet_string2 = vec![ + 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, + 58, 80, 4, 0, 0, + ]; + let sid1 = SID::from_bytes(&octet_string1).unwrap(); + let sid2 = SID::from_bytes(&octet_string2).unwrap(); + assert_eq!(sid1, sid2); + } + + #[test] + fn test_sid_hash() { + let octet_string1 = vec![ + 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, + 58, 80, 4, 0, 0, + ]; + let octet_string2 = vec![ + 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, + 58, 80, 4, 0, 0, + ]; + let sid1 = SID::from_bytes(&octet_string1).unwrap(); + let sid2 = SID::from_bytes(&octet_string2).unwrap(); + + let mut hasher1 = DefaultHasher::new(); + let mut hasher2 = DefaultHasher::new(); + sid1.hash(&mut hasher1); + sid2.hash(&mut hasher2); + + assert_eq!(hasher1.finish(), hasher2.finish()); + } + + #[test] + fn test_from_next_bytes() { + let input = vec![ + // First SID + 1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0, // Second SID + 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 45, 65, 88, 115, 197, 187, 192, 93, 42, 109, 38, + 58, 80, 4, 0, 0, // Additional data + 0xFF, 0xFF, + ]; + + let (remaining, sid1) = SID::from_next_bytes(&input).unwrap(); + assert_eq!(sid1.to_string(), "S-1-5-32-544"); + assert_eq!(remaining.len(), input.len() - 16); + + let (remaining, sid2) = SID::from_next_bytes(remaining).unwrap(); + assert_eq!( + sid2.to_string(), + "S-1-5-21-1935163693-1572912069-975596842-1104" + ); + assert_eq!(remaining, &[0xFF, 0xFF]); + } +}