903 lines
17 KiB
Python
903 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Server-side Hashcat report page.
|
||
"""
|
||
|
||
from html import escape
|
||
|
||
from reports.hashcat import (
|
||
get_database_statistics,
|
||
get_export_statistics,
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# Hashcat page
|
||
# ============================================================
|
||
|
||
def render_hashcat_page(
|
||
conn,
|
||
context=None,
|
||
message=None,
|
||
import_result=None,
|
||
):
|
||
from reports.template import (
|
||
page_begin,
|
||
page_end,
|
||
)
|
||
|
||
if context is None:
|
||
from reports.template import (
|
||
ReportContext,
|
||
)
|
||
|
||
context = ReportContext(
|
||
mode="server"
|
||
)
|
||
|
||
export_statistics = get_export_statistics(
|
||
conn
|
||
)
|
||
|
||
database_statistics = get_database_statistics(
|
||
conn
|
||
)
|
||
|
||
html = page_begin(
|
||
"Hashcat",
|
||
(
|
||
"Export WPA handshakes for Hashcat "
|
||
"and import passwords returned by "
|
||
"hashcat --show."
|
||
),
|
||
"hashcat",
|
||
context
|
||
)
|
||
|
||
if message:
|
||
|
||
message_type = escape(
|
||
str(
|
||
message.get(
|
||
"type",
|
||
"info"
|
||
)
|
||
)
|
||
)
|
||
|
||
message_text = escape(
|
||
str(
|
||
message.get(
|
||
"text",
|
||
""
|
||
)
|
||
)
|
||
)
|
||
|
||
html += f"""
|
||
<section class="card">
|
||
|
||
<div class="info {message_type}">
|
||
|
||
{message_text}
|
||
|
||
</div>
|
||
|
||
</section>
|
||
"""
|
||
|
||
# ========================================================
|
||
# Export
|
||
# ========================================================
|
||
|
||
html += """
|
||
<section class="card hashcat-card">
|
||
|
||
<h2>
|
||
Export
|
||
</h2>
|
||
|
||
<p>
|
||
Export WPA handshake hashes from the database
|
||
for Hashcat.
|
||
</p>
|
||
|
||
<div class="stats-grid">
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{exported_hashes}
|
||
</div>
|
||
<div class="stat-label">
|
||
Hashes to export
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{wpa01}
|
||
</div>
|
||
<div class="stat-label">
|
||
WPA*01
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{wpa02}
|
||
</div>
|
||
<div class="stat-label">
|
||
WPA*02
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>
|
||
The generated file contains the original WPA*01
|
||
and WPA*02 handshake lines stored in the database.
|
||
</p>
|
||
|
||
<a
|
||
class="button"
|
||
draggable="false"
|
||
href="/reports/hashcat/export"
|
||
>
|
||
Export all hashes
|
||
</a>
|
||
|
||
</section>
|
||
""".format(
|
||
exported_hashes=export_statistics[
|
||
"exported_hashes"
|
||
],
|
||
wpa01=export_statistics[
|
||
"wpa01"
|
||
],
|
||
wpa02=export_statistics[
|
||
"wpa02"
|
||
],
|
||
)
|
||
|
||
# ========================================================
|
||
# Import
|
||
# ========================================================
|
||
|
||
html += """
|
||
<section class="card hashcat-card">
|
||
|
||
<h2>
|
||
Import Hashcat results
|
||
</h2>
|
||
|
||
<div class="info">
|
||
|
||
<strong>Требуемый файл:</strong>
|
||
|
||
текстовый результат команды
|
||
<code>hashcat --show</code>.
|
||
|
||
Не загружайте напрямую файл Hashcat
|
||
<code>.potfile</code>.
|
||
|
||
</div>
|
||
|
||
<h3>
|
||
Как создать файл для импорта
|
||
</h3>
|
||
|
||
<p>
|
||
После завершения подбора паролей Hashcat
|
||
выполните <code>hashcat --show</code> с теми же
|
||
параметрами Hashcat и сохраните текстовый
|
||
результат в файл.
|
||
</p>
|
||
|
||
<pre class="hashcat-command">hashcat -m 22000 --show --potfile-path "ПУТЬ_К_ВАШЕМУ_wifi-gps-mapper.potfile" "ПУТЬ_К_ФАЙЛУ_wifi-gps-mapper-all-20260915-002541.hc22000" > wifi-gps-mapper-result.txt</pre>
|
||
|
||
<p>
|
||
Имя и расширение файла не имеют значения.
|
||
Проверяется только его содержимое.
|
||
</p>
|
||
|
||
<h3>
|
||
Ожидаемый формат
|
||
</h3>
|
||
|
||
<pre class="hashcat-format">HASH:AP_BSSID:CLIENT_MAC:ESSID:PASSWORD</pre>
|
||
|
||
<p>
|
||
Каждая строка проверяется по историческим
|
||
данным рукопожатий: HASH, BSSID точки доступа,
|
||
MAC-адрес клиента и ESSID.
|
||
</p>
|
||
|
||
<p>
|
||
Если хотя бы одна строка имеет неверный формат,
|
||
весь импорт отклоняется. Если для результата
|
||
не найдено соответствующее рукопожатие, такая
|
||
строка будет пропущена, а остальные результаты
|
||
могут быть импортированы.
|
||
</p>
|
||
|
||
<p>
|
||
Уже существующие credentials и повторяющиеся
|
||
результаты не создают новых записей. Они будут
|
||
показаны отдельно в статистике импорта.
|
||
</p>
|
||
|
||
<form
|
||
id="hashcat-import-form"
|
||
method="post"
|
||
action="/reports/hashcat/import"
|
||
enctype="multipart/form-data"
|
||
>
|
||
|
||
<div class="hashcat-file-row">
|
||
|
||
<input
|
||
type="file"
|
||
id="hashcat-file"
|
||
name="hashcat_file"
|
||
required
|
||
>
|
||
|
||
<label
|
||
for="hashcat-file"
|
||
class="button"
|
||
>
|
||
Browse
|
||
</label>
|
||
|
||
<span
|
||
class="hashcat-file-name"
|
||
id="hashcat-file-name"
|
||
>
|
||
Файл не выбран
|
||
</span>
|
||
|
||
<button
|
||
type="submit"
|
||
class="button"
|
||
id="hashcat-import-button"
|
||
disabled
|
||
>
|
||
Import results
|
||
</button>
|
||
|
||
</div>
|
||
|
||
<div
|
||
id="hashcat-preflight"
|
||
class="hashcat-preflight"
|
||
hidden
|
||
>
|
||
</div>
|
||
|
||
</form>
|
||
|
||
</section>
|
||
"""
|
||
|
||
html += """
|
||
<script>
|
||
(function () {
|
||
|
||
const form = document.getElementById(
|
||
"hashcat-import-form"
|
||
);
|
||
|
||
const fileInput = document.getElementById(
|
||
"hashcat-file"
|
||
);
|
||
|
||
const fileName = document.getElementById(
|
||
"hashcat-file-name"
|
||
);
|
||
|
||
const importButton = document.getElementById(
|
||
"hashcat-import-button"
|
||
);
|
||
|
||
const preflight = document.getElementById(
|
||
"hashcat-preflight"
|
||
);
|
||
|
||
if (
|
||
!form
|
||
|| !fileInput
|
||
|| !fileName
|
||
|| !importButton
|
||
|| !preflight
|
||
) {
|
||
return;
|
||
}
|
||
|
||
function escapeHtml(
|
||
value
|
||
) {
|
||
const element = document.createElement(
|
||
"div"
|
||
);
|
||
|
||
element.textContent = String(
|
||
value
|
||
);
|
||
|
||
return element.innerHTML;
|
||
}
|
||
|
||
function renderPreflight(
|
||
result
|
||
) {
|
||
const valid = result.valid === true;
|
||
|
||
const message = valid
|
||
? "Файл прошёл проверку. Импорт разрешён."
|
||
: (
|
||
result.error
|
||
|| "Файл не прошёл проверку."
|
||
);
|
||
|
||
preflight.innerHTML = `
|
||
<section class="info ${valid ? "success" : "error"}">
|
||
${escapeHtml(message)}
|
||
</section>
|
||
|
||
<div class="stats-grid">
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
${result.total_lines ?? 0}
|
||
</div>
|
||
<div class="stat-label">
|
||
Total lines
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
${result.valid_lines ?? 0}
|
||
</div>
|
||
<div class="stat-label">
|
||
Valid lines
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
${result.invalid_lines ?? 0}
|
||
</div>
|
||
<div class="stat-label">
|
||
Invalid lines
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
${result.matched_handshakes ?? 0}
|
||
</div>
|
||
<div class="stat-label">
|
||
Matched handshakes
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
${result.missing_handshakes ?? 0}
|
||
</div>
|
||
<div class="stat-label">
|
||
Missing handshakes
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
${result.new_credentials ?? 0}
|
||
</div>
|
||
<div class="stat-label">
|
||
New credentials
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
${result.already_existing ?? 0}
|
||
</div>
|
||
<div class="stat-label">
|
||
Already exists
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
`;
|
||
|
||
if (
|
||
Array.isArray(result.missing)
|
||
&& result.missing.length > 0
|
||
) {
|
||
let missingHtml = `
|
||
<h3>
|
||
Skipped results
|
||
</h3>
|
||
|
||
<div class="table-container">
|
||
|
||
<table>
|
||
|
||
<thead>
|
||
<tr>
|
||
<th>Line</th>
|
||
<th>Hash</th>
|
||
<th>AP BSSID</th>
|
||
<th>Client MAC</th>
|
||
<th>ESSID</th>
|
||
</tr>
|
||
</thead>
|
||
|
||
<tbody>
|
||
`;
|
||
|
||
for (
|
||
const item of result.missing
|
||
) {
|
||
missingHtml += `
|
||
<tr>
|
||
<td>
|
||
${escapeHtml(item.line)}
|
||
</td>
|
||
<td>
|
||
${escapeHtml(item.hash)}
|
||
</td>
|
||
<td>
|
||
${escapeHtml(item.ap_bssid)}
|
||
</td>
|
||
<td>
|
||
${escapeHtml(item.client_mac)}
|
||
</td>
|
||
<td>
|
||
${escapeHtml(item.essid)}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
missingHtml += `
|
||
</tbody>
|
||
|
||
</table>
|
||
|
||
</div>
|
||
`;
|
||
|
||
preflight.insertAdjacentHTML(
|
||
"beforeend",
|
||
missingHtml
|
||
);
|
||
}
|
||
|
||
if (
|
||
Array.isArray(result.errors)
|
||
&& result.errors.length > 0
|
||
) {
|
||
let errorsHtml = `
|
||
<h3>
|
||
Errors
|
||
</h3>
|
||
|
||
<div class="table-container">
|
||
|
||
<table>
|
||
|
||
<thead>
|
||
<tr>
|
||
<th>Line</th>
|
||
<th>Reason</th>
|
||
</tr>
|
||
</thead>
|
||
|
||
<tbody>
|
||
`;
|
||
|
||
for (
|
||
const error of result.errors
|
||
) {
|
||
errorsHtml += `
|
||
<tr>
|
||
<td>
|
||
${escapeHtml(error.line)}
|
||
</td>
|
||
<td>
|
||
${escapeHtml(error.reason)}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
errorsHtml += `
|
||
</tbody>
|
||
|
||
</table>
|
||
|
||
</div>
|
||
`;
|
||
|
||
preflight.insertAdjacentHTML(
|
||
"beforeend",
|
||
errorsHtml
|
||
);
|
||
}
|
||
|
||
preflight.hidden = false;
|
||
const newCredentials =
|
||
result.new_credentials ?? 0;
|
||
|
||
importButton.disabled =
|
||
!valid
|
||
|| newCredentials === 0;
|
||
|
||
if (valid && newCredentials > 0) {
|
||
importButton.textContent =
|
||
"Import "
|
||
+ newCredentials
|
||
+ " new credentials";
|
||
} else if (valid) {
|
||
importButton.textContent =
|
||
"Nothing new to import";
|
||
} else {
|
||
importButton.textContent =
|
||
"Import results";
|
||
}
|
||
|
||
}
|
||
|
||
fileInput.addEventListener(
|
||
"change",
|
||
async function () {
|
||
|
||
importButton.disabled = true;
|
||
preflight.hidden = true;
|
||
preflight.innerHTML = "";
|
||
|
||
if (
|
||
!fileInput.files
|
||
|| fileInput.files.length === 0
|
||
) {
|
||
fileName.textContent =
|
||
"Файл не выбран";
|
||
|
||
return;
|
||
}
|
||
|
||
const file = fileInput.files[0];
|
||
|
||
fileName.textContent =
|
||
file.name;
|
||
|
||
const formData = new FormData();
|
||
|
||
formData.append(
|
||
"hashcat_file",
|
||
file
|
||
);
|
||
|
||
preflight.innerHTML = `
|
||
<section class="info">
|
||
Проверка файла...
|
||
</section>
|
||
`;
|
||
|
||
preflight.hidden = false;
|
||
|
||
try {
|
||
|
||
const response = await fetch(
|
||
"/reports/hashcat/preflight",
|
||
{
|
||
method: "POST",
|
||
body: formData
|
||
}
|
||
);
|
||
|
||
const result =
|
||
await response.json();
|
||
|
||
renderPreflight(
|
||
result
|
||
);
|
||
|
||
} catch (error) {
|
||
|
||
renderPreflight(
|
||
{
|
||
valid: false,
|
||
error:
|
||
"Не удалось выполнить "
|
||
+ "проверку файла."
|
||
}
|
||
);
|
||
}
|
||
}
|
||
);
|
||
|
||
})();
|
||
</script>
|
||
"""
|
||
|
||
# ========================================================
|
||
# Import statistics
|
||
# ========================================================
|
||
|
||
if import_result is not None:
|
||
|
||
html += """
|
||
<section class="card hashcat-card">
|
||
|
||
<h2>
|
||
Import statistics
|
||
</h2>
|
||
|
||
<div class="stats-grid">
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{total_lines}
|
||
</div>
|
||
<div class="stat-label">
|
||
Total lines
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{valid_lines}
|
||
</div>
|
||
<div class="stat-label">
|
||
Valid lines
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{invalid_lines}
|
||
</div>
|
||
<div class="stat-label">
|
||
Invalid lines
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{matched_handshakes}
|
||
</div>
|
||
<div class="stat-label">
|
||
Matched handshakes
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{missing_handshakes}
|
||
</div>
|
||
<div class="stat-label">
|
||
Missing handshakes
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{new_credentials}
|
||
</div>
|
||
<div class="stat-label">
|
||
New credentials
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{duplicate_credentials}
|
||
</div>
|
||
<div class="stat-label">
|
||
Duplicate credentials
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{already_existing}
|
||
</div>
|
||
<div class="stat-label">
|
||
Already exists
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<h3>
|
||
Result
|
||
</h3>
|
||
|
||
<p>
|
||
<strong>
|
||
{result}
|
||
</strong>
|
||
</p>
|
||
""".format(
|
||
total_lines=import_result[
|
||
"total_lines"
|
||
],
|
||
valid_lines=import_result[
|
||
"valid_lines"
|
||
],
|
||
invalid_lines=import_result[
|
||
"invalid_lines"
|
||
],
|
||
matched_handshakes=import_result[
|
||
"matched_handshakes"
|
||
],
|
||
missing_handshakes=import_result[
|
||
"missing_handshakes"
|
||
],
|
||
new_credentials=import_result[
|
||
"new_credentials"
|
||
],
|
||
duplicate_credentials=import_result[
|
||
"duplicate_credentials"
|
||
],
|
||
already_existing=import_result[
|
||
"already_existing"
|
||
],
|
||
result=escape(
|
||
str(
|
||
import_result[
|
||
"result"
|
||
]
|
||
)
|
||
),
|
||
)
|
||
|
||
errors = import_result[
|
||
"errors"
|
||
]
|
||
|
||
if errors:
|
||
|
||
html += """
|
||
<h3>
|
||
Errors
|
||
</h3>
|
||
|
||
<div class="table-container">
|
||
|
||
<table>
|
||
|
||
<thead>
|
||
<tr>
|
||
<th>Line</th>
|
||
<th>Reason</th>
|
||
</tr>
|
||
</thead>
|
||
|
||
<tbody>
|
||
"""
|
||
|
||
for error in errors:
|
||
|
||
html += """
|
||
<tr>
|
||
<td>
|
||
{line}
|
||
</td>
|
||
<td>
|
||
{reason}
|
||
</td>
|
||
</tr>
|
||
""".format(
|
||
line=error[
|
||
"line"
|
||
],
|
||
reason=escape(
|
||
str(
|
||
error[
|
||
"reason"
|
||
]
|
||
)
|
||
),
|
||
)
|
||
|
||
html += """
|
||
</tbody>
|
||
|
||
</table>
|
||
|
||
</div>
|
||
"""
|
||
|
||
html += """
|
||
</section>
|
||
"""
|
||
|
||
# ========================================================
|
||
# Database state
|
||
# ========================================================
|
||
|
||
html += """
|
||
<section class="card hashcat-card">
|
||
|
||
<h2>
|
||
Database
|
||
</h2>
|
||
|
||
<div class="stats-grid">
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{access_points}
|
||
</div>
|
||
<div class="stat-label">
|
||
Access points
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{handshakes}
|
||
</div>
|
||
<div class="stat-label">
|
||
Handshakes
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{unique_hashes}
|
||
</div>
|
||
<div class="stat-label">
|
||
Unique hashes
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{credentials}
|
||
</div>
|
||
<div class="stat-label">
|
||
Handshake Cracked
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-value">
|
||
{cracked_access_points}
|
||
</div>
|
||
<div class="stat-label">
|
||
Cracked access points
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</section>
|
||
""".format(
|
||
access_points=database_statistics[
|
||
"access_points"
|
||
],
|
||
handshakes=database_statistics[
|
||
"handshakes"
|
||
],
|
||
unique_hashes=database_statistics[
|
||
"unique_hashes"
|
||
],
|
||
credentials=database_statistics[
|
||
"credentials"
|
||
],
|
||
cracked_access_points=database_statistics[
|
||
"cracked_access_points"
|
||
],
|
||
)
|
||
|
||
html += page_end()
|
||
|
||
return html |