Skip to content
Merged
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
80 changes: 73 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ cargo build --release

- **Technology Detection**: Automatically detects frameworks like Next.js, React, Vue, Angular, Rails, Django, Laravel, WordPress, and more from URL patterns
- **AI-Powered Wordlists**: Uses Claude API to generate context-aware wordlists tailored to the target's tech stack
- **Authentication Discovery**: Automatically discovers auth endpoints, can register test accounts, and authenticates to access protected routes
- **Optional HTTP Probing**: Gathers additional context from server headers for more accurate wordlist generation
- **Seamless Scanning**: Generated wordlists are automatically used for content discovery (or output separately)

Expand Down Expand Up @@ -144,6 +145,15 @@ feroxagent -u https://target.com --recon-file urls.txt
| `--wordlist-only` | Output generated wordlist to stdout and exit (don't scan) |
| `--json` | Output structured JSON to stdout (canonical endpoints + token usage) |

### Authentication Options

| Flag | Description |
|------|-------------|
| `--auto-register` | Attempt to create a test account if registration endpoint is discovered |
| `--auth-endpoint <URL>` | Manually specify the authentication endpoint (e.g., `/api/auth/login`) |
| `--auth-instructions <TEXT>` | Provide instructions for authentication (e.g., `'use username field instead of email'`) |
| `--no-discover-auth` | Disable automatic authentication endpoint discovery |

### Inherited feroxbuster Options

feroxagent inherits most of feroxbuster's powerful options:
Expand Down Expand Up @@ -177,19 +187,25 @@ feroxagent --help
- Static file structures
- Route conventions

3. **HTTP Probing**: Automatically makes HEAD requests to gather:
3. **Authentication Discovery**: Automatically discovers and attempts authentication:
- Probes common auth endpoints (`/api/auth/login`, `/login`, `/api/auth/register`, etc.)
- Uses AI to generate an authentication plan based on detected endpoints
- With `--auto-register`, creates a test account and logs in
- Injects auth tokens/cookies into subsequent requests to access protected routes

4. **HTTP Probing**: Automatically makes HEAD requests to gather:
- Server headers
- X-Powered-By information
- Content types

4. **Attack Surface Report**: Generates an actionable report highlighting:
5. **Attack Surface Report**: Generates an actionable report highlighting:
- High-value endpoints to target
- Potential vulnerabilities based on detected stack
- Recommended attack vectors (prioritized)

5. **Generate Wordlist**: Creates a targeted wordlist based on the detected tech stack
6. **Generate Wordlist**: Creates a targeted wordlist based on the detected tech stack

6. **Scan Target**: Uses the generated wordlist to perform content discovery (or outputs the wordlist with `--wordlist-only`)
7. **Scan Target**: Uses the generated wordlist to perform content discovery (or outputs the wordlist with `--wordlist-only`)

## Example Output

Expand Down Expand Up @@ -242,9 +258,39 @@ feroxagent --help
"total_tokens": 14432
},
"stats": {
"total_endpoints": 23,
"parameterized_endpoints": 8,
"catch_all_endpoints": 3
"total_paths_tested": 342,
"total_filtered_noise": 156
},
"auth_discovery": {
"discovered": true,
"authenticated": true,
"endpoints": [
{
"url": "https://example.com/api/auth/login",
"type": "Login",
"method": "POST",
"detected_fields": ["email", "password"],
"status_code": 200
},
{
"url": "https://example.com/api/auth/register",
"type": "Register",
"method": "POST",
"detected_fields": ["email", "password", "username"],
"status_code": 201
}
],
"registration_available": true,
"login_endpoint": "https://example.com/api/auth/login",
"register_endpoint": "https://example.com/api/auth/register",
"auth_type": "Bearer",
"user_created": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"credentials": {
"email": "feroxtest_12345@example.com",
"password": "FeroxTest123!"
},
"summary": "JSON-based auth with email/password, returns JWT token in body"
}
}
```
Expand All @@ -264,6 +310,26 @@ katana -u https://example.com -silent | \
feroxagent -u https://example.com --wordlist-only > custom-wordlist.txt
```

### With Authentication

