Files
PocketPentester/src/components/modules/FormBrute.vue
2026-04-19 21:10:40 +07:00

254 lines
9.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Terminal from "../Terminal.vue";
import Prompt from "../Prompt.vue";
import { useTerminal } from "../../composables/useTerminal";
interface Hit {
user: string;
pass: string;
status: number;
size: number;
redirect?: string;
reason: string;
time_ms: number;
}
const url = ref("https://target.com/login.php");
const method = ref("POST");
const bodyTemplate = ref("username={USER}&password={PASS}");
const headersRaw = ref("");
const users = ref("admin\nroot\nuser\ntest");
const passwords = ref("");
const mode = ref("clusterbomb");
const primeUrl = ref("");
const csrfRegex = ref("");
const successRegex = ref("");
const failRegex = ref("(?i)(invalid|wrong|incorrect|failed|error)");
const successStatusStr = ref("302");
const sizeDelta = ref<number | null>(null);
const concurrency = ref(5);
const timeoutMs = ref(10000);
const followRedirects = ref(false);
const stopOnFirst = ref(true);
const running = ref(false);
const progress = ref({ done: 0, total: 0 });
const hits = ref<Hit[]>([]);
const log = useTerminal();
const unlistens: UnlistenFn[] = [];
async function loadCommonPass() {
try {
const list: string[] = await invoke("form_brute_common_passwords");
passwords.value = list.join("\n");
} catch {}
}
function parseHeaders(): Record<string, string> {
const out: Record<string, string> = {};
for (const line of headersRaw.value.split("\n")) {
const i = line.indexOf(":");
if (i === -1) continue;
const k = line.slice(0, i).trim();
const v = line.slice(i + 1).trim();
if (k) out[k] = v;
}
return out;
}
async function run() {
if (running.value) return;
running.value = true;
hits.value = [];
progress.value = { done: 0, total: 0 };
log.clear();
const us = users.value.split(/[\s\n,]+/).map(s => s.trim()).filter(Boolean);
const ps = passwords.value.split(/\n/).map(s => s.trim()).filter(Boolean);
const statuses = successStatusStr.value.split(/[,\s]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n));
const total = mode.value === "pitchfork" ? Math.min(us.length, ps.length) : us.length * ps.length;
log.info(`target: ${method.value} ${url.value}`);
log.info(`${us.length} users × ${ps.length} passwords = ${total} attempts (${mode.value})`);
unlistens.push(await listen<string>("brute:status", (e) => log.dim(String(e.payload))));
unlistens.push(await listen<{ done: number; total: number }>("brute:progress", (e) => { progress.value = e.payload; }));
unlistens.push(await listen<Hit>("brute:hit", (e) => {
const h = e.payload;
hits.value.push(h);
log.hit(`[VALID] ${h.user}:${h.pass} status=${h.status} size=${h.size}b reason: ${h.reason}`);
}));
try {
await invoke("form_brute_run", {
req: {
url: url.value,
method: method.value,
body_template: bodyTemplate.value,
headers: parseHeaders(),
users: us,
passwords: ps,
mode: mode.value,
prime_url: primeUrl.value || null,
csrf_regex: csrfRegex.value || null,
csrf_field: "{CSRF}",
success_regex: successRegex.value || null,
fail_regex: failRegex.value || null,
success_status: statuses.length ? statuses : null,
size_delta_threshold: sizeDelta.value,
concurrency: concurrency.value,
timeout_ms: timeoutMs.value,
follow_redirects: followRedirects.value,
stop_on_first: stopOnFirst.value,
},
});
log.ok(`brute done: ${hits.value.length} valid cred(s)`);
} catch (e: any) {
log.err(String(e));
} finally {
running.value = false;
cleanup();
}
}
function cleanup() { while (unlistens.length) { const u = unlistens.pop(); if (u) u(); } }
onMounted(loadCommonPass);
onUnmounted(cleanup);
</script>
<template>
<div class="module">
<div class="form">
<div class="row">
<select v-model="method" class="method-sel">
<option>POST</option>
<option>GET</option>
<option>PUT</option>
</select>
<input v-model="url" class="url-input" placeholder="https://target.com/login" spellcheck="false" />
</div>
<label class="ta">
<span class="lbl">body template use <code>{{ "{USER}" }}</code> <code>{{ "{PASS}" }}</code> <code>{{ "{CSRF}" }}</code></span>
<textarea class="term-input" v-model="bodyTemplate" rows="2" spellcheck="false" placeholder="username={USER}&password={PASS}&_token={CSRF}" />
</label>
<details class="adv">
<summary>wordlists · {{ users.split(/\s+/).filter(Boolean).length }} users × {{ passwords.split(/\n/).filter(Boolean).length }} passwords</summary>
<div class="two-col">
<label class="ta">
<span class="lbl">users</span>
<textarea class="term-input" v-model="users" rows="5" spellcheck="false" />
</label>
<label class="ta">
<span class="lbl">passwords</span>
<textarea class="term-input" v-model="passwords" rows="5" spellcheck="false" />
</label>
</div>
<div class="row">
<label class="toggle"><input type="radio" value="clusterbomb" v-model="mode" /><span>clusterbomb (all × all)</span></label>
<label class="toggle"><input type="radio" value="pitchfork" v-model="mode" /><span>pitchfork (parallel)</span></label>
</div>
</details>
<details class="adv">
<summary>detection rules</summary>
<label class="ta"><span class="lbl">success regex (match = valid)</span>
<input class="inp" v-model="successRegex" placeholder='(?i)(welcome|dashboard|logout|my-account)' /></label>
<label class="ta"><span class="lbl">fail regex (no match = valid) <span class="hint">used if success regex empty/no-match</span></span>
<input class="inp" v-model="failRegex" placeholder='(?i)(invalid|wrong|incorrect)' /></label>
<div class="row">
<label class="mini wide"><span>success-status</span><input v-model="successStatusStr" placeholder="302" /></label>
<label class="mini"><span>size-delta</span><input v-model.number="sizeDelta" type="number" placeholder="off" /></label>
</div>
</details>
<details class="adv">
<summary>csrf + headers</summary>
<Prompt label="prime url" v-model="primeUrl" placeholder="https://target.com/login (GET first)" />
<label class="ta"><span class="lbl">csrf regex capture group 1</span>
<input class="inp" v-model="csrfRegex" placeholder='name="_token"\s+value="([^"]+)"' /></label>
<label class="ta"><span class="lbl">extra headers (Key: Value per line)</span>
<textarea class="term-input" v-model="headersRaw" rows="3" spellcheck="false" placeholder="Referer: https://target.com/login&#10;X-Requested-With: XMLHttpRequest" /></label>
</details>
<div class="row">
<label class="toggle"><input type="checkbox" v-model="stopOnFirst" /><span>stop on first</span></label>
<label class="toggle"><input type="checkbox" v-model="followRedirects" /><span>follow</span></label>
<label class="mini"><span>conc</span><input v-model.number="concurrency" type="number" min="1" max="50" /></label>
<label class="mini"><span>t/out</span><input v-model.number="timeoutMs" type="number" min="1000" max="60000" /></label>
</div>
<div class="row">
<button class="exec" :disabled="running" @click="run">{{ running ? "[ bruting... ]" : "> brute" }}</button>
</div>
<div v-if="progress.total" class="bar">
<div class="bar-fill" :style="{ width: (progress.done / progress.total) * 100 + '%' }" />
<span class="bar-text">{{ progress.done }} / {{ progress.total }}</span>
</div>
</div>
<Terminal :lines="log.lines.value" title="form-brute // credential attack" @clear="log.clear()" />
</div>
</template>
<style scoped>
.adv { border: 1px solid var(--border); background: var(--bg-panel); padding: 4px 8px; }
.adv summary { cursor: pointer; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.1em; padding: 2px 0; }
.adv summary:hover { color: var(--fg); }
.adv > *:not(summary) { margin-top: 6px; }
.method-sel {
background: var(--bg-panel);
border: 1px solid var(--alert);
color: var(--alert);
padding: 4px 8px;
font-weight: 700;
font-size: 12px;
font-family: inherit;
}
.method-sel option { background: var(--bg); color: var(--fg); }
.url-input {
flex: 1;
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
}
.url-input:focus { border-color: var(--accent); outline: none; }
.inp {
background: var(--bg-panel);
border: 1px solid var(--border);
color: var(--fg);
padding: 4px 8px;
font-size: 12px;
font-family: inherit;
width: 100%;
}
.inp:focus { border-color: var(--info); outline: none; }
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
code {
color: var(--info);
background: var(--bg);
padding: 0 4px;
border: 1px solid var(--border);
font-size: 10px;
}
.hint { color: var(--fg-ghost); font-size: 9px; text-transform: none; font-style: italic; }
@media (max-width: 640px) {
.two-col { grid-template-columns: 1fr; }
}
</style>