├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── img ├── demo.gif ├── demo_windows_adcs_collector.gif ├── demo_windows_fqdn_resolver.gif ├── linux.png ├── rusthound_logo_v3.ico ├── rusthound_logo_v3.png └── windows.png ├── resources └── customqueries.json └── src ├── args.rs ├── banner.rs ├── enums ├── acl.rs ├── constants.rs ├── date.rs ├── forestlevel.rs ├── gplink.rs ├── ldaptype.rs ├── mod.rs ├── secdesc.rs ├── sid.rs ├── spntasks.rs ├── trusts.rs └── uacflags.rs ├── errors.rs ├── exec.rs ├── json ├── checker │ ├── bh_41.rs │ └── mod.rs ├── maker │ └── mod.rs ├── mod.rs ├── parser │ ├── bh_41.rs │ └── mod.rs └── templates │ ├── bh_41.rs │ └── mod.rs ├── ldap.rs ├── lib.rs ├── main.rs └── modules ├── adcs ├── checker.rs ├── flags.rs ├── mod.rs ├── parser.rs └── utils.rs ├── mod.rs └── resolver ├── mod.rs └── resolv.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.json 3 | *.zip 4 | /datas 5 | /.vscode 6 | rusthound* -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["g0h4n "] 3 | name = "rusthound" 4 | description = "Active Directory data collector for Bloodhound written in rust." 5 | keywords = ["bloodhound", "pentest", "ldap", "tokio", "async"] 6 | repository = "https://github.com/OPENCYBER-FR/RustHound" 7 | homepage = "https://github.com/OPENCYBER-FR/RustHound" 8 | documentation = "https://docs.rs/rusthound/" 9 | version = "1.1.69" 10 | edition = "2018" 11 | license = "MIT" 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | tokio = "1.1" 16 | clap = "4.0" 17 | nom7 = { version="7.0", package="nom" } 18 | colored = "2" 19 | chrono = "0.4" 20 | bitflags = "1.0" 21 | regex = "1" 22 | env_logger = "0.10" 23 | log = "0.4" 24 | lazy_static = "1.4.0" 25 | indicatif = "0.17" 26 | x509-parser = "0.15" 27 | trust-dns-resolver = "0.22" 28 | serde_json = { version = "1.0.89", features = ["preserve_order"] } 29 | zip= { version = "0.6.3", default-features = false } 30 | rpassword = "7.2" 31 | ldap3 = { version = "0.11.3", default-features = false } 32 | winreg = { version = "0.50", optional = true } 33 | 34 | [features] 35 | noargs = ["winreg"] # Only available for Windows 36 | nogssapi = ["ldap3/tls-native"] # Used for linux_musl armv7 and macos compilation 37 | default = ["ldap3/tls-rustls","ldap3/gssapi"] 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.74-slim-buster 2 | 3 | WORKDIR /usr/src/rusthound 4 | 5 | RUN \ 6 | apt-get -y update && \ 7 | apt-get -y install gcc clang libclang-dev libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit musl-tools make gcc-mingw-w64-x86-64 && \ 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | ENTRYPOINT ["make"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OPENCYBER 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prog :=rusthound 2 | 3 | cargo := $(shell command -v cargo 2> /dev/null) 4 | cargo_v := $(shell cargo -V| cut -d ' ' -f 2) 5 | rustup := $(shell command -v rustup 2> /dev/null) 6 | 7 | check_cargo: 8 | ifndef cargo 9 | $(error cargo is not available, please install it! curl https://sh.rustup.rs -sSf | sh) 10 | else 11 | @echo "Make sure your cargo version is up to date! Current version is $(cargo_v)" 12 | endif 13 | 14 | check_rustup: 15 | ifndef rustup 16 | $(error rustup is not available, please install it! curl https://sh.rustup.rs -sSf | sh) 17 | endif 18 | 19 | update_rustup: 20 | rustup update 21 | 22 | release: check_cargo 23 | cargo build --release 24 | cp target/release/$(prog) . 25 | @echo -e "[+] You can find \033[1;32m$(prog)\033[0m in your current folder." 26 | 27 | debug: check_cargo 28 | cargo build 29 | cp target/debug/$(prog) ./$(prog)_debug 30 | @echo -e "[+] You can find \033[1;32m$(prog)_debug\033[0m in your current folder." 31 | 32 | doc: check_cargo 33 | cargo doc --open --no-deps 34 | 35 | install: check_cargo 36 | cargo install --path . 37 | @echo "[+] rusthound installed!" 38 | 39 | uninstall: 40 | @cargo uninstall rusthound 41 | 42 | clean: 43 | rm target -rf 44 | 45 | install_windows_deps: update_rustup 46 | @rustup install stable-x86_64-pc-windows-gnu --force-non-host 47 | @rustup target add x86_64-pc-windows-gnu 48 | @rustup install stable-i686-pc-windows-gnu --force-non-host 49 | @rustup target add i686-pc-windows-gnu 50 | 51 | build_windows_x64: 52 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu 53 | cp target/x86_64-pc-windows-gnu/release/$(prog).exe . 54 | @echo -e "[+] You can find \033[1;32m$(prog).exe\033[0m in your current folder." 55 | 56 | build_windows_x86: 57 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target i686-pc-windows-gnu 58 | cp target/i686-pc-windows-gnu/release/$(prog).exe ./$(prog)_x86.exe 59 | @echo -e "[+] You can find \033[1;32m$(prog)_x86.exe\033[0m in your current folder." 60 | 61 | windows: check_rustup install_windows_deps build_windows_x64 62 | 63 | windows_x64: check_rustup install_windows_deps build_windows_x64 64 | 65 | windows_x86: check_rustup install_windows_deps build_windows_x86 66 | 67 | build_windows_noargs: 68 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu --features noargs 69 | cp target/x86_64-pc-windows-gnu/release/$(prog).exe ./$(prog)_noargs.exe 70 | @echo -e "[+] You can find \033[1;32m$(prog)_noargs.exe\033[0m in your current folder." 71 | 72 | windows_noargs: check_rustup install_windows_deps build_windows_noargs 73 | 74 | install_linux_musl_deps: 75 | @rustup install x86_64-unknown-linux-musl --force-non-host 76 | @rustup target add x86_64-unknown-linux-musl 77 | 78 | build_linux_musl: 79 | cross build --target x86_64-unknown-linux-musl --release --features nogssapi --no-default-features 80 | cp target/x86_64-unknown-linux-musl/release/$(prog) ./$(prog)_musl 81 | @echo -e "[+] You can find \033[1;32m$(prog)_musl\033[0m in your current folder." 82 | 83 | linux_musl: check_rustup install_cross build_linux_musl 84 | 85 | install_linux_deps:update_rustup 86 | @rustup install stable-x86_64-unknown-linux-gnu --force-non-host 87 | @rustup target add x86_64-unknown-linux-gnu 88 | 89 | build_linux_aarch64: 90 | cross build --target aarch64-unknown-linux-gnu --release --features nogssapi --no-default-features 91 | cp target/aarch64-unknown-linux-gnu/release/$(prog) ./$(prog)_aarch64 92 | @echo -e "[+] You can find \033[1;32m$(prog)_aarch64\033[0m in your current folder." 93 | 94 | linux_aarch64: check_rustup install_cross build_linux_aarch64 95 | 96 | build_linux_x86_64: 97 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --features nogssapi --target x86_64-unknown-linux-gnu --no-default-features 98 | cp target/x86_64-unknown-linux-gnu/release/$(prog) ./$(prog)_x86_64 99 | @echo -e "[+] You can find \033[1;32m$(prog)_x86_64\033[0m in your current folder." 100 | 101 | linux_x86_64: check_rustup install_linux_deps build_linux_x86_64 102 | 103 | install_macos_deps: 104 | @sudo git clone https://github.com/tpoechtrager/osxcross /usr/local/bin/osxcross || exit 105 | @sudo wget -P /usr/local/bin/osxcross/ -nc https://s3.dockerproject.org/darwin/v2/MacOSX10.10.sdk.tar.xz && sudo mv /usr/local/bin/osxcross/MacOSX10.10.sdk.tar.xz /usr/local/bin/osxcross/tarballs/ 106 | @sudo UNATTENDED=yes OSX_VERSION_MIN=10.7 /usr/local/bin/osxcross/build.sh 107 | @sudo chmod 775 /usr/local/bin/osxcross/ -R 108 | @export PATH="/usr/local/bin/osxcross/target/bin:$PATH" 109 | @grep 'target.x86_64-apple-darwin' ~/.cargo/config || echo "[target.x86_64-apple-darwin]" >> ~/.cargo/config 110 | @grep 'linker = "x86_64-apple-darwin14-clang"' ~/.cargo/config || echo 'linker = "x86_64-apple-darwin14-clang"' >> ~/.cargo/config 111 | @grep 'ar = "x86_64-apple-darwin14-clang"' ~/.cargo/config || echo 'ar = "x86_64-apple-darwin14-clang"' >> ~/.cargo/config 112 | @echo "[?] Now you need to uncomment line 32 and comment line 34 in Cargo.toml for MacOS and run 'make macos'" 113 | 114 | build_macos: 115 | @export PATH="/usr/local/bin/osxcross/target/bin:$PATH" 116 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-apple-darwin --features nogssapi --no-default-features 117 | cp target/x86_64-apple-darwin/release/$(prog).exe ./$(prog)_MacOS 118 | @echo -e "[+] You can find \033[1;32m$(prog)_MacOS\033[0m in your current folder." 119 | 120 | macos: build_macos 121 | 122 | install_cross: 123 | @cargo install --version 0.1.16 cross 124 | 125 | arm_musl: check_rustup install_cross 126 | cross build --target arm-unknown-linux-musleabi --release --features nogssapi --no-default-features 127 | cp target/arm-unknown-linux-musleabi/release/$(prog) ./$(prog)_arm_musl 128 | @echo -e "[+] You can find \033[1;32m$(prog)_arm_musl\033[0m in your current folder." 129 | 130 | armv7: check_rustup install_cross 131 | cross build --target armv7-unknown-linux-gnueabihf --release --features nogssapi --no-default-features 132 | cp target/armv7-unknown-linux-gnueabihf/release/$(prog) ./$(prog)_armv7 133 | @echo -e "[+] You can find \033[1;32m$(prog)_armv7\033[0m in your current folder." 134 | 135 | help: 136 | @echo "" 137 | @echo "Default:" 138 | @echo "usage: make install" 139 | @echo "usage: make uninstall" 140 | @echo "usage: make debug" 141 | @echo "usage: make release" 142 | @echo "" 143 | @echo "Static:" 144 | @echo "usage: make windows" 145 | @echo "usage: make windows_x64" 146 | @echo "usage: make windows_x86" 147 | @echo "usage: make linux_aarch64" 148 | @echo "usage: make linux_x86_64" 149 | @echo "usage: make linux_musl" 150 | @echo "usage: make macos" 151 | @echo "usage: make arm_musl" 152 | @echo "usage: make armv7" 153 | @echo "" 154 | @echo "Without cli argument:" 155 | @echo "usage: make windows_noargs" 156 | @echo "" 157 | @echo "Dependencies:" 158 | @echo "usage: make install_windows_deps" 159 | @echo "usage: make install_linux_musl_deps" 160 | @echo "usage: make install_macos_deps" 161 | @echo "" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > *This version is only compatible with [BloodHound Legacy 4.x](https://github.com/BloodHoundAD/BloodHound)* 2 | > 3 | > *Version compatible with [BloodHound Community Edition (CE)](https://github.com/SpecterOps/BloodHound) can be found here [RustHound-CE](https://github.com/g0h4n/RustHound-CE).* 4 | 5 |
6 | 7 |

8 | 9 |

10 | 11 | 12 |

13 | Crates.io 14 | GitHub 15 | Twitter Follow 16 | Twitter Follow 17 |
18 | Linux supported 19 | Windows supported 20 | macOS supported 21 | 22 |
23 |

24 | 25 | # Summary 26 | 27 | - [Limitation](#limitations) 28 | - [Description](#description) 29 | - [How to compile it?](#how-to-compile-it) 30 | - [Using Makefile](#using-makefile) 31 | - [Using Dockerfile](#using-dockerfile) 32 | - [Using Cargo](#using-cargo) 33 | - [Linux x86_64 static version](#manually-for-linux-x86_64-static-version) 34 | - [Windows static version from Linux](#manually-for-windows-static-version-from-linux) 35 | - [macOS static version from Linux](#manually-for-macos-static-version-from-linux) 36 | - [Optimize the binary size](#optimize-the-binary-size) 37 | 38 | - [How to build documentation?](#how-to-build-documentation) 39 | - [Usage](#usage) 40 | - [Demo](#demo) 41 | - [Simple usage](#simple-usage) 42 | - [Module FQDN resolver](#module-fqdn-resolver) 43 | - [Module ADCS collector](#module-adcs-collector) 44 | - [Statistics](#rocket-statistics) 45 | - [Roadmap](#-roadmap) 46 | - [Links](#link-links) 47 | 48 | # Limitations 49 | 50 | Not all SharpHound features have been implemented. Some exist in RustHound and not in SharpHound or BloodHound-Python. Please refer to the [roadmap](#-roadmap) for more information. 51 | 52 | # Description 53 | 54 | RustHound is a **cross-platform** BloodHound collector tool written in Rust, making it compatible with Linux, Windows, and macOS. 55 | 56 | No AV detection and **cross-compiled**. 57 | 58 | RustHound generates users, groups, computers, OUs, GPOs, containers, and domain JSON files that can be analyzed with BloodHound. 59 | 60 | > 💡 If you can use SharpHound, use it. 61 | > Use RustHound as a backup solution if SharpHound is detected by AV or if it not compatible with your OS. 62 | 63 | 64 | # How to compile it? 65 | 66 | ## Using Makefile 67 | 68 | You can use the **make** command to install RustHound or to compile it for Linux or Windows. 69 | 70 | ```bash 71 | make install 72 | rusthound -h 73 | ``` 74 | 75 | More command in the **Makefile**: 76 | 77 | ```bash 78 | Default: 79 | usage: make install 80 | usage: make uninstall 81 | usage: make debug 82 | usage: make release 83 | 84 | Static: 85 | usage: make windows 86 | usage: make windows_x64 87 | usage: make windows_x86 88 | usage: make linux_aarch64 89 | usage: make linux_x86_64 90 | usage: make linux_musl 91 | usage: make macos 92 | usage: make arm_musl 93 | usage: make armv7 94 | 95 | Without cli argument: 96 | usage: make windows_noargs 97 | 98 | Dependencies: 99 | usage: make install_windows_deps 100 | usage: make install_linux_musl_deps 101 | usage: make install_macos_deps 102 | ``` 103 | 104 | ## Using Dockerfile 105 | 106 | Use RustHound with Docker to make sure to have all dependencies. 107 | 108 | ```bash 109 | docker build --rm -t rusthound . 110 | 111 | # Then 112 | docker run --rm -v ./:/usr/src/rusthound rusthound windows 113 | docker run --rm -v ./:/usr/src/rusthound rusthound linux_musl 114 | docker run --rm -v ./:/usr/src/rusthound rusthound macos 115 | ``` 116 | 117 | ## Using Cargo 118 | 119 | You will need to install Rust on your system. 120 | 121 | [https://www.rust-lang.org/fr/tools/install](https://www.rust-lang.org/fr/tools/install) 122 | 123 | RustHound supports Kerberos and GSSAPI. Therefore, it requires Clang and its development libraries, as well as the Kerberos development libraries. On Debian and Ubuntu, this means **clang-N**, **libclang-N-dev**, and **libkrb5-dev**. 124 | 125 | For example: 126 | ```bash 127 | # Debian/Ubuntu 128 | sudo apt-get -y update && sudo apt-get -y install gcc clang libclang-dev libgssapi-krb5-2 libkrb5-dev libsasl2-modules-gssapi-mit musl-tools gcc-mingw-w64-x86-64 129 | ``` 130 | 131 | Here is how to compile the "release" and "debug" versions using the **cargo** command. 132 | 133 | ```bash 134 | git clone https://github.com/OPENCYBER-FR/RustHound 135 | cd RustHound 136 | cargo build --release 137 | # or debug version 138 | cargo b 139 | ``` 140 | 141 | The result can be found in the target/release or target/debug folder. 142 | 143 | Below you can find the compilation methodology for each of the OS from Linux. 144 | If you need another compilation system, please consult the list in this link: [https://doc.rust-lang.org/nightly/rustc/platform-support.html](https://doc.rust-lang.org/nightly/rustc/platform-support.html) 145 | 146 | 147 | ## Manually for Linux x86_64 static version 148 | 149 | ```bash 150 | # Install rustup and Cargo for Linux 151 | curl https://sh.rustup.rs -sSf | sh 152 | 153 | # Add Linux deps 154 | rustup install stable-x86_64-unknown-linux-gnu 155 | rustup target add x86_64-unknown-linux-gnu 156 | 157 | # Static compilation for Linux 158 | git clone https://github.com/OPENCYBER-FR/RustHound 159 | cd RustHound 160 | CFLAGS="-lrt";LDFLAGS="-lrt";RUSTFLAGS='-C target-feature=+crt-static';cargo build --release --target x86_64-unknown-linux-gnu 161 | ``` 162 | 163 | The result can be found in the target/x86_64-unknown-linux-gnu/release folder. 164 | 165 | 166 | ## Manually for Windows static version from Linux 167 | ```bash 168 | # Install rustup and Cargo in Linux 169 | curl https://sh.rustup.rs -sSf | sh 170 | 171 | # Add Windows deps 172 | rustup install stable-x86_64-pc-windows-gnu 173 | rustup target add x86_64-pc-windows-gnu 174 | 175 | # Static compilation for Windows 176 | git clone https://github.com/OPENCYBER-FR/RustHound 177 | cd RustHound 178 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-pc-windows-gnu 179 | ``` 180 | 181 | The result can be found in the target/x86_64-pc-windows-gnu/release folder. 182 | 183 | 184 | ## Manually for macOS static version from Linux 185 | 186 | Amazing documentation: [https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html](https://wapl.es/rust/2019/02/17/rust-cross-compile-linux-to-macos.html) 187 | 188 | ```bash 189 | # Install rustup and Cargo in Linux 190 | curl https://sh.rustup.rs -sSf | sh 191 | 192 | # Add macOS tool chain 193 | sudo git clone https://github.com/tpoechtrager/osxcross /usr/local/bin/osxcross 194 | sudo wget -P /usr/local/bin/osxcross/ -nc https://s3.dockerproject.org/darwin/v2/MacOSX10.10.sdk.tar.xz && sudo mv /usr/local/bin/osxcross/MacOSX10.10.sdk.tar.xz /usr/local/bin/osxcross/tarballs/ 195 | sudo UNATTENDED=yes OSX_VERSION_MIN=10.7 /usr/local/bin/osxcross/build.sh 196 | sudo chmod 775 /usr/local/bin/osxcross/ -R 197 | export PATH="/usr/local/bin/osxcross/target/bin:$PATH" 198 | 199 | # Cargo needs to be told to use the correct linker for the x86_64-apple-darwin target, so add the following to your project’s .cargo/config file: 200 | grep 'target.x86_64-apple-darwin' ~/.cargo/config || echo "[target.x86_64-apple-darwin]" >> ~/.cargo/config 201 | grep 'linker = "x86_64-apple-darwin14-clang"' ~/.cargo/config || echo 'linker = "x86_64-apple-darwin14-clang"' >> ~/.cargo/config 202 | grep 'ar = "x86_64-apple-darwin14-clang"' ~/.cargo/config || echo 'ar = "x86_64-apple-darwin14-clang"' >> ~/.cargo/config 203 | 204 | # Static compilation for macOS 205 | git clone https://github.com/OPENCYBER-FR/RustHound 206 | cd RustHound 207 | RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target x86_64-apple-darwin --features nogssapi 208 | ``` 209 | 210 | The result can be found in the target/x86_64-apple-darwin/release folder. 211 | 212 | 213 | ## Optimize the binary size 214 | 215 | > 💡 To obtain an optimized compilation of RustHound add the following compilation parameters at the end of the `Cargo.toml` file. 216 | 217 | ```bash 218 | [profile.release] 219 | opt-level = "z" 220 | lto = true 221 | strip = true 222 | codegen-units = 1 223 | panic = "abort" 224 | ``` 225 | 226 | The size of the binary will be considerably minimized. 227 | Basic cargo compiler commands can be used. 228 | 229 | ```bash 230 | make windows 231 | ``` 232 | 233 | More information [here](https://github.com/johnthagen/min-sized-rust) 234 | 235 | 236 | # How to build the documentation? 237 | 238 | ```bash 239 | git clone https://github.com/OPENCYBER-FR/RustHound 240 | cd RustHound 241 | cargo doc --open --no-deps 242 | ``` 243 | 244 | # Usage 245 | 246 | ```bash 247 | Usage: rusthound [OPTIONS] --domain 248 | 249 | Options: 250 | -v... Set the level of verbosity 251 | -h, --help Print help information 252 | -V, --version Print version information 253 | 254 | REQUIRED VALUES: 255 | -d, --domain Domain name like: DOMAIN.LOCAL 256 | 257 | OPTIONAL VALUES: 258 | -u, --ldapusername LDAP username, like: user@domain.local 259 | -p, --ldappassword LDAP password 260 | -f, --ldapfqdn Domain Controler FQDN like: DC01.DOMAIN.LOCAL or just DC01 261 | -i, --ldapip Domain Controller IP address like: 192.168.1.10 262 | -P, --ldapport LDAP port [default: 389] 263 | -n, --name-server Alternative IP address name server to use for DNS queries 264 | -o, --output Output directory where you would like to save JSON files [default: ./] 265 | 266 | OPTIONAL FLAGS: 267 | --ldaps Force LDAPS using for request like: ldaps://DOMAIN.LOCAL/ 268 | --dns-tcp Use TCP instead of UDP for DNS queries 269 | --dc-only Collects data only from the domain controller. Will not try to retrieve CA security/configuration or check for Web Enrollment 270 | --old-bloodhound For ADCS only. Output result as BloodHound data for the original BloodHound version from @BloodHoundAD without PKI support 271 | -z, --zip Compress the JSON files into a zip archive 272 | 273 | OPTIONAL MODULES: 274 | --fqdn-resolver Use fqdn-resolver module to get computers IP address 275 | --adcs Use ADCS module to enumerate Certificate Templates, Certificate Authorities and other configurations. 276 | (For the custom-built BloodHound version from @ly4k with PKI support) 277 | ``` 278 | 279 | # Demo 280 | 281 | Examples are done on the [GOADv2](https://github.com/Orange-Cyberdefense/GOAD) implemented by [mayfly](https://twitter.com/M4yFly): 282 | 283 | ## Simple usage 284 | 285 | ```bash 286 | # Linux with username:password 287 | rusthound -d north.sevenkingdoms.local -u 'jeor.mormont@north.sevenkingdoms.local' -p '_L0ngCl@w_' -o /tmp/demo -z 288 | 289 | # Linux with username:password and ldapip 290 | rusthound -d north.sevenkingdoms.local -i 192.168.56.11 -u 'jeor.mormont@north.sevenkingdoms.local' -p '_L0ngCl@w_' -o /tmp/demo -z 291 | 292 | # Linux with username:password and ldaps 293 | rusthound -d north.sevenkingdoms.local --ldaps -u 'jeor.mormont@north.sevenkingdoms.local' -p '_L0ngCl@w_' -o /tmp/demo -z 294 | # Linux with username:password and ldaps and custom port 295 | rusthound -d north.sevenkingdoms.local --ldaps -P 3636 -u 'jeor.mormont@north.sevenkingdoms.local' -p '_L0ngCl@w_' -o /tmp/demo -z 296 | 297 | # Tips to redirect and append both standard output and standard error to a file > /tmp/rh_output 2>&1 298 | rusthound -d north.sevenkingdoms.local --ldaps -u 'jeor.mormont@north.sevenkingdoms.local' -p '_L0ngCl@w_' -o /tmp/demo --fqdn-resolver > /tmp/rh_output 2>&1 299 | 300 | # Windows with GSSAPI session 301 | rusthound.exe -d sevenkingdoms.local --ldapfqdn kingslanding 302 | # Windows simple bind connection username:password (do not use single or double quotes with cmd.exe) 303 | rusthound.exe -d sevenkingdoms.local -u jeor.mormont@north.sevenkingdoms.local -p _L0ngCl@w_ -o output -z 304 | 305 | # Kerberos authentication (Linux) 306 | export KRB5CCNAME="/tmp/jeor.mormont.ccache" 307 | rusthound -d sevenkingdoms.local -f kingslanding -k -z 308 | # Kerberos authentication (Windows) 309 | rusthound.exe -d sevenkingdoms.local -f kingslanding -k -z 310 | ``` 311 |

312 | 313 |

314 | 315 | ## Module FQDN resolver 316 | 317 | ```bash 318 | # Linux with username:password and FQDN resolver module 319 | rusthound -d essos.local -u 'daenerys.targaryen@essos.local' -p 'BurnThemAll!' -o /tmp/demo --fqdn-resolver -z 320 | # Linux with username:password and ldaps and FQDN resolver module and TCP DNS request and custom name server 321 | rusthound -d essos.local --ldaps -u 'daenerys.targaryen@essos.local' -p 'BurnThemAll!' -o /tmp/demo --fqdn-resolver --tcp-dns --name-server 192.168.56.12 -z 322 | 323 | # Windows with GSSAPI session and FQDN resolver module 324 | rusthound.exe -d essos.local -f meereen -o output --fqdn-resolver -z 325 | # Windows simple bind connection username:password and FQDN resolver module and TCP DNS request and custom name server (do not use single or double quotes with cmd.exe) 326 | rusthound.exe -d essos.local -u daenerys.targaryen@essos.local -p BurnThemAll! -o output -z --fqdn-resolver --tcp-dns --name-server 192.168.56.12 327 | ``` 328 |

329 | 330 |

331 | 332 | 333 | ## Module ADCS collector 334 | 335 | Example using [@ly4k BloodHound version](https://github.com/ly4k/BloodHound). 336 | 337 | ```bash 338 | # Linux with username:password and ADCS module for @ly4k BloodHound version 339 | rusthound -d essos.local -u 'daenerys.targaryen@essos.local' -p 'BurnThemAll!' -o /tmp/adcs --adcs -z 340 | # Linux with username:password and ADCS module and dconly flag (will don't check webenrollment) 341 | rusthound -d essos.local -u 'daenerys.targaryen@essos.local' -p 'BurnThemAll!' -o /tmp/adcs --adcs --dc-only -z 342 | 343 | # Linux with username:password and ADCS module using "--old-bloodhound" argument for official @BloodHoundAd version 344 | rusthound -d essos.local -u 'daenerys.targaryen@essos.local' -p 'BurnThemAll!' -o /tmp/adcs --adcs --old-bloodhound -z 345 | 346 | # Windows with GSSAPI session and ADCS module 347 | rusthound.exe -d essos.local -f meereen -o output -z --adcs 348 | # Windows with GSSAPI session and ADCS module and TCP DNS request and custom name server 349 | rusthound.exe -d essos.local --ldapfqdn meereen -o output -z --adcs --tcp-dns --name-server 192.168.56.12 350 | # Windows simple bind connection username:password (do not use single or double quotes with cmd.exe) 351 | rusthound.exe -d essos.local -u daenerys.targaryen@essos.local -p BurnThemAll! -o output -z --adcs --dc-only 352 | ``` 353 |

354 | 355 |

