Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ you are explicitly authorized to test.
ghosthound -d ghost.local --dc-ip 10.0.0.10 -u alice -o tombstones.json
```

`--output`/`-o` accepts either an explicit file path or an existing directory. When given a
directory, GhostHound writes the default `ghosthound_output.json` inside it:

```bash
# Write to an explicit path
ghosthound -d ghost.local --dc-ip 10.0.0.10 -u alice -o /tmp/tombstones.json

# Write /tmp/ghosthound_output.json
ghosthound -d ghost.local --dc-ip 10.0.0.10 -u alice -o /tmp
```

Parent directories are not created automatically. GhostHound validates the destination before
connecting to LDAP and reports a contextual error if the parent is missing or is not a directory.
On Unix, a newly-created or reused output file is restricted to mode `0600`; on Windows, its access
is controlled by filesystem ACLs.

Password resolution order: `--password` (avoid — visible via `ps`/`/proc/<pid>/cmdline` and shell
history), then the `LDAP_PASSWORD` environment variable, then an interactive prompt if neither is
set.
Expand Down
3 changes: 3 additions & 0 deletions crates/ghosthound/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ rpassword = "7.4"
serde_json = "1.0.150"
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros"] }
zeroize = "1.8"

[dev-dependencies]
tempfile = "3.27.0"
33 changes: 15 additions & 18 deletions crates/ghosthound/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#![forbid(unsafe_code)]

mod output;

use ad_tombstone::{
check_reanimate_rights, check_recycle_bin_enabled, fetch_tombstones, resolve_object_sid,
with_timeout,
Expand All @@ -14,14 +16,10 @@ use clap::Parser;
use ldap3::{LdapConnAsync, LdapConnSettings};
use serde_json::json;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::time::Duration;
use zeroize::Zeroizing;

#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;

#[derive(Parser)]
#[command(
version,
Expand Down Expand Up @@ -65,9 +63,9 @@ struct Args {
#[arg(long, default_value_t = 30)]
timeout_secs: u64,

/// Output JSON file name
#[arg(short, long, default_value = "ghosthound_output.json")]
output: String,
/// Output JSON file path or existing directory
#[arg(short, long, default_value = output::DEFAULT_OUTPUT_FILENAME)]
output: PathBuf,
}

#[tokio::main]
Expand All @@ -78,6 +76,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
return Err("NTLM authentication is currently disabled due to upstream dependencies (sspi-rs) failing strict security checks on the latest compiler toolchain. Please use Simple Bind.".into());
}

let output_path = output::resolve_output_path(&args.output)?;
println!("[*] Output destination: {}", output_path.display());

let password = Zeroizing::new(match args.password {
Some(p) => p,
None => rpassword::prompt_password("Password: ")?,
Expand Down Expand Up @@ -267,16 +268,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

let graph_data = builder.build("GhostHound");
let json_output = serde_json::to_string_pretty(&graph_data)?;
// The output documents privileged principals and attack paths, so restrict it to the owner
// rather than relying on the process umask (typically 644, world-readable) on Unix.
let mut open_options = File::options();
open_options.write(true).create(true).truncate(true);
#[cfg(unix)]
open_options.mode(0o600);
let mut file = open_options.open(&args.output)?;
file.write_all(json_output.as_bytes())?;

println!("[+] Successfully wrote graph data to {}", args.output);
output::write_output(&output_path, json_output.as_bytes())?;

println!(
"[+] Successfully wrote graph data to {}",
output_path.display()
);

ldap.unbind().await?;
Ok(())
Expand Down
Loading