1701 lines
41 KiB
HTML
1701 lines
41 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}
|
||
CrackLab — Create Method
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
|
||
<style>
|
||
.method-form {
|
||
width: 100%;
|
||
max-width: 1500px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
#method-form {
|
||
display: grid;
|
||
grid-template-columns:
|
||
minmax(320px, 0.8fr)
|
||
minmax(620px, 1.6fr);
|
||
gap: 24px;
|
||
align-items: start;
|
||
}
|
||
|
||
.method-form-section {
|
||
margin-bottom: 0;
|
||
padding: 24px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.method-form-section h2 {
|
||
margin: 0 0 20px;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.method-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
}
|
||
|
||
.method-field-full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.method-field label {
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.4px;
|
||
}
|
||
|
||
.method-field input,
|
||
.method-field select,
|
||
.method-field textarea {
|
||
width: 100%;
|
||
padding: 11px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
background: var(--terminal);
|
||
color: var(--text);
|
||
font: inherit;
|
||
outline: none;
|
||
}
|
||
|
||
.method-field input:focus,
|
||
.method-field select:focus,
|
||
.method-field textarea:focus {
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.method-field textarea {
|
||
min-height: 100px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.method-help {
|
||
margin-top: 4px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.method-step {
|
||
margin-bottom: 18px;
|
||
padding: 20px;
|
||
background: var(--surface-light);
|
||
border: 1px solid var(--border);
|
||
border-radius: 9px;
|
||
}
|
||
|
||
.method-step:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.method-step-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 15px;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.method-step-title {
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.method-step-mode {
|
||
margin-top: 4px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.method-step-fields {
|
||
display: grid;
|
||
grid-template-columns:
|
||
repeat(
|
||
auto-fit,
|
||
minmax(240px, 1fr)
|
||
);
|
||
gap: 18px;
|
||
}
|
||
|
||
.method-error {
|
||
margin-bottom: 24px;
|
||
padding: 14px 16px;
|
||
border: 1px solid var(--red);
|
||
border-radius: 8px;
|
||
background: rgba(239, 98, 98, 0.08);
|
||
color: var(--red);
|
||
}
|
||
|
||
.method-actions {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
margin-top: 0;
|
||
}
|
||
|
||
.method-remove-step {
|
||
min-height: 32px;
|
||
padding: 0 11px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: var(--muted);
|
||
font: inherit;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.method-remove-step:hover {
|
||
color: var(--red);
|
||
border-color: var(--red);
|
||
}
|
||
|
||
.method-mode-help {
|
||
grid-column: 1 / -1;
|
||
margin-top: -2px;
|
||
padding: 15px 16px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: var(--terminal);
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.method-mode-help-title {
|
||
margin-bottom: 6px;
|
||
color: var(--text);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.method-mode-help code {
|
||
padding: 2px 5px;
|
||
border-radius: 4px;
|
||
background: var(--surface-light);
|
||
color: var(--text);
|
||
font-family: monospace;
|
||
}
|
||
|
||
.method-keyspace {
|
||
grid-column: 1 / -1;
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
background: var(--terminal);
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.method-keyspace strong {
|
||
color: var(--text);
|
||
}
|
||
|
||
.method-steps-section {
|
||
grid-column: 2;
|
||
grid-row: 1 / span 2;
|
||
}
|
||
|
||
.method-mask-bulk {
|
||
grid-column: 1;
|
||
grid-row: 2 / span 4;
|
||
align-self: start;
|
||
padding: 20px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 9px;
|
||
background: var(--surface-light);
|
||
}
|
||
|
||
.method-mask-bulk-title {
|
||
margin-bottom: 7px;
|
||
color: var(--text);
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.method-mask-bulk textarea {
|
||
display: block;
|
||
width: 100%;
|
||
min-height: 250px;
|
||
padding: 11px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
background: var(--terminal);
|
||
color: var(--text);
|
||
font: inherit;
|
||
font-family: monospace;
|
||
outline: none;
|
||
resize: none;
|
||
white-space: pre;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.method-mask-bulk textarea:focus {
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.method-mask-bulk-actions {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.method-step-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-top: 18px;
|
||
}
|
||
|
||
.method-mask-bulk {
|
||
display: none;
|
||
}
|
||
|
||
.method-mask-bulk.is-visible {
|
||
display: block;
|
||
}
|
||
.method-mask-bulk-status {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.method-mask-bulk-status-error {
|
||
color: var(--red);
|
||
}
|
||
|
||
.method-mask-bulk-status-success {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.method-mask-bulk-status ul {
|
||
margin: 6px 0 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.method-mask-bulk button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
@media (max-width: 1000px) {
|
||
#method-form {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.method-actions {
|
||
grid-column: auto;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.method-actions {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.method-step-actions {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.method-mask-bulk-actions {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
|
||
<div class="method-form">
|
||
|
||
<div class="methods-page-header">
|
||
|
||
<div>
|
||
<h1 class="page-title">
|
||
Создание метода
|
||
</h1>
|
||
|
||
<div class="page-description">
|
||
Новый рабочий метод будет сохранён в базе данных.
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
{% if error %}
|
||
|
||
<div class="method-error">
|
||
{{ error }}
|
||
</div>
|
||
|
||
{% endif %}
|
||
|
||
|
||
<form
|
||
method="post"
|
||
id="method-form"
|
||
>
|
||
|
||
<section class="method-form-section">
|
||
|
||
<h2>
|
||
Основная информация
|
||
</h2>
|
||
|
||
<div class="method-form-grid">
|
||
|
||
<div class="method-field">
|
||
<label for="name">
|
||
Название
|
||
</label>
|
||
|
||
<input
|
||
id="name"
|
||
name="name"
|
||
type="text"
|
||
required
|
||
autocomplete="off"
|
||
>
|
||
</div>
|
||
|
||
|
||
<div class="method-field">
|
||
|
||
<label for="hash_mode">
|
||
Тип хэша
|
||
</label>
|
||
|
||
<select
|
||
id="hash_mode"
|
||
name="hash_mode"
|
||
required
|
||
>
|
||
<option value="22000">
|
||
WPA-PBKDF2-PMKID+EAPOL (22000)
|
||
</option>
|
||
</select>
|
||
|
||
<div class="method-help">
|
||
Сейчас CrackLab работает с WPA/WPA2 hash mode 22000.
|
||
Дополнительные Wi-Fi hash modes можно будет добавить
|
||
позже.
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
<div class="method-field method-field-full">
|
||
|
||
<label for="description">
|
||
Описание
|
||
</label>
|
||
|
||
<textarea
|
||
id="description"
|
||
name="description"
|
||
placeholder="Например: имя + четыре цифры"
|
||
></textarea>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</section>
|
||
|
||
|
||
<section class="method-form-section method-steps-section">
|
||
|
||
<h2>
|
||
Шаги метода
|
||
</h2>
|
||
|
||
<div id="steps"></div>
|
||
|
||
<div class="method-step-actions">
|
||
|
||
<button
|
||
type="button"
|
||
class="methods-button-secondary"
|
||
id="add-step"
|
||
>
|
||
+ Добавить шаг
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
class="methods-button-secondary"
|
||
id="add-mask-bulk"
|
||
>
|
||
+ Добавить списком
|
||
</button>
|
||
|
||
</div>
|
||
|
||
<input
|
||
type="hidden"
|
||
id="step_count"
|
||
name="step_count"
|
||
value="1"
|
||
>
|
||
|
||
</section>
|
||
|
||
<div
|
||
class="method-mask-bulk"
|
||
id="mask-bulk"
|
||
>
|
||
<div class="method-mask-bulk-title">
|
||
Список масок
|
||
</div>
|
||
|
||
<textarea
|
||
data-mask-bulk
|
||
placeholder="?d?d?d?dwest
|
||
?u?u?u-west
|
||
?u?uwest?l?l"
|
||
spellcheck="false"
|
||
></textarea>
|
||
|
||
<div class="method-help">
|
||
Только для attack mode 3.
|
||
Одна маска на строку.
|
||
Пустые строки игнорируются.
|
||
Дубликаты удаляются после нормализации.
|
||
Минимальная длина кандидата — 8 позиций.
|
||
</div>
|
||
|
||
<div class="method-mask-bulk-actions">
|
||
<button
|
||
type="button"
|
||
class="methods-button-secondary"
|
||
data-mask-bulk-add
|
||
disabled
|
||
>
|
||
Добавить маски в шаги
|
||
</button>
|
||
|
||
<div
|
||
class="method-mask-bulk-status"
|
||
data-mask-bulk-status
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="method-actions">
|
||
|
||
<button
|
||
type="submit"
|
||
class="methods-button"
|
||
>
|
||
Сохранить метод
|
||
</button>
|
||
|
||
<a
|
||
class="methods-button-secondary"
|
||
href="{{ url_for('cracklab.methods_page') }}"
|
||
>
|
||
Отмена
|
||
</a>
|
||
|
||
</div>
|
||
|
||
</form>
|
||
|
||
</div>
|
||
|
||
|
||
<script>
|
||
const stepsContainer =
|
||
document.getElementById("steps");
|
||
|
||
const stepCountInput =
|
||
document.getElementById("step_count");
|
||
|
||
const addStepButton =
|
||
document.getElementById("add-step");
|
||
|
||
const addMaskBulkButton =
|
||
document.getElementById("add-mask-bulk");
|
||
|
||
const methodForm =
|
||
document.getElementById("method-form");
|
||
|
||
let stepCount = 0;
|
||
|
||
|
||
const modeNames = {
|
||
"0": "Dictionary",
|
||
"1": "Combination",
|
||
"3": "Mask",
|
||
"6": "Dictionary + Mask",
|
||
"7": "Mask + Dictionary",
|
||
};
|
||
|
||
|
||
const maskCharsetSizes = {
|
||
"?l": 26n,
|
||
"?u": 26n,
|
||
"?d": 10n,
|
||
"?s": 33n,
|
||
"?a": 95n,
|
||
"?b": 256n,
|
||
};
|
||
|
||
|
||
function escapeHtml(value) {
|
||
return value
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
|
||
function dictionaryField(
|
||
name,
|
||
label,
|
||
help,
|
||
) {
|
||
return `
|
||
<div class="method-field">
|
||
<label>
|
||
${escapeHtml(label)}
|
||
</label>
|
||
|
||
<input
|
||
name="${escapeHtml(name)}"
|
||
type="text"
|
||
placeholder="insidepro-wpa"
|
||
required
|
||
autocomplete="off"
|
||
>
|
||
|
||
<div class="method-help">
|
||
${escapeHtml(help)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
|
||
function maskField(name) {
|
||
return `
|
||
<div class="method-field">
|
||
<label>
|
||
Маска
|
||
</label>
|
||
|
||
<input
|
||
name="${escapeHtml(name)}"
|
||
type="text"
|
||
placeholder="?d?d?d?d?d?d?d?d"
|
||
required
|
||
autocomplete="off"
|
||
>
|
||
|
||
<div class="method-help">
|
||
Charset:
|
||
<code>?l</code>,
|
||
<code>?u</code>,
|
||
<code>?d</code>,
|
||
<code>?s</code>,
|
||
<code>?a</code>,
|
||
<code>?b</code>.
|
||
Для буквального <code>?</code> используйте
|
||
<code>??</code>.
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
|
||
function maskBulkField() {
|
||
return `
|
||
<div class="method-mask-bulk">
|
||
<div class="method-mask-bulk-title">
|
||
Список масок
|
||
</div>
|
||
|
||
<textarea
|
||
data-mask-bulk
|
||
placeholder="?d?d?d?dwest
|
||
?u?u?u-west
|
||
?u?uwest?l?l"
|
||
spellcheck="false"
|
||
></textarea>
|
||
|
||
<div class="method-help">
|
||
Только для attack mode 3.
|
||
Одна маска на строку.
|
||
Пустые строки игнорируются.
|
||
Дубликаты удаляются после нормализации.
|
||
Минимальная длина кандидата — 8 позиций.
|
||
</div>
|
||
|
||
<div class="method-mask-bulk-actions">
|
||
<button
|
||
type="button"
|
||
class="methods-button-secondary"
|
||
data-mask-bulk-add
|
||
disabled
|
||
>
|
||
Добавить маски в шаги
|
||
</button>
|
||
|
||
<div
|
||
class="method-mask-bulk-status"
|
||
data-mask-bulk-status
|
||
></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
|
||
function modeHelp(mode) {
|
||
const helps = {
|
||
"0": `
|
||
<div class="method-mode-help-title">
|
||
Как работает словарь
|
||
</div>
|
||
|
||
Hashcat последовательно проверяет каждую строку
|
||
словаря как отдельный кандидат пароля.
|
||
|
||
Например, словарь может содержать
|
||
<code>password</code>,
|
||
<code>qwerty</code>,
|
||
<code>welcome</code>.
|
||
`,
|
||
|
||
"1": `
|
||
<div class="method-mode-help-title">
|
||
Как работает комбинация
|
||
</div>
|
||
|
||
Каждое слово левого словаря объединяется
|
||
с каждым словом правого словаря.
|
||
|
||
Например:
|
||
<code>alex</code> + <code>1234</code>
|
||
→ <code>alex1234</code>.
|
||
|
||
<br>
|
||
|
||
<strong>Порядок важен:</strong>
|
||
левый словарь образует левую часть кандидата,
|
||
правый — правую.
|
||
`,
|
||
|
||
"3": `
|
||
<div class="method-mode-help-title">
|
||
Как работает маска
|
||
</div>
|
||
|
||
Маска задаёт шаблон кандидата.
|
||
Hashcat перебирает все варианты символов,
|
||
разрешённых в каждой позиции.
|
||
|
||
<br><br>
|
||
|
||
<code>?l</code> — строчная буква a-z<br>
|
||
<code>?u</code> — заглавная буква A-Z<br>
|
||
<code>?d</code> — цифра 0-9<br>
|
||
<code>?s</code> — специальный символ<br>
|
||
<code>?a</code> — буква, цифра или специальный символ<br>
|
||
<code>?b</code> — полный байтовый charset<br>
|
||
<code>??</code> — буквальный символ <code>?</code>
|
||
|
||
<br><br>
|
||
|
||
В режиме 3 кандидат должен содержать
|
||
минимум 8 позиций.
|
||
|
||
Например:
|
||
<code>?d?d?d?dwest</code>
|
||
→ 8 позиций.
|
||
|
||
<br><br>
|
||
|
||
Текст без charset является буквальной
|
||
частью кандидата.
|
||
Например:
|
||
<code>u?u?west?l?l</code>
|
||
→ 8 позиций.
|
||
`,
|
||
|
||
"6": `
|
||
<div class="method-mode-help-title">
|
||
Как работает Dictionary + Mask
|
||
</div>
|
||
|
||
К каждому слову словаря добавляются все варианты,
|
||
соответствующие маске.
|
||
|
||
<br>
|
||
|
||
Например, слово <code>alex</code> и маска
|
||
<code>?d?d</code> дают кандидатов от
|
||
<code>alex00</code> до <code>alex99</code>.
|
||
|
||
<br><br>
|
||
|
||
<strong>Порядок:</strong>
|
||
словарь слева, маска справа.
|
||
`,
|
||
|
||
"7": `
|
||
<div class="method-mode-help-title">
|
||
Как работает Mask + Dictionary
|
||
</div>
|
||
|
||
К каждой комбинации, созданной маской,
|
||
добавляется слово из словаря справа.
|
||
|
||
<br>
|
||
|
||
Например, маска <code>?d?d</code> и слово
|
||
<code>alex</code> дают
|
||
<code>00alex</code>,
|
||
<code>01alex</code>,
|
||
<code>02alex</code>
|
||
и так далее.
|
||
|
||
<br><br>
|
||
|
||
<strong>Порядок:</strong>
|
||
маска слева, словарь справа.
|
||
`,
|
||
};
|
||
|
||
return `
|
||
<div class="method-mode-help">
|
||
${helps[mode] || ""}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
|
||
function analyzeMask(mask) {
|
||
if (!mask) {
|
||
return {
|
||
length: 0,
|
||
keyspace: 1n,
|
||
error: "Маска не должна быть пустой.",
|
||
};
|
||
}
|
||
|
||
let length = 0;
|
||
let keyspace = 1n;
|
||
|
||
for (
|
||
let index = 0;
|
||
index < mask.length;
|
||
) {
|
||
if (mask[index] !== "?") {
|
||
length += 1;
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
if (index + 1 >= mask.length) {
|
||
return {
|
||
length,
|
||
keyspace,
|
||
error:
|
||
"Одиночный символ '?' в позиции " +
|
||
`${index + 1}. Для буквального ? используйте ??.`,
|
||
};
|
||
}
|
||
|
||
const token =
|
||
mask.slice(
|
||
index,
|
||
index + 2,
|
||
);
|
||
|
||
if (token === "??") {
|
||
length += 1;
|
||
index += 2;
|
||
continue;
|
||
}
|
||
|
||
if (
|
||
!Object.prototype.hasOwnProperty.call(
|
||
maskCharsetSizes,
|
||
token,
|
||
)
|
||
) {
|
||
return {
|
||
length,
|
||
keyspace,
|
||
error:
|
||
`Неизвестный charset ${token} ` +
|
||
`в позиции ${index + 1}.`,
|
||
};
|
||
}
|
||
|
||
length += 1;
|
||
keyspace *=
|
||
maskCharsetSizes[token];
|
||
|
||
index += 2;
|
||
}
|
||
|
||
return {
|
||
length,
|
||
keyspace,
|
||
error: null,
|
||
};
|
||
}
|
||
|
||
|
||
function calculateMaskKeyspace(mask) {
|
||
const analysis =
|
||
analyzeMask(mask);
|
||
|
||
if (analysis.error) {
|
||
return null;
|
||
}
|
||
|
||
return analysis.keyspace;
|
||
}
|
||
|
||
|
||
function validateBulkMasks(value) {
|
||
const masks = [];
|
||
const seen = new Set();
|
||
const errors = [];
|
||
|
||
value.split(/\r?\n/).forEach(
|
||
(rawLine, lineIndex) => {
|
||
const mask =
|
||
rawLine.trim();
|
||
|
||
if (!mask) {
|
||
return;
|
||
}
|
||
|
||
const analysis =
|
||
analyzeMask(mask);
|
||
|
||
if (analysis.error) {
|
||
errors.push(
|
||
`Строка ${lineIndex + 1}: ` +
|
||
analysis.error,
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (analysis.length < 8) {
|
||
errors.push(
|
||
`Строка ${lineIndex + 1}: ` +
|
||
"маска даёт только " +
|
||
`${analysis.length} позиций, ` +
|
||
"минимум — 8.",
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (seen.has(mask)) {
|
||
return;
|
||
}
|
||
|
||
seen.add(mask);
|
||
masks.push(mask);
|
||
},
|
||
);
|
||
|
||
return {
|
||
masks,
|
||
errors,
|
||
};
|
||
}
|
||
|
||
|
||
function formatNumber(value) {
|
||
return new Intl.NumberFormat(
|
||
"ru-RU",
|
||
).format(value);
|
||
}
|
||
|
||
|
||
function updateMaskKeyspace(
|
||
step,
|
||
mask,
|
||
) {
|
||
const keyspace =
|
||
step.querySelector(
|
||
"[data-keyspace]",
|
||
);
|
||
|
||
if (!keyspace) {
|
||
return;
|
||
}
|
||
|
||
const maskInput =
|
||
step.querySelector(
|
||
'input[name$="_mask"]',
|
||
);
|
||
|
||
const analysis =
|
||
analyzeMask(mask);
|
||
|
||
const modeSelect =
|
||
step.querySelector(
|
||
"[data-attack-mode]",
|
||
);
|
||
|
||
const minimumLengthRequired =
|
||
modeSelect &&
|
||
modeSelect.value === "3";
|
||
|
||
let error =
|
||
analysis.error;
|
||
|
||
if (
|
||
!error &&
|
||
minimumLengthRequired &&
|
||
analysis.length < 8
|
||
) {
|
||
error =
|
||
"Маска должна давать минимум " +
|
||
`8 позиций, сейчас ${analysis.length}.`;
|
||
}
|
||
|
||
if (maskInput) {
|
||
maskInput.setCustomValidity(
|
||
error || "",
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
keyspace.innerHTML =
|
||
"<strong>Ошибка маски:</strong> " +
|
||
escapeHtml(error);
|
||
return;
|
||
}
|
||
|
||
keyspace.innerHTML =
|
||
"<strong>Длина кандидата:</strong> " +
|
||
formatNumber(
|
||
analysis.length,
|
||
) +
|
||
"<br>" +
|
||
"<strong>Количество кандидатов:</strong> " +
|
||
formatNumber(
|
||
analysis.keyspace,
|
||
);
|
||
}
|
||
|
||
|
||
function renderStepFields(
|
||
stepNumber,
|
||
mode,
|
||
) {
|
||
const prefix =
|
||
`step_${stepNumber}`;
|
||
|
||
let html = "";
|
||
|
||
|
||
if (mode === "0") {
|
||
html += dictionaryField(
|
||
`${prefix}_dictionary`,
|
||
"Словарь",
|
||
"Укажите логическое имя wordlist, которое настроено на клиенте.",
|
||
);
|
||
}
|
||
|
||
|
||
if (mode === "1") {
|
||
html += dictionaryField(
|
||
`${prefix}_dictionary_1`,
|
||
"Словарь — Left",
|
||
"Этот словарь образует левую часть кандидата.",
|
||
);
|
||
|
||
html += dictionaryField(
|
||
`${prefix}_dictionary_2`,
|
||
"Словарь — Right",
|
||
"Этот словарь образует правую часть кандидата.",
|
||
);
|
||
}
|
||
|
||
|
||
if (mode === "3") {
|
||
html += maskField(
|
||
`${prefix}_mask`,
|
||
);
|
||
|
||
html += `
|
||
<div
|
||
class="method-keyspace"
|
||
data-keyspace
|
||
>
|
||
<strong>Длина кандидата:</strong>
|
||
укажите маску
|
||
</div>
|
||
`;
|
||
|
||
}
|
||
|
||
|
||
if (mode === "6") {
|
||
html += dictionaryField(
|
||
`${prefix}_dictionary`,
|
||
"Словарь — Left",
|
||
"Словарь образует левую часть кандидата.",
|
||
);
|
||
|
||
html += maskField(
|
||
`${prefix}_mask`,
|
||
);
|
||
|
||
html += `
|
||
<div
|
||
class="method-keyspace"
|
||
data-keyspace
|
||
>
|
||
<strong>Количество кандидатов:</strong>
|
||
укажите маску
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
|
||
if (mode === "7") {
|
||
html += maskField(
|
||
`${prefix}_mask`,
|
||
);
|
||
|
||
html += `
|
||
<div
|
||
class="method-keyspace"
|
||
data-keyspace
|
||
>
|
||
<strong>Количество кандидатов:</strong>
|
||
укажите маску
|
||
</div>
|
||
`;
|
||
|
||
html += dictionaryField(
|
||
`${prefix}_dictionary`,
|
||
"Словарь — Right",
|
||
"Словарь образует правую часть кандидата.",
|
||
);
|
||
}
|
||
|
||
|
||
return html;
|
||
}
|
||
|
||
|
||
function bindMaskInput(step) {
|
||
const mask =
|
||
step.querySelector(
|
||
'input[name$="_mask"]',
|
||
);
|
||
|
||
if (!mask) {
|
||
return;
|
||
}
|
||
|
||
mask.addEventListener(
|
||
"input",
|
||
() => {
|
||
updateMaskKeyspace(
|
||
step,
|
||
mask.value,
|
||
);
|
||
},
|
||
);
|
||
|
||
updateMaskKeyspace(
|
||
step,
|
||
mask.value,
|
||
);
|
||
}
|
||
|
||
|
||
function bindMaskBulk() {
|
||
const bulk =
|
||
document.getElementById("mask-bulk");
|
||
|
||
const textarea =
|
||
bulk.querySelector(
|
||
"[data-mask-bulk]",
|
||
);
|
||
|
||
const button =
|
||
bulk.querySelector(
|
||
"[data-mask-bulk-add]",
|
||
);
|
||
|
||
const status =
|
||
bulk.querySelector(
|
||
"[data-mask-bulk-status]",
|
||
);
|
||
|
||
const refresh = () => {
|
||
const result =
|
||
validateBulkMasks(
|
||
textarea.value,
|
||
);
|
||
|
||
button.disabled =
|
||
result.errors.length > 0 ||
|
||
result.masks.length === 0;
|
||
|
||
if (result.errors.length > 0) {
|
||
status.className =
|
||
"method-mask-bulk-status " +
|
||
"method-mask-bulk-status-error";
|
||
|
||
status.innerHTML =
|
||
"<ul>" +
|
||
result.errors
|
||
.map(
|
||
(error) =>
|
||
`<li>${escapeHtml(error)}</li>`,
|
||
)
|
||
.join("") +
|
||
"</ul>";
|
||
|
||
return;
|
||
}
|
||
|
||
if (result.masks.length > 0) {
|
||
status.className =
|
||
"method-mask-bulk-status " +
|
||
"method-mask-bulk-status-success";
|
||
|
||
status.textContent =
|
||
`Готово: ${result.masks.length} ` +
|
||
"уникальных масок.";
|
||
|
||
return;
|
||
}
|
||
|
||
status.className =
|
||
"method-mask-bulk-status";
|
||
|
||
status.textContent =
|
||
"Введите по одной маске на строку.";
|
||
};
|
||
|
||
textarea.addEventListener(
|
||
"input",
|
||
refresh,
|
||
);
|
||
|
||
button.addEventListener(
|
||
"click",
|
||
() => {
|
||
const result =
|
||
validateBulkMasks(
|
||
textarea.value,
|
||
);
|
||
|
||
if (
|
||
result.errors.length > 0 ||
|
||
result.masks.length === 0
|
||
) {
|
||
refresh();
|
||
return;
|
||
}
|
||
|
||
addBulkMasks(
|
||
result.masks,
|
||
);
|
||
|
||
textarea.value = "";
|
||
|
||
button.disabled = true;
|
||
|
||
status.className =
|
||
"method-mask-bulk-status " +
|
||
"method-mask-bulk-status-success";
|
||
|
||
status.textContent =
|
||
`Добавлено: ${result.masks.length} ` +
|
||
"масок.";
|
||
},
|
||
);
|
||
|
||
refresh();
|
||
}
|
||
|
||
function updateMaskBulkVisibility() {
|
||
const bulk =
|
||
document.getElementById("mask-bulk");
|
||
|
||
const hasMaskMode =
|
||
Array.from(
|
||
stepsContainer.querySelectorAll(
|
||
"[data-attack-mode]",
|
||
),
|
||
).some(
|
||
(modeSelect) =>
|
||
modeSelect.value === "3",
|
||
);
|
||
|
||
addMaskBulkButton.disabled =
|
||
!hasMaskMode;
|
||
|
||
if (!hasMaskMode) {
|
||
bulk.classList.remove(
|
||
"is-visible",
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
function addBulkMasks(
|
||
masks,
|
||
) {
|
||
masks.forEach(
|
||
(mask) => {
|
||
const newStep =
|
||
addStep();
|
||
|
||
const modeSelect =
|
||
newStep.querySelector(
|
||
"[data-attack-mode]",
|
||
);
|
||
|
||
modeSelect.value = "3";
|
||
|
||
modeSelect.dispatchEvent(
|
||
new Event("change"),
|
||
);
|
||
|
||
const newMask =
|
||
newStep.querySelector(
|
||
'input[name$="_mask"]',
|
||
);
|
||
|
||
newMask.value =
|
||
mask;
|
||
|
||
newMask.dispatchEvent(
|
||
new Event(
|
||
"input",
|
||
{ bubbles: true },
|
||
),
|
||
);
|
||
},
|
||
);
|
||
|
||
stepCountInput.value =
|
||
stepCount;
|
||
}
|
||
|
||
|
||
function addStep() {
|
||
stepCount += 1;
|
||
|
||
const currentStepNumber =
|
||
stepCount;
|
||
|
||
const step =
|
||
document.createElement("div");
|
||
|
||
step.className =
|
||
"method-step";
|
||
|
||
step.dataset.step =
|
||
currentStepNumber;
|
||
|
||
step.innerHTML = `
|
||
<div class="method-step-header">
|
||
|
||
<div>
|
||
|
||
<div class="method-step-title">
|
||
Step ${currentStepNumber}
|
||
</div>
|
||
|
||
<div
|
||
class="method-step-mode"
|
||
data-mode-label
|
||
>
|
||
Attack mode
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
<button
|
||
type="button"
|
||
class="method-remove-step"
|
||
data-remove-step
|
||
>
|
||
Удалить
|
||
</button>
|
||
|
||
</div>
|
||
|
||
|
||
<div class="method-step-fields">
|
||
|
||
<div class="method-field">
|
||
|
||
<label>
|
||
Режим атаки
|
||
</label>
|
||
|
||
<select
|
||
name="step_${currentStepNumber}_attack_mode"
|
||
data-attack-mode
|
||
required
|
||
>
|
||
|
||
<option value="">
|
||
Выберите режим
|
||
</option>
|
||
|
||
<option value="0">
|
||
0 — Dictionary
|
||
</option>
|
||
|
||
<option value="1">
|
||
1 — Combination
|
||
</option>
|
||
|
||
<option value="3">
|
||
3 — Mask
|
||
</option>
|
||
|
||
<option value="6">
|
||
6 — Dictionary + Mask
|
||
</option>
|
||
|
||
<option value="7">
|
||
7 — Mask + Dictionary
|
||
</option>
|
||
|
||
</select>
|
||
|
||
</div>
|
||
|
||
|
||
<div class="method-field">
|
||
|
||
<label>
|
||
Профиль нагрузки
|
||
</label>
|
||
|
||
<select
|
||
name="step_${currentStepNumber}_workload_profile"
|
||
data-workload-profile
|
||
>
|
||
|
||
<option value="1">
|
||
1 — Low
|
||
</option>
|
||
|
||
<option
|
||
value="2"
|
||
selected
|
||
>
|
||
2 — Default
|
||
</option>
|
||
|
||
<option value="3">
|
||
3 — High
|
||
</option>
|
||
|
||
<option value="4">
|
||
4 — Nightmare
|
||
</option>
|
||
|
||
</select>
|
||
|
||
<div class="method-help">
|
||
Определяет, насколько агрессивно Hashcat
|
||
использует ресурсы компьютера.
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
<div
|
||
class="method-field-full"
|
||
data-step-fields
|
||
></div>
|
||
|
||
</div>
|
||
`;
|
||
|
||
|
||
stepsContainer.appendChild(
|
||
step,
|
||
);
|
||
|
||
|
||
const modeSelect =
|
||
step.querySelector(
|
||
"[data-attack-mode]",
|
||
);
|
||
|
||
const fields =
|
||
step.querySelector(
|
||
"[data-step-fields]",
|
||
);
|
||
|
||
const modeLabel =
|
||
step.querySelector(
|
||
"[data-mode-label]",
|
||
);
|
||
|
||
|
||
function updateStep() {
|
||
const mode =
|
||
modeSelect.value;
|
||
|
||
const number =
|
||
Number(
|
||
step.dataset.step,
|
||
);
|
||
|
||
fields.innerHTML =
|
||
renderStepFields(
|
||
number,
|
||
mode,
|
||
);
|
||
|
||
modeLabel.textContent =
|
||
mode
|
||
? `${modeNames[mode]} (mode ${mode})`
|
||
: "Attack mode";
|
||
|
||
|
||
if (mode) {
|
||
fields.insertAdjacentHTML(
|
||
"beforeend",
|
||
modeHelp(mode),
|
||
);
|
||
}
|
||
|
||
|
||
bindMaskInput(step);
|
||
updateMaskBulkVisibility();
|
||
}
|
||
|
||
|
||
modeSelect.addEventListener(
|
||
"change",
|
||
updateStep,
|
||
);
|
||
|
||
|
||
step.querySelector(
|
||
"[data-remove-step]",
|
||
).addEventListener(
|
||
"click",
|
||
() => {
|
||
|
||
if (
|
||
stepsContainer.children.length <= 1
|
||
) {
|
||
return;
|
||
}
|
||
|
||
step.remove();
|
||
|
||
rebuildStepNumbers();
|
||
},
|
||
);
|
||
|
||
|
||
updateStep();
|
||
|
||
stepCountInput.value =
|
||
stepCount;
|
||
|
||
return step;
|
||
}
|
||
|
||
|
||
function rebuildStepNumbers() {
|
||
const stepElements =
|
||
Array.from(
|
||
stepsContainer.children,
|
||
);
|
||
|
||
stepCount =
|
||
stepElements.length;
|
||
|
||
stepElements.forEach(
|
||
(step, index) => {
|
||
const number =
|
||
index + 1;
|
||
|
||
step.dataset.step =
|
||
number;
|
||
|
||
const title =
|
||
step.querySelector(
|
||
".method-step-title",
|
||
);
|
||
|
||
if (title) {
|
||
title.textContent =
|
||
`Step ${number}`;
|
||
}
|
||
|
||
const modeSelect =
|
||
step.querySelector(
|
||
"[data-attack-mode]",
|
||
);
|
||
|
||
if (modeSelect) {
|
||
modeSelect.name =
|
||
`step_${number}_attack_mode`;
|
||
}
|
||
|
||
const workload =
|
||
step.querySelector(
|
||
"[data-workload-profile]",
|
||
);
|
||
|
||
if (workload) {
|
||
workload.name =
|
||
`step_${number}_workload_profile`;
|
||
}
|
||
|
||
const namedFields =
|
||
step.querySelectorAll(
|
||
"[data-step-fields] [name]",
|
||
);
|
||
|
||
namedFields.forEach(
|
||
(field) => {
|
||
field.name =
|
||
field.name.replace(
|
||
/^step_\d+_/,
|
||
`step_${number}_`,
|
||
);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
|
||
stepCountInput.value =
|
||
stepCount;
|
||
}
|
||
|
||
|
||
methodForm.addEventListener(
|
||
"submit",
|
||
(event) => {
|
||
const maskInputs =
|
||
stepsContainer.querySelectorAll(
|
||
'input[name$="_mask"]',
|
||
);
|
||
|
||
let firstInvalidMask = null;
|
||
|
||
maskInputs.forEach(
|
||
(maskInput) => {
|
||
const step =
|
||
maskInput.closest(
|
||
".method-step",
|
||
);
|
||
|
||
const analysis =
|
||
analyzeMask(
|
||
maskInput.value.trim(),
|
||
);
|
||
|
||
const modeSelect =
|
||
step.querySelector(
|
||
"[data-attack-mode]",
|
||
);
|
||
|
||
const mode =
|
||
modeSelect
|
||
? modeSelect.value
|
||
: "";
|
||
|
||
let error =
|
||
analysis.error;
|
||
|
||
if (
|
||
!error &&
|
||
mode === "3" &&
|
||
analysis.length < 8
|
||
) {
|
||
error =
|
||
"Маска должна давать минимум " +
|
||
`8 позиций, сейчас ${analysis.length}.`;
|
||
}
|
||
|
||
maskInput.setCustomValidity(
|
||
error || "",
|
||
);
|
||
|
||
if (
|
||
error &&
|
||
!firstInvalidMask
|
||
) {
|
||
firstInvalidMask =
|
||
maskInput;
|
||
}
|
||
},
|
||
);
|
||
|
||
if (firstInvalidMask) {
|
||
event.preventDefault();
|
||
firstInvalidMask.reportValidity();
|
||
}
|
||
},
|
||
);
|
||
|
||
addMaskBulkButton.addEventListener(
|
||
"click",
|
||
() => {
|
||
if (addMaskBulkButton.disabled) {
|
||
return;
|
||
}
|
||
const bulk =
|
||
document.getElementById(
|
||
"mask-bulk",
|
||
);
|
||
|
||
bulk.classList.add(
|
||
"is-visible",
|
||
);
|
||
|
||
bulk.querySelector(
|
||
"[data-mask-bulk]",
|
||
).focus();
|
||
},
|
||
);
|
||
|
||
addStepButton.addEventListener(
|
||
"click",
|
||
() => {
|
||
addStep();
|
||
},
|
||
);
|
||
|
||
|
||
addStep();
|
||
</script>
|
||
|
||
{% endblock %}
|