356 | 357 | 358 | You can find the custom queries used in the demo in the resource folder. 359 | 360 | Use the following command to install it: 361 | 362 | ```bash 363 | cp resources/customqueries.json ~/.config/bloodhound/customqueries.json 364 | ``` 365 | 366 | # :rocket: Statistics 367 | 368 | In order to make statistics on a DC with more LDAP objects, run the [BadBlood](https://github.com/davidprowe/BadBlood) on the domain controller ESSOS.local from [GOAD](https://github.com/Orange-Cyberdefense/GOAD). The DC should now have around 3500 objects. Below is the average time it takes to run the following tools: 369 | 370 | | Tool | Environment | Objects | Time | Command | 371 | | -------------------------- | ----------------- | ---------- | ------- | ------- | 372 | | SharpHound.exe | Windows | ~3500 | ~51.605s | Measure-Command { sharphound.exe -d essos.local --ldapusername 'khal.drogo' --ldappassword 'horse' --domaincontroller '192.168.56.12' -c All } | 373 | | BloodHound.py | Linux | ~3500 | ~9.657s | time python3 bloodhound.py -u khal.drogo -p horse -d essos.local -ns 192.168.56.12 --zip -c all | 374 | | RustHound.exe | Windows | ~3500 | **~5.315s** | Measure-Command { rusthound.exe -d essos.local -u khal.drogo@essos.local -p horse -z } | 375 | | RustHound | Linux | ~3500 | **~3.166s** | time rusthound -d essos.local -u khal.drogo@essos.local -p horse -z | 376 | 377 | # 🚥 Roadmap 378 | 379 | ## Authentification 380 | - [x] LDAP (389) 381 | - [x] LDAPS (636) 382 | - [x] `BIND` 383 | - [ ] `NTLM` 384 | - [x] `Kerberos` 385 | - [x] Prompt for password 386 | 387 | ## Outputs 388 | - [x] users.json 389 | - [x] groups.json 390 | - [x] computers.json 391 | - [x] ous.json 392 | - [x] gpos.json 393 | - [x] containers.json 394 | - [x] domains.json 395 | - [x] cas.json 396 | - [x] templates.json 397 | - [x] args and function to zip JSON files **--zip** 398 | 399 | ## Modules 400 | 401 | - [x] Retreive LAPS password if your user can read them **automatic** 402 | - [x] Resolve FQDN computers found to IP address **--fqdn-resolver** 403 | - [x] Retrieve certificates for ESC exploitation with [Certipy](https://github.com/ly4k/Certipy) **--adcs** 404 | - [ ] Kerberos attack module (ASREPROASTING and KERBEROASTING) **--attack-kerberos** 405 | - [ ] Retrieve datas from trusted domains **--follow-trust** (Currently working on it, got beta version of this module) 406 | 407 | 408 | ## BloodHound v4.2 409 | 410 | - Parsing Features 411 | - Users & Computers 412 | - [ ] `HasSIDHistory` 413 | - Users 414 | - [ ] `Properties` : `sfupassword` 415 | 416 | - **DCERPC (dependencies)** 417 | - Computers 418 | - [ ] `Sessions` 419 | - OUs & Domains 420 | - [ ] `LocalAdmins` 421 | - [ ] `RemoteDesktopUsers` 422 | - [ ] `DcomUsers` 423 | - [ ] `PSRemoteUsers` 424 | - CAs 425 | - [ ] `User Specified SAN` 426 | - [ ] `Request Disposition` 427 | 428 | # :link: Links 429 | 430 | - Blog post: [https://www.opencyber.com/rusthound-data-collector-for-bloodhound-written-in-rust/](https://www.opencyber.com/rusthound-data-collector-for-bloodhound-written-in-rust/) 431 | - BloodHound.py: [https://github.com/fox-it/BloodHound.py](https://github.com/fox-it/BloodHound.py) 432 | - SharpHound: [https://github.com/BloodHoundAD/SharpHound](https://github.com/BloodHoundAD/SharpHound) 433 | - BloodHound: [https://github.com/BloodHoundAD/BloodHound](https://github.com/BloodHoundAD/BloodHound) 434 | - BloodHound docs: [https://bloodhound.readthedocs.io/en/latest/index.html](https://bloodhound.readthedocs.io/en/latest/index.html) 435 | - GOAD: [https://github.com/Orange-Cyberdefense/GOAD](https://github.com/Orange-Cyberdefense/GOAD) 436 | - ly4k BloodHound version: [https://github.com/ly4k/BloodHound](https://github.com/ly4k/BloodHound) 437 | - Certipy: [https://github.com/ly4k/Certipy](https://github.com/ly4k/Certipy) 438 | -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NH-RED-TEAM/RustHound/241751cbdb9911404d8f4530e9151d1a3e227321/img/demo.gif -------------------------------------------------------------------------------- /img/demo_windows_adcs_collector.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NH-RED-TEAM/RustHound/241751cbdb9911404d8f4530e9151d1a3e227321/img/demo_windows_adcs_collector.gif -------------------------------------------------------------------------------- /img/demo_windows_fqdn_resolver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NH-RED-TEAM/RustHound/241751cbdb9911404d8f4530e9151d1a3e227321/img/demo_windows_fqdn_resolver.gif -------------------------------------------------------------------------------- /img/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NH-RED-TEAM/RustHound/241751cbdb9911404d8f4530e9151d1a3e227321/img/linux.png -------------------------------------------------------------------------------- /img/rusthound_logo_v3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NH-RED-TEAM/RustHound/241751cbdb9911404d8f4530e9151d1a3e227321/img/rusthound_logo_v3.ico -------------------------------------------------------------------------------- /img/rusthound_logo_v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NH-RED-TEAM/RustHound/241751cbdb9911404d8f4530e9151d1a3e227321/img/rusthound_logo_v3.png -------------------------------------------------------------------------------- /img/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NH-RED-TEAM/RustHound/241751cbdb9911404d8f4530e9151d1a3e227321/img/windows.png -------------------------------------------------------------------------------- /resources/customqueries.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "_comment": "Custom queries v1.0", 4 | "_comment2": "Certipy customqueries https://github.com/ly4k/Certipy/blob/main/customqueries.json", 5 | "queries": [ 6 | 7 | { 8 | "name": "[A1] Return all users: MATCH (u:User) return u", 9 | "category": "Simple custom queries", 10 | "queryList": [ 11 | { 12 | "final": true, 13 | "query": "MATCH (u:User) return u" 14 | } 15 | ] 16 | }, 17 | { 18 | "name": "[A2] Return all computers: MATCH (c:Computer) return c", 19 | "category": "Simple custom queries", 20 | "queryList": [ 21 | { 22 | "final": true, 23 | "query": "MATCH (c:Computer) return c" 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "[A3] Return all groups: MATCH (g:Group) return g", 29 | "category": "Simple custom queries", 30 | "queryList": [ 31 | { 32 | "final": true, 33 | "query": "MATCH (g:Group) return g" 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "[A4] Return all ous: MATCH (o:OU) return o", 39 | "category": "Simple custom queries", 40 | "queryList": [ 41 | { 42 | "final": true, 43 | "query": "MATCH (o:OU) return o" 44 | } 45 | ] 46 | }, 47 | { 48 | "name": "[A5] Return all gpos: MATCH (g:GPO) return g", 49 | "category": "Simple custom queries", 50 | "queryList": [ 51 | { 52 | "final": true, 53 | "query": "MATCH (g:GPO) return g" 54 | } 55 | ] 56 | }, 57 | { 58 | "name": "[A6] Return all containers: MATCH (c:Container) return c", 59 | "category": "Simple custom queries", 60 | "queryList": [ 61 | { 62 | "final": true, 63 | "query": "MATCH (c:Container) return c" 64 | } 65 | ] 66 | }, 67 | { 68 | "name": "[A7] Return all domains: MATCH (d:Domain) return d", 69 | "category": "Simple custom queries", 70 | "queryList": [ 71 | { 72 | "final": true, 73 | "query": "MATCH (d:Domain) return d" 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "[A8] Return users content specified word: MATCH (u:User) WHERE u.name CONTAINS 'g0h4n' return u", 79 | "category": "Simple custom queries", 80 | "queryList": [ 81 | { 82 | "final": true, 83 | "query": "MATCH (u:User) WHERE u.name CONTAINS 'g0h4n' return u" 84 | } 85 | ] 86 | }, 87 | { 88 | "name": "[B1] List all owned users", 89 | "category": "Other RustHound custom queries", 90 | "queryList": [ 91 | { 92 | "final": true, 93 | "query": "MATCH (m:User) WHERE m.owned=TRUE RETURN m" 94 | } 95 | ] 96 | }, 97 | { 98 | "name": "[B2] List all owned computers", 99 | "category": "Other RustHound custom queries", 100 | "queryList": [ 101 | { 102 | "final": true, 103 | "query": "MATCH (m:Computer) WHERE m.owned=TRUE RETURN m" 104 | } 105 | ] 106 | }, 107 | { 108 | "name": "[B3] List all owned groups", 109 | "category": "Other RustHound custom queries", 110 | "queryList": [ 111 | { 112 | "final": true, 113 | "query": "MATCH (m:Group) WHERE m.owned=TRUE RETURN m" 114 | } 115 | ] 116 | }, 117 | { 118 | "name": "[B4] List all owned computers", 119 | "category": "Other RustHound custom queries", 120 | "queryList": [ 121 | { 122 | "final": true, 123 | "query": "MATCH (m:Computer) WHERE m.owned=TRUE RETURN m" 124 | } 125 | ] 126 | }, 127 | { 128 | "name": "[B5] List the groups of all owned users", 129 | "category": "Other RustHound custom queries", 130 | "queryList": [ 131 | { 132 | "final": true, 133 | "query": "MATCH (m:User) WHERE m.owned=TRUE WITH m MATCH p=(m)-[:MemberOf*1..]->(n:Group) RETURN p" 134 | } 135 | ] 136 | }, 137 | { 138 | "name": "[B6] Find the Shortest path to a high value target from an owned object", 139 | "category": "Other RustHound custom queries", 140 | "queryList": [ 141 | { 142 | "final": true, 143 | "query": "MATCH p=shortestPath((g {owned:true})-[*1..]->(n {highvalue:true})) WHERE g<>n return p" 144 | } 145 | ] 146 | }, 147 | { 148 | "name": "[B7] Find the Shortest path to a unconstrained delegation system from an owned object", 149 | "category": "Other RustHound custom queries", 150 | "queryList": [ 151 | { 152 | "final": true, 153 | "query": "MATCH (n) MATCH p=shortestPath((n)-[*1..]->(m:Computer {unconstraineddelegation: true})) WHERE NOT n=m AND n.owned = true RETURN p" 154 | } 155 | ] 156 | }, 157 | { 158 | 159 | "name": "[B8] Find all Kerberoastable Users where n.hasspn=true (Kerberosting attack)", 160 | "category": "Other RustHound custom queries", 161 | "queryList": [ 162 | { 163 | "final": true, 164 | "query": "MATCH (n:User) WHERE n.hasspn=true RETURN n", 165 | "allowCollapse": false 166 | } 167 | ] 168 | }, 169 | { 170 | "name": "[B9] Find Kerberoastable Users with a path to DA", 171 | "category": "Other RustHound custom queries", 172 | "queryList": [ 173 | { 174 | "final": true, 175 | "query": "MATCH (u:User {hasspn:true}) MATCH (g:Group) WHERE g.objectid ENDS WITH '-512' MATCH p = shortestPath( (u)-[*1..]->(g) ) RETURN p" 176 | } 177 | ] 178 | }, 179 | { 180 | "name": "[B10] Find users that can be AS-REP roasted (ASREPRoast)", 181 | "category": "Other RustHound custom queries", 182 | "queryList": [ 183 | { 184 | "final": true, 185 | "query": "MATCH (u:User {dontreqpreauth: true}) RETURN u" 186 | } 187 | ] 188 | }, 189 | { 190 | "name": "[B11] Find groups that can reset passwords (Warning: Heavy)", 191 | "category": "Other RustHound custom queries", 192 | "queryList": [ 193 | { 194 | "final": true, 195 | "query": "MATCH p=(m:Group)-[r:ForceChangePassword]->(n:User) RETURN p" 196 | } 197 | ] 198 | }, 199 | { 200 | "name": "[B12] Find groups that have local admin rights (Warning: Heavy)", 201 | "category": "Other RustHound custom queries", 202 | "queryList": [ 203 | { 204 | "final": true, 205 | "query": "MATCH p=(m:Group)-[r:AdminTo]->(n:Computer) RETURN p" 206 | } 207 | ] 208 | }, 209 | { 210 | "name": "[B13] Find all users that have local admin rights", 211 | "category": "Other RustHound custom queries", 212 | "queryList": [ 213 | { 214 | "final": true, 215 | "query": "MATCH p=(m:User)-[r:AdminTo]->(n:Computer) RETURN p" 216 | } 217 | ] 218 | }, 219 | { 220 | "name": "[B14] Find all active Domain Admin sessions", 221 | "category": "Other RustHound custom queries", 222 | "queryList": [ 223 | { 224 | "final": true, 225 | "query": "MATCH (n:User)-[:MemberOf]->(g:Group) WHERE g.objectid ENDS WITH '-512' MATCH p = (c:Computer)-[:HasSession]->(n) return p" 226 | } 227 | ] 228 | }, 229 | { 230 | "name": "[B15] Find all computers with Unconstrained Delegation", 231 | "category": "Other RustHound custom queries", 232 | "queryList": [ 233 | { 234 | "final": true, 235 | "query": "MATCH (c:Computer {unconstraineddelegation:true}) return c" 236 | } 237 | ] 238 | }, 239 | { 240 | "name": "[B16] Find all computers with unsupported operating systems", 241 | "category": "Other RustHound custom queries", 242 | "queryList": [ 243 | { 244 | "final": true, 245 | "query": "MATCH (H:Computer) WHERE H.operatingsystem = '.*(2000|2003|2008|xp|vista|7|me).*' RETURN H" 246 | } 247 | ] 248 | }, 249 | { 250 | "name": "[B17.1] Return the name of every computer in the database where at least one SPN for the computer contains the string 'MSSQL'", 251 | "category": "Other RustHound custom queries", 252 | "queryList": [ 253 | { 254 | "final": true, 255 | "query": "MATCH (c:Computer) WHERE ANY (x IN c.serviceprincipalnames WHERE toUpper(x) CONTAINS 'MSSQL') RETURN c" 256 | } 257 | ] 258 | }, 259 | { 260 | "name": "[B17.2] Return the name of every user in the database where at least one SPN for the computer contains the string 'MSSQL'", 261 | "category": "Other RustHound custom queries", 262 | "queryList": [ 263 | { 264 | "final": true, 265 | "query": "MATCH (u:User) WHERE ANY (x IN u.serviceprincipalnames WHERE toUpper(x) CONTAINS 'MSSQL') RETURN u" 266 | } 267 | ] 268 | }, 269 | { 270 | "name": "[B18] View all groups that contain the word 'admin'", 271 | "category": "Other RustHound custom queries", 272 | "queryList": [ 273 | { 274 | "final": true, 275 | "query": "Match (n:Group) WHERE n.name CONTAINS 'ADMIN' RETURN n" 276 | } 277 | ] 278 | }, 279 | { 280 | "name": "[B19] Show all high value target's groups", 281 | "category": "Other RustHound custom queries", 282 | "queryList": [ 283 | { 284 | "final": true, 285 | "query": "MATCH p=(n:User)-[r:MemberOf*1..]->(m:Group {highvalue:true}) RETURN p" 286 | } 287 | ] 288 | }, 289 | { 290 | "name": "[B20] Find groups that contain both users and computers", 291 | "category": "Other RustHound custom queries", 292 | "queryList": [ 293 | { 294 | "final": true, 295 | "query": "MATCH (c:Computer)-[r:MemberOf*1..]->(groupsWithComps:Group) WITH groupsWithComps MATCH (u:User)-[r:MemberOf*1..]->(groupsWithComps) RETURN DISTINCT(groupsWithComps) as groupsWithCompsAndUsers" 296 | } 297 | ] 298 | }, 299 | { 300 | "name": "[B21] Find all users a part of the VPN group", 301 | "category": "Other RustHound custom queries", 302 | "queryList": [ 303 | { 304 | "final": true, 305 | "query": "Match p=(u:User)-[:MemberOf]->(g:Group) WHERE toUPPER (g.name) CONTAINS 'VPN' return p" 306 | } 307 | ] 308 | }, 309 | { 310 | "name": "[B22] Find if any domain user has interesting permissions against a GPO (Warning: Heavy)", 311 | "category": "Other RustHound custom queries", 312 | "queryList": [ 313 | { 314 | "final": true, 315 | "query": "MATCH p=(u:User)-[r:AllExtendedRights|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|GpLink*1..]->(g:GPO) RETURN p" 316 | } 317 | ] 318 | }, 319 | { 320 | "name": "[B23] Find All edges any owned user has on a computer", 321 | "category": "Other RustHound custom queries", 322 | "queryList": [ 323 | { 324 | "final": true, 325 | "query": "MATCH p=shortestPath((m:User)-[r*]->(b:Computer)) WHERE m.owned RETURN p" 326 | } 327 | ] 328 | }, 329 | { 330 | "name": "[B24] Find all computer or objects who can read GMSA password (ReadGMSAPassword limit 25)", 331 | "category": "Other RustHound custom queries", 332 | "queryList": [ 333 | { 334 | "final": true, 335 | "query": "MATCH p=()-[r:ReadGMSAPassword]->() RETURN p LIMIT 25" 336 | } 337 | ] 338 | }, 339 | { 340 | "name": "[B25] Find all computer or objects who can DCSync (limit 25)", 341 | "category": "Other RustHound custom queries", 342 | "queryList": [ 343 | { 344 | "final": true, 345 | "query": "MATCH p=(n:Computer)-[r:DCSync]->() RETURN p LIMIT 25" 346 | } 347 | ] 348 | }, 349 | { 350 | "name": "[B26.1] Find all users who description contains 'pass'", 351 | "category": "Other RustHound custom queries", 352 | "queryList": [ 353 | { 354 | "final": true, 355 | "query": "MATCH (n:User WHERE n.description CONTAINS 'pass') RETURN n" 356 | } 357 | ] 358 | }, 359 | { 360 | "name": "[B26.2] Find all computers who description contains 'pass'", 361 | "category": "Other RustHound custom queries", 362 | "queryList": [ 363 | { 364 | "final": true, 365 | "query": "MATCH (n:Computer WHERE n.description CONTAINS 'pass') RETURN n" 366 | } 367 | ] 368 | }, 369 | { 370 | "name": "[B26.3] Find all groups who description contains 'pass'", 371 | "category": "Other RustHound custom queries", 372 | "queryList": [ 373 | { 374 | "final": true, 375 | "query": "MATCH (n:Group WHERE n.description CONTAINS 'pass') RETURN n" 376 | } 377 | ] 378 | }, 379 | { 380 | "name": "[C1] KUD (Kerberos Unconstrained Delegation): Find unconstrained delegation", 381 | "category": "Kerberos", 382 | "_comment": "https://mayfly277.github.io/posts/GOADv2-pwning-part10/", 383 | "queryList": [ 384 | { 385 | "final": true, 386 | "query": "MATCH (c {unconstraineddelegation:true}) return c" 387 | } 388 | ] 389 | }, 390 | { 391 | "name": "[C2] KUD: search for unconstrained delegation system (out of domain controller)", 392 | "category": "Kerberos", 393 | "queryList": [ 394 | { 395 | "final": true, 396 | "query": "MATCH (c1:Computer)-[:MemberOf*1..]->(g:Group) WHERE g.objectid ENDS WITH '-516' WITH COLLECT(c1.name) AS domainControllers MATCH (c2 {unconstraineddelegation:true}) WHERE NOT c2.name IN domainControllers RETURN c2" 397 | } 398 | ] 399 | }, 400 | { 401 | "name": "[C3] KUD: Find the Shortest path to a unconstrained delegation system from an owned object", 402 | "category": "Kerberos", 403 | "queryList": [ 404 | { 405 | "final": true, 406 | "query": "MATCH (n) MATCH p=shortestPath((n)-[*1..]->(m:Computer {unconstraineddelegation: true})) WHERE NOT n=m AND n.owned = true RETURN p" 407 | } 408 | ] 409 | }, 410 | { 411 | "name": "[C4] KCD (Kerberos Constrained Delegation): Find constrained delegation (User to Computer)", 412 | "category": "Kerberos", 413 | "queryList": [ 414 | { 415 | "final": true, 416 | "query": "MATCH p=(u:User)-[:AllowedToDelegate]->(c) RETURN p" 417 | } 418 | ] 419 | }, 420 | { 421 | "name": "[C5] KCD: Find constrained delegation (Computer to Computer)", 422 | "category": "Kerberos", 423 | "queryList": [ 424 | { 425 | "final": true, 426 | "query": "MATCH p=(u:Computer)-[:AllowedToDelegate]->(c) RETURN p" 427 | } 428 | ] 429 | }, 430 | { 431 | "name": "[C6] RBCD (Resource Based Constrained Delegation): Computer with msDS-AllowedToActOnBehalfOfOtherIdentity value", 432 | "category": "Kerberos", 433 | "queryList": [ 434 | { 435 | "final": true, 436 | "query": "MATCH p=(c)-[:AllowedToAct]->(c) RETURN p" 437 | } 438 | ] 439 | }, 440 | { 441 | "name": "[C7] RBCD: User with GenericAll or GenericWrite or WriteDACL on Computer", 442 | "category": "Kerberos", 443 | "queryList": [ 444 | { 445 | "final": true, 446 | "query": "MATCH (n) MATCH p=shortestPath((n:User)-[:AllExtendedRights|GenericAll|GenericWrite|Owns|WriteDacl*1..]->(m:Computer)) RETURN p" 447 | } 448 | ] 449 | }, 450 | { 451 | "name": "[C8] KDC:RBCD: Find all computer who can AllowedToAct or AllowToDelegate (limit 25)", 452 | "category": "Kerberos", 453 | "queryList": [ 454 | { 455 | "final": true, 456 | "query": "MATCH (m:Computer),(n {highvalue:true}),p=shortestPath((m)-[r*1..]->(n)) WHERE NONE (r IN relationships(p) WHERE type(r)= 'GetChanges') AND NONE (r in relationships(p) WHERE type(r)='GetChangesAll') AND NOT m=n RETURN p LIMIT 25" 457 | } 458 | ] 459 | }, 460 | { 461 | "name": "Find all Certificate Templates", 462 | "category": "Certificates", 463 | "queryList": [ 464 | { 465 | "final": true, 466 | "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' RETURN n" 467 | } 468 | ] 469 | }, 470 | { 471 | "name": "Find enabled Certificate Templates", 472 | "category": "Certificates", 473 | "queryList": [ 474 | { 475 | "final": true, 476 | "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' and n.Enabled = true RETURN n" 477 | } 478 | ] 479 | }, 480 | { 481 | "name": "Find Certificate Authorities", 482 | "category": "Certificates", 483 | "queryList": [ 484 | { 485 | "final": true, 486 | "query": "MATCH (n:GPO) WHERE n.type = 'Enrollment Service' RETURN n" 487 | } 488 | ] 489 | }, 490 | { 491 | "name": "Show Enrollment Rights for Certificate Template", 492 | "category": "Certificates", 493 | "queryList": [ 494 | { 495 | "final": false, 496 | "title": "Select a Certificate Template...", 497 | "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' RETURN n.name" 498 | }, 499 | { 500 | "final": true, 501 | "query": "MATCH p=(g)-[:Enroll|AutoEnroll]->(n:GPO {name:$result}) WHERE n.type = 'Certificate Template' return p", 502 | "allowCollapse": false 503 | } 504 | ] 505 | }, 506 | { 507 | "name": "Show Rights for Certificate Authority", 508 | "category": "Certificates", 509 | "queryList": [ 510 | { 511 | "final": false, 512 | "title": "Select a Certificate Authority...", 513 | "query": "MATCH (n:GPO) WHERE n.type = 'Enrollment Service' RETURN n.name" 514 | }, 515 | { 516 | "final": true, 517 | "query": "MATCH p=(g)-[:ManageCa|ManageCertificates|Auditor|Operator|Read|Enroll]->(n:GPO {name:$result}) return p", 518 | "allowCollapse": false 519 | } 520 | ] 521 | }, 522 | { 523 | "name": "Find Misconfigured Certificate Templates (ESC1)", 524 | "category": "Domain Escalation", 525 | "queryList": [ 526 | { 527 | "final": true, 528 | "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' and n.`Enrollee Supplies Subject` = true and n.`Client Authentication` = true and n.`Enabled` = true RETURN n" 529 | } 530 | ] 531 | }, 532 | { 533 | "name": "Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)", 534 | "category": "Domain Escalation", 535 | "queryList": [ 536 | { 537 | "final": true, 538 | "query": "MATCH p=allShortestPaths((g {owned:true})-[*1..]->(n:GPO)) WHERE g<>n and n.type = 'Certificate Template' and n.`Enrollee Supplies Subject` = true and n.`Client Authentication` = true and n.`Enabled` = true return p" 539 | } 540 | ] 541 | }, 542 | { 543 | "name": "Find Misconfigured Certificate Templates (ESC2)", 544 | "category": "Domain Escalation", 545 | "queryList": [ 546 | { 547 | "final": true, 548 | "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' and n.`Enabled` = true and (n.`Extended Key Usage` = [] or 'Any Purpose' IN n.`Extended Key Usage`) RETURN n" 549 | } 550 | ] 551 | }, 552 | { 553 | "name": "Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC2)", 554 | "category": "Domain Escalation", 555 | "queryList": [ 556 | { 557 | "final": true, 558 | "query": "MATCH p=allShortestPaths((g {owned:true})-[*1..]->(n:GPO)) WHERE g<>n and n.type = 'Certificate Template' and n.`Enabled` = true and (n.`Extended Key Usage` = [] or 'Any Purpose' IN n.`Extended Key Usage`) return p" 559 | } 560 | ] 561 | }, 562 | { 563 | "name": "Find Enrollment Agent Templates (ESC3)", 564 | "category": "Domain Escalation", 565 | "queryList": [ 566 | { 567 | "final": true, 568 | "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' and n.`Enabled` = true and (n.`Extended Key Usage` = [] or 'Any Purpose' IN n.`Extended Key Usage` or 'Certificate Request Agent' IN n.`Extended Key Usage`) RETURN n" 569 | } 570 | ] 571 | }, 572 | { 573 | "name": "Shortest Paths to Enrollment Agent Templates from Owned Principals (ESC3)", 574 | "category": "Domain Escalation", 575 | "queryList": [ 576 | { 577 | "final": true, 578 | "query": "MATCH p=allShortestPaths((g {owned:true})-[*1..]->(n:GPO)) WHERE g<>n and n.type = 'Certificate Template' and n.`Enabled` = true and (n.`Extended Key Usage` = [] or 'Any Purpose' IN n.`Extended Key Usage` or 'Certificate Request Agent' IN n.`Extended Key Usage`) return p" 579 | } 580 | ] 581 | }, 582 | { 583 | "name": "Shortest Paths to Vulnerable Certificate Template Access Control (ESC4)", 584 | "category": "Domain Escalation", 585 | "queryList": [ 586 | { 587 | "final": true, 588 | "query": "MATCH p=shortestPath((g)-[:GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner*1..]->(n:GPO)) WHERE g<>n and n.type = 'Certificate Template' and n.`Enabled` = true RETURN p" 589 | } 590 | ] 591 | }, 592 | { 593 | "name": "Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)", 594 | "category": "Domain Escalation", 595 | "queryList": [ 596 | { 597 | "final": true, 598 | "query": "MATCH p=allShortestPaths((g {owned:true})-[r*1..]->(n:GPO)) WHERE g<>n and n.type = 'Certificate Template' and n.Enabled = true and NONE(x in relationships(p) WHERE type(x) = 'Enroll' or type(x) = 'AutoEnroll') return p" 599 | } 600 | ] 601 | }, 602 | { 603 | "name": "Find Certificate Authorities with User Specified SAN (ESC6)", 604 | "category": "Domain Escalation", 605 | "queryList": [ 606 | { 607 | "final": true, 608 | "query": "MATCH (n:GPO) WHERE n.type = 'Enrollment Service' and n.`User Specified SAN` = 'Enabled' RETURN n" 609 | } 610 | ] 611 | }, 612 | { 613 | "name": "Shortest Paths to Vulnerable Certificate Authority Access Control (ESC7)", 614 | "category": "Domain Escalation", 615 | "queryList": [ 616 | { 617 | "final": true, 618 | "query": "MATCH p=shortestPath((g)-[r:GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ManageCa|ManageCertificates*1..]->(n:GPO)) WHERE g<>n and n.type = 'Enrollment Service' RETURN p" 619 | } 620 | ] 621 | }, 622 | { 623 | "name": "Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)", 624 | "category": "Domain Escalation", 625 | "queryList": [ 626 | { 627 | "final": true, 628 | "query": "MATCH p=allShortestPaths((g {owned:true})-[*1..]->(n:GPO)) WHERE g<>n and n.type = 'Enrollment Service' and NONE(x in relationships(p) WHERE type(x) = 'Enroll' or type(x) = 'AutoEnroll') RETURN p" 629 | } 630 | ] 631 | }, 632 | { 633 | "name": "Find Certificate Authorities with HTTP Web Enrollment (ESC8)", 634 | "category": "Domain Escalation", 635 | "queryList": [ 636 | { 637 | "final": true, 638 | "query": "MATCH (n:GPO) WHERE n.type = 'Enrollment Service' and n.`Web Enrollment` = 'Enabled' RETURN n" 639 | } 640 | ] 641 | }, 642 | { 643 | "name": "Find Unsecured Certificate Templates (ESC9)", 644 | "category": "Domain Escalation", 645 | "queryList": [ 646 | { 647 | "final": true, 648 | "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' and n.`Enrollee Supplies Subject` = true and n.`Client Authentication` = true and n.`Enabled` = true RETURN n" 649 | } 650 | ] 651 | }, 652 | { 653 | "name": "Find Unsecured Certificate Templates (ESC9)", 654 | "category": "PKI", 655 | "queryList": [ 656 | { 657 | "final": true, 658 | "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' and 'NoSecurityExtension' in n.`Enrollment Flag` and n.`Enabled` = true RETURN n" 659 | } 660 | ] 661 | }, 662 | { 663 | "name": "Shortest Paths to Unsecured Certificate Templates from Owned Principals (ESC9)", 664 | "category": "PKI", 665 | "queryList": [ 666 | { 667 | "final": true, 668 | "query": "MATCH p=allShortestPaths((g {owned:true})-[r*1..]->(n:GPO)) WHERE n.type = 'Certificate Template' and g<>n and 'NoSecurityExtension' in n.`Enrollment Flag` and n.`Enabled` = true and NONE(rel in r WHERE type(rel) in ['EnabledBy','Read','ManageCa','ManageCertificates']) return p" 669 | } 670 | ] 671 | } 672 | ] 673 | } 674 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | //! Parsing arguments 2 | #[cfg(not(feature = "noargs"))] 3 | use clap::{Arg, ArgAction, value_parser, Command}; 4 | 5 | #[cfg(feature = "noargs")] 6 | use winreg::{RegKey,{enums::*}}; 7 | #[cfg(feature = "noargs")] 8 | use crate::exec::run; 9 | #[cfg(feature = "noargs")] 10 | use regex::Regex; 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct Options { 14 | pub domain: String, 15 | pub username: String, 16 | pub password: String, 17 | pub ldapfqdn: String, 18 | pub ip: String, 19 | pub port: String, 20 | pub name_server: String, 21 | pub path: String, 22 | pub ldaps: bool, 23 | pub dns_tcp: bool, 24 | pub fqdn_resolver: bool, 25 | pub adcs: bool, 26 | pub old_bloodhound: bool, 27 | pub dc_only: bool, 28 | pub kerberos: bool, 29 | pub zip: bool, 30 | pub verbose: log::LevelFilter, 31 | } 32 | 33 | #[cfg(not(feature = "noargs"))] 34 | fn cli() -> Command { 35 | Command::new("rusthound") 36 | .version("1.1.69") 37 | .about("Active Directory data collector for BloodHound.\ng0h4n ") 38 | .arg(Arg::new("v") 39 | .short('v') 40 | .help("Set the level of verbosity") 41 | .action(ArgAction::Count), 42 | ) 43 | .next_help_heading("REQUIRED VALUES") 44 | .arg(Arg::new("domain") 45 | .short('d') 46 | .long("domain") 47 | .help("Domain name like: DOMAIN.LOCAL") 48 | .required(true) 49 | .value_parser(value_parser!(String)) 50 | ) 51 | .next_help_heading("OPTIONAL VALUES") 52 | .arg(Arg::new("ldapusername") 53 | .short('u') 54 | .long("ldapusername") 55 | .help("LDAP username, like: user@domain.local") 56 | .required(false) 57 | .value_parser(value_parser!(String)) 58 | ) 59 | .arg(Arg::new("ldappassword") 60 | .short('p') 61 | .long("ldappassword") 62 | .help("LDAP password") 63 | .required(false) 64 | .value_parser(value_parser!(String)) 65 | ) 66 | .arg(Arg::new("ldapfqdn") 67 | .short('f') 68 | .long("ldapfqdn") 69 | .help("Domain Controler FQDN like: DC01.DOMAIN.LOCAL or just DC01") 70 | .required(false) 71 | .value_parser(value_parser!(String)) 72 | ) 73 | .arg(Arg::new("ldapip") 74 | .short('i') 75 | .long("ldapip") 76 | .help("Domain Controller IP address like: 192.168.1.10") 77 | .required(false) 78 | .value_parser(value_parser!(String)) 79 | ) 80 | .arg(Arg::new("ldapport") 81 | .short('P') 82 | .long("ldapport") 83 | .help("LDAP port [default: 389]") 84 | .required(false) 85 | .value_parser(value_parser!(String)) 86 | ) 87 | .arg(Arg::new("name-server") 88 | .short('n') 89 | .long("name-server") 90 | .help("Alternative IP address name server to use for DNS queries") 91 | .required(false) 92 | .value_parser(value_parser!(String)) 93 | ) 94 | .arg(Arg::new("output") 95 | .short('o') 96 | .long("output") 97 | .help("Output directory where you would like to save JSON files [default: ./]") 98 | .required(false) 99 | .value_parser(value_parser!(String)) 100 | ) 101 | .next_help_heading("OPTIONAL FLAGS") 102 | .arg(Arg::new("ldaps") 103 | .long("ldaps") 104 | .help("Force LDAPS using for request like: ldaps://DOMAIN.LOCAL/") 105 | .required(false) 106 | .action(ArgAction::SetTrue) 107 | .global(false) 108 | ) 109 | .arg(Arg::new("kerberos") 110 | .short('k') 111 | .long("kerberos") 112 | .help("Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters for Linux.") 113 | .required(false) 114 | .action(ArgAction::SetTrue) 115 | .global(false) 116 | ) 117 | .arg(Arg::new("dns-tcp") 118 | .long("dns-tcp") 119 | .help("Use TCP instead of UDP for DNS queries") 120 | .required(false) 121 | .action(ArgAction::SetTrue) 122 | .global(false) 123 | ) 124 | .arg(Arg::new("dc-only") 125 | .long("dc-only") 126 | .help("Collects data only from the domain controller. Will not try to retrieve CA security/configuration or check for Web Enrollment") 127 | .required(false) 128 | .action(ArgAction::SetTrue) 129 | .global(false) 130 | ) 131 | .arg(Arg::new("old-bloodhound") 132 | .long("old-bloodhound") 133 | .help("For ADCS only. Output result as BloodHound data for the original BloodHound version from @BloodHoundAD without PKI support") 134 | .required(false) 135 | .action(ArgAction::SetTrue) 136 | .global(false) 137 | ) 138 | .arg(Arg::new("zip") 139 | .long("zip") 140 | .short('z') 141 | .help("Compress the JSON files into a zip archive") 142 | .required(false) 143 | .action(ArgAction::SetTrue) 144 | .global(false) 145 | ) 146 | .next_help_heading("OPTIONAL MODULES") 147 | .arg(Arg::new("fqdn-resolver") 148 | .long("fqdn-resolver") 149 | .help("Use fqdn-resolver module to get computers IP address") 150 | .required(false) 151 | .action(ArgAction::SetTrue) 152 | .global(false) 153 | ) 154 | .arg(Arg::new("adcs") 155 | .long("adcs") 156 | .help("Use ADCS module to enumerate Certificate Templates, Certificate Authorities and other configurations.\n(For the custom-built BloodHound version from @ly4k with PKI support)") 157 | .required(false) 158 | .action(ArgAction::SetTrue) 159 | .global(false) 160 | ) 161 | } 162 | 163 | #[cfg(not(feature = "noargs"))] 164 | /// Function to extract all argument and put it in 'Options' structure. 165 | pub fn extract_args() -> Options { 166 | 167 | // Get arguments 168 | let matches = cli().get_matches(); 169 | 170 | // Now get values 171 | let d = matches.get_one::("domain").map(|s| s.as_str()).unwrap(); 172 | let u = matches.get_one::("ldapusername").map(|s| s.as_str()).unwrap_or("not set"); 173 | let p = matches.get_one::("ldappassword").map(|s| s.as_str()).unwrap_or("not set"); 174 | let f = matches.get_one::("ldapfqdn").map(|s| s.as_str()).unwrap_or("not set"); 175 | let ip = matches.get_one::("ldapip").map(|s| s.as_str()).unwrap_or("not set"); 176 | let port = matches.get_one::("ldapport").map(|s| s.as_str()).unwrap_or("not set"); 177 | let n = matches.get_one::("name-server").map(|s| s.as_str()).unwrap_or("not set"); 178 | let path = matches.get_one::("output").map(|s| s.as_str()).unwrap_or("./"); 179 | let ldaps = matches.get_one::("ldaps").map(|s| s.to_owned()).unwrap_or(false); 180 | let dns_tcp = matches.get_one::("dns-tcp").map(|s| s.to_owned()).unwrap_or(false); 181 | let dc_only = matches.get_one::("dc-only").map(|s| s.to_owned()).unwrap_or(false); 182 | let old_bh = matches.get_one::("old-bloodhound").map(|s| s.to_owned()).unwrap_or(false); 183 | let z = matches.get_one::("zip").map(|s| s.to_owned()).unwrap_or(false); 184 | let fqdn_resolver = matches.get_one::("fqdn-resolver").map(|s| s.to_owned()).unwrap_or(false); 185 | let adcs = matches.get_one::("adcs").map(|s| s.to_owned()).unwrap_or(false); 186 | let kerberos = matches.get_one::("kerberos").map(|s| s.to_owned()).unwrap_or(false); 187 | let v = match matches.get_count("v") { 188 | 0 => log::LevelFilter::Info, 189 | 1 => log::LevelFilter::Debug, 190 | _ => log::LevelFilter::Trace, 191 | }; 192 | 193 | // Return all 194 | Options { 195 | domain: d.to_string(), 196 | username: u.to_string(), 197 | password: p.to_string(), 198 | ldapfqdn: f.to_string(), 199 | ip: ip.to_string(), 200 | port: port.to_string(), 201 | name_server: n.to_string(), 202 | path: path.to_string(), 203 | ldaps: ldaps, 204 | dns_tcp: dns_tcp, 205 | dc_only: dc_only, 206 | old_bloodhound: old_bh, 207 | fqdn_resolver: fqdn_resolver, 208 | adcs: adcs, 209 | kerberos: kerberos, 210 | zip: z, 211 | verbose: v, 212 | } 213 | } 214 | 215 | #[cfg(feature = "noargs")] 216 | /// Function to automatically get all informations needed and put it in 'Options' structure. 217 | pub fn auto_args() -> Options { 218 | 219 | // Request registry key to get informations 220 | let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); 221 | let cur_ver = hklm.open_subkey("SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters").unwrap(); 222 | //Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Domain 223 | let domain: String = match cur_ver.get_value("Domain") { 224 | Ok(domain) => domain, 225 | Err(err) => { 226 | panic!("Error: {:?}",err); 227 | } 228 | }; 229 | 230 | // Get LDAP fqdn 231 | let _fqdn: String = run(&format!("nslookup -query=srv _ldap._tcp.{}",&domain)); 232 | let re = Regex::new(r"hostname.*= (?[0-9a-zA-Z]{1,})").unwrap(); 233 | let mut values = re.captures_iter(&_fqdn); 234 | let caps = values.next().unwrap(); 235 | let fqdn = caps["ldap_fqdn"].to_string(); 236 | 237 | // Get LDAP port 238 | let re = Regex::new(r"port.*= (?[0-9]{3,})").unwrap(); 239 | let mut values = re.captures_iter(&_fqdn); 240 | let caps = values.next().unwrap(); 241 | let port = caps["ldap_port"].to_string(); 242 | let mut ldaps: bool = false; 243 | if port == "636" { 244 | ldaps = true; 245 | } 246 | 247 | // Return all 248 | Options { 249 | domain: domain.to_string(), 250 | username: "not set".to_string(), 251 | password: "not set".to_string(), 252 | ldapfqdn: fqdn.to_string(), 253 | ip: "not set".to_string(), 254 | port: port.to_string(), 255 | name_server: "127.0.0.1".to_string(), 256 | path: "./output".to_string(), 257 | ldaps: ldaps, 258 | dns_tcp: false, 259 | dc_only: false, 260 | old_bloodhound: false, 261 | fqdn_resolver: false, 262 | adcs: true, 263 | kerberos: true, 264 | zip: true, 265 | verbose: log::LevelFilter::Info, 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/banner.rs: -------------------------------------------------------------------------------- 1 | //! Launch and end banners 2 | use colored::*; 3 | use crate::enums::date::{return_current_date,return_current_time}; 4 | use indicatif::{ProgressBar, ProgressStyle}; 5 | 6 | /// Banner when RustHound start. 7 | pub fn print_banner() { 8 | // https://docs.rs/colored/2.0.0/x86_64-pc-windows-msvc/colored/control/fn.set_virtual_terminal.html 9 | #[cfg(windows)] 10 | control::set_virtual_terminal(true).unwrap(); 11 | 12 | // Banner for RustHound 13 | println!("{}","---------------------------------------------------".clear().bold()); 14 | println!("Initializing {} at {} on {}", 15 | "RustHound".truecolor(247,76,0,), 16 | return_current_time(), 17 | return_current_date() 18 | ); 19 | println!("Powered by g0h4n from {}","OpenCyber".truecolor(97,221,179)); 20 | println!("{}\n","---------------------------------------------------".clear().bold()); 21 | } 22 | 23 | /// Banner when RustHound finish. 24 | pub fn print_end_banner() { 25 | // End banner for RustHound 26 | println!("\n{} Enumeration Completed at {} on {}! Happy Graphing!\n", 27 | "RustHound".truecolor(247,76,0,), 28 | return_current_time(), 29 | return_current_date() 30 | ); 31 | } 32 | 33 | /// Progress Bar used in RustHound. 34 | pub fn progress_bar( 35 | pb: ProgressBar, 36 | message: String, 37 | count: u64, 38 | end_message: String, 39 | ) { 40 | pb.set_style(ProgressStyle::with_template("{prefix:.bold.dim}{spinner} {wide_msg}") 41 | .unwrap() 42 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")); 43 | pb.inc(count); 44 | pb.with_message(format!("{}: {}{}",message,count,end_message)); 45 | } -------------------------------------------------------------------------------- /src/enums/constants.rs: -------------------------------------------------------------------------------- 1 | pub const ACCESS_ALLOWED_ACE_TYPE: u8 = 0x00; 2 | pub const ACCESS_DENIED_ACE_TYPE: u8 = 0x01; 3 | pub const ACCESS_ALLOWED_OBJECT_ACE_TYPE: u8 = 0x05; 4 | pub const ACCESS_DENIED_OBJECT_ACE_TYPE: u8 = 0x06; 5 | 6 | pub const CONTAINER_INHERIT_ACE: u8 = 0x01; 7 | pub const FAILED_ACCESS_ACE_FLAG: u8 = 0x80; 8 | pub const INHERIT_ONLY_ACE: u8 = 0x08; 9 | pub const INHERITED_ACE: u8 = 0x10; 10 | pub const NO_PROPAGATE_INHERIT_ACE: u8 = 0x04; 11 | pub const OBJECT_INHERIT_ACE: u8 = 0x01; 12 | pub const SUCCESSFUL_ACCESS_ACE_FLAG: u8 = 0x04; 13 | 14 | pub const ACE_OBJECT_TYPE_PRESENT: u32 = 0x0001; 15 | pub const ACE_INHERITED_OBJECT_TYPE_PRESENT: u32 = 0x0002; 16 | 17 | // EXTRIGHTS_GUID_MAPPING 18 | pub const GET_CHANGES: &str = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"; 19 | pub const GET_CHANGES_ALL: &str = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"; 20 | pub const GET_CHANGES_IN_FILTERED_SET: &str = "89e95b76-444d-4c62-991a-0facbeda640c"; 21 | pub const WRITE_MEMBER: &str = "bf9679c0-0de6-11d0-a285-00aa003049e2"; 22 | pub const USER_FORCE_CHANGE_PASSWORD: &str = "00299570-246d-11d0-a768-00aa006e0529"; 23 | pub const ALLOWED_TO_ACT: &str = "3f78c3e5-f79a-46bd-a0b8-9d18116ddc79"; 24 | pub const USER_ACCOUNT_RESTRICTIONS_SET: &str = "4c164200-20c0-11d0-a768-00aa006e0529"; 25 | // ADCS 26 | pub const ENROLL: &str = "0e10c968-78fb-11d2-90d4-00c04f79dc55"; 27 | pub const AUTO_ENROLL: &str = "a05b8cc2-17bc-4802-a710-e7c15ab866a2"; -------------------------------------------------------------------------------- /src/enums/date.rs: -------------------------------------------------------------------------------- 1 | use chrono::{NaiveDateTime, Local}; 2 | //use log::trace; 3 | 4 | /// Change date timestamp format to epoch format. 5 | pub fn convert_timestamp(timestamp: i64) -> i64 6 | { 7 | let offset: i64 = 134774*24*60*60; 8 | let epoch: i64 = timestamp/10000000-offset; 9 | return epoch 10 | } 11 | 12 | pub fn string_to_epoch(date: &String) -> i64 { 13 | // yyyyMMddHHmmss.0z to epoch format 14 | let split = date.split("."); 15 | let vec = split.collect::>(); 16 | let date = NaiveDateTime::parse_from_str(&vec[0],"%Y%m%d%H%M%S").unwrap(); 17 | //trace!("whencreated timestamp: {:?}", date.timestamp()); 18 | return date.timestamp() 19 | } 20 | 21 | /// Function to return current hours. 22 | pub fn return_current_time() -> String 23 | { 24 | let now = Local::now(); 25 | return now.format("%T").to_string() 26 | } 27 | 28 | /// Function to return current date. 29 | pub fn return_current_date() -> String 30 | { 31 | let now = Local::now(); 32 | return now.format("%D").to_string() 33 | } 34 | 35 | /// Function to return current date. 36 | pub fn return_current_fulldate() -> String 37 | { 38 | let now = Local::now(); 39 | return now.format("%Y%m%d%H%M%S").to_string() 40 | } -------------------------------------------------------------------------------- /src/enums/forestlevel.rs: -------------------------------------------------------------------------------- 1 | /// Get the forest level from "msDS-Behavior-Version" LDAP attribut. 2 | pub fn get_forest_level(level: String) -> String 3 | { 4 | match level.as_str() { 5 | "7" => { return "2016".to_string(); }, 6 | "6" => { return "2012 R2".to_string(); }, 7 | "5" => { return "2012".to_string(); }, 8 | "4" => { return "2008 R2".to_string(); }, 9 | "3" => { return "2008".to_string(); }, 10 | "2" => { return "2003".to_string(); }, 11 | "1" => { return "2003 Interim".to_string(); }, 12 | "0" => { return "2000 Mixed/Native".to_string(); }, 13 | _ => { return "Unknown".to_string(); }, 14 | } 15 | } -------------------------------------------------------------------------------- /src/enums/gplink.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use crate::json::templates::bh_41::prepare_gplink_json_template; 3 | 4 | /// Function to parse gplink and push it in json format 5 | pub fn parse_gplink(all_link: String) -> Vec 6 | { 7 | let mut gplinks: Vec = Vec::new(); 8 | 9 | let re = Regex::new(r"[a-zA-Z0-9-]{36}").unwrap(); 10 | let mut cpaths: Vec = Vec::new(); 11 | for cpath in re.captures_iter(&all_link) 12 | { 13 | cpaths.push(cpath[0].to_owned().to_string()); 14 | } 15 | 16 | let re2 = Regex::new(r"[;][0-4]{1}").unwrap(); 17 | let mut status: Vec = Vec::new(); 18 | for enforced in re2.captures_iter(&all_link){ 19 | status.push(enforced[0].to_owned().to_string()); 20 | } 21 | 22 | for i in 0..cpaths.len() 23 | { 24 | let mut gplink = prepare_gplink_json_template(); 25 | gplink["GUID"] = cpaths[i].to_string().into(); 26 | 27 | // Thanks to: https://techibee.com/group-policies/find-link-status-and-enforcement-status-of-group-policies-using-powershell/2424 28 | if status[i].to_string().contains(";2"){ 29 | gplink["IsEnforced"] = true.into(); 30 | } 31 | if status[i].to_string().contains(";3"){ 32 | gplink["IsEnforced"] = true.into(); 33 | } 34 | 35 | //trace!("gpo link: {:?}",cpaths[i]); 36 | gplinks.push(gplink); 37 | } 38 | 39 | return gplinks 40 | } -------------------------------------------------------------------------------- /src/enums/ldaptype.rs: -------------------------------------------------------------------------------- 1 | use ldap3::SearchEntry; 2 | use std::collections::HashMap; 3 | //use log::trace; 4 | 5 | /// Enum to get ldap object type. 6 | pub enum Type { 7 | User, 8 | Computer, 9 | Group, 10 | Ou, 11 | Domain, 12 | Gpo, 13 | ForeignSecurityPrincipal, 14 | Container, 15 | Trust, 16 | AdcsAuthority, 17 | AdcsTemplate, 18 | Unknown 19 | } 20 | 21 | /// Get object type, like ("user","group","computer","ou", "container", "gpo", "domain" "trust"). 22 | pub fn get_type(result: SearchEntry) -> std::result::Result 23 | { 24 | let result_attrs: HashMap>; 25 | result_attrs = result.attrs; 26 | 27 | //trace!("{:?}",&result_attrs); 28 | 29 | // For all entries I checked if is an user,group,computer,ou,domain 30 | for (key, value) in &result_attrs 31 | { 32 | // Type is user 33 | if key == "objectClass" && value.contains(&String::from("person")) && value.contains(&String::from("user")) && !value.contains(&String::from("computer")) && !value.contains(&String::from("group")) 34 | { 35 | return Ok(Type::User) 36 | } 37 | // Type is user if is service-account 38 | if key == "objectClass" && value.contains(&String::from("msDS-GroupManagedServiceAccount")) 39 | { 40 | return Ok(Type::User) 41 | } 42 | // Type is group 43 | if key == "objectClass" && value.contains(&String::from("group")) 44 | { 45 | return Ok(Type::Group) 46 | } 47 | // Type is computer 48 | if key == "objectClass" && value.contains(&String::from("computer")) 49 | { 50 | return Ok(Type::Computer) 51 | } 52 | // Type is ou 53 | if key == "objectClass" && value.contains(&String::from("organizationalUnit")) 54 | { 55 | return Ok(Type::Ou) 56 | } 57 | // Type is domain 58 | if key == "objectClass" && value.contains(&String::from("domain")) 59 | { 60 | return Ok(Type::Domain) 61 | } 62 | // Type is Gpo 63 | if key == "objectClass" && value.contains(&String::from("groupPolicyContainer")) 64 | { 65 | return Ok(Type::Gpo) 66 | } 67 | // Type is foreignSecurityPrincipal 68 | if key == "objectClass" && value.contains(&String::from("top")) && value.contains(&String::from("foreignSecurityPrincipal")) 69 | { 70 | return Ok(Type::ForeignSecurityPrincipal) 71 | } 72 | // Type is Container 73 | if key == "objectClass" && (value.contains(&String::from("top")) && value.contains(&String::from("container"))) && !value.contains(&String::from("groupPolicyContainer")) 74 | { 75 | return Ok(Type::Container) 76 | } 77 | // Type is Trust domain 78 | if key == "objectClass" && value.contains(&String::from("trustedDomain")) 79 | { 80 | return Ok(Type::Trust) 81 | } 82 | // Type is ADCS Certificate Authority 83 | if key == "objectClass" && value.contains(&String::from("pKIEnrollmentService")) 84 | { 85 | return Ok(Type::AdcsAuthority) 86 | } 87 | // Type is ADCS Certificate Template 88 | if key == "objectClass" && value.contains(&String::from("pKICertificateTemplate")) 89 | { 90 | return Ok(Type::AdcsTemplate) 91 | } 92 | } 93 | return Err(Type::Unknown) 94 | } -------------------------------------------------------------------------------- /src/enums/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utils to extract data from ldap network packets 2 | #[doc(inline)] 3 | pub use uacflags::*; 4 | #[doc(inline)] 5 | pub use ldaptype::*; 6 | #[doc(inline)] 7 | pub use sid::*; 8 | #[doc(inline)] 9 | pub use forestlevel::*; 10 | #[doc(inline)] 11 | pub use acl::*; 12 | #[doc(inline)] 13 | pub use secdesc::*; 14 | #[doc(inline)] 15 | pub use spntasks::*; 16 | #[doc(inline)] 17 | pub use gplink::*; 18 | 19 | pub mod uacflags; 20 | pub mod ldaptype; 21 | pub mod date; 22 | pub mod sid; 23 | pub mod forestlevel; 24 | pub mod acl; 25 | pub mod secdesc; 26 | pub mod spntasks; 27 | pub mod gplink; 28 | pub mod constants; 29 | pub mod trusts; -------------------------------------------------------------------------------- /src/enums/sid.rs: -------------------------------------------------------------------------------- 1 | use crate::enums::secdesc::LdapSid; 2 | use log::{trace,error}; 3 | 4 | /// Function to make SID String from ldap_sid struct 5 | pub fn sid_maker(sid: LdapSid, domain: &String) -> String { 6 | let mut sub: String = "".to_owned(); 7 | trace!("sid_maker before: {:?}",&sid); 8 | for v in &sid.sub_authority { 9 | sub.push_str(&"-".to_owned()); 10 | sub.push_str(&v.to_string()); 11 | } 12 | 13 | let mut result: String = "S-".to_owned(); 14 | result.push_str(&sid.revision.to_string()); 15 | result.push_str(&"-"); 16 | result.push_str(&sid.identifier_authority.value[5].to_string()); 17 | result.push_str(&sub); 18 | 19 | let mut final_sid: String = "".to_owned(); 20 | if result.len() <= 16 { 21 | final_sid.push_str(&domain.to_uppercase()); 22 | final_sid.push_str(&"-".to_owned()); 23 | final_sid.push_str(&result.to_owned()); 24 | } else { 25 | final_sid = result; 26 | } 27 | trace!("sid_maker value: {}",final_sid); 28 | if final_sid.contains("S-0-0"){ 29 | error!("SID contains null bytes!\n[INPUT: {:?}]\n[OUTPUT: {}]", &sid, final_sid); 30 | } 31 | return final_sid; 32 | } 33 | 34 | /// Change SID value to correct format. 35 | pub fn objectsid_to_vec8(sid: &String) -> Vec 36 | { 37 | // \u{1} to vec parsable 38 | let mut vec_sid: Vec = Vec::new(); 39 | for value in sid.as_bytes() { 40 | vec_sid.push(*value); 41 | } 42 | return vec_sid 43 | } 44 | 45 | /// Function to decode objectGUID binary to string value. 46 | /// src: 47 | /// Thanks to: 48 | pub fn decode_guid(raw_guid: &Vec) -> String 49 | { 50 | // A byte-based String representation in the form of \[0]\[1]\[2]\[3]\[4]\[5]\[6]\[7]\[8]\[9]\[10]\[11]\[12]\[13]\[14]\[15] 51 | // A string representing the decoded value in the form of [3][2][1][0]-[5][4]-[7][6]-[8][9]-[10][11][12][13][14][15]. 52 | let mut str_guid: String = "".to_owned(); 53 | 54 | let mut part1 = vec![]; 55 | part1.push(raw_guid[3] & 0xFF); 56 | part1.push(raw_guid[2] & 0xFF); 57 | part1.push(raw_guid[1] & 0xFF); 58 | part1.push(raw_guid[0] & 0xFF); 59 | str_guid.push_str(&hex_push(&part1)); 60 | 61 | str_guid.push_str("-"); 62 | 63 | let mut part2 = vec![]; 64 | part2.push(raw_guid[5] & 0xFF); 65 | part2.push(raw_guid[4] & 0xFF); 66 | str_guid.push_str(&hex_push(&part2)); 67 | 68 | str_guid.push_str("-"); 69 | 70 | let mut part3 = vec![]; 71 | part3.push(raw_guid[7] & 0xFF); 72 | part3.push(raw_guid[6] & 0xFF); 73 | str_guid.push_str(&hex_push(&part3)); 74 | 75 | str_guid.push_str("-"); 76 | 77 | let mut part4 = vec![]; 78 | part4.push(raw_guid[8] & 0xFF); 79 | part4.push(raw_guid[9] & 0xFF); 80 | str_guid.push_str(&hex_push(&part4)); 81 | 82 | str_guid.push_str("-"); 83 | 84 | let mut part5 = vec![]; 85 | part5.push(raw_guid[10] & 0xFF); 86 | part5.push(raw_guid[11] & 0xFF); 87 | part5.push(raw_guid[12] & 0xFF); 88 | part5.push(raw_guid[13] & 0xFF); 89 | part5.push(raw_guid[14] & 0xFF); 90 | part5.push(raw_guid[15] & 0xFF); 91 | str_guid.push_str(&hex_push(&part5)); 92 | 93 | return str_guid 94 | } 95 | 96 | /// Function to get a hexadecimal representation from bytes 97 | /// Thanks to: 98 | pub fn hex_push(blob: &[u8]) -> String { 99 | let mut buf: String = "".to_owned(); 100 | for ch in blob { 101 | fn hex_from_digit(num: u8) -> char { 102 | if num < 10 { 103 | (b'0' + num) as char 104 | } else { 105 | (b'A' + num - 10) as char 106 | } 107 | } 108 | buf.push(hex_from_digit(ch / 16)); 109 | buf.push(hex_from_digit(ch % 16)); 110 | } 111 | return buf; 112 | } 113 | 114 | 115 | /// Function to get uuid from bin to string format 116 | pub fn bin_to_string(raw_guid: &Vec) -> String 117 | { 118 | // before: e2 49 30 00 aa 00 85 a2 11 d0 0d e6 bf 96 7a ba 119 | // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 120 | // after: bf 96 7a ba - 0d e6 - 11 d0 - a2 85 - 00 aa 00 30 49 e2 121 | // 12 13 14 15 10 11 8 9 7 6 5 4 3 2 1 0 122 | 123 | let mut str_guid: String = "".to_owned(); 124 | 125 | let mut part1 = vec![]; 126 | part1.push(raw_guid[12] & 0xFF); 127 | part1.push(raw_guid[13] & 0xFF); 128 | part1.push(raw_guid[14] & 0xFF); 129 | part1.push(raw_guid[15] & 0xFF); 130 | str_guid.push_str(&hex_push(&part1)); 131 | 132 | str_guid.push_str("-"); 133 | 134 | let mut part2 = vec![]; 135 | part2.push(raw_guid[10] & 0xFF); 136 | part2.push(raw_guid[11] & 0xFF); 137 | str_guid.push_str(&hex_push(&part2)); 138 | 139 | str_guid.push_str("-"); 140 | 141 | let mut part3 = vec![]; 142 | part3.push(raw_guid[8] & 0xFF); 143 | part3.push(raw_guid[9] & 0xFF); 144 | str_guid.push_str(&hex_push(&part3)); 145 | 146 | str_guid.push_str("-"); 147 | 148 | let mut part4 = vec![]; 149 | part4.push(raw_guid[7] & 0xFF); 150 | part4.push(raw_guid[6] & 0xFF); 151 | str_guid.push_str(&hex_push(&part4)); 152 | 153 | str_guid.push_str("-"); 154 | 155 | let mut part5 = vec![]; 156 | part5.push(raw_guid[5] & 0xFF); 157 | part5.push(raw_guid[4] & 0xFF); 158 | part5.push(raw_guid[3] & 0xFF); 159 | part5.push(raw_guid[2] & 0xFF); 160 | part5.push(raw_guid[1] & 0xFF); 161 | part5.push(raw_guid[0] & 0xFF); 162 | str_guid.push_str(&hex_push(&part5)); 163 | 164 | return str_guid 165 | } 166 | 167 | /* Another way to decode objectSID binary to string value. 168 | // src: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/f992ad60-0fe4-4b87-9fed-beb478836861 169 | pub fn decode_sid(raw_sid: &Vec, domain: &String) -> String 170 | { 171 | let mut str_sid: String = "".to_owned(); 172 | if raw_sid.len() <= 16 { 173 | str_sid.push_str(&domain.to_uppercase()); 174 | str_sid.push_str(&"-S-".to_owned()); 175 | } 176 | else 177 | { 178 | str_sid.push_str(&"S-".to_owned()); 179 | } 180 | 181 | // get byte(0) - revision level 182 | let revision = String::from(raw_sid[0].to_string()); 183 | str_sid.push_str(&revision); 184 | 185 | //next byte byte(1) - count of sub-authorities 186 | let count_sub_auths = usize::from(raw_sid[1]) & 0xFF; 187 | 188 | //byte(2-7) - 48 bit authority ([Big-Endian]) 189 | let mut authority = 0; 190 | //String rid = ""; 191 | for i in 2..=7 192 | { 193 | authority = (usize::from(raw_sid[i])) << (8 * (5 - (i - 2))); 194 | } 195 | str_sid.push_str(&"-".to_owned()); 196 | str_sid.push_str(&authority.to_string()); 197 | 198 | //iterate all the sub-auths and then countSubAuths x 32 bit sub authorities ([Little-Endian]) 199 | let mut offset = 8; 200 | let size = 4; //4 bytes for each sub auth 201 | 202 | for _j in 0..count_sub_auths 203 | { 204 | let mut sub_authority = 0; 205 | for k in 0..size 206 | { 207 | sub_authority |= (usize::from(raw_sid[offset + k] & 0xFF)) << (8 * k); 208 | } 209 | // format it 210 | str_sid.push_str(&"-".to_owned()); 211 | str_sid.push_str(&sub_authority.to_string()); 212 | offset += size; 213 | } 214 | 215 | return str_sid 216 | } 217 | */ 218 | -------------------------------------------------------------------------------- /src/enums/spntasks.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use crate::json::templates::bh_41::prepare_mssqlsvc_spn_json_template; 3 | //use log::trace; 4 | 5 | /// Function to check if spns start with mssqlsvc to make SPNTargets 6 | /// 7 | pub fn check_spn(serviceprincipalname: &String) -> serde_json::value::Value 8 | { 9 | let mut mssqlsvc_spn = prepare_mssqlsvc_spn_json_template(); 10 | if serviceprincipalname.to_lowercase().contains("mssqlsvc") 11 | { 12 | //trace!("{:?}",serviceprincipalname); 13 | if serviceprincipalname.to_lowercase().contains(":") 14 | { 15 | let split = serviceprincipalname.split(":"); 16 | let vec = split.collect::>(); 17 | let mut fqdn = vec[0].to_owned(); 18 | let value = vec[1].to_owned(); 19 | 20 | //trace!("{:?}",value); 21 | let port = value.parse::().unwrap_or(1433); 22 | 23 | // I temporarily add the fqdn which will be replaced by the SID at the end of the parsing. 24 | // This avoids making a new request to the LDAP server and parsing off-line. 25 | let split = fqdn.split("/"); 26 | let vec = split.collect::>(); 27 | fqdn = vec[1].to_owned().to_uppercase(); 28 | 29 | //trace!("{:?}",fqdn); 30 | mssqlsvc_spn["ComputerSID"] = fqdn.into(); 31 | mssqlsvc_spn["Port"] = port.into(); 32 | } 33 | else 34 | { 35 | // I temporarily add the fqdn which will be replaced by the SID at the end of the parsing. 36 | // This avoids making a new request to the LDAP server and parsing off-line. 37 | let split = serviceprincipalname.split("/"); 38 | let vec = split.collect::>(); 39 | let fqdn = vec[1].to_owned().to_uppercase(); 40 | let port = 1433; 41 | 42 | //trace!("{:?}",fqdn); 43 | mssqlsvc_spn["ComputerSID"] = fqdn.into(); 44 | mssqlsvc_spn["Port"] = port.into(); 45 | } 46 | } 47 | else 48 | { 49 | mssqlsvc_spn = json!({}); 50 | } 51 | return mssqlsvc_spn 52 | } -------------------------------------------------------------------------------- /src/enums/trusts.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | bitflags! { 4 | struct Flags: u32 { 5 | // TRUST FLAG 6 | // From: https://msdn.microsoft.com/en-us/library/cc223779.aspx 7 | const NON_TRANSITIVE= 0x00000001; 8 | const UPLEVEL_ONLY= 0x00000002; 9 | const QUARANTINED_DOMAIN= 0x00000004; 10 | const FOREST_TRANSITIVE= 0x00000008; 11 | const CROSS_ORGANIZATION= 0x00000010; 12 | const WITHIN_FOREST= 0x00000020; 13 | const TREAT_AS_EXTERNAL= 0x00000040; 14 | const USES_RC4_ENCRYPTION= 0x00000080; 15 | const CROSS_ORGANIZATION_NO_TGT_DELEGATION= 0x00000200; 16 | const CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION= 0x00000800; 17 | const PIM_TRUST= 0x00000400; 18 | } 19 | } 20 | 21 | /// Get the trust flags from "trustDomain". 22 | pub fn get_trust_flag(trustflag: u32, trust_json: &mut serde_json::value::Value) 23 | { 24 | let mut is_transitive = false; 25 | let mut sid_filtering = false; 26 | 27 | if (Flags::WITHIN_FOREST.bits() | trustflag) == trustflag 28 | { 29 | let trust_type = "ParentChild"; //0 = ParentChild 30 | trust_json["TrustType"] = trust_type.into(); 31 | is_transitive = true; 32 | if (Flags::QUARANTINED_DOMAIN.bits() | trustflag) == trustflag { 33 | sid_filtering = true; 34 | } 35 | } 36 | else if (Flags::FOREST_TRANSITIVE.bits() | trustflag) == trustflag 37 | { 38 | let trust_type = "Forest"; //2 = Forest 39 | trust_json["TrustType"] = trust_type.into(); 40 | is_transitive = true; 41 | sid_filtering = true; 42 | } 43 | else if (Flags::TREAT_AS_EXTERNAL.bits() | trustflag) == trustflag || (Flags::CROSS_ORGANIZATION.bits() | trustflag) == trustflag 44 | { 45 | let trust_type = "External"; //3 = External 46 | trust_json["TrustType"] = trust_type.into(); 47 | is_transitive = false; 48 | sid_filtering = true; 49 | } 50 | else 51 | { 52 | let trust_type = "Unknown"; //4 = Unknown 53 | trust_json["TrustType"] = trust_type.into(); 54 | if (Flags::NON_TRANSITIVE.bits() | trustflag) != trustflag { 55 | is_transitive = true; 56 | } 57 | sid_filtering = true; 58 | } 59 | 60 | // change value in mut vec json 61 | trust_json["SidFilteringEnabled"] = sid_filtering.into(); 62 | trust_json["IsTransitive"] = is_transitive.into(); 63 | } -------------------------------------------------------------------------------- /src/enums/uacflags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | bitflags! { 4 | struct Flags: u32 { 5 | const SCRIPT = 0x0001; 6 | const ACCOUNT_DISABLE = 0x0002; 7 | const HOME_DIR_REQUIRED = 0x0008; 8 | const LOCKOUT = 0x0010; 9 | const PASSWORD_NOT_REQUIRED = 0x0020; 10 | const PASSWORD_CANT_CHANGE = 0x0040; 11 | const ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080; 12 | const TEMP_DUPLICATE_ACCOUNT = 0x0100; 13 | const NORMAL_ACCOUNT = 0x0200; 14 | const INTER_DOMAIN_TRUST_ACCOUNT = 0x0800; 15 | const WORKSTATION_TRUST_ACCOUNT = 0x1000; 16 | const SERVER_TRUST_ACCOUNT = 0x2000; 17 | const DONT_EXPIRE_PASSWORD = 0x10000; 18 | const MNS_LOGON_ACCOUNT = 0x20000; 19 | const SMART_CARD_REQUIRED = 0x40000; 20 | const TRUSTED_FOR_DELEGATION = 0x80000; 21 | const NOT_DELEGATED = 0x100000; 22 | const USE_DES_KEY_ONLY = 0x200000; 23 | const DONT_REQ_PRE_AUTH = 0x400000; 24 | const PASSWORD_EXPIRED = 0x800000; 25 | const TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000; 26 | const PARTIAL_SECRETS_ACCOUNT = 0x04000000; 27 | } 28 | } 29 | 30 | /// Get the UAC flags from "userAccountControl" LDAP attribut. 31 | pub fn get_flag(uac: u32) -> Vec 32 | { 33 | let mut uac_flags: Vec = Vec::new(); 34 | 35 | if (Flags::SCRIPT.bits() | uac) == uac 36 | { 37 | uac_flags.push("Script".to_string()); 38 | } 39 | if (Flags::ACCOUNT_DISABLE.bits() | uac) == uac 40 | { 41 | uac_flags.push("AccountDisable".to_string()); 42 | } 43 | if (Flags::HOME_DIR_REQUIRED.bits() | uac) == uac 44 | { 45 | uac_flags.push("HomeDirRequired".to_string()); 46 | } 47 | if (Flags::LOCKOUT.bits() | uac) == uac 48 | { 49 | uac_flags.push("Lockout".to_string()); 50 | } 51 | if (Flags::PASSWORD_NOT_REQUIRED.bits() | uac) == uac 52 | { 53 | uac_flags.push("PasswordNotRequired".to_string()); 54 | } 55 | if (Flags::PASSWORD_CANT_CHANGE.bits() | uac) == uac 56 | { 57 | uac_flags.push("PasswordCantChange".to_string()); 58 | } 59 | if (Flags::ENCRYPTED_TEXT_PWD_ALLOWED.bits() | uac) == uac 60 | { 61 | uac_flags.push("EncryptedTextPwdAllowed".to_string()); 62 | } 63 | if (Flags::TEMP_DUPLICATE_ACCOUNT.bits() | uac) == uac 64 | { 65 | uac_flags.push("TempDuplicateAccount".to_string()); 66 | } 67 | if (Flags::NORMAL_ACCOUNT.bits() | uac) == uac 68 | { 69 | uac_flags.push("NormalAccount".to_string()); 70 | } 71 | if (Flags::INTER_DOMAIN_TRUST_ACCOUNT.bits() | uac) == uac 72 | { 73 | uac_flags.push("InterdomainTrustAccount".to_string()); 74 | } 75 | if (Flags::WORKSTATION_TRUST_ACCOUNT.bits() | uac) == uac 76 | { 77 | uac_flags.push("WorkstationTrustAccount".to_string()); 78 | } 79 | if (Flags::SERVER_TRUST_ACCOUNT.bits() | uac) == uac 80 | { 81 | uac_flags.push("ServerTrustAccount".to_string()); 82 | } 83 | if (Flags::DONT_EXPIRE_PASSWORD.bits() | uac) == uac 84 | { 85 | uac_flags.push("DontExpirePassword".to_string()); 86 | } 87 | if (Flags::MNS_LOGON_ACCOUNT.bits() | uac) == uac 88 | { 89 | uac_flags.push("MnsLogonAccount".to_string()); 90 | } 91 | if (Flags::SMART_CARD_REQUIRED.bits() | uac) == uac 92 | { 93 | uac_flags.push("SmartcardRequired".to_string()); 94 | } 95 | if (Flags::TRUSTED_FOR_DELEGATION.bits() | uac) == uac 96 | { 97 | uac_flags.push("TrustedForDelegation".to_string()); 98 | } 99 | if (Flags::NOT_DELEGATED.bits() | uac) == uac 100 | { 101 | uac_flags.push("NotDelegated".to_string()); 102 | } 103 | if (Flags::USE_DES_KEY_ONLY.bits() | uac) == uac 104 | { 105 | uac_flags.push("UseDesKeyOnly".to_string()); 106 | } 107 | if (Flags::DONT_REQ_PRE_AUTH.bits() | uac) == uac 108 | { 109 | uac_flags.push("DontReqPreauth".to_string()); 110 | } 111 | if (Flags::PASSWORD_EXPIRED.bits() | uac) == uac 112 | { 113 | uac_flags.push("PasswordExpired".to_string()); 114 | } 115 | if (Flags::TRUSTED_TO_AUTH_FOR_DELEGATION.bits() | uac) == uac 116 | { 117 | uac_flags.push("TrustedToAuthForDelegation".to_string()); 118 | } 119 | if (Flags::PARTIAL_SECRETS_ACCOUNT.bits() | uac) == uac 120 | { 121 | uac_flags.push("PartialSecretsAccount".to_string()); 122 | } 123 | return uac_flags 124 | } -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Errors management 2 | use ldap3::LdapError; 3 | use std::error::Error as StdError; 4 | use std::fmt; 5 | //use std::num::ParseIntError; 6 | use std::sync::Arc; 7 | 8 | /// This is a shorthand for `rusthound`-based error results 9 | pub type Result = std::result::Result; 10 | pub type Cause = Arc; 11 | pub type BoxError = Box; 12 | 13 | /// RustHound error's type 14 | pub struct Error { 15 | kind: Kind, 16 | cause: Option, 17 | desc: Option, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub enum Kind { 22 | /// Connection 23 | Connection(Connection), 24 | /// LdapError 25 | LdapError, 26 | /// Parse Error 27 | ParseError, 28 | /// Other 29 | Other, 30 | } 31 | 32 | #[derive(Clone, Debug, PartialEq, Eq)] 33 | pub enum Connection { 34 | Login, 35 | Host, 36 | } 37 | 38 | impl Error { 39 | /// Construct an error from scratch 40 | /// You can chain this method to `with`, as shown below. 41 | /// ``` 42 | /// Error::new(Kind::Other).with() 43 | /// ``` 44 | pub fn new(kind: Kind) -> Error { 45 | Error { 46 | kind, 47 | cause: None, 48 | desc: None, 49 | } 50 | } 51 | 52 | /// Specify a cause 53 | pub fn with(mut self, cause: C) -> Error { 54 | self.cause = Some(Arc::new(cause)); 55 | self 56 | } 57 | 58 | pub fn desc>(mut self, desc: D) -> Error { 59 | self.desc = Some(desc.into()); 60 | self 61 | } 62 | 63 | /// Get error kind 64 | pub fn kind(&self) -> &Kind { 65 | &self.kind 66 | } 67 | 68 | pub fn new_login() -> Error { 69 | Error::new(Kind::Connection(Connection::Login)) 70 | } 71 | 72 | pub fn new_host() -> Error { 73 | Error::new(Kind::Connection(Connection::Host)) 74 | } 75 | 76 | pub fn new_ldap_error(error: LdapError) -> Error { 77 | Error::new(Kind::LdapError).with(error) 78 | } 79 | 80 | /// Internally used for Display trait 81 | fn kind_description(&self) -> &str { 82 | match self.kind { 83 | Kind::Connection(Connection::Login) => "[!] LDAP Connection Failed, Invalid Credentials.\n", 84 | Kind::Connection(Connection::Host) => "[!] LDAP Connection Failed, No Route To Host.\n", 85 | Kind::LdapError => &"LDAP Error.\n", 86 | Kind::ParseError => &"Parsing Json Error.\n", 87 | Kind::Other => "[!] Other Error\n", 88 | } 89 | } 90 | 91 | /// Backtrace error source to find a cause matching given type 92 | pub fn find_source(&self) -> Option<&E> { 93 | let mut source = self.source(); 94 | while let Some(err) = source { 95 | if let Some(ref original) = err.downcast_ref() { 96 | return Some(original); 97 | } 98 | source = err.source(); 99 | } 100 | // Not found 101 | None 102 | } 103 | } 104 | 105 | impl fmt::Debug for Error { 106 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 107 | f.write_str(self.kind_description())?; 108 | if let Some(ref desc) = self.desc { 109 | write!(f, ": {}", desc)?; 110 | } 111 | if let Some(ref cause) = self.cause { 112 | write!(f, ": {:?}", cause)?; 113 | } 114 | Ok(()) 115 | } 116 | } 117 | 118 | impl fmt::Display for Error { 119 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 120 | f.write_str(self.kind_description())?; 121 | if let Some(ref desc) = self.desc { 122 | write!(f, ": {}", desc)?; 123 | } 124 | if let Some(ref cause) = self.cause { 125 | write!(f, ": {}", cause)?; 126 | } 127 | Ok(()) 128 | } 129 | } 130 | 131 | impl StdError for Error { 132 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 133 | self.cause 134 | .as_ref() 135 | .map(|cause| &**cause as &(dyn StdError + 'static)) 136 | } 137 | } 138 | 139 | /// Converting from `LdapsearchError` 140 | impl From for Error { 141 | fn from(err: LdapError) -> Error { 142 | Error::new(Kind::LdapError).with(err) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/exec.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | /// Function to run commands 4 | pub fn run(input: &str) -> String 5 | { 6 | let output = if cfg!(target_os = "windows") { 7 | Command::new("cmd") 8 | .args(["/k",input]) 9 | .output() 10 | .expect("failed") 11 | } else { 12 | Command::new("sh") 13 | .arg("-c") 14 | .arg(input) 15 | .output() 16 | .expect("failed") 17 | }; 18 | if output.status.success() { 19 | let stdout = String::from_utf8_lossy(&output.stdout).to_string(); 20 | format!("{}", stdout) 21 | } else { 22 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 23 | format!("[FAILED] Command error:\n{}", stderr) 24 | } 25 | } -------------------------------------------------------------------------------- /src/json/checker/bh_41.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use regex::Regex; 3 | //use log::{info,debug,trace}; 4 | use crate::json::templates::*; 5 | use crate::ldap::prepare_ldap_dc; 6 | use indicatif::ProgressBar; 7 | use crate::banner::progress_bar; 8 | use std::convert::TryInto; 9 | 10 | /// Function to add default groups 11 | /// 12 | pub fn add_default_groups(vec_groups: &mut Vec, vec_computers: &Vec, domain: String) 13 | { 14 | let mut domain_sid = "".to_owned(); 15 | let mut template_json = bh_41::prepare_default_group_json_template(); 16 | template_json["Properties"]["domain"] = domain.to_owned().to_uppercase().into(); 17 | let mut template_member = bh_41::prepare_member_json_template(); 18 | template_member["ObjectType"] = "Computer".into(); 19 | 20 | // ENTERPRISE DOMAIN CONTROLLERS 21 | let mut edc_group = template_json.to_owned(); 22 | let mut sid = domain.to_uppercase(); 23 | sid.push_str("-S-1-5-9"); 24 | 25 | let mut name = "ENTERPRISE DOMAIN CONTROLLERS@".to_owned(); 26 | name.push_str(&domain.to_uppercase()); 27 | 28 | let mut vec_members: Vec = Vec::new(); 29 | for computer in vec_computers { 30 | if computer["Properties"]["unconstraineddelegation"].as_bool().unwrap() 31 | { 32 | template_member["ObjectIdentifier"] = computer["ObjectIdentifier"].as_str().unwrap().to_string().into(); 33 | vec_members.push(template_member.to_owned()); 34 | let re = Regex::new(r"^S-[0-9]{1}-[0-9]{1}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}").unwrap(); 35 | let mut sids: Vec = Vec::new(); 36 | for sid in re.captures_iter(&computer["ObjectIdentifier"].as_str().unwrap().to_string()) 37 | { 38 | sids.push(sid[0].to_owned().to_string()); 39 | } 40 | domain_sid = sids[0].to_string(); 41 | } 42 | } 43 | 44 | edc_group["ObjectIdentifier"] = sid.into(); 45 | edc_group["Properties"]["name"] = name.into(); 46 | edc_group["Members"] = vec_members.into(); 47 | vec_groups.push(edc_group); 48 | 49 | // ACCOUNT OPERATORS 50 | let mut account_operators_group = template_json.to_owned(); 51 | sid = domain.to_uppercase(); 52 | sid.push_str("-S-1-5-32-548"); 53 | let mut name = "ACCOUNT OPERATORS@".to_owned(); 54 | name.push_str(&domain.to_uppercase()); 55 | 56 | account_operators_group["ObjectIdentifier"] = sid.into(); 57 | account_operators_group["Properties"]["name"] = name.into(); 58 | account_operators_group["Properties"]["highvalue"] = true.into(); 59 | vec_groups.push(account_operators_group); 60 | 61 | // WINDOWS AUTHORIZATION ACCESS GROUP 62 | let mut waag_group = template_json.to_owned(); 63 | sid = domain.to_uppercase(); 64 | sid.push_str("-S-1-5-32-560"); 65 | let mut name = "WINDOWS AUTHORIZATION ACCESS GROUP@".to_owned(); 66 | name.push_str(&domain.to_uppercase()); 67 | 68 | waag_group["ObjectIdentifier"] = sid.into(); 69 | waag_group["Properties"]["name"] = name.into(); 70 | vec_groups.push(waag_group); 71 | 72 | // EVERYONE 73 | let mut everyone_group = template_json.to_owned(); 74 | sid = domain.to_uppercase(); 75 | sid.push_str("-S-1-1-0"); 76 | let mut name = "EVERYONE@".to_owned(); 77 | name.push_str(&domain.to_uppercase()); 78 | 79 | let mut vec_everyone_members: Vec = Vec::new(); 80 | let mut member_id = domain_sid.to_owned(); 81 | member_id.push_str("-515"); 82 | template_member["ObjectIdentifier"] = member_id.to_owned().into(); 83 | template_member["ObjectType"] = "Group".into(); 84 | vec_everyone_members.push(template_member.to_owned()); 85 | 86 | member_id = domain_sid.to_owned(); 87 | member_id.push_str("-513"); 88 | template_member["ObjectIdentifier"] = member_id.to_owned().into(); 89 | template_member["ObjectType"] = "Group".into(); 90 | vec_everyone_members.push(template_member.to_owned()); 91 | 92 | everyone_group["ObjectIdentifier"] = sid.into(); 93 | everyone_group["Properties"]["name"] = name.into(); 94 | everyone_group["Members"] = vec_everyone_members.into(); 95 | vec_groups.push(everyone_group); 96 | 97 | // AUTHENTICATED USERS 98 | let mut auth_users_group = template_json.to_owned(); 99 | sid = domain.to_uppercase(); 100 | sid.push_str("-S-1-5-11"); 101 | let mut name = "AUTHENTICATED USERS@".to_owned(); 102 | name.push_str(&domain.to_uppercase()); 103 | 104 | let mut vec_auth_users_members: Vec = Vec::new(); 105 | member_id = domain_sid.to_owned(); 106 | member_id.push_str("-515"); 107 | template_member["ObjectIdentifier"] = member_id.to_owned().into(); 108 | template_member["ObjectType"] = "Group".into(); 109 | vec_auth_users_members.push(template_member.to_owned()); 110 | 111 | member_id = domain_sid.to_owned(); 112 | member_id.push_str("-513"); 113 | template_member["ObjectIdentifier"] = member_id.to_owned().into(); 114 | template_member["ObjectType"] = "Group".into(); 115 | vec_auth_users_members.push(template_member.to_owned()); 116 | 117 | auth_users_group["ObjectIdentifier"] = sid.into(); 118 | auth_users_group["Properties"]["name"] = name.into(); 119 | auth_users_group["Members"] = vec_auth_users_members.into(); 120 | vec_groups.push(auth_users_group); 121 | 122 | // ADMINISTRATORS 123 | let mut administrators_group = template_json.to_owned(); 124 | sid = domain.to_uppercase(); 125 | sid.push_str("-S-1-5-32-544"); 126 | let mut name = "ADMINISTRATORS@".to_owned(); 127 | name.push_str(&domain.to_uppercase()); 128 | 129 | administrators_group["ObjectIdentifier"] = sid.into(); 130 | administrators_group["Properties"]["name"] = name.into(); 131 | administrators_group["Properties"]["highvalue"] = true.into(); 132 | vec_groups.push(administrators_group); 133 | 134 | // PRE-WINDOWS 2000 COMPATIBLE ACCESS 135 | let mut pw2000ca_group = template_json.to_owned(); 136 | sid = domain.to_uppercase(); 137 | sid.push_str("-S-1-5-32-554"); 138 | let mut name = "PRE-WINDOWS 2000 COMPATIBLE ACCESS@".to_owned(); 139 | name.push_str(&domain.to_uppercase()); 140 | 141 | pw2000ca_group["ObjectIdentifier"] = sid.into(); 142 | pw2000ca_group["Properties"]["name"] = name.into(); 143 | vec_groups.push(pw2000ca_group); 144 | 145 | // INTERACTIVE 146 | let mut interactive_group = template_json.to_owned(); 147 | sid = domain.to_uppercase(); 148 | sid.push_str("-S-1-5-4"); 149 | let mut name = "INTERACTIVE@".to_owned(); 150 | name.push_str(&domain.to_uppercase()); 151 | 152 | interactive_group["ObjectIdentifier"] = sid.into(); 153 | interactive_group["Properties"]["name"] = name.into(); 154 | vec_groups.push(interactive_group); 155 | 156 | // PRINT OPERATORS 157 | let mut print_operators_group = template_json.to_owned(); 158 | sid = domain.to_uppercase(); 159 | sid.push_str("-S-1-5-32-550"); 160 | let mut name = "PRINT OPERATORS@".to_owned(); 161 | name.push_str(&domain.to_uppercase()); 162 | 163 | print_operators_group["ObjectIdentifier"] = sid.into(); 164 | print_operators_group["Properties"]["name"] = name.into(); 165 | print_operators_group["Properties"]["highvalue"] = true.into(); 166 | vec_groups.push(print_operators_group); 167 | 168 | // TERMINAL SERVER LICENSE SERVERS 169 | let mut tsls_group = template_json.to_owned(); 170 | sid = domain.to_uppercase(); 171 | sid.push_str("-S-1-5-32-561"); 172 | let mut name = "TERMINAL SERVER LICENSE SERVERS@".to_owned(); 173 | name.push_str(&domain.to_uppercase()); 174 | 175 | tsls_group["ObjectIdentifier"] = sid.into(); 176 | tsls_group["Properties"]["name"] = name.into(); 177 | vec_groups.push(tsls_group); 178 | 179 | // INCOMING FOREST TRUST BUILDERS 180 | let mut iftb_group = template_json.to_owned(); 181 | sid = domain.to_uppercase(); 182 | sid.push_str("-S-1-5-32-557"); 183 | let mut name = "INCOMING FOREST TRUST BUILDERS@".to_owned(); 184 | name.push_str(&domain.to_uppercase()); 185 | 186 | iftb_group["ObjectIdentifier"] = sid.into(); 187 | iftb_group["Properties"]["name"] = name.into(); 188 | vec_groups.push(iftb_group); 189 | 190 | // THIS ORGANIZATION 191 | let mut this_organization_group = template_json.to_owned(); 192 | sid = domain.to_uppercase(); 193 | sid.push_str("-S-1-5-15"); 194 | let mut name = "THIS ORGANIZATION@".to_owned(); 195 | name.push_str(&domain.to_uppercase()); 196 | 197 | this_organization_group["ObjectIdentifier"] = sid.into(); 198 | this_organization_group["Properties"]["name"] = name.into(); 199 | vec_groups.push(this_organization_group); 200 | } 201 | 202 | 203 | /// Function to add default user 204 | /// 205 | pub fn add_default_users(vec_users: &mut Vec, domain: String) 206 | { 207 | let mut template_json = bh_41::prepare_default_user_json_template(); 208 | template_json["Properties"]["domain"] = domain.to_owned().to_uppercase().into(); 209 | 210 | // NT AUTHORITY 211 | let mut ntauthority_user = template_json.to_owned(); 212 | let mut sid = domain.to_uppercase(); 213 | sid.push_str("-S-1-5-20"); 214 | let mut name = "NT AUTHORITY@".to_owned(); 215 | name.push_str(&domain.to_uppercase()); 216 | ntauthority_user["Properties"]["name"] = name.into(); 217 | ntauthority_user["ObjectIdentifier"] = sid.into(); 218 | ntauthority_user["Properties"]["domainsid"] = vec_users[0]["Properties"]["domainsid"].as_str().unwrap().to_string().into(); 219 | 220 | vec_users.push(ntauthority_user); 221 | } 222 | 223 | /// This function is to push user SID in ChildObjects bh4.1+ 224 | pub fn add_childobjects_members(vec_replaced: &mut Vec, dn_sid: &HashMap, sid_type: &HashMap) 225 | { 226 | // Needed for progress bar stats 227 | let pb = ProgressBar::new(1); 228 | let mut count = 0; 229 | let total = vec_replaced.len(); 230 | 231 | //trace!("add_childobjects_members"); 232 | 233 | for object in vec_replaced 234 | { 235 | // Manage progress bar 236 | count += 1; 237 | let pourcentage = 100 * count / total; 238 | progress_bar(pb.to_owned(),"Adding childobjects members".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 239 | 240 | let mut direct_members: Vec = Vec::new(); 241 | let mut affected_computers: Vec = Vec::new(); 242 | 243 | let null: String = "NULL".to_string(); 244 | let dn = object["Properties"]["distinguishedname"].as_str().unwrap().to_string().to_uppercase(); 245 | let mut name = object["Properties"]["name"].as_str().unwrap().to_string(); 246 | let sid = dn_sid.get(&object["Properties"]["distinguishedname"].as_str().unwrap().to_string()).unwrap_or(&null); 247 | let otype = sid_type.get(sid).unwrap(); 248 | //trace!("SID OBJECT: {:?} : {:?} : {:?}",&dn,&sid,&otype); 249 | 250 | if otype != "Domain" 251 | { 252 | let split = name.split("@"); 253 | let vec = split.collect::>(); 254 | name = vec[0].to_owned(); 255 | } 256 | 257 | for value in dn_sid 258 | { 259 | let dn_object = value.0.to_string().to_uppercase(); 260 | //trace!("{:?}", &dn_object); 261 | let split = dn_object.split(","); 262 | let vec = split.collect::>(); 263 | let mut first = vec[1].to_owned(); 264 | //trace!("{:?}", &first); 265 | let split = first.split("="); 266 | let vec = split.collect::>(); 267 | if vec.len() >= 2 { 268 | //trace!("{:?}", &vec.len()); 269 | first = vec[1].to_owned(); 270 | } 271 | else 272 | { 273 | continue 274 | } 275 | //trace!("{:?}", &first); 276 | 277 | if otype != "Domain"{ 278 | if (dn_object.contains(&dn)) && (&dn_object != &dn) && (&first == &name) 279 | { 280 | let mut object = bh_41::prepare_member_json_template(); 281 | object["ObjectIdentifier"] = value.1.as_str().to_string().into(); 282 | let object_type = sid_type.get(&value.1.as_str().to_string()).unwrap(); 283 | object["ObjectType"] = object_type.to_string().into(); 284 | direct_members.push(object.to_owned()); 285 | 286 | // if the direct object is one computer add it in affected_computers to push it in OU 287 | if object_type.to_string() == "Computer" 288 | { 289 | affected_computers.push(object.to_owned()); 290 | } 291 | } 292 | } 293 | else 294 | { 295 | let mut object = bh_41::prepare_member_json_template(); 296 | let split = name.split("."); 297 | let vec = split.collect::>(); 298 | let cn = vec[0].to_owned(); 299 | if first.contains(&cn) 300 | { 301 | object["ObjectIdentifier"] = value.1.as_str().to_string().into(); 302 | let object_type = sid_type.get(&value.1.as_str().to_string()).unwrap(); 303 | object["ObjectType"] = object_type.to_string().into(); 304 | direct_members.push(object); 305 | } 306 | } 307 | } 308 | //trace!("direct_members for Object '{}': {:?}",name,direct_members); 309 | 310 | object["ChildObjects"] = direct_members.into(); 311 | if otype == "OU" 312 | { 313 | object["GPOChanges"]["AffectedComputers"] = affected_computers.into(); 314 | } 315 | } 316 | pb.finish_and_clear(); 317 | } 318 | 319 | /// This function check Guid for all Gplink to replace with correct guid 320 | pub fn replace_guid_gplink(vec_replaced: &mut Vec, dn_sid: &HashMap) 321 | { 322 | // Needed for progress bar stats 323 | let pb = ProgressBar::new(1); 324 | let mut count = 0; 325 | let total = vec_replaced.len(); 326 | 327 | for i in 0..vec_replaced.len() 328 | { 329 | // Manage progress bar 330 | count += 1; 331 | let pourcentage = 100 * count / total; 332 | progress_bar(pb.to_owned(),"Replacing GUID for gplink".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 333 | 334 | // ACE by ACE 335 | if vec_replaced[i]["Links"].as_array().unwrap().len() != 0 { 336 | for j in 0..vec_replaced[i]["Links"].as_array().unwrap().len() 337 | { 338 | for value in dn_sid 339 | { 340 | //trace!("{:?}",&vec_replaced[i]["Links"][j]["Guid"].as_str().unwrap().to_string()); 341 | if value.0.contains(&vec_replaced[i]["Links"][j]["GUID"].as_str().unwrap().to_string()) 342 | { 343 | vec_replaced[i]["Links"][j]["GUID"] = value.1.to_owned().into(); 344 | } 345 | } 346 | } 347 | } 348 | } 349 | pb.finish_and_clear(); 350 | } 351 | 352 | /// This function will ad domainsid 353 | pub fn add_domain_sid( 354 | vec_replaced: &mut Vec, 355 | dn_sid: &HashMap) 356 | { 357 | // Needed for progress bar stats 358 | let pb = ProgressBar::new(1); 359 | let mut count = 0; 360 | let total = vec_replaced.len(); 361 | 362 | let mut domain_sid = "".to_owned(); 363 | for value in dn_sid 364 | { 365 | // Manage progress bar 366 | count += 1; 367 | let pourcentage = 100 * count / total; 368 | progress_bar(pb.to_owned(),"Getting domain SID".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 369 | 370 | let sid = value.1.to_owned(); 371 | let re = Regex::new(r"^S-[0-9]{1}-[0-9]{1}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}").unwrap(); 372 | for value in re.captures_iter(&sid) 373 | { 374 | domain_sid = value[0].to_owned().to_string(); 375 | break 376 | } 377 | if domain_sid.len() > 0 { 378 | break 379 | } 380 | } 381 | pb.finish_and_clear(); 382 | //trace!("domain_sid: {:?}",&domain_sid); 383 | 384 | // Needed for progress bar stats 385 | let pb = ProgressBar::new(1); 386 | let mut count = 0; 387 | let total = vec_replaced.len(); 388 | 389 | for i in 0..vec_replaced.len() 390 | { 391 | // Manage progress bar 392 | count += 1; 393 | let pourcentage = 100 * count / total; 394 | progress_bar(pb.to_owned(),"Adding domain SID".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 395 | 396 | //let name = vec_replaced[i]["Properties"]["name"].as_str().unwrap().to_string(); 397 | //trace!("name: {:?}",&name); 398 | vec_replaced[i]["Properties"]["domainsid"] = domain_sid.to_owned().into(); 399 | } 400 | pb.finish_and_clear(); 401 | } 402 | 403 | /// This function push computer sid in domain GpoChanges 404 | pub fn add_affected_computers(vec_domains: &mut Vec, sid_type: &HashMap) 405 | { 406 | let mut vec_affected_computers: Vec = Vec::new(); 407 | 408 | for value in sid_type 409 | { 410 | if value.1 == "Computer" 411 | { 412 | let mut json_template_object = bh_41::prepare_member_json_template(); 413 | json_template_object["ObjectType"] = "Computer".into(); 414 | json_template_object["ObjectIdentifier"] = value.0.to_owned().to_string().into(); 415 | vec_affected_computers.push(json_template_object); 416 | } 417 | } 418 | 419 | vec_domains[0]["GPOChanges"]["AffectedComputers"] = vec_affected_computers.into(); 420 | } 421 | 422 | /// This function is to replace fqdn by sid in users SPNTargets:ComputerSID 423 | pub fn replace_fqdn_by_sid(vec_src: &mut Vec, fqdn_sid: &HashMap) 424 | { 425 | // Needed for progress bar stats 426 | let pb = ProgressBar::new(1); 427 | let mut count = 0; 428 | let total = vec_src.len(); 429 | 430 | for i in 0..vec_src.len() 431 | { 432 | // Manage progress bar 433 | count += 1; 434 | let pourcentage = 100 * count / total; 435 | progress_bar(pb.to_owned(),"Replacing FQDN by SID".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 436 | 437 | if vec_src[i]["SPNTargets"].as_array().unwrap_or(&Vec::new()).len() != 0 { 438 | for j in 0..vec_src[i]["SPNTargets"].as_array().unwrap().len() 439 | { 440 | let default = &vec_src[i]["SPNTargets"][j]["ComputerSID"].as_str().unwrap().to_string(); 441 | let sid = fqdn_sid.get(&vec_src[i]["SPNTargets"][j]["ComputerSID"].as_str().unwrap().to_string()).unwrap_or(default); 442 | //trace!("SPNTargets: {} = {}",&vec_users[i]["SPNTargets"][j]["ComputerSID"].to_string(),&sid); 443 | vec_src[i]["SPNTargets"][j]["ComputerSID"] = sid.to_owned().into(); 444 | } 445 | } 446 | if vec_src[i]["AllowedToDelegate"].as_array().unwrap_or(&Vec::new()).len() != 0 { 447 | for j in 0..vec_src[i]["AllowedToDelegate"].as_array().unwrap().len() 448 | { 449 | let default = &vec_src[i]["AllowedToDelegate"][j]["ObjectIdentifier"].as_str().unwrap().to_string(); 450 | let sid = fqdn_sid.get(&vec_src[i]["AllowedToDelegate"][j]["ObjectIdentifier"].as_str().unwrap().to_string()).unwrap_or(default); 451 | //trace!("AllowedToDelegate: {} = {}",&vec_users[i]["AllowedToDelegate"][j]["ObjectIdentifier"].to_string(),&sid); 452 | vec_src[i]["AllowedToDelegate"][j]["ObjectIdentifier"] = sid.to_owned().into(); 453 | } 454 | } 455 | } 456 | pb.finish_and_clear(); 457 | } 458 | 459 | /// This function is to check and replace object name by SID in group members. 460 | pub fn replace_sid_members(vec_groups: &mut Vec, dn_sid: &HashMap, sid_type: &HashMap, vec_trusts: &Vec) 461 | { 462 | // Needed for progress bar stats 463 | let pb = ProgressBar::new(1); 464 | let mut count = 0; 465 | let total = vec_groups.len(); 466 | 467 | // GROUP by GROUP 468 | for i in 0..vec_groups.len() 469 | { 470 | // Manage progress bar 471 | count += 1; 472 | let pourcentage = 100 * count / total; 473 | progress_bar(pb.to_owned(),"Replacing SID for groups".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 474 | 475 | // MEMBER by MEMBER 476 | if vec_groups[i]["Members"].as_array().unwrap().len() != 0 { 477 | for j in 0..vec_groups[i]["Members"].as_array().unwrap().len() 478 | { 479 | let null: String = "NULL".to_string(); 480 | let sid = dn_sid.get(&vec_groups[i]["Members"][j]["ObjectIdentifier"].as_str().unwrap().to_string()).unwrap_or(&null); 481 | if sid.contains("NULL"){ 482 | let dn = &vec_groups[i]["Members"][j]["ObjectIdentifier"].as_str().unwrap().to_string(); 483 | // Check if DN match trust domain to get SID and Type 484 | let sid = sid_maker_from_another_domain(vec_trusts, dn); 485 | let type_object = "Group".to_string(); 486 | vec_groups[i]["Members"][j]["ObjectIdentifier"] = sid.to_owned().into(); 487 | vec_groups[i]["Members"][j]["ObjectType"] = type_object.to_owned().into(); 488 | } 489 | else 490 | { 491 | let type_object = sid_type.get(sid).unwrap_or(&null); 492 | vec_groups[i]["Members"][j]["ObjectIdentifier"] = sid.to_owned().into(); 493 | vec_groups[i]["Members"][j]["ObjectType"] = type_object.to_owned().into(); 494 | } 495 | 496 | } 497 | } 498 | } 499 | pb.finish_and_clear(); 500 | } 501 | // Make the SID from domain present in trust 502 | fn sid_maker_from_another_domain(vec_trusts: &Vec, object_identifier: &String) -> String 503 | { 504 | for i in 0..vec_trusts.len() { 505 | let ldap_dc = prepare_ldap_dc(&vec_trusts[i]["TargetDomainName"].as_str().unwrap().to_string(),false); 506 | //trace!("LDAP_DC TRUSTED {:?}: {:?}", &i,&vec_trusts[i]); 507 | if object_identifier.contains(ldap_dc[0].as_str()) 508 | { 509 | //trace!("object_identifier '{}' contains trust domain '{}'",&object_identifier, &ldap_dc); 510 | let id = get_id_from_objectidentifier(object_identifier); 511 | let sid = vec_trusts[i]["TargetDomainSid"].as_str().unwrap().to_string() + id.as_str(); 512 | return sid 513 | } 514 | } 515 | if object_identifier.contains("CN=S-") { 516 | let re = Regex::new(r"S-[0-9]{1}-[0-9]{1}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}-[0-9]{1,}").unwrap(); 517 | for sid in re.captures_iter(&object_identifier) 518 | { 519 | return sid[0].to_owned().to_string(); 520 | } 521 | } 522 | return object_identifier.to_string() 523 | } 524 | 525 | // Get id from objectidentifier for all common group (Administrators ...) 526 | // https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers 527 | fn get_id_from_objectidentifier(object_identifier: &String) -> String 528 | { 529 | // Hashmap to link GROUP NAME to RID 530 | let mut name_to_rid = HashMap::new(); 531 | name_to_rid.insert("DOMAIN ADMINS".to_string(), "-512".to_string()); 532 | name_to_rid.insert("ADMINISTRATEURS DU DOMAINE".to_string(), "-512".to_string()); 533 | name_to_rid.insert("DOMAIN USERS".to_string(), "-513".to_string()); 534 | name_to_rid.insert("UTILISATEURS DU DOMAINE".to_string(), "-513".to_string()); 535 | name_to_rid.insert("DOMAIN GUESTS".to_string(), "-514".to_string()); 536 | name_to_rid.insert("INVITES DE DOMAINE".to_string(), "-514".to_string()); 537 | name_to_rid.insert("DOMAIN COMPUTERS".to_string(), "-515".to_string()); 538 | name_to_rid.insert("ORDINATEURS DE DOMAINE".to_string(), "-515".to_string()); 539 | name_to_rid.insert("DOMAIN CONTROLLERS".to_string(), "-516".to_string()); 540 | name_to_rid.insert("CONTRÔLEURS DE DOMAINE".to_string(), "-516".to_string()); 541 | name_to_rid.insert("CERT PUBLISHERS".to_string(), "-517".to_string()); 542 | name_to_rid.insert("EDITEURS DE CERTIFICATS".to_string(), "-517".to_string()); 543 | name_to_rid.insert("SCHEMA ADMINS".to_string(), "-518".to_string()); 544 | name_to_rid.insert("ADMINISTRATEURS DU SCHEMA".to_string(), "-518".to_string()); 545 | name_to_rid.insert("ENTERPRISE ADMINS".to_string(), "-519".to_string()); 546 | name_to_rid.insert("ADMINISTRATEURS DE L'ENTREPRISE".to_string(), "-519".to_string()); 547 | 548 | for value in name_to_rid { 549 | if object_identifier.contains(value.0.as_str()) 550 | { 551 | //trace!("name_to_rid: {:?}", value); 552 | return value.1.to_string() 553 | } 554 | } 555 | return "NULL_ID1".to_string() 556 | } 557 | 558 | /// This function push trust domain values in domain 559 | pub fn add_trustdomain(vec_domains: &mut Vec, vec_trusts: &mut Vec) 560 | { 561 | if !&vec_trusts[0]["TargetDomainSid"].to_string().contains("SID") { 562 | let mut trusts: Vec = Vec::new(); 563 | for trust in vec_trusts { 564 | trusts.push(trust.to_owned()); 565 | } 566 | vec_domains[0]["Trusts"] = trusts.to_owned().into(); 567 | } 568 | } -------------------------------------------------------------------------------- /src/json/checker/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use log::{info,debug}; 3 | use indicatif::ProgressBar; 4 | use crate::banner::progress_bar; 5 | use std::convert::TryInto; 6 | 7 | pub mod bh_41; 8 | 9 | /// Functions to replace and add missing values 10 | pub fn check_all_result( 11 | domain: &String, 12 | 13 | vec_users: &mut Vec, 14 | vec_groups: &mut Vec, 15 | vec_computers: &mut Vec, 16 | vec_ous: &mut Vec, 17 | vec_domains: &mut Vec, 18 | vec_gpos: &mut Vec, 19 | _vec_fsps: &mut Vec, 20 | vec_containers: &mut Vec, 21 | vec_trusts: &mut Vec, 22 | 23 | dn_sid: &mut HashMap, 24 | sid_type: &mut HashMap, 25 | fqdn_sid: &mut HashMap, 26 | _fqdn_ip: &mut HashMap, 27 | ) 28 | { 29 | info!("Starting checker to replace some values..."); 30 | debug!("Replace SID with checker.rs started"); 31 | bh_41::replace_fqdn_by_sid(vec_users, &fqdn_sid); 32 | bh_41::replace_fqdn_by_sid(vec_computers, &fqdn_sid); 33 | bh_41::replace_sid_members(vec_groups, &dn_sid, &sid_type, &vec_trusts); 34 | debug!("Replace SID finished!"); 35 | 36 | debug!("Adding defaults groups and default users"); 37 | bh_41::add_default_groups(vec_groups, &vec_computers, domain.to_owned()); 38 | bh_41::add_default_users(vec_users, domain.to_owned()); 39 | debug!("Defaults groups and default users added!"); 40 | 41 | debug!("Adding PrincipalType for ACEs started"); 42 | add_type_for_ace(vec_users, &sid_type); 43 | add_type_for_ace(vec_groups, &sid_type); 44 | add_type_for_ace(vec_computers, &sid_type); 45 | add_type_for_ace(vec_gpos, &sid_type); 46 | add_type_for_ace(vec_ous, &sid_type); 47 | add_type_for_ace(vec_domains, &sid_type); 48 | add_type_for_ace(vec_containers, &sid_type); 49 | add_type_for_allowtedtoact(vec_computers, &sid_type); 50 | debug!("PrincipalType for ACEs added!"); 51 | 52 | debug!("Adding ChildObject members started"); 53 | bh_41::add_childobjects_members(vec_ous, &dn_sid, &sid_type); 54 | bh_41::add_childobjects_members(vec_domains, &dn_sid, &sid_type); 55 | bh_41::add_childobjects_members(vec_containers, &dn_sid, &sid_type); 56 | debug!("ChildObject members added!"); 57 | 58 | debug!("Adding domainsid started"); 59 | bh_41::add_domain_sid(vec_groups, &dn_sid); 60 | bh_41::add_domain_sid(vec_gpos, &dn_sid); 61 | bh_41::add_domain_sid(vec_ous, &dn_sid); 62 | bh_41::add_domain_sid(vec_containers, &dn_sid); 63 | debug!("domainsid added!"); 64 | 65 | debug!("Adding affected computers in domain GpoChanges"); 66 | bh_41::add_affected_computers(vec_domains, &sid_type); 67 | debug!("affected computers added!"); 68 | 69 | debug!("Replacing guid for gplinks started"); 70 | bh_41::replace_guid_gplink(vec_ous, &dn_sid); 71 | bh_41::replace_guid_gplink(vec_domains, &dn_sid); 72 | debug!("guid for gplinks added!"); 73 | 74 | if vec_trusts.len() > 0 { 75 | debug!("Adding trust domain relation"); 76 | bh_41::add_trustdomain(vec_domains, vec_trusts); 77 | debug!("Trust domain relation added!"); 78 | } 79 | info!("Checking and replacing some values finished!"); 80 | } 81 | 82 | /// This function check PrincipalSID for all Ace and add the PrincipalType "Group","User","Computer" 83 | pub fn add_type_for_ace(vec_replaced: &mut Vec, sid_type: &HashMap) 84 | { 85 | // Needed for progress bar stats 86 | let pb = ProgressBar::new(1); 87 | let mut count = 0; 88 | let total = vec_replaced.len(); 89 | 90 | for i in 0..vec_replaced.len() 91 | { 92 | // Manage progress bar 93 | count += 1; 94 | let pourcentage = 100 * count / total; 95 | progress_bar(pb.to_owned(),"Adding Type for ACE objects".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 96 | 97 | // ACE by ACE 98 | if vec_replaced[i]["Aces"].as_array().unwrap().len() != 0 { 99 | for j in 0..vec_replaced[i]["Aces"].as_array().unwrap().len() 100 | { 101 | let group: String = "Group".to_string(); 102 | let type_object = sid_type.get(&vec_replaced[i]["Aces"][j]["PrincipalSID"].as_str().unwrap().to_string()).unwrap_or(&group); 103 | vec_replaced[i]["Aces"][j]["PrincipalType"] = type_object.to_owned().into(); 104 | } 105 | } 106 | } 107 | pb.finish_and_clear(); 108 | } 109 | 110 | /// This function check PrincipalSID for all AllowedToAct object and add the PrincipalType "Group","User","Computer" 111 | pub fn add_type_for_allowtedtoact(vec_replaced: &mut Vec, sid_type: &HashMap) 112 | { 113 | // Needed for progress bar stats 114 | let pb = ProgressBar::new(1); 115 | let mut count = 0; 116 | let total = vec_replaced.len(); 117 | 118 | for i in 0..vec_replaced.len() 119 | { 120 | // Manage progress bar 121 | count += 1; 122 | let pourcentage = 100 * count / total; 123 | progress_bar(pb.to_owned(),"Adding Type for AllowedToAct objects".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 124 | 125 | if vec_replaced[i]["AllowedToAct"].as_array().unwrap().len() != 0 { 126 | for j in 0..vec_replaced[i]["AllowedToAct"].as_array().unwrap().len() 127 | { 128 | let default: String = "Computer".to_string(); 129 | let type_object = sid_type.get(&vec_replaced[i]["AllowedToAct"][j]["ObjectIdentifier"].as_str().unwrap().to_string()).unwrap_or(&default); 130 | vec_replaced[i]["AllowedToAct"][j]["ObjectType"] = type_object.to_owned().into(); 131 | } 132 | } 133 | } 134 | pb.finish_and_clear(); 135 | } 136 | -------------------------------------------------------------------------------- /src/json/maker/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use colored::Colorize; 3 | use log::{info,debug,trace}; 4 | 5 | use std::fs; 6 | use std::fs::File; 7 | use std::io::{Seek, Write}; 8 | use zip::result::ZipResult; 9 | use zip::write::{FileOptions, ZipWriter}; 10 | 11 | extern crate zip; 12 | use crate::json::templates::*; 13 | use crate::args::Options; 14 | use crate::enums::date::return_current_fulldate; 15 | 16 | /// Current Bloodhound version 4.2+ 17 | pub const BLOODHOUND_VERSION_4: i8 = 5; 18 | 19 | /// This function will create json output and zip output 20 | pub fn make_result( 21 | common_args: &Options, 22 | vec_users: Vec, 23 | vec_groups: Vec, 24 | vec_computers: Vec, 25 | vec_ous: Vec, 26 | vec_domains: Vec, 27 | vec_gpos: Vec, 28 | vec_containers: Vec, 29 | vec_cas: &mut Vec, 30 | vec_templates: &mut Vec, 31 | ) -> std::io::Result<()> 32 | { 33 | // Format domain name 34 | let filename = common_args.domain.replace(".", "-").to_lowercase(); 35 | 36 | // Hashmap for json files 37 | let mut json_result = HashMap::new(); 38 | 39 | // Datetime for output file 40 | let datetime = return_current_fulldate(); 41 | 42 | // Add all in json files 43 | add_file( 44 | &datetime, 45 | "users".to_string(), 46 | &filename, 47 | vec_users, 48 | &mut json_result, 49 | common_args, 50 | )?; 51 | add_file( 52 | &datetime, 53 | "groups".to_string(), 54 | &filename, 55 | vec_groups, 56 | &mut json_result, 57 | common_args, 58 | )?; 59 | add_file( 60 | &datetime, 61 | "computers".to_string(), 62 | &filename, 63 | vec_computers, 64 | &mut json_result, 65 | common_args, 66 | )?; 67 | add_file( 68 | &datetime, 69 | "ous".to_string(), 70 | &filename, 71 | vec_ous, 72 | &mut json_result, 73 | common_args, 74 | )?; 75 | add_file( 76 | &datetime, 77 | "domains".to_string(), 78 | &filename, 79 | vec_domains, 80 | &mut json_result, 81 | common_args, 82 | )?; 83 | // Not @ly4k BloodHound version? 84 | if common_args.old_bloodhound { 85 | let mut _vec_gpos_cas_templates = vec_gpos.to_owned(); 86 | if common_args.adcs { 87 | info!("{} {} parsed!", &vec_cas.len().to_string().bold(),&"cas"); 88 | _vec_gpos_cas_templates.append(vec_cas); 89 | info!("{} {} parsed!", &vec_templates.len().to_string().bold(),&"templates"); 90 | } 91 | _vec_gpos_cas_templates.append(vec_templates); 92 | info!("{} {} parsed!", &vec_gpos.len().to_string().bold(),&"gpos"); 93 | add_file( 94 | &datetime, 95 | "gpos".to_string(), 96 | &filename, 97 | _vec_gpos_cas_templates.to_vec(), 98 | &mut json_result, 99 | common_args, 100 | )?; 101 | } else { 102 | // Is @ly4k BloodHound version? 103 | add_file( 104 | &datetime, 105 | "gpos".to_string(), 106 | &filename, 107 | vec_gpos, 108 | &mut json_result, 109 | common_args, 110 | )?; 111 | } 112 | add_file( 113 | &datetime, 114 | "containers".to_string(), 115 | &filename, 116 | vec_containers, 117 | &mut json_result, 118 | common_args, 119 | )?; 120 | // ADCS and is @ly4k BloodHound version? 121 | if common_args.adcs && !common_args.old_bloodhound { 122 | add_file( 123 | &datetime, 124 | "cas".to_string(), 125 | &filename, 126 | vec_cas.to_vec(), 127 | &mut json_result, 128 | common_args, 129 | )?; 130 | add_file( 131 | &datetime, 132 | "templates".to_string(), 133 | &filename, 134 | vec_templates.to_vec(), 135 | &mut json_result, 136 | common_args, 137 | )?; 138 | } 139 | // All in zip file 140 | if common_args.zip { 141 | make_a_zip( 142 | &datetime, 143 | &filename, 144 | &common_args.path, 145 | &json_result); 146 | } 147 | Ok(()) 148 | } 149 | 150 | /// Function to create the .json file. 151 | fn add_file( 152 | datetime: &String, 153 | name: String, 154 | domain_format: &String, 155 | vec_json: Vec, 156 | json_result: &mut HashMap, 157 | common_args: &Options, 158 | ) -> std::io::Result<()> 159 | { 160 | debug!("Making {}.json",&name); 161 | 162 | let path = &common_args.path; 163 | let zip = common_args.zip; 164 | 165 | // Prepare template and get result in const var 166 | let mut final_json = bh_41::prepare_final_json_file_template(BLOODHOUND_VERSION_4, name.to_owned()); 167 | 168 | // Add all object found 169 | final_json["data"] = vec_json.to_owned().into(); 170 | // change count number 171 | let count = vec_json.len(); 172 | final_json["meta"]["count"] = count.into(); 173 | 174 | if &name != "gpos" || !common_args.old_bloodhound { 175 | info!("{} {} parsed!", count.to_string().bold(),&name); 176 | } 177 | 178 | // result 179 | fs::create_dir_all(path)?; 180 | 181 | // Create json file if isn't zip 182 | if ! zip 183 | { 184 | let final_path = format!("{}/{}_{}_{}.json",path,datetime,domain_format,name); 185 | fs::write(&final_path, &final_json.to_string())?; 186 | info!("{} created!",final_path.bold()); 187 | } 188 | else 189 | { 190 | json_result.insert(format!("{}_{}.json",datetime,name).to_string(),final_json.to_owned().to_string()); 191 | } 192 | 193 | Ok(()) 194 | } 195 | 196 | /// Function to compress the JSON files into a zip archive 197 | fn make_a_zip( 198 | datetime: &String, 199 | domain: &String, 200 | path: &String, 201 | json_result: &HashMap 202 | ){ 203 | let final_path = format!("{}/{}_{}_rusthound.zip",path,datetime,domain); 204 | let mut file = File::create(&final_path).expect("Couldn't create file"); 205 | create_zip_archive(&mut file, json_result).expect("Couldn't create archive"); 206 | 207 | info!("{} created!",&final_path.bold()); 208 | } 209 | 210 | 211 | fn create_zip_archive(zip_filename: &mut T,json_result: &HashMap) -> ZipResult<()> { 212 | let mut writer = ZipWriter::new(zip_filename); 213 | // json file by json file 214 | trace!("Making the ZIP file"); 215 | 216 | for file in json_result 217 | { 218 | let filename = file.0; 219 | let content = file.1; 220 | trace!("Adding file {}",filename.bold()); 221 | writer.start_file(filename, FileOptions::default())?; 222 | writer.write_all(content.as_bytes())?; 223 | } 224 | 225 | writer.finish()?; 226 | Ok(()) 227 | } -------------------------------------------------------------------------------- /src/json/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utils to parse json output from ldap library 2 | pub use checker::*; 3 | pub use maker::*; 4 | pub use parser::*; 5 | pub use templates::*; 6 | 7 | pub mod checker; 8 | pub mod maker; 9 | pub mod parser; 10 | pub mod templates; -------------------------------------------------------------------------------- /src/json/parser/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use ldap3::SearchEntry; 3 | use regex::Regex; 4 | use indicatif::ProgressBar; 5 | use std::convert::TryInto; 6 | 7 | use log::info; 8 | use crate::args::Options; 9 | use crate::banner::progress_bar; 10 | use crate::enums::ldaptype::*; 11 | use crate::modules::adcs::parser::{parse_adcs_ca,parse_adcs_template}; 12 | 13 | pub mod bh_41; 14 | 15 | /// Function to get type for object by object 16 | pub fn parse_result_type( 17 | common_args: &Options, 18 | result: Vec, 19 | vec_users: &mut Vec, 20 | vec_groups: &mut Vec, 21 | vec_computers: &mut Vec, 22 | vec_ous: &mut Vec, 23 | vec_domains: &mut Vec, 24 | vec_gpos: &mut Vec, 25 | vec_fsps: &mut Vec, 26 | vec_containers: &mut Vec, 27 | vec_trusts: &mut Vec, 28 | vec_cas: &mut Vec, 29 | vec_templates: &mut Vec, 30 | 31 | dn_sid: &mut HashMap, 32 | sid_type: &mut HashMap, 33 | fqdn_sid: &mut HashMap, 34 | fqdn_ip: &mut HashMap, 35 | adcs_templates: &mut HashMap>, 36 | ) 37 | { 38 | // Domain name 39 | let domain = &common_args.domain; 40 | 41 | // Needed for progress bar stats 42 | let pb = ProgressBar::new(1); 43 | let mut count = 0; 44 | let total = result.len(); 45 | 46 | info!("Starting the LDAP objects parsing..."); 47 | for entry in result { 48 | // Start parsing with Type matching 49 | let cloneresult = entry.clone(); 50 | //println!("{:?}",&entry); 51 | let atype = get_type(entry).unwrap_or(Type::Unknown); 52 | match atype { 53 | Type::User => { 54 | let user = parse_user( 55 | cloneresult, 56 | domain, 57 | dn_sid, 58 | sid_type, 59 | common_args.adcs, 60 | ); 61 | vec_users.push(user); 62 | } 63 | Type::Group => { 64 | let group = parse_group( 65 | cloneresult, 66 | domain, 67 | dn_sid, 68 | sid_type, 69 | ); 70 | vec_groups.push(group); 71 | } 72 | Type::Computer => { 73 | let computer = parse_computer( 74 | cloneresult, 75 | domain, 76 | dn_sid, 77 | sid_type, 78 | fqdn_sid, 79 | fqdn_ip, 80 | ); 81 | vec_computers.push(computer); 82 | } 83 | Type::Ou => { 84 | let ou = parse_ou( 85 | cloneresult, 86 | domain, 87 | dn_sid, 88 | sid_type, 89 | ); 90 | vec_ous.push(ou); 91 | } 92 | Type::Domain => { 93 | let domain = parse_domain( 94 | cloneresult, 95 | domain, 96 | dn_sid, 97 | sid_type, 98 | ); 99 | vec_domains.push(domain); 100 | } 101 | Type::Gpo => { 102 | let gpo = parse_gpo( 103 | cloneresult, 104 | domain, 105 | dn_sid, 106 | sid_type, 107 | ); 108 | vec_gpos.push(gpo); 109 | } 110 | Type::ForeignSecurityPrincipal => { 111 | let security_principal = parse_fsp( 112 | cloneresult, 113 | domain, 114 | dn_sid, 115 | sid_type, 116 | ); 117 | vec_fsps.push(security_principal); 118 | } 119 | Type::Container => { 120 | let re = Regex::new(r"[0-9a-z-A-Z]{1,}-[0-9a-z-A-Z]{1,}-[0-9a-z-A-Z]{1,}-[0-9a-z-A-Z]{1,}").unwrap(); 121 | if re.is_match(&cloneresult.dn.to_uppercase()) 122 | { 123 | //trace!("Container not to add: {}",&cloneresult.dn.to_uppercase()); 124 | continue 125 | } 126 | let re = Regex::new(r"CN=DOMAINUPDATES,CN=SYSTEM,").unwrap(); 127 | if re.is_match(&cloneresult.dn.to_uppercase()) 128 | { 129 | //trace!("Container not to add: {}",&cloneresult.dn.to_uppercase()); 130 | continue 131 | } 132 | //trace!("Container: {}",&cloneresult.dn.to_uppercase()); 133 | let container = parse_container( 134 | cloneresult, 135 | domain, 136 | dn_sid, 137 | sid_type, 138 | ); 139 | vec_containers.push(container); 140 | } 141 | Type::Trust => { 142 | let trust = parse_trust( 143 | cloneresult, 144 | domain 145 | ); 146 | vec_trusts.push(trust); 147 | } 148 | Type::AdcsAuthority => { 149 | let adcs_ca = parse_adcs_ca( 150 | cloneresult.to_owned(), 151 | domain, 152 | adcs_templates, 153 | common_args.old_bloodhound, 154 | ); 155 | vec_cas.push(adcs_ca); 156 | } 157 | Type::AdcsTemplate => { 158 | let adcs_template = parse_adcs_template( 159 | cloneresult.to_owned(), 160 | domain, 161 | common_args.old_bloodhound, 162 | ); 163 | vec_templates.push(adcs_template); 164 | } 165 | Type::Unknown => { 166 | let _unknown = parse_unknown(cloneresult, domain); 167 | } 168 | } 169 | // Manage progress bar 170 | // Pourcentage (%) = 100 x Valeur partielle/Valeur totale 171 | count += 1; 172 | let pourcentage = 100 * count / total; 173 | progress_bar(pb.to_owned(),"Parsing LDAP objects".to_string(),pourcentage.try_into().unwrap(),"%".to_string()); 174 | } 175 | pb.finish_and_clear(); 176 | info!("Parsing LDAP objects finished!"); 177 | } 178 | 179 | 180 | /// Parse user. Select parser based on BH version. 181 | pub fn parse_user( 182 | result: SearchEntry, 183 | domain: &String, 184 | dn_sid: &mut HashMap, 185 | sid_type: &mut HashMap, 186 | adcs: bool, 187 | ) -> serde_json::value::Value { 188 | bh_41::parse_user(result, domain, dn_sid, sid_type, adcs) 189 | } 190 | 191 | /// Parse group. Select parser based on BH version. 192 | pub fn parse_group( 193 | result: SearchEntry, 194 | domain: &String, 195 | dn_sid: &mut HashMap, 196 | sid_type: &mut HashMap, 197 | ) -> serde_json::value::Value { 198 | bh_41::parse_group(result, domain, dn_sid, sid_type) 199 | } 200 | 201 | /// Parse computer. Select parser based on BH version. 202 | pub fn parse_computer( 203 | result: SearchEntry, 204 | domain: &String, 205 | dn_sid: &mut HashMap, 206 | sid_type: &mut HashMap, 207 | fqdn_sid: &mut HashMap, 208 | fqdn_ip: &mut HashMap, 209 | ) -> serde_json::value::Value { 210 | bh_41::parse_computer(result, domain, dn_sid, sid_type, fqdn_sid, fqdn_ip) 211 | } 212 | 213 | /// Parse ou. Select parser based on BH version. 214 | pub fn parse_ou( 215 | result: SearchEntry, 216 | domain: &String, 217 | dn_sid: &mut HashMap, 218 | sid_type: &mut HashMap, 219 | ) -> serde_json::value::Value { 220 | bh_41::parse_ou(result, domain, dn_sid, sid_type) 221 | } 222 | 223 | /// Parse gpo. Select parser based on BH version. 224 | pub fn parse_gpo( 225 | result: SearchEntry, 226 | domain: &String, 227 | dn_sid: &mut HashMap, 228 | sid_type: &mut HashMap, 229 | ) -> serde_json::value::Value { 230 | bh_41::parse_gpo(result, domain, dn_sid, sid_type) 231 | } 232 | 233 | /// Parse domain. Select parser based on BH version. 234 | pub fn parse_domain( 235 | result: SearchEntry, 236 | domain: &String, 237 | dn_sid: &mut HashMap, 238 | sid_type: &mut HashMap, 239 | ) -> serde_json::value::Value { 240 | bh_41::parse_domain(result, domain, dn_sid, sid_type) 241 | } 242 | 243 | /// Parse ForeignSecurityPrincipal object. Select parser based on BH version. 244 | pub fn parse_fsp( 245 | result: SearchEntry, 246 | domain: &String, 247 | dn_sid: &mut HashMap, 248 | sid_type: &mut HashMap, 249 | ) -> serde_json::value::Value { 250 | bh_41::parse_fsp(result, domain, dn_sid, sid_type) 251 | } 252 | 253 | /// Parse Containers object. Select parser based on BH version new in BH4.1+ 254 | pub fn parse_container( 255 | result: SearchEntry, 256 | domain: &String, 257 | dn_sid: &mut HashMap, 258 | sid_type: &mut HashMap, 259 | ) -> serde_json::value::Value { 260 | bh_41::parse_container(result, domain, dn_sid, sid_type) 261 | } 262 | 263 | /// Parse Trust domain object. Select parser based on BH version. 264 | pub fn parse_trust( 265 | result: SearchEntry, 266 | _domain: &String, 267 | ) -> serde_json::value::Value { 268 | bh_41::parse_trust(result, _domain) 269 | } 270 | 271 | /// Parse unknown object. Select parser based on BH version. 272 | pub fn parse_unknown( 273 | result: SearchEntry, 274 | _domain: &String, 275 | ) -> serde_json::value::Value { 276 | bh_41::parse_unknown(result, _domain) 277 | } -------------------------------------------------------------------------------- /src/json/templates/bh_41.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | 3 | /// Return the json template for one user 4 | pub fn prepare_user_json_template() -> serde_json::value::Value 5 | { 6 | return json!({ 7 | "ObjectIdentifier": "SID", 8 | "IsDeleted": false, 9 | "IsACLProtected": false, 10 | "Properties": { 11 | "domain": "domain.com", 12 | "name": "name@domain.com", 13 | "domainsid": "SID", 14 | "distinguishedname": "CN=name,DN=domain,DN=com", 15 | "highvalue": false, 16 | "description": null, 17 | "whencreated": -1, 18 | "sensitive": false, 19 | "dontreqpreauth": false, 20 | "passwordnotreqd": false, 21 | "unconstraineddelegation": false, 22 | "pwdneverexpires": false, 23 | "enabled": true, 24 | "trustedtoauth": false, 25 | "lastlogon": -1, 26 | "lastlogontimestamp": -1, 27 | "pwdlastset": -1, 28 | "serviceprincipalnames": [], 29 | "hasspn": false, 30 | "displayname": null, 31 | "email": null, 32 | "title": null, 33 | "homedirectory": null, 34 | "logonscript": null, 35 | "samaccountname": null, 36 | "userpassword": null, 37 | "unixpassword": null, 38 | "unicodepassword": null, 39 | "sfupassword": null, 40 | "admincount": false, 41 | "sidhistory": [], 42 | "allowedtodelegate": [] 43 | }, 44 | "PrimaryGroupSID": null, 45 | "SPNTargets": [], 46 | "Aces": [], 47 | "AllowedToDelegate": [], 48 | // TODO 49 | "HasSIDHistory": [], 50 | }); 51 | } 52 | 53 | /// Return the json template for one group 54 | pub fn prepare_group_json_template() -> serde_json::value::Value 55 | { 56 | return json!({ 57 | "ObjectIdentifier": "SID", 58 | "IsDeleted": false, 59 | "IsACLProtected": false, 60 | "Properties": { 61 | "domain": "domain.com", 62 | "domainsid": "SID", 63 | "name": "name@domain.com", 64 | "distinguishedname": "DN", 65 | "samaccountname": null, 66 | "highvalue": false, 67 | "admincount": false, 68 | "description": null, 69 | "whencreated": -1 70 | }, 71 | "Members": [], 72 | "Aces": [], 73 | }); 74 | } 75 | 76 | /// Return the json template for one computer 77 | pub fn prepare_computer_json_template() -> serde_json::value::Value 78 | { 79 | return json!({ 80 | "ObjectIdentifier": "SID", 81 | "IsDeleted": false, 82 | "IsACLProtected": false, 83 | "Properties": { 84 | "domain": "domain.com", 85 | "name": "name.domain.com", 86 | "distinguishedname": "DN", 87 | "highvalue": false, 88 | "samaccountname": null, 89 | "domainsid": "SID", 90 | "haslaps": false, 91 | "description": null, 92 | "whencreated": -1, 93 | "enabled": true, 94 | "unconstraineddelegation": false, 95 | "trustedtoauth": false, 96 | "lastlogon": -1, 97 | "lastlogontimestamp": -1, 98 | "pwdlastset": -1, 99 | "serviceprincipalnames": [], 100 | "operatingsystem": null, 101 | "sidhistory": [], 102 | }, 103 | "PrimaryGroupSID": "PGSID", 104 | "Aces": [], 105 | "AllowedToDelegate": [], 106 | "AllowedToAct": [], 107 | "Status": null, 108 | //TODO 109 | "HasSIDHistory": [], 110 | "Sessions": { 111 | "Results": [], 112 | "Collected": false, 113 | "FailureReason": null 114 | }, 115 | "PrivilegedSessions": { 116 | "Results": [], 117 | "Collected": false, 118 | "FailureReason": null 119 | }, 120 | "RegistrySessions": { 121 | "Results": [], 122 | "Collected": false, 123 | "FailureReason": null 124 | }, 125 | "LocalAdmins": { 126 | "Results": [], 127 | "Collected": false, 128 | "FailureReason": null 129 | }, 130 | "RemoteDesktopUsers": { 131 | "Results": [], 132 | "Collected": false, 133 | "FailureReason": null 134 | }, 135 | "DcomUsers": { 136 | "Results": [], 137 | "Collected": false, 138 | "FailureReason": null 139 | }, 140 | "PSRemoteUsers": { 141 | "Results": [], 142 | "Collected": false, 143 | "FailureReason": null 144 | }, 145 | }); 146 | } 147 | 148 | /// Return the json template for one OU 149 | pub fn prepare_ou_json_template() -> serde_json::value::Value 150 | { 151 | return json!({ 152 | "ObjectIdentifier": "SID", 153 | "IsDeleted": false, 154 | "IsACLProtected": false, 155 | "Properties": { 156 | "name": "name@domain.com", 157 | "domain": "domain.com", 158 | "domainsid": "SID", 159 | "distinguishedname": "DN", 160 | "highvalue": false, 161 | "description": null, 162 | "blocksinheritance": false, 163 | "whencreated": -1 164 | }, 165 | "ACLProtected": false, 166 | "Links": [], 167 | "ChildObjects": [], 168 | "Aces": [], 169 | // TODO 170 | "GPOChanges": { 171 | "LocalAdmins" : [], 172 | "RemoteDesktopUsers" : [], 173 | "DcomUsers" : [], 174 | "PSRemoteUsers" : [], 175 | // OK for affected computers 176 | "AffectedComputers" : [] 177 | }, 178 | }); 179 | } 180 | 181 | /// Return the json template for one GPO 182 | pub fn prepare_gpo_json_template() -> serde_json::value::Value 183 | { 184 | return json!({ 185 | "IsDeleted": false, 186 | "IsACLProtected": false, 187 | "Properties": { 188 | "name": "name@domain.com", 189 | "domain": "domain.com", 190 | "domainsid": "SID", 191 | "distinguishedname": "DN", 192 | "highvalue": false, 193 | "description": null, 194 | "gpcpath": "GPO_PATH", 195 | "whencreated": -1 196 | }, 197 | "ObjectIdentifier": "SID", 198 | "Aces": [], 199 | }); 200 | } 201 | 202 | /// Return the json template for one domain 203 | pub fn prepare_domain_json_template() -> serde_json::value::Value 204 | { 205 | return json!({ 206 | "ChildObjects": [], 207 | "Trusts": [], 208 | "Aces": [], 209 | "ObjectIdentifier": "SID", 210 | "IsACLProtected": false, 211 | "IsDeleted": false, 212 | "Properties": { 213 | "domain": "domain.com", 214 | "name": "domain.com", 215 | "distinguishedname": "DN", 216 | "domainsid": "SID", 217 | "description": null, 218 | "highvalue": true, 219 | "whencreated": -1, 220 | "functionallevel": "Unknown", 221 | }, 222 | // Todo 223 | "GPOChanges": { 224 | "LocalAdmins" : [], 225 | "RemoteDesktopUsers" : [], 226 | "DcomUsers" : [], 227 | "PSRemoteUsers" : [], 228 | // Ok for affected computers 229 | "AffectedComputers" : [] 230 | }, 231 | // Todo 232 | "Links": [], 233 | }); 234 | } 235 | 236 | /// Return the json template for one ForeignSecurityPrincipal 237 | pub fn prepare_fsp_json_template() -> serde_json::value::Value 238 | { 239 | return json!({ 240 | "ObjectIdentifier": "SID", 241 | "IsDeleted": false, 242 | "IsACLProtected": false, 243 | "Properties": { 244 | "name": "domain.com", 245 | "domainsid": "SID", 246 | "distinguishedname": "DN", 247 | "type":"Unknown", 248 | "whencreated": -1 249 | }, 250 | }); 251 | } 252 | 253 | /// Return the json template for one Container 254 | pub fn prepare_container_json_template() -> serde_json::value::Value 255 | { 256 | return json!({ 257 | "ObjectIdentifier": "SID", 258 | "IsDeleted": false, 259 | "IsACLProtected": false, 260 | "Properties": { 261 | "name": "xyz@domain.com", 262 | "domain": "domain.local", 263 | "domainsid": "SID", 264 | "distinguishedname": "DN", 265 | "highvalue": false, 266 | }, 267 | "ChildObjects": [], 268 | "Aces": [], 269 | }); 270 | } 271 | 272 | /// Return the json template for one member 273 | pub fn prepare_member_json_template() -> serde_json::value::Value 274 | { 275 | return json!({ 276 | "ObjectIdentifier": "", 277 | "ObjectType": "" 278 | }); 279 | } 280 | 281 | /// Return the json template for one acl relation 282 | pub fn prepare_acl_relation_template() -> serde_json::value::Value 283 | { 284 | return json!({ 285 | "RightName": "", 286 | "IsInherited": false, 287 | "PrincipalSID": "", 288 | "PrincipalType": "" 289 | }); 290 | } 291 | 292 | /// Return the json template for final file 293 | pub fn prepare_final_json_file_template(version: i8, bh_type: String) -> serde_json::value::Value 294 | { 295 | return json!({ 296 | "data": [], 297 | "meta": { 298 | "methods": 0, 299 | "type": bh_type, 300 | "count": 0, 301 | "version": version 302 | } 303 | }); 304 | } 305 | 306 | /// Return the json template for gplink 307 | pub fn prepare_gplink_json_template() -> serde_json::value::Value 308 | { 309 | return json!({ 310 | "IsEnforced": false, 311 | "GUID": "GUID" 312 | }); 313 | } 314 | 315 | /// Return the json template for default group 316 | pub fn prepare_default_group_json_template() -> serde_json::value::Value 317 | { 318 | return json!({ 319 | "Members": [], 320 | "Aces": [], 321 | "ObjectIdentifier": "SID", 322 | "IsDeleted": false, 323 | "IsACLProtected": false, 324 | "Properties": { 325 | "name": "name@domain.com", 326 | "domainsid": "SID", 327 | "domain": "domain.com", 328 | "highvalue": false, 329 | }, 330 | }); 331 | } 332 | 333 | /// Return the json template for default user 334 | pub fn prepare_default_user_json_template() -> serde_json::value::Value 335 | { 336 | return json!({ 337 | "AllowedToDelegate": [], 338 | "IsDeleted": false, 339 | "IsACLProtected": false, 340 | "ObjectIdentifier": "SID", 341 | "PrimaryGroupSID": null, 342 | "Properties": { 343 | "domain": "domain.com", 344 | "domainsid": "SID", 345 | "name": "name@domain.com", 346 | }, 347 | "SPNTargets": [], 348 | "Aces": [], 349 | // TODO 350 | "HasSIDHistory": [], 351 | }); 352 | } 353 | 354 | /// Return the json template for mssqlsvc spn 355 | pub fn prepare_mssqlsvc_spn_json_template() -> serde_json::value::Value 356 | { 357 | return json!({ 358 | "ComputerSID": "", 359 | "Port": 1433, 360 | "Service": "SQLAdmin" 361 | }); 362 | } 363 | 364 | /// Return the json template for one trust domain 365 | pub fn prepare_trust_json_template() -> serde_json::value::Value 366 | { 367 | return json!({ 368 | "TargetDomainSid": "SID", 369 | "TargetDomainName": "DOMAIN.LOCAL", 370 | "IsTransitive": null, 371 | "SidFilteringEnabled": null, 372 | "TrustDirection": 0, 373 | "TrustType": 0 374 | }); 375 | } 376 | 377 | /// ADCS needed template for BloodHound ly4k version 378 | /// Return the json template for one Certificate Authority (CA) 379 | pub fn prepare_adcs_ca_json_template() -> serde_json::value::Value 380 | { 381 | return json!({ 382 | "Properties": { 383 | "name": "CANAME@DOMAIN.LOCAL", 384 | "highvalue": false, // 385 | "CA Name": "CANAME", 386 | "DNS Name": "fqdn.domain.local", 387 | "Certificate Subject": "DN", 388 | "Certificate Serial Number": "0000000000000000000000000000", 389 | "Certificate Validity Start": "0000-00-00 00:00:00+00:00", 390 | "Certificate Validity End": "0000-00-00 00:00:00+00:00", 391 | "Web Enrollment": "Enabled", 392 | "User Specified SAN": "Enabled", //TODO Need DCERPC 393 | "Request Disposition": "Issue", //TODO Need DCERPC 394 | "domain": "DOMAIN.LOCAL" 395 | }, 396 | "ObjectIdentifier": "GUID", 397 | "Aces": [] 398 | }); 399 | } 400 | 401 | /// ADCS needed template for BloodHound ly4k version 402 | /// Return the json template for one Certificate Template 403 | pub fn prepare_adcs_template_json_template() -> serde_json::value::Value 404 | { 405 | return json!({ 406 | "Properties": { 407 | "name": "NAME@DOMAIN.LOCAL", 408 | "highvalue": false, 409 | "Template Name": "NAME", 410 | "Display Name": "NAME", 411 | "Certificate Authorities": [], 412 | "Enabled": false, 413 | "Client Authentication": false, 414 | "Enrollment Agent": false, 415 | "Any Purpose": false, 416 | "Enrollee Supplies Subject": false, 417 | "Certificate Name Flag": [], 418 | "Enrollment Flag": [], 419 | "Private Key Flag": [], 420 | "Extended Key Usage": [], 421 | "Requires Manager Approval": false, 422 | "Requires Key Archival": false, 423 | "Authorized Signatures Required": 0, 424 | "Validity Period": "x", 425 | "Renewal Period": "x", 426 | "domain": "DOMAIN.LOCAL" 427 | }, 428 | "ObjectIdentifier": "GUID", 429 | "Aces": [], 430 | //"cas_ids": [] //automatically add if is ly4k BloodHound version 431 | }); 432 | } -------------------------------------------------------------------------------- /src/json/templates/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bh_41; -------------------------------------------------------------------------------- /src/ldap.rs: -------------------------------------------------------------------------------- 1 | //! Run a LDAP enumeration and parse results 2 | //! 3 | //! This module will prepare your connection and request the LDAP server to retrieve all the information needed to create the json files. 4 | //! 5 | //! rusthound sends only one request to the LDAP server, if the result of this one is higher than the limit of the LDAP server limit it will be split in several requests to avoid having an error 4 (LDAP_SIZELIMIT_EXCEED). 6 | //! 7 | //! Example in rust 8 | //! 9 | //! ``` 10 | //! let search = ldap_search(...) 11 | //! ``` 12 | use crate::errors::{Result}; 13 | use colored::Colorize; 14 | use ldap3::adapters::{Adapter, EntriesOnly}; 15 | use ldap3::{adapters::PagedResults, controls::RawControl, LdapConnAsync, LdapConnSettings}; 16 | use ldap3::{Scope, SearchEntry}; 17 | use log::{info, debug, error}; 18 | use std::process; 19 | use indicatif::ProgressBar; 20 | use crate::banner::progress_bar; 21 | use std::io::{self, Write, stdin}; 22 | 23 | /// Function to request all AD values. 24 | pub async fn ldap_search( 25 | ldaps: bool, 26 | ip: &String, 27 | port: &String, 28 | domain: &String, 29 | ldapfqdn: &String, 30 | username: &String, 31 | password: &String, 32 | adcs: bool, 33 | kerberos: bool, 34 | ) -> Result> { 35 | // Construct LDAP args 36 | let ldap_args = ldap_constructor(ldaps, ip, port, domain, ldapfqdn, username, password, adcs, kerberos); 37 | 38 | // LDAP connection 39 | let consettings = LdapConnSettings::new().set_no_tls_verify(true); 40 | let (conn, mut ldap) = LdapConnAsync::with_settings(consettings, &ldap_args.s_url).await?; 41 | ldap3::drive!(conn); 42 | 43 | if !kerberos { 44 | debug!("Trying to connect with simple_bind() function (username:password)"); 45 | let res = ldap.simple_bind(&ldap_args.s_username, &ldap_args.s_password).await?.success(); 46 | match res { 47 | Ok(_res) => { 48 | info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green()); 49 | info!("Starting data collection..."); 50 | }, 51 | Err(err) => { 52 | error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red()); 53 | process::exit(0x0100); 54 | } 55 | } 56 | } 57 | else 58 | { 59 | debug!("Trying to connect with sasl_gssapi_bind() function (kerberos session)"); 60 | if !&ldapfqdn.contains("not set") { 61 | #[cfg(not(feature = "nogssapi"))] 62 | gssapi_connection(&mut ldap,&ldapfqdn,&domain).await?; 63 | #[cfg(feature = "nogssapi")]{ 64 | error!("Kerberos auth and GSSAPI not compatible with current os!"); 65 | process::exit(0x0100); 66 | } 67 | } else { 68 | error!("Need Domain Controler FQDN to bind GSSAPI connection. Please use '{}'\n", "-f DC01.DOMAIN.LAB".bold()); 69 | process::exit(0x0100); 70 | } 71 | } 72 | 73 | // Prepare LDAP result vector 74 | let mut rs: Vec = Vec::new(); 75 | 76 | // For the following naming context 77 | // namingContexts: DC=domain,DC=local 78 | // namingContexts: CN=Configuration,DC=domain,DC=local (needed for AD CS datas) 79 | for cn in &ldap_args.s_dc { 80 | // Set control LDAP_SERVER_SD_FLAGS_OID to get nTSecurityDescriptor 81 | // https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID 82 | let ctrls = RawControl { 83 | ctype: String::from("1.2.840.113556.1.4.801"), 84 | crit: true, 85 | val: Some(vec![48,3,2,1,5]), 86 | }; 87 | ldap.with_controls(ctrls.to_owned()); 88 | 89 | // Prepare filter 90 | let mut _s_filter: &str = ""; 91 | if cn.contains("Configuration") { 92 | _s_filter = "(|(objectclass=pKIEnrollmentService)(objectclass=pkicertificatetemplate)(objectclass=subschema))"; 93 | } else { 94 | _s_filter = "(objectClass=*)"; 95 | } 96 | 97 | // Every 999 max value in ldap response (err 4 ldap) 98 | let adapters: Vec>> = vec![ 99 | Box::new(EntriesOnly::new()), 100 | Box::new(PagedResults::new(999)), 101 | ]; 102 | 103 | // Streaming search with adaptaters and filters 104 | let mut search = ldap.streaming_search_with( 105 | adapters, // Adapter which fetches Search results with a Paged Results control. 106 | cn, 107 | Scope::Subtree, 108 | _s_filter, 109 | vec!["*", "nTSecurityDescriptor"], 110 | // Without the presence of this control, the server returns an SD only when the SD attribute name is explicitly mentioned in the requested attribute list. 111 | // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/932a7a8d-8c93-4448-8093-c79b7d9ba499 112 | ).await?; 113 | 114 | // Wait and get next values 115 | let pb = ProgressBar::new(1); 116 | let mut count = 0; 117 | while let Some(entry) = search.next().await? { 118 | let entry = SearchEntry::construct(entry); 119 | //trace!("{:?}", &entry); 120 | // Manage progress bar 121 | count += 1; 122 | progress_bar(pb.to_owned(),"LDAP objects retreived".to_string(),count,"#".to_string()); 123 | // Push all result in rs vec() 124 | rs.push(entry); 125 | } 126 | pb.finish_and_clear(); 127 | 128 | let res = search.finish().await.success(); 129 | match res { 130 | Ok(_res) => info!("All data collected for NamingContext {}",&cn.bold()), 131 | Err(err) => { 132 | error!("No data collected on {}! Reason: {err}",&cn.bold().red()); 133 | } 134 | } 135 | } 136 | // If no result exit program 137 | if rs.len() <= 0 { 138 | process::exit(0x0100); 139 | } 140 | 141 | // Terminate the connection to the server 142 | ldap.unbind().await?; 143 | 144 | // Return the vector with the result 145 | return Ok(rs); 146 | } 147 | 148 | /// Structure containing the LDAP connection arguments. 149 | struct LdapArgs { 150 | s_url: String, 151 | s_dc: Vec, 152 | _s_email: String, 153 | s_username: String, 154 | s_password: String, 155 | } 156 | 157 | /// Function to prepare LDAP arguments. 158 | fn ldap_constructor( 159 | ldaps: bool, 160 | ip: &String, 161 | port: &String, 162 | domain: &String, 163 | ldapfqdn: &String, 164 | username: &String, 165 | password: &String, 166 | adcs: bool, 167 | kerberos: bool, 168 | ) -> LdapArgs { 169 | // Prepare ldap url 170 | let s_url = prepare_ldap_url(ldaps, ip, port, domain); 171 | 172 | // Prepare full DC chain 173 | let s_dc = prepare_ldap_dc(domain,adcs); 174 | 175 | // Username prompt 176 | let mut s=String::new(); 177 | let mut _s_username: String; 178 | if username.contains("not set") && !kerberos { 179 | print!("Username: "); 180 | io::stdout().flush().unwrap(); 181 | stdin().read_line(&mut s).expect("Did not enter a correct username"); 182 | io::stdout().flush().unwrap(); 183 | if let Some('\n')=s.chars().next_back() { 184 | s.pop(); 185 | } 186 | if let Some('\r')=s.chars().next_back() { 187 | s.pop(); 188 | } 189 | _s_username = s.to_owned(); 190 | } else { 191 | _s_username = username.to_owned(); 192 | } 193 | 194 | // Format username and email 195 | let mut s_email: String = "".to_owned(); 196 | if !_s_username.contains("@") { 197 | s_email.push_str(&_s_username.to_string()); 198 | s_email.push_str("@"); 199 | s_email.push_str(domain); 200 | _s_username = s_email.to_string(); 201 | } else { 202 | s_email = _s_username.to_string().to_lowercase(); 203 | } 204 | 205 | // Password prompt 206 | let mut _s_password: String = String::new(); 207 | if !_s_username.contains("not set") && !kerberos { 208 | if password.contains("not set") { 209 | // Prompt for user password 210 | let rpass: String = rpassword::prompt_password("Password: ").unwrap_or("not set".to_string()); 211 | _s_password = rpass; 212 | } else { 213 | _s_password = password.to_owned(); 214 | } 215 | } else { 216 | _s_password = password.to_owned(); 217 | } 218 | 219 | // Print infos if verbose mod is set 220 | debug!("IP: {}", ip); 221 | debug!("PORT: {}", port); 222 | debug!("FQDN: {}", ldapfqdn); 223 | debug!("Url: {}", s_url); 224 | debug!("Domain: {}", domain); 225 | debug!("Username: {}", _s_username); 226 | debug!("Email: {}", s_email.to_lowercase()); 227 | debug!("Password: {}", _s_password); 228 | debug!("DC: {:?}", s_dc); 229 | debug!("ADCS: {:?}", adcs); 230 | debug!("Kerberos: {:?}", kerberos); 231 | 232 | LdapArgs { 233 | s_url: s_url.to_string(), 234 | s_dc: s_dc, 235 | _s_email: s_email.to_string().to_lowercase(), 236 | s_username: s_email.to_string().to_lowercase(), 237 | s_password: _s_password.to_string(), 238 | } 239 | } 240 | 241 | /// Function to prepare LDAP url. 242 | fn prepare_ldap_url(ldaps: bool, ip: &String, port: &String, domain: &String) -> String { 243 | let mut url: String = "".to_owned(); 244 | 245 | // ldap or ldaps? 246 | if port.contains("636") || ldaps { 247 | url.push_str("ldaps://"); 248 | } 249 | else 250 | { 251 | url.push_str("ldap://"); 252 | } 253 | 254 | // If ldapip is set apply it to ldap url 255 | if ip.contains("not set") { 256 | url.push_str(&domain); 257 | } else { 258 | url.push_str(&ip); 259 | } 260 | 261 | // Push the port 262 | //trace!("port: {:?}", port); 263 | if port.contains("not set") || port == "636" || port == "389" { 264 | return url 265 | } 266 | else 267 | { 268 | //trace!("port set"); 269 | let mut final_port: String = ":".to_owned(); 270 | final_port.push_str(&port); 271 | url.push_str(&final_port); 272 | return url 273 | } 274 | } 275 | 276 | /// Function to prepare LDAP DC from DOMAIN.LOCAL 277 | pub fn prepare_ldap_dc(domain: &String, adcs: bool) -> Vec { 278 | 279 | let mut dc: String = "".to_owned(); 280 | let mut naming_context: Vec = Vec::new(); 281 | 282 | // Format DC 283 | if !domain.contains(".") { 284 | dc.push_str("DC="); 285 | dc.push_str(&domain); 286 | naming_context.push(dc[..].to_string()); 287 | } 288 | else 289 | { 290 | let split = domain.split("."); 291 | let slen = split.to_owned().count(); 292 | let mut cpt = 1; 293 | for s in split { 294 | if cpt < slen { 295 | dc.push_str("DC="); 296 | dc.push_str(&s); 297 | dc.push_str(","); 298 | } 299 | else 300 | { 301 | dc.push_str("DC="); 302 | dc.push_str(&s); 303 | } 304 | cpt += 1; 305 | } 306 | naming_context.push(dc[..].to_string()); 307 | } 308 | 309 | if adcs { 310 | naming_context.push(format!("{}{}","CN=Configuration,",dc[..].to_string())); 311 | } 312 | 313 | return naming_context 314 | } 315 | 316 | /// Function to make GSSAPI ldap connection. 317 | #[cfg(not(feature = "nogssapi"))] 318 | async fn gssapi_connection( 319 | ldap: &mut ldap3::Ldap, 320 | ldapfqdn: &String, 321 | domain: &String, 322 | ) -> Result<()> { 323 | let res = ldap.sasl_gssapi_bind(ldapfqdn).await?.success(); 324 | match res { 325 | Ok(_res) => { 326 | info!("Connected to {} Active Directory!", domain.to_uppercase().bold().green()); 327 | info!("Starting data collection..."); 328 | }, 329 | Err(err) => { 330 | error!("Failed to authenticate to {} Active Directory. Reason: {err}\n", domain.to_uppercase().bold().red()); 331 | process::exit(0x0100); 332 | } 333 | } 334 | Ok(()) 335 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //!