```bash
# Auto-register a test account and scan authenticated routes
katana -u https://example.com -silent | \
feroxagent -u https://example.com --auto-register

# Manually specify auth endpoint with custom instructions
katana -u https://example.com -silent | \
feroxagent -u https://example.com \
--auth-endpoint /api/login \
--auth-instructions "POST JSON with username and password fields" \
--auto-register

# Get JSON output with auth credentials for use in other tools
katana -u https://example.com -silent | \
feroxagent -u https://example.com --auto-register --json | \
jq '.auth_discovery.token'
```

## Technology Detection

feroxagent can detect the following technologies from URL patterns:
Expand Down
47 changes: 47 additions & 0 deletions src/config/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use std::{
fs::read_to_string,
io::BufRead,
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
use url::form_urlencoded;

Expand Down Expand Up @@ -105,6 +106,22 @@ pub struct Configuration {
#[serde(default)]
pub discover_methods: bool,

/// Manually specified authentication endpoint (ex: /api/auth/login)
#[serde(default)]
pub auth_endpoint: String,

/// Instructions for authentication (ex: 'POST JSON with email and password fields')
#[serde(default)]
pub auth_instructions: String,

/// Whether to attempt creating a test account if registration is discovered
#[serde(default)]
pub auto_register: bool,

/// Whether to disable automatic auth endpoint discovery (default: false, i.e. discovery enabled)
#[serde(default)]
pub no_discover_auth: bool,

/// Anthropic API key for LLM wordlist generation
#[serde(default)]
pub anthropic_key: String,
Expand Down Expand Up @@ -242,6 +259,10 @@ pub struct Configuration {
#[serde(default)]
pub headers: HashMap<String, String>,

/// Auth headers set at runtime after authentication discovery (interior mutability for Arc<Configuration>)
#[serde(skip)]
pub auth_headers: Arc<RwLock<HashMap<String, String>>>,

/// URL query parameters
#[serde(default)]
pub queries: Vec<(String, String)>,
Expand Down Expand Up @@ -480,11 +501,16 @@ impl Default for Configuration {
filter_status: Vec::new(),
filter_similar: Vec::new(),
headers: HashMap::new(),
auth_headers: Arc::new(RwLock::new(HashMap::new())),
depth: depth(),
threads: threads(),
recon_file: String::new(),
wordlist_only: false,
discover_methods: false,
auth_endpoint: String::new(),
auth_instructions: String::new(),
auto_register: false,
no_discover_auth: false,
anthropic_key: std::env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
generated_wordlist: Vec::new(),
dont_collect: ignored_extensions(),
Expand Down Expand Up @@ -1106,6 +1132,23 @@ impl Configuration {
config.discover_methods = true;
}

// Authentication discovery options
update_config_if_present!(&mut config.auth_endpoint, args, "auth_endpoint", String);
update_config_if_present!(
&mut config.auth_instructions,
args,
"auth_instructions",
String
);

if came_from_cli!(args, "auto_register") {
config.auto_register = true;
}

if came_from_cli!(args, "no_discover_auth") {
config.no_discover_auth = true;
}

if came_from_cli!(args, "unique") {
config.unique = true;
}
Expand Down Expand Up @@ -1454,6 +1497,10 @@ impl Configuration {
update_if_not_default!(&mut conf.recon_file, new.recon_file, "");
update_if_not_default!(&mut conf.wordlist_only, new.wordlist_only, false);
update_if_not_default!(&mut conf.discover_methods, new.discover_methods, false);
update_if_not_default!(&mut conf.auth_endpoint, new.auth_endpoint, "");
update_if_not_default!(&mut conf.auth_instructions, new.auth_instructions, "");
update_if_not_default!(&mut conf.auto_register, new.auto_register, false);
update_if_not_default!(&mut conf.no_discover_auth, new.no_discover_auth, false);
update_if_not_default!(&mut conf.status_codes, new.status_codes, status_codes());
// status_codes() is the default for replay_codes, if they're not provided
update_if_not_default!(&mut conf.replay_codes, new.replay_codes, status_codes());
Expand Down
68 changes: 66 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use feroxagent::{
smart_wordlist::{
self, confirm_methods_batch, detect_parameterized_endpoint, discover_methods_for_405s,
fingerprint_api_prefixes, generate_canonical_inventory_with_wildcards, output_wordlist,
DiscoveredEndpoint, GeneratorConfig, PentestReport,
AuthTokenType, DiscoveredEndpoint, GeneratorConfig, PentestReport,
},
utils::{fmt_err, slugify_filename},
};
Expand Down Expand Up @@ -201,6 +201,19 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
} else {
Some(config.recon_file.clone())
},
auth_endpoint: if config.auth_endpoint.is_empty() {
None
} else {
Some(config.auth_endpoint.clone())
},
auth_instructions: if config.auth_instructions.is_empty() {
None
} else {
Some(config.auth_instructions.clone())
},
auto_register: config.auto_register,
no_discover_auth: config.no_discover_auth,
json: config.json,
};

let generation_result =
Expand All @@ -213,6 +226,53 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
}
};

// Inject auth headers if authentication was successful
if let Some((_, _, ref auth_result)) = result.auth_result {
if auth_result.success {
if let Ok(mut auth_headers) = config.auth_headers.write() {
match auth_result.token_type {
AuthTokenType::Bearer => {
if let Some(ref token) = auth_result.token {
auth_headers
.insert("Authorization".to_string(), format!("Bearer {}", token));
log::info!("Added Bearer token to requests");
if !config.json {
eprintln!("[+] Authentication successful - added Bearer token to requests");
}
}
}
AuthTokenType::Cookie => {
// Combine all cookies into a single Cookie header
if !auth_result.cookies.is_empty() {
let cookie_header = auth_result.cookies.join("; ");
auth_headers.insert("Cookie".to_string(), cookie_header);
log::info!("Added session cookies to requests");
if !config.json {
eprintln!("[+] Authentication successful - added session cookies to requests");
}
}
}
AuthTokenType::ApiKey => {
if let Some(ref token) = auth_result.token {
// For API keys, the auth_plan should have specified where to put it
// Default to X-API-Key header
auth_headers.insert("X-API-Key".to_string(), token.clone());
log::info!("Added API key to requests");
if !config.json {
eprintln!(
"[+] Authentication successful - added API key to requests"
);
}
}
}
AuthTokenType::None => {}
}
}
} else if !config.json {
eprintln!("[-] Authentication attempted but was not successful");
}
}

// Initialize the pentest report
let mut pentest_report = PentestReport::new(config.target_url.clone());
pentest_report.set_recon_urls(result.recon_urls.clone());
Expand All @@ -223,6 +283,9 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
// Store token usage for JSON output
let token_usage = result.token_usage.clone();

// Store auth result for JSON output
let auth_result_for_report = result.auth_result.clone();

if !config.json {
eprintln!(
"\n[+] Generated {} paths for scanning",
Expand Down Expand Up @@ -719,7 +782,8 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
// Output the comprehensive report
if config.json {
// JSON output to stdout only
let json_output = pentest_report.to_json_output(&token_usage);
let json_output =
pentest_report.to_json_output(&token_usage, auth_result_for_report.as_ref());
println!(
"{}",
serde_json::to_string_pretty(&json_output).unwrap_or_else(|e| {
Expand Down
30 changes: 30 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,36 @@ pub fn initialize() -> Command {
.help_heading("Smart wordlist settings")
.help("Run OPTIONS requests on 405 endpoints to discover allowed methods"),
)
.arg(
Arg::new("auth_endpoint")
.long("auth-endpoint")
.value_name("URL")
.num_args(1)
.help_heading("Authentication settings")
.help("Manually specify the authentication endpoint (ex: /api/auth/login)"),
)
.arg(
Arg::new("auth_instructions")
.long("auth-instructions")
.value_name("TEXT")
.num_args(1)
.help_heading("Authentication settings")
.help("Provide instructions for authentication (ex: 'POST JSON with email and password fields')"),
)
.arg(
Arg::new("auto_register")
.long("auto-register")
.num_args(0)
.help_heading("Authentication settings")
.help("Attempt to create a test account if registration endpoint is discovered"),
)
.arg(
Arg::new("no_discover_auth")
.long("no-discover-auth")
.num_args(0)
.help_heading("Authentication settings")
.help("Disable automatic authentication endpoint discovery"),
)
.arg(
Arg::new("auto_tune")
.long("auto-tune")
Expand Down
Loading