mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-06-30 07:15:30 +02:00
v3.5.0: automated login — execute the login flow and capture the live session
- harness/creds::login(): performs the real HTTP login (POST/GET form), captures a session Cookie from Set-Cookie or a Bearer token from the JSON body, with a soft success check (no hard fail on 302). Redirects not followed so Set-Cookie is visible. - apply_creds is now async: direct material (jwt/header/cookie) used as-is; a `login:` flow is EXECUTED to obtain a live session; on failure, falls back to instructing the agents to log in themselves. - --creds + --focus added to `run` (authenticated black-box) too. - Verified live against a local mock: POST /login → 302 + Set-Cookie captured as the auth header used on subsequent requests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,12 @@ enum Cmd {
|
||||
/// support MCP fall back to their built-in tools).
|
||||
#[arg(long)]
|
||||
mcp: bool,
|
||||
/// Credentials YAML for authenticated testing (jwt/header/cookie/login).
|
||||
#[arg(long)]
|
||||
creds: Option<String>,
|
||||
/// Free-text focus, e.g. "injection and broken access control".
|
||||
#[arg(long)]
|
||||
focus: Option<String>,
|
||||
/// Verbose: log each agent as it launches, recon, and votes.
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
@@ -162,7 +168,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp, verbose } => {
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp, creds, focus, verbose } => {
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.max_agents = max_agents;
|
||||
@@ -170,9 +176,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
cfg.instructions = focus;
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
apply_creds(&mut cfg, creds.as_deref()).await;
|
||||
let out = run_engagement(&base, cfg, mcp, false).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
@@ -202,7 +210,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
apply_creds(&mut cfg, creds.as_deref());
|
||||
apply_creds(&mut cfg, creds.as_deref()).await;
|
||||
let out = run_greybox_engagement(&base, cfg, mcp).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
@@ -210,22 +218,38 @@ async fn main() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a creds.yaml into the run config: derive the auth header and prepend any
|
||||
/// login flow to the operator instructions.
|
||||
pub(crate) fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
|
||||
/// Load a creds.yaml into the run config. Direct material (jwt/header/cookie) is
|
||||
/// used as-is; a `login:` flow is EXECUTED now (real HTTP) to capture a live
|
||||
/// session cookie/token. If the auto-login fails, fall back to instructing the
|
||||
/// agents to authenticate themselves.
|
||||
pub(crate) async fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
|
||||
let Some(p) = path else { return };
|
||||
match harness::creds::Creds::load(Path::new(p)) {
|
||||
Some(c) => {
|
||||
if cfg.auth.is_none() {
|
||||
cfg.auth = c.auth_header();
|
||||
let Some(c) = harness::creds::Creds::load(Path::new(p)) else {
|
||||
eprintln!(" [!] no usable credentials in {p}");
|
||||
return;
|
||||
};
|
||||
println!(" [*] loaded credentials from {p}");
|
||||
if cfg.auth.is_none() {
|
||||
cfg.auth = c.auth_header();
|
||||
}
|
||||
// No direct material but a login flow → perform it now.
|
||||
if cfg.auth.is_none() {
|
||||
if let Some(login) = &c.login {
|
||||
println!(" [*] auto-login: {} {} ...", login.method, login.url);
|
||||
match harness::creds::login(login).await {
|
||||
Ok((auth, note)) => {
|
||||
println!(" [*] authenticated — {note}");
|
||||
cfg.auth = Some(auth);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" [!] auto-login failed ({e}); agents will attempt to log in themselves");
|
||||
if let Some(instr) = c.login_instruction() {
|
||||
let base = cfg.instructions.clone().unwrap_or_default();
|
||||
cfg.instructions = Some(format!("{instr}\n{base}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(login) = c.login_instruction() {
|
||||
let base = cfg.instructions.clone().unwrap_or_default();
|
||||
cfg.instructions = Some(format!("{login}\n{base}"));
|
||||
}
|
||||
println!(" [*] loaded credentials from {p}");
|
||||
}
|
||||
None => eprintln!(" [!] no usable credentials in {p}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ async fn run(base: &Path, s: &Session) {
|
||||
if let M::Grey { repo, .. } = &m {
|
||||
cfg.repo = Some(repo.clone());
|
||||
}
|
||||
crate::apply_creds(&mut cfg, s.creds.as_deref());
|
||||
crate::apply_creds(&mut cfg, s.creds.as_deref()).await;
|
||||
|
||||
let result = match m {
|
||||
M::Grey { .. } => crate::run_greybox_engagement(base, cfg, s.mcp).await,
|
||||
|
||||
@@ -116,6 +116,61 @@ impl Creds {
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform the login flow now (real HTTP POST) and return an auth header to
|
||||
/// reuse on every subsequent request: a `Cookie:` from Set-Cookie, or an
|
||||
/// `Authorization: Bearer` from a token in the JSON response. Returns
|
||||
/// (auth_header, note). Redirects are not followed so the login response's
|
||||
/// Set-Cookie is visible.
|
||||
pub async fn login(l: &Login) -> anyhow::Result<(String, String)> {
|
||||
use reqwest::header::SET_COOKIE;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()?;
|
||||
let form: Vec<(String, String)> = vec![
|
||||
(l.username_field.clone(), l.username.clone()),
|
||||
(l.password_field.clone(), l.password.clone()),
|
||||
];
|
||||
let req = if l.method == "GET" {
|
||||
client.get(&l.url).query(&form)
|
||||
} else {
|
||||
client.post(&l.url).form(&form)
|
||||
};
|
||||
let resp = req.send().await?;
|
||||
let status = resp.status();
|
||||
|
||||
// 1) session cookies from Set-Cookie on the login response
|
||||
let mut cookie_pairs = Vec::new();
|
||||
for hv in resp.headers().get_all(SET_COOKIE) {
|
||||
if let Ok(s) = hv.to_str() {
|
||||
if let Some(pair) = s.split(';').next() {
|
||||
let p = pair.trim();
|
||||
if !p.is_empty() {
|
||||
cookie_pairs.push(p.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
|
||||
// 2) bearer token from a JSON response body
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
|
||||
for k in ["access_token", "token", "jwt", "id_token", "accessToken"] {
|
||||
if let Some(t) = v.get(k).and_then(|x| x.as_str()).filter(|t| !t.is_empty()) {
|
||||
return Ok((format!("Authorization: Bearer {t}"), format!("bearer token from JSON `{k}` (HTTP {status})")));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cookie_pairs.is_empty() {
|
||||
let cookie = cookie_pairs.join("; ");
|
||||
// Soft success check (don't fail hard — many apps 302 on success).
|
||||
let ok = l.success.is_empty() || body.contains(&l.success) || status.is_redirection() || status.is_success();
|
||||
let note = format!("session cookie captured (HTTP {status}{})", if ok { "" } else { ", success marker not seen" });
|
||||
return Ok((format!("Cookie: {cookie}"), note));
|
||||
}
|
||||
anyhow::bail!("login returned no Set-Cookie or token (HTTP {status})")
|
||||
}
|
||||
|
||||
fn unquote(s: &str) -> String {
|
||||
let s = s.trim();
|
||||
if (s.starts_with('"') && s.ends_with('"') && s.len() >= 2)
|
||||
|
||||
Reference in New Issue
Block a user