2 | //! 3 | //!

4 | //! 5 | //! RustHound is a cross-platform and cross-compiled BloodHound collector tool, written in Rust. 6 | //! RustHound generate users,groups,computers,ous,gpos,containers,domains json files to analyze it with BloodHound application. 7 | //! 8 | //! You can either run the binary: 9 | //!``` 10 | //!--------------------------------------------------- 11 | //!Initializing RustHound at 13:37:00 UTC on 10/04/22 12 | //!Powered by g0h4n from OpenCyber 13 | //!--------------------------------------------------- 14 | //! 15 | //!RustHound 16 | //!g0h4n https://twitter.com/g0h4n_0 17 | //!Active Directory data collector for BloodHound. 18 | //! 19 | //!Usage: rusthound_musl [OPTIONS] --domain 20 | //! 21 | //!Options: 22 | //! -v... Set the level of verbosity 23 | //! -h, --help Print help 24 | //! -V, --version Print version 25 | //! 26 | //!REQUIRED VALUES: 27 | //! -d, --domain Domain name like: DOMAIN.LOCAL 28 | //! 29 | //!OPTIONAL VALUES: 30 | //! -u, --ldapusername LDAP username, like: user@domain.local 31 | //! -p, --ldappassword LDAP password 32 | //! -f, --ldapfqdn Domain Controler FQDN like: DC01.DOMAIN.LOCAL or just DC01 33 | //! -i, --ldapip Domain Controller IP address like: 192.168.1.10 34 | //! -P, --ldapport LDAP port [default: 389] 35 | //! -n, --name-server Alternative IP address name server to use for DNS queries 36 | //! -o, --output Output directory where you would like to save JSON files [default: ./] 37 | //! 38 | //!OPTIONAL FLAGS: 39 | //! --ldaps Force LDAPS using for request like: ldaps://DOMAIN.LOCAL/ 40 | //! -k, --kerberos Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters for Linux. 41 | //! --dns-tcp Use TCP instead of UDP for DNS queries 42 | //! --dc-only Collects data only from the domain controller. Will not try to retrieve CA security/configuration or check for Web Enrollment 43 | //! --old-bloodhound For ADCS only. Output result as BloodHound data for the original BloodHound version from @BloodHoundAD without PKI support 44 | //! -z, --zip Compress the JSON files into a zip archive 45 | //! 46 | //!OPTIONAL MODULES: 47 | //! --fqdn-resolver Use fqdn-resolver module to get computers IP address 48 | //! --adcs Use ADCS module to enumerate Certificate Templates, Certificate Authorities and other configurations. 49 | //! (For the custom-built BloodHound version from @ly4k with PKI support) 50 | //!``` 51 | //! Or build your own using the ldap_search() function: 52 | //! ``` 53 | //!let result = ldap_search( 54 | //! &ldaps, 55 | //! &ip, 56 | //! &port, 57 | //! &domain, 58 | //! &ldapfqdn, 59 | //! &username, 60 | //! &password, 61 | //!); 62 | //!``` 63 | //! Here is an example of how to use rusthound: 64 | //! ![demo](https://raw.githubusercontent.com/OPENCYBER-FR/RustHound/main/img/demo.gif) 65 | //! 66 | pub mod args; 67 | pub mod banner; 68 | pub mod errors; 69 | pub mod ldap; 70 | pub mod exec; 71 | 72 | pub mod enums; 73 | pub mod json; 74 | pub mod modules; 75 | 76 | extern crate bitflags; 77 | extern crate chrono; 78 | extern crate regex; 79 | 80 | // Reimport key functions and structure 81 | #[doc(inline)] 82 | pub use crate::errors::Error; 83 | #[doc(inline)] 84 | pub use ldap::ldap_search; 85 | #[doc(inline)] 86 | pub use ldap3::SearchEntry; 87 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod modules; 2 | pub mod enums; 3 | pub mod json; 4 | 5 | pub mod args; 6 | pub mod banner; 7 | pub mod errors; 8 | pub mod ldap; 9 | pub mod exec; 10 | 11 | use log::{info,trace,error}; 12 | use std::collections::HashMap; 13 | 14 | use crate::errors::Result; 15 | use args::*; 16 | use banner::*; 17 | use env_logger::Builder; 18 | use ldap::*; 19 | 20 | use modules::*; 21 | use json::checker::*; 22 | use json::maker::make_result; 23 | use json::parser::*; 24 | 25 | /// Main of RustHound 26 | #[tokio::main] 27 | async fn main() -> Result<()> { 28 | // Banner 29 | print_banner(); 30 | 31 | // Get args 32 | #[cfg(not(feature = "noargs"))] 33 | let common_args: Options = extract_args(); 34 | #[cfg(feature = "noargs")] 35 | let common_args = auto_args(); 36 | 37 | // Build logger 38 | Builder::new() 39 | .filter(Some("rusthound"), common_args.verbose) 40 | .filter_level(log::LevelFilter::Error) 41 | .init(); 42 | 43 | // Get verbose level 44 | info!("Verbosity level: {:?}", common_args.verbose); 45 | 46 | // LDAP request to get all informations in result 47 | let result = ldap_search( 48 | common_args.ldaps, 49 | &common_args.ip, 50 | &common_args.port, 51 | &common_args.domain, 52 | &common_args.ldapfqdn, 53 | &common_args.username, 54 | &common_args.password, 55 | common_args.adcs, 56 | common_args.kerberos, 57 | ).await?; 58 | 59 | // Vector for content all 60 | let mut vec_users: Vec = Vec::new(); 61 | let mut vec_groups: Vec = Vec::new(); 62 | let mut vec_computers: Vec = Vec::new(); 63 | let mut vec_ous: Vec = Vec::new(); 64 | let mut vec_domains: Vec = Vec::new(); 65 | let mut vec_gpos: Vec = Vec::new(); 66 | let mut vec_fsps: Vec = Vec::new(); 67 | let mut vec_containers: Vec = Vec::new(); 68 | let mut vec_trusts: Vec = Vec::new(); 69 | let mut vec_cas: Vec = Vec::new(); 70 | let mut vec_templates: Vec = Vec::new(); 71 | 72 | // Hashmap to link DN to SID 73 | let mut dn_sid = HashMap::new(); 74 | // Hashmap to link DN to Type 75 | let mut sid_type = HashMap::new(); 76 | // Hashmap to link FQDN to SID 77 | let mut fqdn_sid = HashMap::new(); 78 | // Hashmap to link fqdn to an ip address 79 | let mut fqdn_ip = HashMap::new(); 80 | // Hashmap to link CA to enabled Templates 81 | let mut adcs_templates = HashMap::new(); 82 | 83 | // Analyze object by object 84 | // Get type and parse it to get values 85 | parse_result_type( 86 | &common_args, 87 | result, 88 | &mut vec_users, 89 | &mut vec_groups, 90 | &mut vec_computers, 91 | &mut vec_ous, 92 | &mut vec_domains, 93 | &mut vec_gpos, 94 | &mut vec_fsps, 95 | &mut vec_containers, 96 | &mut vec_trusts, 97 | &mut vec_cas, 98 | &mut vec_templates, 99 | &mut dn_sid, 100 | &mut sid_type, 101 | &mut fqdn_sid, 102 | &mut fqdn_ip, 103 | &mut adcs_templates, 104 | ); 105 | 106 | // Functions to replace and add missing values 107 | check_all_result( 108 | &common_args.domain, 109 | &mut vec_users, 110 | &mut vec_groups, 111 | &mut vec_computers, 112 | &mut vec_ous, 113 | &mut vec_domains, 114 | &mut vec_gpos, 115 | &mut vec_fsps, 116 | &mut vec_containers, 117 | &mut vec_trusts, 118 | &mut dn_sid, 119 | &mut sid_type, 120 | &mut fqdn_sid, 121 | &mut fqdn_ip, 122 | ); 123 | 124 | // Running modules 125 | run_modules( 126 | &common_args, 127 | &mut fqdn_ip, 128 | &mut vec_computers, 129 | &mut vec_cas, 130 | &mut vec_templates, 131 | &mut adcs_templates, 132 | &mut sid_type, 133 | ).await; 134 | 135 | // Add all in json files 136 | let res = make_result( 137 | &common_args, 138 | vec_users, 139 | vec_groups, 140 | vec_computers, 141 | vec_ous, 142 | vec_domains, 143 | vec_gpos, 144 | vec_containers, 145 | &mut vec_cas, 146 | &mut vec_templates, 147 | ); 148 | match res { 149 | Ok(_res) => trace!("Making json/zip files finished!"), 150 | Err(err) => error!("Error. Reason: {err}") 151 | } 152 | 153 | // End banner 154 | print_end_banner(); 155 | Ok(()) 156 | } -------------------------------------------------------------------------------- /src/modules/adcs/checker.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use std::collections::HashMap; 3 | use log::{info, debug, trace, error}; 4 | 5 | use std::io::prelude::*; 6 | use std::net::TcpStream; 7 | use std::str; 8 | 9 | use crate::modules::resolver::resolv; 10 | 11 | /// Check if template is enabled 12 | pub fn check_enabled_template( 13 | vec_cas: &mut Vec, 14 | vec_templates: &mut Vec, 15 | adcs_templates: &mut HashMap>, 16 | old_bloodhound: bool, 17 | ) { 18 | for i in 0..vec_templates.len() { 19 | for ca in adcs_templates.to_owned() { 20 | if ca.1.contains(&vec_templates[i]["Properties"]["Display Name"].as_str().unwrap().to_string()) { 21 | trace!("Certificate template {} enabled",&vec_templates[i]["Properties"]["Display Name"].as_str().unwrap().to_string().green().bold()); 22 | vec_templates[i]["Properties"]["Enabled"] = true.into(); 23 | 24 | // Is @ly4k BloodHound version? 25 | if !old_bloodhound { 26 | let mut vcasids: Vec = Vec::new(); 27 | vcasids.push(ca.0.to_string()); 28 | vec_templates[i]["cas_ids"] = vcasids.into(); 29 | } 30 | 31 | // Push CANAME in the Template 32 | let mut caname = Vec::new(); 33 | for j in 0..vec_cas.len() { 34 | if ca.0.contains(&vec_cas[j]["ObjectIdentifier"].as_str().unwrap().to_string()) { 35 | caname.push(vec_cas[j]["Properties"]["CA Name"].as_str().unwrap().to_string()); 36 | } else { 37 | continue 38 | } 39 | } 40 | vec_templates[i]["Properties"]["Certificate Authorities"] = caname.to_owned().into(); 41 | } 42 | } 43 | } 44 | } 45 | 46 | /// Get web_enrollment, user_specified_san, request_disposition configuration 47 | pub async fn get_conf( 48 | vec_cas: &mut Vec, 49 | dc_only: bool, 50 | dns_tcp: bool, 51 | name_server: &String, 52 | ) { 53 | for i in 0..vec_cas.len() { 54 | if dc_only { 55 | vec_cas[i]["Properties"]["Web Enrollment"] = String::from("Unknown").into(); 56 | vec_cas[i]["Properties"]["User Specified SAN"] = String::from("Unknown").into(); 57 | vec_cas[i]["Properties"]["Request Disposition"] = String::from("Unknown").into(); 58 | } else { 59 | // Checking if web enrollment is enabled 60 | let web_enrollment = web_enrollment( 61 | vec_cas[i]["Properties"]["DNS Name"].as_str().unwrap().to_string(), 62 | dns_tcp, 63 | name_server, 64 | ).await; 65 | vec_cas[i]["Properties"]["Web Enrollment"] = web_enrollment.to_owned().into(); 66 | vec_cas[i]["Properties"]["User Specified SAN"] = String::from("Unknown").into(); 67 | vec_cas[i]["Properties"]["Request Disposition"] = String::from("Unknown").into(); 68 | } 69 | } 70 | } 71 | 72 | 73 | /// HEAD request on /certsrv/ to check web enrrollment 74 | async fn web_enrollment( 75 | target: String, 76 | dns_tcp: bool, 77 | name_server: &String, 78 | ) -> String { 79 | 80 | debug!("Checking web enrollment on {}",&target); 81 | let ip = resolv::resolver( 82 | target.to_owned(), 83 | dns_tcp, 84 | name_server).await; 85 | let url = format!("http://{}/certsrv/",target); 86 | trace!("Resolved {} to {}",&target,&ip); 87 | 88 | if let Ok(mut stream) = TcpStream::connect(format!("{}:80",&ip)) 89 | { 90 | trace!("Connected to the server {}",format!("http://{}/certsrv/",target.to_owned()).bold().green()); 91 | // Send HTTP HEAD request 92 | stream.set_read_timeout(None).expect("set_read_timeout call failed"); 93 | stream.write(format!("HEAD /certsrv/ HTTP/1.1\nHost: {}\r\n\n",target.to_owned()).as_bytes()).unwrap(); 94 | 95 | // Waiting for response 96 | let mut buffer = [0; 256]; 97 | let result = stream.read(&mut buffer[..]).unwrap(); 98 | trace!("Result: {:?}", str::from_utf8(&buffer[..result][..])); 99 | 100 | // If response not contain 404 status code enrollment is enabled 101 | if !str::from_utf8(&buffer[..result][..]).unwrap_or("Error").contains(&"404".to_string()) 102 | { 103 | info!("Web enrollment {} on {}","enabled".bold().green(),&url.bold()); 104 | return "Enabled".to_string() 105 | } else { 106 | return "Disabled".to_string() 107 | } 108 | } else 109 | { 110 | error!("Couldn't connect to server {}, please try manually and check for https access if EPA is enable.",format!("http://{}/certsrv/",target).bold().red()); 111 | } 112 | return "Unknown".to_string() 113 | } -------------------------------------------------------------------------------- /src/modules/adcs/flags.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | use crate::modules::adcs::parser::Template; 3 | 4 | bitflags! { 5 | struct PkiCertificateNameFlag: u64 { 6 | const NONE = 0x00000000; 7 | const ENROLLEE_SUPPLIES_SUBJECT = 0x00000001; 8 | const ADD_EMAIL = 0x00000002; 9 | const ADD_OBJ_GUID = 0x00000004; 10 | const OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME = 0x00000008; 11 | const ADD_DIRECTORY_PATH = 0x00000100; 12 | const ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME = 0x00010000; 13 | const SUBJECT_ALT_REQUIRE_DOMAIN_DNS = 0x00400000; 14 | const SUBJECT_ALT_REQUIRE_SPN = 0x00800000; 15 | const SUBJECT_ALT_REQUIRE_DIRECTORY_GUID = 0x01000000; 16 | const SUBJECT_ALT_REQUIRE_UPN = 0x02000000; 17 | const SUBJECT_ALT_REQUIRE_EMAIL = 0x04000000; 18 | const SUBJECT_ALT_REQUIRE_DNS = 0x08000000; 19 | const SUBJECT_REQUIRE_DNS_AS_CN = 0x10000000; 20 | const SUBJECT_REQUIRE_EMAIL = 0x20000000; 21 | const SUBJECT_REQUIRE_COMMON_NAME = 0x40000000; 22 | const SUBJECT_REQUIRE_DIRECTORY_PATH = 0x80000000; 23 | } 24 | } 25 | 26 | /// Get the PKI flags from "msPKI-Certificate-Name-Flag" LDAP attribut. 27 | /// MS: 28 | pub fn get_pki_cert_name_flags( 29 | template: &mut Template, 30 | template_json: &mut serde_json::value::Value, 31 | value: u64, 32 | ) -> Vec 33 | { 34 | let mut flags: Vec = Vec::new(); 35 | 36 | if (PkiCertificateNameFlag::NONE.bits() | value) == value 37 | { 38 | //nothing 39 | } 40 | if (PkiCertificateNameFlag::ENROLLEE_SUPPLIES_SUBJECT.bits() | value) == value 41 | { 42 | flags.push("EnrolleeSuppliesSubject".to_string()); 43 | template.enrollee_supplies_subject = true; 44 | template_json["Properties"]["Enrollee Supplies Subject"] = template.enrollee_supplies_subject.to_owned().into(); 45 | } 46 | if (PkiCertificateNameFlag::ADD_EMAIL.bits() | value) == value 47 | { 48 | flags.push("AddEmail".to_string()); 49 | } 50 | if (PkiCertificateNameFlag::ADD_OBJ_GUID.bits() | value) == value 51 | { 52 | flags.push("AddObjGuid".to_string()); 53 | } 54 | if (PkiCertificateNameFlag::OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME.bits() | value) == value 55 | { 56 | flags.push("OldCertSuppliesSubjectAndAltName".to_string()); 57 | } 58 | if (PkiCertificateNameFlag::ADD_DIRECTORY_PATH.bits() | value) == value 59 | { 60 | flags.push("AddDirectoryPath".to_string()); 61 | } 62 | if (PkiCertificateNameFlag::ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME.bits() | value) == value 63 | { 64 | flags.push("EnrolleeSuppliesSubjectAltName".to_string()); 65 | } 66 | if (PkiCertificateNameFlag::SUBJECT_ALT_REQUIRE_DOMAIN_DNS.bits() | value) == value 67 | { 68 | flags.push("SubjectAltRequireDomainDns".to_string()); 69 | } 70 | if (PkiCertificateNameFlag::SUBJECT_ALT_REQUIRE_SPN.bits() | value) == value 71 | { 72 | flags.push("SubjectAltRequireSpn".to_string()); 73 | } 74 | if (PkiCertificateNameFlag::SUBJECT_ALT_REQUIRE_DIRECTORY_GUID.bits() | value) == value 75 | { 76 | flags.push("SubjectAltRequireGuid".to_string()); 77 | } 78 | if (PkiCertificateNameFlag::SUBJECT_ALT_REQUIRE_UPN.bits() | value) == value 79 | { 80 | flags.push("SubjectAltRequireUpn".to_string()); 81 | } 82 | if (PkiCertificateNameFlag::SUBJECT_ALT_REQUIRE_EMAIL.bits() | value) == value 83 | { 84 | flags.push("SubjectAltRequireEmail".to_string()); 85 | } 86 | if (PkiCertificateNameFlag::SUBJECT_ALT_REQUIRE_DNS.bits() | value) == value 87 | { 88 | flags.push("SubjectAltRequireDns".to_string()); 89 | } 90 | if (PkiCertificateNameFlag::SUBJECT_REQUIRE_DNS_AS_CN.bits() | value) == value 91 | { 92 | flags.push("SubjectRequireDnsAsCn".to_string()); 93 | } 94 | if (PkiCertificateNameFlag::SUBJECT_REQUIRE_EMAIL.bits() | value) == value 95 | { 96 | flags.push("SubjectRequireEmail".to_string()); 97 | } 98 | if (PkiCertificateNameFlag::SUBJECT_REQUIRE_COMMON_NAME.bits() | value) == value 99 | { 100 | flags.push("SubjectRequireCommonName".to_string()); 101 | } 102 | if (PkiCertificateNameFlag::SUBJECT_REQUIRE_DIRECTORY_PATH.bits() | value) == value 103 | { 104 | flags.push("SubjectRequireDirectoryPath".to_string()); 105 | } 106 | return flags 107 | } 108 | 109 | 110 | bitflags! { 111 | struct PkiEnrollmentFlag: u64 { 112 | const NONE = 0x00000000; 113 | const INCLUDE_SYMMETRIC_ALGORITHMS = 0x00000001; 114 | const PEND_ALL_REQUESTS = 0x00000002; 115 | const PUBLISH_TO_KRA_CONTAINER = 0x00000004; 116 | const PUBLISH_TO_DS = 0x00000008; 117 | const AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE = 0x00000010; 118 | const AUTO_ENROLLMENT = 0x00000020; 119 | const CT_FLAG_DOMAIN_AUTHENTICATION_NOT_REQUIRED = 0x80; 120 | const PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT = 0x00000040; 121 | const USER_INTERACTION_REQUIRED = 0x00000100; 122 | const ADD_TEMPLATE_NAME = 0x200; 123 | const REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE = 0x00000400; 124 | const ALLOW_ENROLL_ON_BEHALF_OF = 0x00000800; 125 | const ADD_OCSP_NOCHECK = 0x00001000; 126 | const ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL = 0x00002000; 127 | const NOREVOCATIONINFOINISSUEDCERTS = 0x00004000; 128 | const INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS = 0x00008000; 129 | const ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT = 0x00010000; 130 | const ISSUANCE_POLICIES_FROM_REQUEST = 0x00020000; 131 | const SKIP_AUTO_RENEWAL = 0x00040000; 132 | const NO_SECURITY_EXTENSION = 0x00080000; 133 | } 134 | } 135 | 136 | /// Get the PKI flags from "msPKI-Enrollment-Flag" LDAP attribut. 137 | /// MS: 138 | pub fn get_pki_enrollment_flags( 139 | template: &mut Template, 140 | template_json: &mut serde_json::value::Value, 141 | value: u64, 142 | ) -> Vec 143 | { 144 | let mut flags: Vec = Vec::new(); 145 | 146 | if (PkiEnrollmentFlag::NONE.bits() | value) == value 147 | { 148 | //nothing 149 | } 150 | if (PkiEnrollmentFlag::INCLUDE_SYMMETRIC_ALGORITHMS.bits() | value) == value 151 | { 152 | flags.push("IncludeSymmetricAlgorithms".to_string()); 153 | } 154 | if (PkiEnrollmentFlag::PEND_ALL_REQUESTS.bits() | value) == value 155 | { 156 | flags.push("PendAllRequests".to_string()); 157 | template.requires_manager_approval = true; 158 | template_json["Properties"]["Requires Manager Approval"] = template.requires_manager_approval.to_owned().into(); 159 | } 160 | if (PkiEnrollmentFlag::PUBLISH_TO_KRA_CONTAINER.bits() | value) == value 161 | { 162 | flags.push("PublishToKraContainer".to_string()); 163 | } 164 | if (PkiEnrollmentFlag::PUBLISH_TO_DS.bits() | value) == value 165 | { 166 | flags.push("PublishToDs".to_string()); 167 | } 168 | if (PkiEnrollmentFlag::AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE.bits() | value) == value 169 | { 170 | flags.push("AutoEnrollmentCheckUserDsCertificate".to_string()); 171 | } 172 | if (PkiEnrollmentFlag::AUTO_ENROLLMENT.bits() | value) == value 173 | { 174 | flags.push("AutoEnrollment".to_string()); 175 | } 176 | if (PkiEnrollmentFlag::CT_FLAG_DOMAIN_AUTHENTICATION_NOT_REQUIRED.bits() | value) == value 177 | { 178 | flags.push("CtFlagDomainAuthentificationNotRequired".to_string()); 179 | } 180 | if (PkiEnrollmentFlag::PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT.bits() | value) == value 181 | { 182 | flags.push("PreviousApprovalValidateReenrollment".to_string()); 183 | } 184 | if (PkiEnrollmentFlag::USER_INTERACTION_REQUIRED.bits() | value) == value 185 | { 186 | flags.push("UserInteractionRequired".to_string()); 187 | } 188 | if (PkiEnrollmentFlag::ADD_TEMPLATE_NAME.bits() | value) == value 189 | { 190 | flags.push("AddTemplateName".to_string()); 191 | } 192 | if (PkiEnrollmentFlag::REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE.bits() | value) == value 193 | { 194 | flags.push("RemoveInvalidCertificateFromPersonalStore".to_string()); 195 | } 196 | if (PkiEnrollmentFlag::ALLOW_ENROLL_ON_BEHALF_OF.bits() | value) == value 197 | { 198 | flags.push("AllowEnrollOnBehalfOf".to_string()); 199 | } 200 | if (PkiEnrollmentFlag::ADD_OCSP_NOCHECK.bits() | value) == value 201 | { 202 | flags.push("AddOcspNocheck".to_string()); 203 | } 204 | if (PkiEnrollmentFlag::ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL.bits() | value) == value 205 | { 206 | flags.push("EnbaleKeyReuseOnNtTokenKeysetStorageFull".to_string()); 207 | } 208 | if (PkiEnrollmentFlag::NOREVOCATIONINFOINISSUEDCERTS.bits() | value) == value 209 | { 210 | flags.push("NorevocationInforInIssuedCerts".to_string()); 211 | } 212 | if (PkiEnrollmentFlag::INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS.bits() | value) == value 213 | { 214 | flags.push("IncludeBasicConstraintsForEeCerts".to_string()); 215 | } 216 | if (PkiEnrollmentFlag::ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT.bits() | value) == value 217 | { 218 | flags.push("AllowPreviousApprovalKeybasedrenewalValidateReenrollment".to_string()); 219 | } 220 | if (PkiEnrollmentFlag::ISSUANCE_POLICIES_FROM_REQUEST.bits() | value) == value 221 | { 222 | flags.push("IssuancePoliciesFromRequest".to_string()); 223 | } 224 | if (PkiEnrollmentFlag::SKIP_AUTO_RENEWAL.bits() | value) == value 225 | { 226 | flags.push("SkipAutoRenewal".to_string()); 227 | } 228 | if (PkiEnrollmentFlag::NO_SECURITY_EXTENSION.bits() | value) == value 229 | { 230 | flags.push("NoSecurityExtension".to_string()); 231 | template.no_security_extension = true; 232 | } 233 | return flags 234 | } 235 | 236 | bitflags! { 237 | struct PkiPrivateKeyFlag: u64 { 238 | const REQUIRE_PRIVATE_KEY_ARCHIVAL = 0x00000001; 239 | const EXPORTABLE_KEY = 0x00000010; 240 | const STRONG_KEY_PROTECTION_REQUIRED = 0x00000020; 241 | const REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM = 0x00000040; 242 | const REQUIRE_SAME_KEY_RENEWAL = 0x00000080; 243 | const USE_LEGACY_PROVIDER = 0x00000100; 244 | const ATTEST_NONE = 0x00000000; 245 | const ATTEST_REQUIRED = 0x00002000; 246 | const ATTEST_PREFERRED = 0x00001000; 247 | const ATTESTATION_WITHOUT_POLICY = 0x00004000; 248 | const EK_TRUST_ON_USE = 0x00000200; 249 | const EK_VALIDATE_CERT = 0x00000400; 250 | const EK_VALIDATE_KEY = 0x00000800; 251 | const HELLO_LOGON_KEY = 0x00200000; 252 | } 253 | } 254 | 255 | /// Get the PKI flags from "msPKI-Private-Key-Flag" LDAP attribut. 256 | /// MS: 257 | pub fn get_pki_private_flags( 258 | template: &mut Template, 259 | template_json: &mut serde_json::value::Value, 260 | value: u64, 261 | ) -> Vec 262 | { 263 | let mut flags: Vec = Vec::new(); 264 | 265 | if (PkiPrivateKeyFlag::REQUIRE_PRIVATE_KEY_ARCHIVAL.bits() | value) == value 266 | { 267 | flags.push("RequirePrivateKeyArchival".to_string()); 268 | template.requires_key_archival = true; 269 | template_json["Properties"]["Requires Key Archival"] = template.requires_key_archival.to_owned().into(); 270 | } 271 | if (PkiPrivateKeyFlag::EXPORTABLE_KEY.bits() | value) == value 272 | { 273 | flags.push("ExportableKey".to_string()); 274 | } 275 | if (PkiPrivateKeyFlag::STRONG_KEY_PROTECTION_REQUIRED.bits() | value) == value 276 | { 277 | flags.push("StringKeyProtectionRequired".to_string()); 278 | } 279 | if (PkiPrivateKeyFlag::REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM.bits() | value) == value 280 | { 281 | flags.push("RequireAlternateSignatureAlgorithm".to_string()); 282 | } 283 | if (PkiPrivateKeyFlag::REQUIRE_SAME_KEY_RENEWAL.bits() | value) == value 284 | { 285 | flags.push("RequireSameKeyRenewal".to_string()); 286 | } 287 | if (PkiPrivateKeyFlag::USE_LEGACY_PROVIDER.bits() | value) == value 288 | { 289 | flags.push("UseLegacyProvider".to_string()); 290 | } 291 | if (PkiPrivateKeyFlag::ATTEST_NONE.bits() | value) == value 292 | { 293 | flags.push("AttestNone".to_string()); 294 | } 295 | if (PkiPrivateKeyFlag::ATTEST_REQUIRED.bits() | value) == value 296 | { 297 | flags.push("AttestRequired".to_string()); 298 | } 299 | if (PkiPrivateKeyFlag::ATTEST_PREFERRED.bits() | value) == value 300 | { 301 | flags.push("AttestPrefeered".to_string()); 302 | } 303 | if (PkiPrivateKeyFlag::ATTESTATION_WITHOUT_POLICY.bits() | value) == value 304 | { 305 | flags.push("AttestationWithoutPolicy".to_string()); 306 | } 307 | if (PkiPrivateKeyFlag::EK_TRUST_ON_USE.bits() | value) == value 308 | { 309 | flags.push("EkTrustOnUse".to_string()); 310 | } 311 | if (PkiPrivateKeyFlag::EK_VALIDATE_CERT.bits() | value) == value 312 | { 313 | flags.push("EkValidateCert".to_string()); 314 | } 315 | if (PkiPrivateKeyFlag::EK_VALIDATE_KEY.bits() | value) == value 316 | { 317 | flags.push("EkValidateKey".to_string()); 318 | } 319 | if (PkiPrivateKeyFlag::HELLO_LOGON_KEY.bits() | value) == value 320 | { 321 | flags.push("HelloLogonKey".to_string()); 322 | } 323 | return flags 324 | } -------------------------------------------------------------------------------- /src/modules/adcs/mod.rs: -------------------------------------------------------------------------------- 1 | //! ADCS enumeration 2 | //! 3 | //! This module will request the Active Directory to enumerate ADCS certificate templates, certificate authorities and other configurations. 4 | //! The adcs output it's only for the custom-built BloodHound version from @ly4k. (certipy developper) 5 | //! 6 | //! 7 | //! 8 | //! 9 | pub mod parser; 10 | pub mod checker; 11 | pub mod flags; 12 | pub mod utils; -------------------------------------------------------------------------------- /src/modules/adcs/utils.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | /// Function to convert pKIExpirationPeriod Vec format to u64. 4 | pub fn filetime_to_span(filetime: Vec) -> u64 { 5 | if filetime.len() > 0 { 6 | let mut span = i64::from_ne_bytes(filetime[0..8].try_into().unwrap()) as f64; 7 | span *= -0.0000001; 8 | return span as u64 9 | } 10 | return 0 11 | } 12 | 13 | /// Function to change span format to String output date. 14 | /// Thanks to ly4k works: 15 | pub fn span_to_string(span: u64) -> String { 16 | if (span % 31536000 == 0) && ((span / 31536000) >= 1) { 17 | if (span / 31536000) == 1 { 18 | return "1 year".to_string() 19 | } else { 20 | return format!("{} years",(span / 31536000)) 21 | } 22 | } else if (span % 2592000 == 0) && ((span / 2592000) >= 1) { 23 | if (span / 2592000) == 1 { 24 | return "1 month".to_string() 25 | } else { 26 | return format!("{} months",(span / 2592000)) 27 | } 28 | } else if (span % 604800 == 0) && ((span / 604800) >= 1) { 29 | if (span / 604800) == 1 { 30 | return "1 week".to_string() 31 | } else { 32 | return format!("{} weeks",(span / 604800)) 33 | } 34 | } else if (span % 86400 == 0) && ((span / 86400) >= 1) { 35 | if (span / 86400) == 1 { 36 | return "1 day".to_string() 37 | } else { 38 | return format!("{} days",(span / 86400)) 39 | } 40 | } else if (span % 3600 == 0) && ((span / 3600) >= 1) { 41 | if (span / 3600) == 1 { 42 | return "1 hour".to_string() 43 | } else { 44 | return format!("{} hours",(span / 3600)) 45 | } 46 | } else { 47 | return "".to_string() 48 | } 49 | } -------------------------------------------------------------------------------- /src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | //! List of RustHound add-on modules 2 | pub mod resolver; 3 | pub mod adcs; 4 | 5 | use log::info; 6 | use std::collections::HashMap; 7 | use crate::args::*; 8 | use crate::json::checker::add_type_for_ace; 9 | 10 | /// Function to run all modules requested 11 | pub async fn run_modules( 12 | common_args: &Options, 13 | fqdn_ip: &mut HashMap, 14 | vec_computers: &mut Vec, 15 | vec_cas: &mut Vec, 16 | vec_templates: &mut Vec, 17 | adcs_templates: &mut HashMap>, 18 | sid_type: &mut HashMap, 19 | ) { 20 | // [MODULE - RESOLVER] Running module to resolve FQDN to IP address? 21 | if common_args.fqdn_resolver { 22 | resolver::resolv::resolving_all_fqdn( 23 | common_args.dns_tcp, 24 | &common_args.name_server, 25 | fqdn_ip, &vec_computers 26 | ).await; 27 | } 28 | 29 | // [MODULE - ADCS] Running last function for adcs templates 30 | if common_args.adcs { 31 | info!("Starting checker for ADCS values..."); 32 | adcs::checker::check_enabled_template( 33 | vec_cas, 34 | vec_templates, 35 | adcs_templates, 36 | common_args.old_bloodhound, 37 | ); 38 | // Getting conf if dc-only isn't set 39 | // 40 | adcs::checker::get_conf( 41 | vec_cas, 42 | common_args.dc_only, 43 | common_args.dns_tcp, 44 | &common_args.name_server, 45 | ).await; 46 | add_type_for_ace(vec_cas, &sid_type); 47 | add_type_for_ace(vec_templates, &sid_type); 48 | info!("Checking for ADCS values finished!"); 49 | } 50 | 51 | // Other modules need to be add here... 52 | } -------------------------------------------------------------------------------- /src/modules/resolver/mod.rs: -------------------------------------------------------------------------------- 1 | //! FQDN resolver 2 | //! 3 | //! This module will resolve IP address from the ldap FQDN 4 | //! Resolver can be used with UDP or TCP DNS request with **--dns-tcp** args 5 | //! Resolver can be used with custome DNS name server with **-n 127.0.0.1** or **--name-server 127.0.0.1** 6 | //! 7 | //! 8 | //! 9 | //! 10 | pub mod resolv; -------------------------------------------------------------------------------- /src/modules/resolver/resolv.rs: -------------------------------------------------------------------------------- 1 | use log::{info,debug}; 2 | use colored::Colorize; 3 | 4 | use trust_dns_resolver::TokioAsyncResolver; 5 | use trust_dns_resolver::config::*; 6 | 7 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 8 | use std::collections::HashMap; 9 | use std::time::Duration; 10 | 11 | /// Function to resolve all IP address from the LDAP FQDN vector 12 | /// 13 | /// 14 | pub async fn resolving_all_fqdn( 15 | dns_tcp: bool, 16 | name_server: &String, 17 | fqdn_ip: &mut HashMap, 18 | vec_computer: &Vec 19 | ) { 20 | info!("Resolving FQDN to IP address started..."); 21 | for value in fqdn_ip.to_owned() 22 | { 23 | for i in 0..vec_computer.len() 24 | { 25 | if (vec_computer[i]["Properties"]["name"].as_str().unwrap().to_string() == value.0.to_owned().to_string()) && (vec_computer[i]["Properties"]["enabled"] == true) { 26 | debug!("Trying to resolve FQDN: {}",value.0.to_string()); 27 | // Resolve FQDN to IP address 28 | let address = resolver(value.0.to_string(),dns_tcp,name_server).await; 29 | if !address.contains("Not found"){ 30 | fqdn_ip.insert(value.0.to_owned().to_string(),address.to_owned().to_string()); 31 | info!("IP address for {}: {}",&value.0.to_string().yellow().bold(),&address.yellow().bold()); 32 | } 33 | } 34 | continue 35 | } 36 | } 37 | info!("Resolving FQDN to IP address finished!"); 38 | } 39 | 40 | /// Asynchron function to resolve IP address from the ldap FQDN 41 | pub async fn resolver( 42 | fqdn: String, 43 | dns_tcp: bool, 44 | name_server: &String, 45 | ) -> String 46 | { 47 | // Get configuration and options for resolver 48 | let (c,o) = make_resolver_conf(dns_tcp,name_server); 49 | 50 | // Construct a new Resolver with default configuration options 51 | let resolver = TokioAsyncResolver::tokio(c,o).unwrap(); 52 | 53 | // Resolver 54 | let result = resolver.lookup_ip(fqdn); 55 | 56 | match result.await{ 57 | Ok(response) => { 58 | let address = response.iter().next().expect("no addresses returned!"); 59 | if address.is_ipv4() { 60 | return address.to_string() 61 | } 62 | } 63 | Err(_err) => {}, 64 | }; 65 | return "Not found".to_string() 66 | } 67 | 68 | /// Function to prepare resolver configuration 69 | pub fn make_resolver_conf( 70 | dns_tcp: bool, 71 | name_server: &String, 72 | ) -> (ResolverConfig,ResolverOpts) { 73 | let mut c = ResolverConfig::new(); 74 | let mut socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 53); 75 | let mut dns_protocol = Protocol::Udp; 76 | if dns_tcp == true 77 | { 78 | dns_protocol = Protocol::Tcp; 79 | } 80 | if !name_server.contains("127.0.0.1") { 81 | let address = name_server.parse::().unwrap_or(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); 82 | socket.set_ip(address); 83 | } 84 | 85 | debug!("Protocol DNS: {:?}",&dns_protocol); 86 | debug!("Name server DNS: {:?}",name_server.parse::()); 87 | 88 | c.add_name_server(NameServerConfig { 89 | socket_addr: socket, 90 | protocol: dns_protocol, 91 | tls_dns_name: None, 92 | trust_nx_responses: false, 93 | bind_addr: None, 94 | }); 95 | 96 | let mut o = ResolverOpts::default(); 97 | o.timeout = Duration::new(0, 10); 98 | return (c,o) 99 | } --------------------------------------------------------------------------------