pentool 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pentool/__init__.py +4 -0
- pentool/__main__.py +16 -0
- pentool/api/__init__.py +41 -0
- pentool/api/base_api.py +48 -0
- pentool/api/comparer_api.py +19 -0
- pentool/api/decoder_api.py +24 -0
- pentool/api/intruder_api.py +171 -0
- pentool/api/proxy_api.py +210 -0
- pentool/api/repeater_api.py +57 -0
- pentool/api/scanner_api.py +266 -0
- pentool/api/sequencer_api.py +17 -0
- pentool/api/spider_api.py +106 -0
- pentool/api/target_api.py +87 -0
- pentool/cli/__init__.py +1 -0
- pentool/cli/main.py +98 -0
- pentool/cli/project.py +88 -0
- pentool/cli/proxy.py +173 -0
- pentool/cli/scan.py +115 -0
- pentool/core/__init__.py +1 -0
- pentool/core/config.py +171 -0
- pentool/core/crash_reporter.py +75 -0
- pentool/core/database.py +139 -0
- pentool/core/event_bus.py +207 -0
- pentool/core/events.py +160 -0
- pentool/core/features.py +373 -0
- pentool/core/license.py +274 -0
- pentool/core/logging.py +54 -0
- pentool/core/plugin_manager.py +312 -0
- pentool/core/project.py +31 -0
- pentool/core/storage_interface.py +307 -0
- pentool/core/updater.py +96 -0
- pentool/core/utils.py +36 -0
- pentool/modules/__init__.py +1 -0
- pentool/modules/comparer.py +177 -0
- pentool/modules/decoder.py +281 -0
- pentool/modules/intruder.py +428 -0
- pentool/modules/intruder_turbo.py +176 -0
- pentool/modules/match_replace.py +142 -0
- pentool/modules/proxy.py +797 -0
- pentool/modules/repeater.py +195 -0
- pentool/modules/sequencer.py +492 -0
- pentool/modules/spider.py +679 -0
- pentool/modules/target.py +185 -0
- pentool/modules/websocket_handler.py +249 -0
- pentool/plugins/__init__.py +1 -0
- pentool/plugins/builtin/__init__.py +1 -0
- pentool/plugins/builtin/payloads_pro.py +381 -0
- pentool/plugins/builtin/reports_pro.py +436 -0
- pentool/plugins/builtin/scanner_pro.py +291 -0
- pentool/plugins/example_plugin.py +39 -0
- pentool/services/__init__.py +1 -0
- pentool/services/base_service.py +60 -0
- pentool/services/intruder_service.py +94 -0
- pentool/services/proxy_service.py +178 -0
- pentool/services/repeater_service.py +85 -0
- pentool/services/scan_service.py +330 -0
- pentool/storage/__init__.py +0 -0
- pentool/storage/http_storage.py +506 -0
- pentool/storage/large_body_handler.py +50 -0
- pentool/storage/lru_cache.py +45 -0
- pentool/t.py +213 -0
- pentool/tui/__init__.py +1 -0
- pentool/tui/app.py +1429 -0
- pentool/tui/constants.py +79 -0
- pentool/tui/dialogs/__init__.py +0 -0
- pentool/tui/dialogs/cert_dialog.py +81 -0
- pentool/tui/dialogs/file_selector.py +226 -0
- pentool/tui/dialogs/load_from_proxy.py +125 -0
- pentool/tui/dialogs/match_replace_dialog.py +229 -0
- pentool/tui/dialogs/project_dialog.py +82 -0
- pentool/tui/dialogs/scope_dialog.py +224 -0
- pentool/tui/messages.py +103 -0
- pentool/tui/mixins/__init__.py +0 -0
- pentool/tui/mixins/app_mixin.py +59 -0
- pentool/tui/mixins/dialog_cancel.py +21 -0
- pentool/tui/mixins/request_context_menu.py +247 -0
- pentool/tui/mixins/tab_rename.py +89 -0
- pentool/tui/screens/__init__.py +31 -0
- pentool/tui/screens/comparer/__init__.py +3 -0
- pentool/tui/screens/comparer/screen.py +229 -0
- pentool/tui/screens/dashboard/__init__.py +2 -0
- pentool/tui/screens/dashboard/live_dashboard.py +730 -0
- pentool/tui/screens/dashboard/screen.py +771 -0
- pentool/tui/screens/decoder/__init__.py +3 -0
- pentool/tui/screens/decoder/screen.py +306 -0
- pentool/tui/screens/extensions/__init__.py +3 -0
- pentool/tui/screens/extensions/screen.py +24 -0
- pentool/tui/screens/intruder/__init__.py +3 -0
- pentool/tui/screens/intruder/screen.py +1356 -0
- pentool/tui/screens/proxy/__init__.py +3 -0
- pentool/tui/screens/proxy/screen.py +1554 -0
- pentool/tui/screens/repeater/__init__.py +3 -0
- pentool/tui/screens/repeater/screen.py +621 -0
- pentool/tui/screens/scanner/__init__.py +3 -0
- pentool/tui/screens/scanner/screen.py +1956 -0
- pentool/tui/screens/sequencer/__init__.py +3 -0
- pentool/tui/screens/sequencer/screen.py +516 -0
- pentool/tui/screens/settings/__init__.py +5 -0
- pentool/tui/screens/settings/hotkeys.py +134 -0
- pentool/tui/screens/settings/screen.py +540 -0
- pentool/tui/screens/spider/__init__.py +3 -0
- pentool/tui/screens/spider/screen.py +380 -0
- pentool/tui/screens/target/__init__.py +4 -0
- pentool/tui/screens/target/screen.py +358 -0
- pentool/tui/screens/terminal/__init__.py +5 -0
- pentool/tui/screens/terminal/screen.py +179 -0
- pentool/tui/widgets/__init__.py +1 -0
- pentool/tui/widgets/context_menu.py +148 -0
- pentool/tui/widgets/filter_bar.py +204 -0
- pentool/tui/widgets/inspector_panel.py +111 -0
- pentool/tui/widgets/menu.py +86 -0
- pentool/tui/widgets/menu_bar.py +238 -0
- pentool/tui/widgets/module_tabs.py +68 -0
- pentool/tui/widgets/payload_drop_zone.py +65 -0
- pentool/tui/widgets/request_editor.py +495 -0
- pentool/tui/widgets/resize_handle.py +116 -0
- pentool/tui/widgets/search_bar.py +112 -0
- pentool/tui/widgets/statusbar.py +96 -0
- pentool/tui/widgets/toolbar_button.py +68 -0
- pentool/utils/__init__.py +1 -0
- pentool/utils/cert.py +314 -0
- pentool/utils/coder.py +170 -0
- pentool/utils/copy_as.py +250 -0
- pentool/utils/diff.py +82 -0
- pentool/utils/http_client.py +145 -0
- pentool/utils/parser.py +227 -0
- pentool/utils/terminal_check.py +31 -0
- pentool-0.1.0.dist-info/METADATA +231 -0
- pentool-0.1.0.dist-info/RECORD +133 -0
- pentool-0.1.0.dist-info/WHEEL +5 -0
- pentool-0.1.0.dist-info/entry_points.txt +2 -0
- pentool-0.1.0.dist-info/licenses/LICENSE +34 -0
- pentool-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Intruder — модуль автоматизированных атак с подстановкой payload'ов."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import csv
|
|
7
|
+
import itertools
|
|
8
|
+
import re
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import AsyncIterator, Callable, Iterator
|
|
15
|
+
|
|
16
|
+
from pentool.core.logging import get_logger
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AttackType(str, Enum):
|
|
22
|
+
SNIPER = "sniper"
|
|
23
|
+
BATTERING_RAM = "battering_ram"
|
|
24
|
+
PITCHFORK = "pitchfork"
|
|
25
|
+
CLUSTER_BOMB = "cluster_bomb"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class IntruderResult:
|
|
30
|
+
id: str
|
|
31
|
+
attack_id: str
|
|
32
|
+
request_number: int
|
|
33
|
+
payload_values: list[str] # значения по каждой позиции
|
|
34
|
+
request_raw: str
|
|
35
|
+
response_status: int | None
|
|
36
|
+
response_length: int | None
|
|
37
|
+
response_time_ms: int | None
|
|
38
|
+
error: str | None
|
|
39
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class IntruderConfig:
|
|
44
|
+
template: str # raw HTTP с маркерами §payload§
|
|
45
|
+
attack_type: AttackType
|
|
46
|
+
payload_sets: list[list[str]] # один список на каждую позицию
|
|
47
|
+
threads: int = 10
|
|
48
|
+
delay_ms: int = 0
|
|
49
|
+
follow_redirects: bool = False
|
|
50
|
+
timeout: int = 30
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_markers(template: str) -> tuple[str, list[tuple[int, int]]]:
|
|
54
|
+
"""Найти §маркеры§ в шаблоне.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
(clean_template, positions) где positions — список (start, end)
|
|
58
|
+
в clean_template для каждой позиции.
|
|
59
|
+
"""
|
|
60
|
+
positions: list[tuple[int, int]] = []
|
|
61
|
+
result = []
|
|
62
|
+
i = 0
|
|
63
|
+
offset = 0 # смещение из-за удалённых §
|
|
64
|
+
|
|
65
|
+
while i < len(template):
|
|
66
|
+
if template[i] == "§":
|
|
67
|
+
# Ищем закрывающий §
|
|
68
|
+
j = template.find("§", i + 1)
|
|
69
|
+
if j == -1:
|
|
70
|
+
# Нет закрывающего — оставляем как есть
|
|
71
|
+
result.append(template[i])
|
|
72
|
+
i += 1
|
|
73
|
+
continue
|
|
74
|
+
# Вырезаем содержимое маркера
|
|
75
|
+
inner = template[i + 1 : j]
|
|
76
|
+
start = len(result)
|
|
77
|
+
result.append(inner)
|
|
78
|
+
end = len(result)
|
|
79
|
+
positions.append((start, end))
|
|
80
|
+
i = j + 1
|
|
81
|
+
else:
|
|
82
|
+
result.append(template[i])
|
|
83
|
+
i += 1
|
|
84
|
+
|
|
85
|
+
return "".join(result), positions
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _find_marker_positions(template: str) -> list[tuple[int, int]]:
|
|
89
|
+
"""Найти позиции §...§ пар в строке. Возвращает (start_§, end_§)."""
|
|
90
|
+
positions = []
|
|
91
|
+
i = 0
|
|
92
|
+
while i < len(template):
|
|
93
|
+
start = template.find("§", i)
|
|
94
|
+
if start == -1:
|
|
95
|
+
break
|
|
96
|
+
end = template.find("§", start + 1)
|
|
97
|
+
if end == -1:
|
|
98
|
+
break
|
|
99
|
+
positions.append((start, end + 1)) # включая оба §
|
|
100
|
+
i = end + 1
|
|
101
|
+
return positions
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def substitute_payload(template: str, payload_values: list[str]) -> str:
|
|
105
|
+
"""Подставить payload'ы в §маркеры§ шаблона.
|
|
106
|
+
|
|
107
|
+
Заменяет маркеры слева направо. Если payload_values меньше чем маркеров —
|
|
108
|
+
оставшиеся маркеры заменяются пустой строкой.
|
|
109
|
+
"""
|
|
110
|
+
result = template
|
|
111
|
+
idx = 0
|
|
112
|
+
while True:
|
|
113
|
+
start = result.find("§")
|
|
114
|
+
if start == -1:
|
|
115
|
+
break
|
|
116
|
+
end = result.find("§", start + 1)
|
|
117
|
+
if end == -1:
|
|
118
|
+
break
|
|
119
|
+
value = payload_values[idx] if idx < len(payload_values) else ""
|
|
120
|
+
result = result[:start] + value + result[end + 1:]
|
|
121
|
+
idx += 1
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def count_markers(template: str) -> int:
|
|
126
|
+
"""Подсчитать количество §маркеров§ в шаблоне."""
|
|
127
|
+
count = 0
|
|
128
|
+
i = 0
|
|
129
|
+
while True:
|
|
130
|
+
start = template.find("§", i)
|
|
131
|
+
if start == -1:
|
|
132
|
+
break
|
|
133
|
+
end = template.find("§", start + 1)
|
|
134
|
+
if end == -1:
|
|
135
|
+
break
|
|
136
|
+
count += 1
|
|
137
|
+
i = end + 1
|
|
138
|
+
return count
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def extract_marker_defaults(template: str) -> list[str]:
|
|
142
|
+
"""Извлечь оригинальные значения из §маркеров§ шаблона.
|
|
143
|
+
|
|
144
|
+
Например: 'user=§admin§&pass=§secret§' → ['admin', 'secret']
|
|
145
|
+
"""
|
|
146
|
+
defaults = []
|
|
147
|
+
i = 0
|
|
148
|
+
while i < len(template):
|
|
149
|
+
start = template.find("§", i)
|
|
150
|
+
if start == -1:
|
|
151
|
+
break
|
|
152
|
+
end = template.find("§", start + 1)
|
|
153
|
+
if end == -1:
|
|
154
|
+
break
|
|
155
|
+
defaults.append(template[start + 1:end])
|
|
156
|
+
i = end + 1
|
|
157
|
+
return defaults
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def load_payloads_from_file(path: str) -> list[str]:
|
|
161
|
+
result = []
|
|
162
|
+
try:
|
|
163
|
+
text = Path(path).read_text(encoding="utf-8", errors="replace")
|
|
164
|
+
for line in text.splitlines():
|
|
165
|
+
stripped = line.strip()
|
|
166
|
+
if stripped and not stripped.startswith("#"):
|
|
167
|
+
result.append(stripped)
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def generate_numeric_payloads(start: int, end: int, step: int = 1) -> list[str]:
|
|
174
|
+
"""Числовой диапазон [start, end) с шагом step."""
|
|
175
|
+
return [str(n) for n in range(start, end, step)]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def generate_char_payloads(charset: str, min_len: int, max_len: int) -> list[str]:
|
|
179
|
+
"""Перебор строк по набору символов заданной длины.
|
|
180
|
+
|
|
181
|
+
Осторожно: может быть очень большим. Для min_len > max_len возвращает [].
|
|
182
|
+
"""
|
|
183
|
+
result = []
|
|
184
|
+
for length in range(min_len, max_len + 1):
|
|
185
|
+
for combo in itertools.product(charset, repeat=length):
|
|
186
|
+
result.append("".join(combo))
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def process_payload(payload: str, operations: list[str]) -> str:
|
|
191
|
+
"""Применить операции из utils/coder.py к payload.
|
|
192
|
+
|
|
193
|
+
Операции применяются последовательно (pipeline).
|
|
194
|
+
Неизвестные операции — игнорируются.
|
|
195
|
+
"""
|
|
196
|
+
from pentool.utils.coder import OPERATIONS
|
|
197
|
+
result = payload
|
|
198
|
+
for op in operations:
|
|
199
|
+
func = OPERATIONS.get(op)
|
|
200
|
+
if func is not None:
|
|
201
|
+
try:
|
|
202
|
+
result = func(result)
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class IntruderAttack:
|
|
209
|
+
"""Выполняет intruder-атаку заданного типа."""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
config: IntruderConfig,
|
|
214
|
+
db_path: str | None = None,
|
|
215
|
+
http_client=None,
|
|
216
|
+
) -> None:
|
|
217
|
+
self._config = config
|
|
218
|
+
self._db_path = db_path
|
|
219
|
+
self._http_client = http_client
|
|
220
|
+
self._attack_id = str(uuid.uuid4())
|
|
221
|
+
self._is_running = False
|
|
222
|
+
self._paused = False
|
|
223
|
+
self._stopped = False
|
|
224
|
+
self._done = 0
|
|
225
|
+
self._total = 0
|
|
226
|
+
self._results: list[IntruderResult] = []
|
|
227
|
+
self._pause_event = asyncio.Event()
|
|
228
|
+
self._pause_event.set() # не на паузе изначально
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def attack_id(self) -> str:
|
|
232
|
+
return self._attack_id
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def is_running(self) -> bool:
|
|
236
|
+
return self._is_running
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def total_requests(self) -> int:
|
|
240
|
+
return self._total
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def progress(self) -> tuple[int, int]:
|
|
244
|
+
return self._done, self._total
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def results(self) -> list[IntruderResult]:
|
|
248
|
+
return list(self._results)
|
|
249
|
+
|
|
250
|
+
def _iter_sniper(self) -> Iterator[list[str]]:
|
|
251
|
+
"""Один набор payload'ов, по одной позиции за раз.
|
|
252
|
+
N_positions × M запросов.
|
|
253
|
+
Незатрагиваемые позиции сохраняют оригинальное значение из шаблона.
|
|
254
|
+
"""
|
|
255
|
+
n_positions = count_markers(self._config.template)
|
|
256
|
+
defaults = extract_marker_defaults(self._config.template)
|
|
257
|
+
# Дополняем defaults до n_positions пустыми строками на случай рассинхрона
|
|
258
|
+
while len(defaults) < n_positions:
|
|
259
|
+
defaults.append("")
|
|
260
|
+
# Берём только первый payload set для всех позиций
|
|
261
|
+
payloads = self._config.payload_sets[0] if self._config.payload_sets else []
|
|
262
|
+
for pos_idx in range(n_positions):
|
|
263
|
+
for payload in payloads:
|
|
264
|
+
# В позиции pos_idx подставляем payload, в остальных — оригинал
|
|
265
|
+
values = list(defaults)
|
|
266
|
+
values[pos_idx] = payload
|
|
267
|
+
yield values
|
|
268
|
+
|
|
269
|
+
def _iter_battering_ram(self) -> Iterator[list[str]]:
|
|
270
|
+
"""Один payload во все позиции одновременно. M запросов."""
|
|
271
|
+
n_positions = count_markers(self._config.template)
|
|
272
|
+
payloads = self._config.payload_sets[0] if self._config.payload_sets else []
|
|
273
|
+
for payload in payloads:
|
|
274
|
+
yield [payload] * n_positions
|
|
275
|
+
|
|
276
|
+
def _iter_pitchfork(self) -> Iterator[list[str]]:
|
|
277
|
+
"""Параллельно берёт по одному из каждого набора (zip). min(M) запросов."""
|
|
278
|
+
sets = self._config.payload_sets
|
|
279
|
+
if not sets:
|
|
280
|
+
return
|
|
281
|
+
for combo in zip(*sets):
|
|
282
|
+
yield list(combo)
|
|
283
|
+
|
|
284
|
+
def _iter_cluster_bomb(self) -> Iterator[list[str]]:
|
|
285
|
+
"""Декартово произведение всех наборов. M1×M2×... запросов."""
|
|
286
|
+
sets = self._config.payload_sets
|
|
287
|
+
if not sets:
|
|
288
|
+
return
|
|
289
|
+
for combo in itertools.product(*sets):
|
|
290
|
+
yield list(combo)
|
|
291
|
+
|
|
292
|
+
def _get_iterator(self) -> Iterator[list[str]]:
|
|
293
|
+
t = self._config.attack_type
|
|
294
|
+
if t == AttackType.SNIPER:
|
|
295
|
+
return self._iter_sniper()
|
|
296
|
+
elif t == AttackType.BATTERING_RAM:
|
|
297
|
+
return self._iter_battering_ram()
|
|
298
|
+
elif t == AttackType.PITCHFORK:
|
|
299
|
+
return self._iter_pitchfork()
|
|
300
|
+
elif t == AttackType.CLUSTER_BOMB:
|
|
301
|
+
return self._iter_cluster_bomb()
|
|
302
|
+
return iter([])
|
|
303
|
+
|
|
304
|
+
def _count_total(self) -> int:
|
|
305
|
+
"""Подсчитать общее количество запросов (без реального выполнения)."""
|
|
306
|
+
return sum(1 for _ in self._get_iterator())
|
|
307
|
+
|
|
308
|
+
async def run(
|
|
309
|
+
self,
|
|
310
|
+
on_result: Callable[[IntruderResult], None],
|
|
311
|
+
on_progress: Callable[[int, int], None],
|
|
312
|
+
) -> None:
|
|
313
|
+
self._is_running = True
|
|
314
|
+
self._stopped = False
|
|
315
|
+
self._done = 0
|
|
316
|
+
self._results = []
|
|
317
|
+
|
|
318
|
+
combinations = list(self._get_iterator())
|
|
319
|
+
self._total = len(combinations)
|
|
320
|
+
logger.info("INTRUDER: run() started, total=%d, type=%s, threads=%d", self._total, self._config.attack_type, self._config.threads)
|
|
321
|
+
on_progress(0, self._total)
|
|
322
|
+
|
|
323
|
+
sem = asyncio.Semaphore(self._config.threads)
|
|
324
|
+
|
|
325
|
+
async def _run_one(req_num: int, payload_values: list[str]) -> None:
|
|
326
|
+
if self._stopped:
|
|
327
|
+
return
|
|
328
|
+
# Ожидаем снятия паузы
|
|
329
|
+
await self._pause_event.wait()
|
|
330
|
+
if self._stopped:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
async with sem:
|
|
334
|
+
if self._stopped:
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Подставляем payload'ы
|
|
338
|
+
request_raw = substitute_payload(self._config.template, payload_values)
|
|
339
|
+
|
|
340
|
+
# Задержка
|
|
341
|
+
if self._config.delay_ms > 0:
|
|
342
|
+
await asyncio.sleep(self._config.delay_ms / 1000.0)
|
|
343
|
+
|
|
344
|
+
result = await self._send_request(req_num, payload_values, request_raw)
|
|
345
|
+
self._results.append(result)
|
|
346
|
+
on_result(result)
|
|
347
|
+
self._done += 1
|
|
348
|
+
on_progress(self._done, self._total)
|
|
349
|
+
|
|
350
|
+
tasks = [
|
|
351
|
+
asyncio.create_task(_run_one(i + 1, vals))
|
|
352
|
+
for i, vals in enumerate(combinations)
|
|
353
|
+
]
|
|
354
|
+
if tasks:
|
|
355
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
356
|
+
|
|
357
|
+
self._is_running = False
|
|
358
|
+
|
|
359
|
+
async def _send_request(
|
|
360
|
+
self,
|
|
361
|
+
req_num: int,
|
|
362
|
+
payload_values: list[str],
|
|
363
|
+
request_raw: str,
|
|
364
|
+
) -> IntruderResult:
|
|
365
|
+
import time
|
|
366
|
+
t0 = time.monotonic()
|
|
367
|
+
status = None
|
|
368
|
+
length = None
|
|
369
|
+
error_msg = None
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
from pentool.utils.parser import parse_http_request
|
|
373
|
+
from pentool.utils.http_client import HTTPClient
|
|
374
|
+
|
|
375
|
+
req = parse_http_request(request_raw)
|
|
376
|
+
async with HTTPClient(timeout=self._config.timeout) as client:
|
|
377
|
+
resp = await client.send(req)
|
|
378
|
+
status = resp.status
|
|
379
|
+
body = resp.body if isinstance(resp.body, (bytes, str)) else b""
|
|
380
|
+
length = len(body) if isinstance(body, bytes) else len(body.encode("utf-8", errors="replace"))
|
|
381
|
+
except Exception as exc:
|
|
382
|
+
error_msg = str(exc)
|
|
383
|
+
logger.warning("INTRUDER: _send_request #%d error: %s", req_num, exc)
|
|
384
|
+
|
|
385
|
+
elapsed_ms = int((time.monotonic() - t0) * 1000)
|
|
386
|
+
logger.debug("INTRUDER: #%d payload=%s status=%s length=%s time=%dms", req_num, payload_values, status, length, elapsed_ms)
|
|
387
|
+
|
|
388
|
+
return IntruderResult(
|
|
389
|
+
id=str(uuid.uuid4()),
|
|
390
|
+
attack_id=self._attack_id,
|
|
391
|
+
request_number=req_num,
|
|
392
|
+
payload_values=payload_values,
|
|
393
|
+
request_raw=request_raw,
|
|
394
|
+
response_status=status,
|
|
395
|
+
response_length=length,
|
|
396
|
+
response_time_ms=elapsed_ms,
|
|
397
|
+
error=error_msg,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
async def pause(self) -> None:
|
|
401
|
+
self._paused = True
|
|
402
|
+
self._pause_event.clear()
|
|
403
|
+
|
|
404
|
+
async def resume(self) -> None:
|
|
405
|
+
self._paused = False
|
|
406
|
+
self._pause_event.set()
|
|
407
|
+
|
|
408
|
+
async def stop(self) -> None:
|
|
409
|
+
self._stopped = True
|
|
410
|
+
self._pause_event.set() # разблокировать ожидающие
|
|
411
|
+
self._is_running = False
|
|
412
|
+
|
|
413
|
+
def export_csv(self, path: str) -> None:
|
|
414
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
415
|
+
writer = csv.writer(f)
|
|
416
|
+
writer.writerow([
|
|
417
|
+
"#", "Payloads", "Status", "Length", "Time(ms)", "Error", "Timestamp"
|
|
418
|
+
])
|
|
419
|
+
for r in self._results:
|
|
420
|
+
writer.writerow([
|
|
421
|
+
r.request_number,
|
|
422
|
+
" | ".join(r.payload_values),
|
|
423
|
+
r.response_status or "",
|
|
424
|
+
r.response_length or "",
|
|
425
|
+
r.response_time_ms or "",
|
|
426
|
+
r.error or "",
|
|
427
|
+
r.timestamp.isoformat(),
|
|
428
|
+
])
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Turbo Mode для Intruder — оптимизированная версия с connection pooling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
|
|
11
|
+
from pentool.modules.intruder import IntruderResult, IntruderConfig, substitute_payload
|
|
12
|
+
from pentool.core.logging import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TurboIntruderAttack:
|
|
18
|
+
"""Turbo Mode для Intruder — 10x быстрее через connection pooling."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: IntruderConfig) -> None:
|
|
21
|
+
self._config = config
|
|
22
|
+
self._is_running = False
|
|
23
|
+
self._stopped = False
|
|
24
|
+
self._done = 0
|
|
25
|
+
self._total = 0
|
|
26
|
+
self._results: list[IntruderResult] = []
|
|
27
|
+
self._session: aiohttp.ClientSession | None = None
|
|
28
|
+
|
|
29
|
+
async def run(
|
|
30
|
+
self,
|
|
31
|
+
on_result: Callable[[IntruderResult], None],
|
|
32
|
+
on_progress: Callable[[int, int], None],
|
|
33
|
+
) -> None:
|
|
34
|
+
from pentool.modules.intruder import IntruderAttack
|
|
35
|
+
|
|
36
|
+
# Используем базовый класс для генерации комбинаций
|
|
37
|
+
base_attack = IntruderAttack(self._config)
|
|
38
|
+
combinations = list(base_attack._get_iterator())
|
|
39
|
+
self._total = len(combinations)
|
|
40
|
+
|
|
41
|
+
logger.info("TURBO INTRUDER: started, total=%d, threads=%d (Turbo Mode)",
|
|
42
|
+
self._total, self._config.threads)
|
|
43
|
+
|
|
44
|
+
self._is_running = True
|
|
45
|
+
self._stopped = False
|
|
46
|
+
self._done = 0
|
|
47
|
+
self._results = []
|
|
48
|
+
|
|
49
|
+
on_progress(0, self._total)
|
|
50
|
+
|
|
51
|
+
# Создаём connection pool
|
|
52
|
+
connector = aiohttp.TCPConnector(
|
|
53
|
+
limit=self._config.threads * 2, # Больше соединений для Keep-Alive
|
|
54
|
+
limit_per_host=self._config.threads,
|
|
55
|
+
ttl_dns_cache=300,
|
|
56
|
+
enable_cleanup_closed=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
timeout = aiohttp.ClientTimeout(total=self._config.timeout)
|
|
60
|
+
|
|
61
|
+
async with aiohttp.ClientSession(
|
|
62
|
+
connector=connector,
|
|
63
|
+
timeout=timeout,
|
|
64
|
+
headers={"Connection": "keep-alive"}, # Keep-Alive!
|
|
65
|
+
) as session:
|
|
66
|
+
self._session = session
|
|
67
|
+
|
|
68
|
+
# Используем семафор для контроля параллелизма
|
|
69
|
+
sem = asyncio.Semaphore(self._config.threads)
|
|
70
|
+
|
|
71
|
+
async def _run_one(req_num: int, payload_values: list[str]) -> None:
|
|
72
|
+
if self._stopped:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
async with sem:
|
|
76
|
+
if self._stopped:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
request_raw = substitute_payload(self._config.template, payload_values)
|
|
80
|
+
|
|
81
|
+
# Задержка
|
|
82
|
+
if self._config.delay_ms > 0:
|
|
83
|
+
await asyncio.sleep(self._config.delay_ms / 1000.0)
|
|
84
|
+
|
|
85
|
+
result = await self._send_request_turbo(req_num, payload_values, request_raw)
|
|
86
|
+
self._results.append(result)
|
|
87
|
+
on_result(result)
|
|
88
|
+
self._done += 1
|
|
89
|
+
on_progress(self._done, self._total)
|
|
90
|
+
|
|
91
|
+
# Создаём все задачи
|
|
92
|
+
tasks = [
|
|
93
|
+
asyncio.create_task(_run_one(i + 1, vals))
|
|
94
|
+
for i, vals in enumerate(combinations)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
if tasks:
|
|
98
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
99
|
+
|
|
100
|
+
self._is_running = False
|
|
101
|
+
self._session = None
|
|
102
|
+
|
|
103
|
+
async def _send_request_turbo(
|
|
104
|
+
self,
|
|
105
|
+
req_num: int,
|
|
106
|
+
payload_values: list[str],
|
|
107
|
+
request_raw: str,
|
|
108
|
+
) -> IntruderResult:
|
|
109
|
+
import uuid
|
|
110
|
+
from pentool.utils.parser import parse_http_request
|
|
111
|
+
|
|
112
|
+
t0 = time.monotonic()
|
|
113
|
+
status = None
|
|
114
|
+
length = None
|
|
115
|
+
error_msg = None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
req = parse_http_request(request_raw)
|
|
119
|
+
|
|
120
|
+
# Формируем URL
|
|
121
|
+
scheme = "https" if req.port == 443 else "http"
|
|
122
|
+
url = f"{scheme}://{req.host}:{req.port}{req.path}"
|
|
123
|
+
|
|
124
|
+
# Отправляем через aiohttp (Keep-Alive автоматом)
|
|
125
|
+
async with self._session.request(
|
|
126
|
+
method=req.method,
|
|
127
|
+
url=url,
|
|
128
|
+
headers=dict(req.headers),
|
|
129
|
+
data=req.body.encode("utf-8") if req.body else None,
|
|
130
|
+
allow_redirects=self._config.follow_redirects,
|
|
131
|
+
) as response:
|
|
132
|
+
status = response.status
|
|
133
|
+
body = await response.read()
|
|
134
|
+
length = len(body)
|
|
135
|
+
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
error_msg = str(exc)
|
|
138
|
+
logger.debug("TURBO: request #%d error: %s", req_num, exc)
|
|
139
|
+
|
|
140
|
+
elapsed_ms = int((time.monotonic() - t0) * 1000)
|
|
141
|
+
|
|
142
|
+
return IntruderResult(
|
|
143
|
+
id=str(uuid.uuid4()),
|
|
144
|
+
attack_id="turbo",
|
|
145
|
+
request_number=req_num,
|
|
146
|
+
payload_values=payload_values,
|
|
147
|
+
request_raw=request_raw,
|
|
148
|
+
response_status=status,
|
|
149
|
+
response_length=length,
|
|
150
|
+
response_time_ms=elapsed_ms,
|
|
151
|
+
error=error_msg,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def stop(self) -> None:
|
|
155
|
+
self._stopped = True
|
|
156
|
+
self._is_running = False
|
|
157
|
+
|
|
158
|
+
def get_results(self) -> list[IntruderResult]:
|
|
159
|
+
return self._results
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def is_running(self) -> bool:
|
|
163
|
+
"""Проверить, запущена ли атака."""
|
|
164
|
+
return self._is_running
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def attack_id(self) -> str:
|
|
168
|
+
return "turbo"
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def results(self) -> list[IntruderResult]:
|
|
172
|
+
return self._results
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def progress(self) -> tuple[int, int]:
|
|
176
|
+
return (self._done, self._total)
|