This commit is contained in:
taqin
2026-04-19 21:10:40 +07:00
parent 5fdd214fdc
commit 27381d4e37
211 changed files with 53571 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
<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>