Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #1
161 lines
6.0 KiB
Rust
161 lines
6.0 KiB
Rust
use compliance_core::error::CoreError;
|
|
use compliance_core::models::dast::{DastEvidence, DastFinding, DastTarget, DastVulnType};
|
|
use compliance_core::models::Severity;
|
|
use compliance_core::traits::dast_agent::{DastAgent, DastContext};
|
|
use tracing::info;
|
|
|
|
/// Server-Side Request Forgery (SSRF) testing agent
|
|
pub struct SsrfAgent {
|
|
http: reqwest::Client,
|
|
}
|
|
|
|
impl SsrfAgent {
|
|
pub fn new(http: reqwest::Client) -> Self {
|
|
Self { http }
|
|
}
|
|
|
|
fn payloads(&self) -> Vec<(&str, &str)> {
|
|
vec![
|
|
("http://127.0.0.1", "localhost IPv4"),
|
|
("http://[::1]", "localhost IPv6"),
|
|
("http://0.0.0.0", "zero address"),
|
|
("http://169.254.169.254/latest/meta-data/", "AWS metadata"),
|
|
("http://metadata.google.internal/", "GCP metadata"),
|
|
("http://127.0.0.1:22", "SSH port probe"),
|
|
("http://127.0.0.1:3306", "MySQL port probe"),
|
|
("http://localhost/admin", "localhost admin"),
|
|
]
|
|
}
|
|
|
|
fn internal_indicators(&self) -> Vec<&str> {
|
|
vec![
|
|
"ami-id",
|
|
"instance-id",
|
|
"local-hostname",
|
|
"public-hostname",
|
|
"iam/security-credentials",
|
|
"computeMetadata",
|
|
"OpenSSH",
|
|
"mysql_native_password",
|
|
"root:x:0:",
|
|
"<!DOCTYPE html>",
|
|
]
|
|
}
|
|
}
|
|
|
|
impl DastAgent for SsrfAgent {
|
|
fn name(&self) -> &str {
|
|
"ssrf"
|
|
}
|
|
|
|
async fn run(
|
|
&self,
|
|
target: &DastTarget,
|
|
context: &DastContext,
|
|
) -> Result<Vec<DastFinding>, CoreError> {
|
|
let mut findings = Vec::new();
|
|
let target_id = target
|
|
.id
|
|
.map(|oid| oid.to_hex())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
|
|
// Find endpoints with URL-like parameters
|
|
for endpoint in &context.endpoints {
|
|
let url_params: Vec<_> = endpoint
|
|
.parameters
|
|
.iter()
|
|
.filter(|p| {
|
|
let name_lower = p.name.to_lowercase();
|
|
name_lower.contains("url")
|
|
|| name_lower.contains("uri")
|
|
|| name_lower.contains("link")
|
|
|| name_lower.contains("src")
|
|
|| name_lower.contains("redirect")
|
|
|| name_lower.contains("callback")
|
|
|| name_lower.contains("fetch")
|
|
|| name_lower.contains("load")
|
|
})
|
|
.collect();
|
|
|
|
if url_params.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
for param in &url_params {
|
|
for (payload, technique) in self.payloads() {
|
|
let request = if endpoint.method == "POST" {
|
|
self.http
|
|
.post(&endpoint.url)
|
|
.form(&[(param.name.as_str(), payload)])
|
|
} else {
|
|
let test_url = format!("{}?{}={}", endpoint.url, param.name, payload);
|
|
self.http.get(&test_url)
|
|
};
|
|
|
|
let response = match request.send().await {
|
|
Ok(r) => r,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
let status = response.status().as_u16();
|
|
let body = response.text().await.unwrap_or_default();
|
|
|
|
// Check for SSRF indicators
|
|
let body_lower = body.to_lowercase();
|
|
let is_vulnerable = self
|
|
.internal_indicators()
|
|
.iter()
|
|
.any(|indicator| body_lower.contains(&indicator.to_lowercase()));
|
|
|
|
if is_vulnerable {
|
|
let snippet = body.chars().take(500).collect::<String>();
|
|
|
|
let evidence = DastEvidence {
|
|
request_method: endpoint.method.clone(),
|
|
request_url: endpoint.url.clone(),
|
|
request_headers: None,
|
|
request_body: Some(format!("{}={}", param.name, payload)),
|
|
response_status: status,
|
|
response_headers: None,
|
|
response_snippet: Some(snippet),
|
|
screenshot_path: None,
|
|
payload: Some(payload.to_string()),
|
|
response_time_ms: None,
|
|
};
|
|
|
|
let mut finding = DastFinding::new(
|
|
String::new(),
|
|
target_id.clone(),
|
|
DastVulnType::Ssrf,
|
|
format!("SSRF ({technique}) via parameter '{}'", param.name),
|
|
format!(
|
|
"Server-side request forgery detected in parameter '{}' at {}. \
|
|
The application made a request to an internal resource ({}).",
|
|
param.name, endpoint.url, payload
|
|
),
|
|
Severity::High,
|
|
endpoint.url.clone(),
|
|
endpoint.method.clone(),
|
|
);
|
|
finding.parameter = Some(param.name.clone());
|
|
finding.exploitable = true;
|
|
finding.evidence = vec![evidence];
|
|
finding.cwe = Some("CWE-918".to_string());
|
|
finding.remediation = Some(
|
|
"Validate and sanitize all user-supplied URLs. \
|
|
Use allowlists for permitted domains and block internal IP ranges."
|
|
.to_string(),
|
|
);
|
|
|
|
findings.push(finding);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
info!(findings = findings.len(), "SSRF scan complete");
|
|
Ok(findings)
|
|
}
|
|
}
|