wafaHell 0.2.0__py3-none-any.whl → 1.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.
- wafaHell/app.py +31 -0
- wafaHell/globals.py +5 -0
- wafaHell/logger.py +102 -14
- wafaHell/middleware.py +398 -81
- wafaHell/mock.py +53 -0
- wafaHell/model.py +98 -10
- wafaHell/panel.py +406 -0
- wafaHell/rateLimiter.py +0 -1
- wafaHell/utils.py +413 -7
- {wafahell-0.2.0.dist-info → wafahell-1.1.0.dist-info}/METADATA +5 -1
- wafahell-1.1.0.dist-info/RECORD +15 -0
- {wafahell-0.2.0.dist-info → wafahell-1.1.0.dist-info}/WHEEL +1 -1
- wafaHell/teste.py +0 -16
- wafahell-0.2.0.dist-info/RECORD +0 -12
- {wafahell-0.2.0.dist-info → wafahell-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {wafahell-0.2.0.dist-info → wafahell-1.1.0.dist-info}/top_level.txt +0 -0
wafaHell/middleware.py
CHANGED
|
@@ -1,151 +1,468 @@
|
|
|
1
|
+
from .model import Base, Blocked, WafLog, Whitelist, get_session, engine
|
|
2
|
+
from .logger import Logger
|
|
3
|
+
from .rateLimiter import RateLimiter
|
|
4
|
+
from .panel import setup_dashboard
|
|
5
|
+
from .utils import Admin, seed_default_whitelist
|
|
6
|
+
from .globals import waf_cache
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
1
8
|
import re
|
|
2
|
-
|
|
9
|
+
import time
|
|
10
|
+
from flask import request as req, abort, g
|
|
3
11
|
from urllib.parse import unquote
|
|
4
|
-
from model import Blocked, get_session
|
|
5
|
-
from logger import Logger
|
|
6
|
-
from utils import is_block_expired
|
|
7
|
-
from rateLimiter import RateLimiter
|
|
8
12
|
from sqlalchemy.exc import OperationalError
|
|
13
|
+
from sqlalchemy import text
|
|
14
|
+
import hashlib
|
|
15
|
+
import uuid
|
|
16
|
+
import socket
|
|
17
|
+
import ipaddress
|
|
9
18
|
|
|
10
19
|
# Inicializa o RateLimiter
|
|
11
20
|
limiter = RateLimiter(limit=100, window=60)
|
|
12
21
|
|
|
13
|
-
class
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
self
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
self.
|
|
22
|
+
class Wafahell:
|
|
23
|
+
_instance = None
|
|
24
|
+
|
|
25
|
+
def __new__(cls, *args, **kwargs):
|
|
26
|
+
if not cls._instance:
|
|
27
|
+
cls._instance = super(Wafahell, cls).__new__(cls)
|
|
28
|
+
return cls._instance
|
|
29
|
+
|
|
30
|
+
def __init__(self, app=None, block_code=403, block_durantion=5, block_ip=False, log_func=None, monitor_mode=False, rate_limit=False, dashboard_path=None):
|
|
31
|
+
if not hasattr(self, 'initialized'):
|
|
32
|
+
self.initialized = True
|
|
33
|
+
|
|
34
|
+
self.app = app
|
|
35
|
+
self.block_code = block_code
|
|
36
|
+
self.log = log_func or Logger()
|
|
37
|
+
self.monitor_mode = monitor_mode
|
|
38
|
+
self.block_ip = block_ip
|
|
39
|
+
self.rate_limit = rate_limit
|
|
40
|
+
self.dashboard_path = dashboard_path
|
|
41
|
+
self.block_durantion = block_durantion
|
|
42
|
+
self.recent_blocks_cache = {}
|
|
43
|
+
|
|
44
|
+
self.rules_sqli = [
|
|
45
|
+
r"(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDROP\b)",
|
|
46
|
+
r"' OR '1'='1"
|
|
47
|
+
]
|
|
48
|
+
self.rules_xss = [
|
|
49
|
+
r"<script.*?>.*?</script>",
|
|
50
|
+
r"javascript:"
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
if app is not None:
|
|
54
|
+
self.init_app(app)
|
|
31
55
|
|
|
32
56
|
def init_app(self, app):
|
|
33
|
-
|
|
57
|
+
Base.metadata.create_all(engine)
|
|
58
|
+
setup_dashboard(app, self.dashboard_path)
|
|
59
|
+
seed_default_whitelist()
|
|
60
|
+
Admin.create_admin_user(get_session())
|
|
61
|
+
|
|
62
|
+
if not app.secret_key:
|
|
63
|
+
def create_secret_key():
|
|
64
|
+
mac_address = str(uuid.getnode())
|
|
65
|
+
hostname = socket.gethostname()
|
|
66
|
+
project_salt = "wafahell-security-core-v1"
|
|
67
|
+
fingerprint = f"{mac_address}-{hostname}-{project_salt}"
|
|
68
|
+
return hashlib.sha256(fingerprint.encode()).hexdigest()
|
|
69
|
+
app.secret_key = create_secret_key()
|
|
70
|
+
|
|
34
71
|
@app.before_request
|
|
35
72
|
def create_session():
|
|
36
73
|
try:
|
|
37
|
-
req.session = get_session()
|
|
74
|
+
req.session = get_session()
|
|
38
75
|
except Exception as e:
|
|
39
76
|
self.log.error(f"Erro ao criar sessão para requisição: {e}")
|
|
40
|
-
abort(self.block_code)
|
|
77
|
+
abort(self.block_code)
|
|
41
78
|
|
|
42
|
-
# Fecha a sessão após cada requisição
|
|
43
79
|
@app.teardown_request
|
|
44
80
|
def close_session(exc=None):
|
|
45
81
|
if hasattr(req, 'session'):
|
|
46
82
|
try:
|
|
47
|
-
req.session.close()
|
|
83
|
+
req.session.close()
|
|
48
84
|
except Exception as e:
|
|
49
85
|
self.log.error(f"Erro ao fechar sessão: {e}")
|
|
86
|
+
|
|
50
87
|
|
|
51
88
|
@app.before_request
|
|
52
89
|
def waf_check():
|
|
90
|
+
if self.check_whitelist(req):
|
|
91
|
+
return
|
|
53
92
|
self.verify_client_blocked(req)
|
|
54
93
|
self.verify_rate_limit(req)
|
|
55
|
-
is_malicious, attack_local, payload = self.is_malicious(req)
|
|
94
|
+
is_malicious, attack_local, payload, attack_type = self.is_malicious(req)
|
|
56
95
|
|
|
57
96
|
if not is_malicious:
|
|
58
97
|
return
|
|
59
98
|
|
|
99
|
+
self.log_attack(req, attack_type, payload, attack_local)
|
|
100
|
+
|
|
60
101
|
if not self.monitor_mode:
|
|
61
|
-
self.log.warning(self.parse_req(req, payload,attack_local))
|
|
102
|
+
self.log.warning(self.parse_req(req, payload, attack_local, attack_type))
|
|
62
103
|
self.block_ip_address(req.remote_addr, req.headers.get("User-Agent", "unknown"))
|
|
104
|
+
abort(self.block_code)
|
|
63
105
|
else:
|
|
64
|
-
self.log.info(self.parse_req(req, payload, attack_local))
|
|
106
|
+
self.log.info(self.parse_req(req, payload, attack_local, attack_type))
|
|
107
|
+
|
|
108
|
+
@app.before_request
|
|
109
|
+
def start_timer():
|
|
110
|
+
g.waf_start_time = time.time()
|
|
111
|
+
|
|
112
|
+
@app.after_request
|
|
113
|
+
def stop_timer(response):
|
|
114
|
+
ignored_paths = [self.dashboard_path, f'{self.dashboard_path}/stats', '/static']
|
|
115
|
+
|
|
116
|
+
# 1. Ignora rotas do próprio painel para não sujar os logs e métricas
|
|
117
|
+
if any(req.path.startswith(path) for path in ignored_paths):
|
|
118
|
+
return response
|
|
119
|
+
|
|
120
|
+
# 2. LOG DE TRÁFEGO LEGÍTIMO
|
|
121
|
+
# Se o status_code for menor que 400, significa que o WAF não deu abort()
|
|
122
|
+
# e a requisição seguiu o fluxo normal.
|
|
123
|
+
if response.status_code != self.block_code:
|
|
124
|
+
self.log_legit_access(req)
|
|
125
|
+
|
|
126
|
+
# 3. RPS Logic (Bucketing by second)
|
|
127
|
+
current_timestamp = int(time.time())
|
|
128
|
+
rps_key = f"rps_{current_timestamp}"
|
|
129
|
+
waf_cache.incr(rps_key, default=0)
|
|
130
|
+
waf_cache.expire(rps_key, 10)
|
|
131
|
+
|
|
132
|
+
# 4. Latency Logic
|
|
133
|
+
if hasattr(g, 'waf_start_time'):
|
|
134
|
+
latency = (time.time() - g.waf_start_time) * 1000
|
|
135
|
+
|
|
136
|
+
# Exponential Moving Average
|
|
137
|
+
old_avg = waf_cache.get('latency_avg', default=0.0)
|
|
138
|
+
new_avg = (old_avg * 0.95) + (latency * 0.05) if old_avg > 0 else latency
|
|
139
|
+
|
|
140
|
+
waf_cache.set('latency_avg', new_avg, expire=3600)
|
|
141
|
+
|
|
142
|
+
return response
|
|
143
|
+
|
|
144
|
+
def log_legit_access(self, req):
|
|
145
|
+
entry = {
|
|
146
|
+
"timestamp": datetime.now(timezone.utc),
|
|
147
|
+
"attack_type": 'INFO',
|
|
148
|
+
"ip": req.remote_addr,
|
|
149
|
+
"path": req.path,
|
|
150
|
+
"method": req.method,
|
|
151
|
+
"level": 'INFO',
|
|
152
|
+
"payload": None, # INFO não tem payload
|
|
153
|
+
"attack_local": None # INFO não tem local de ataque
|
|
154
|
+
}
|
|
155
|
+
# Manda para o gerenciador de lote
|
|
156
|
+
self._push_to_batch(entry)
|
|
157
|
+
|
|
158
|
+
def log_attack(self, req, attack_type, payload, attack_local):
|
|
159
|
+
# Decodifica o payload para ficar legível no banco
|
|
160
|
+
safe_payload = unquote(payload) if payload else "---"
|
|
161
|
+
|
|
162
|
+
entry = {
|
|
163
|
+
"timestamp": datetime.now(timezone.utc),
|
|
164
|
+
"attack_type": attack_type, # Ex: SQLI, XSS
|
|
165
|
+
"ip": req.remote_addr,
|
|
166
|
+
"path": req.path,
|
|
167
|
+
"method": req.method,
|
|
168
|
+
"level": 'WARNING',
|
|
169
|
+
"payload": safe_payload,
|
|
170
|
+
"attack_local": attack_local # Ex: URL, BODY, HEADER
|
|
171
|
+
}
|
|
172
|
+
self.log_block(req)
|
|
173
|
+
self._push_to_batch(entry)
|
|
174
|
+
|
|
175
|
+
def log_block(self, req):
|
|
176
|
+
"""
|
|
177
|
+
Registra especificamente bloqueios de conexão (Blacklist/Manual Block).
|
|
178
|
+
"""
|
|
179
|
+
entry = {
|
|
180
|
+
"timestamp": datetime.now(timezone.utc),
|
|
181
|
+
"attack_type": "IP BLOCK",
|
|
182
|
+
"ip": req.remote_addr,
|
|
183
|
+
"path": req.path,
|
|
184
|
+
"method": req.method,
|
|
185
|
+
"level": 'INFO',
|
|
186
|
+
"payload": "---",
|
|
187
|
+
"attack_local": "WAF"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Envia para o mesmo lote que os ataques normais
|
|
191
|
+
self._push_to_batch(entry)
|
|
192
|
+
|
|
193
|
+
def _push_to_batch(self, log_entry):
|
|
194
|
+
"""
|
|
195
|
+
Função central que gerencia o Buffer de Logs (Memória -> Banco).
|
|
196
|
+
Usada tanto por logs legítimos quanto por ataques.
|
|
197
|
+
"""
|
|
198
|
+
# 1. Recupera os logs pendentes do cache global
|
|
199
|
+
pending_logs = waf_cache.get('pending_logs_batch', default=[])
|
|
200
|
+
pending_logs.append(log_entry)
|
|
201
|
+
|
|
202
|
+
# 2. Verifica Gatilhos: 50 logs OU 3 segundos (reduzi de 10 pra 3 pra ficar mais "real time")
|
|
203
|
+
current_time = time.time()
|
|
204
|
+
last_flush = waf_cache.get('last_log_flush_time', default=0)
|
|
205
|
+
|
|
206
|
+
if len(pending_logs) >= 50 or (current_time - last_flush) > 3:
|
|
207
|
+
|
|
208
|
+
# Função interna de flush (Abre sessão dedicada para o lote)
|
|
209
|
+
session = get_session()
|
|
210
|
+
try:
|
|
211
|
+
# bulk_insert_mappings é OTIMIZADO para grandes volumes
|
|
212
|
+
session.bulk_insert_mappings(WafLog, pending_logs)
|
|
213
|
+
session.commit()
|
|
214
|
+
|
|
215
|
+
# Sucesso: Limpa o cache
|
|
216
|
+
waf_cache.set('pending_logs_batch', [], expire=60)
|
|
217
|
+
waf_cache.set('last_log_flush_time', current_time, expire=60)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
session.rollback()
|
|
220
|
+
# Não usamos self.log.error aqui para não criar loop infinito se o erro for no logger
|
|
221
|
+
print(f" [ERRO CRÍTICO] Falha no Batch Insert do WAF: {e}")
|
|
222
|
+
|
|
223
|
+
# Mantém os dados no cache para tentar na próxima requisição
|
|
224
|
+
waf_cache.set('pending_logs_batch', pending_logs, expire=60)
|
|
225
|
+
finally:
|
|
226
|
+
session.close()
|
|
227
|
+
else:
|
|
228
|
+
# Apenas atualiza a lista no cache esperando o gatilho
|
|
229
|
+
waf_cache.set('pending_logs_batch', pending_logs, expire=60)
|
|
65
230
|
|
|
66
231
|
def detect_attack(self, data: str) -> bool:
|
|
67
|
-
for pattern in self.
|
|
232
|
+
for pattern in self.rules_xss:
|
|
68
233
|
if re.search(pattern, data, re.IGNORECASE):
|
|
69
|
-
return
|
|
70
|
-
|
|
234
|
+
return "XSS"
|
|
235
|
+
for pattern in self.rules_sqli:
|
|
236
|
+
if re.search(pattern, data, re.IGNORECASE):
|
|
237
|
+
return "SQLI"
|
|
238
|
+
return None
|
|
71
239
|
|
|
72
|
-
def is_malicious(self, req) -> tuple
|
|
240
|
+
def is_malicious(self, req) -> tuple:
|
|
241
|
+
attack = self.detect_attack(req.base_url)
|
|
242
|
+
if attack:
|
|
243
|
+
print(f"[DEBUG] Attack detected in URL: {attack}")
|
|
244
|
+
return True, "URL", req.base_url, attack
|
|
245
|
+
|
|
246
|
+
for key, value in req.form.items():
|
|
247
|
+
attack = self.detect_attack(value)
|
|
248
|
+
if attack:
|
|
249
|
+
print(f"[DEBUG] Attack detected in FORM '{key}': {attack}")
|
|
250
|
+
return True, f"FORM '{key}'", value, attack
|
|
73
251
|
|
|
74
|
-
if self.detect_attack(req.base_url):
|
|
75
|
-
return True, "URL", req.base_url
|
|
76
|
-
|
|
77
252
|
for key, value in req.args.items():
|
|
78
|
-
|
|
79
|
-
|
|
253
|
+
attack = self.detect_attack(value)
|
|
254
|
+
if attack:
|
|
255
|
+
print(f"[DEBUG] Attack detected in QUERY '{key}': {attack}")
|
|
256
|
+
return True, f"QUERY '{key}'", value, attack
|
|
80
257
|
|
|
81
258
|
for key, value in req.headers.items():
|
|
82
|
-
|
|
83
|
-
|
|
259
|
+
attack = self.detect_attack(value)
|
|
260
|
+
if attack:
|
|
261
|
+
print(f"[DEBUG] Attack detected in HEADER '{key}': {attack}")
|
|
262
|
+
return True, f"HEADER '{key}'", value, attack
|
|
84
263
|
|
|
85
264
|
if req.data:
|
|
86
265
|
body_content = req.data.decode(errors="ignore")
|
|
87
|
-
|
|
88
|
-
|
|
266
|
+
attack = self.detect_attack(body_content)
|
|
267
|
+
if attack:
|
|
268
|
+
print(f"[DEBUG] Attack detected in BODY: {attack}")
|
|
269
|
+
return True, "BODY", body_content, attack
|
|
89
270
|
|
|
90
271
|
if req.is_json:
|
|
91
272
|
json_data = req.get_json(silent=True)
|
|
92
273
|
if json_data:
|
|
93
274
|
import json
|
|
94
275
|
json_str = json.dumps(json_data)
|
|
95
|
-
|
|
96
|
-
|
|
276
|
+
attack = self.detect_attack(json_str)
|
|
277
|
+
if attack:
|
|
278
|
+
print(f"[DEBUG] Attack detected in JSON BODY: {attack}")
|
|
279
|
+
return True, "JSON BODY", json_str, attack
|
|
97
280
|
|
|
98
|
-
return False, None, None
|
|
281
|
+
return False, None, None, None
|
|
99
282
|
|
|
100
283
|
def verify_client_blocked(self, req) -> None:
|
|
101
|
-
|
|
284
|
+
ip = req.remote_addr
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------
|
|
287
|
+
# 1. FAST PATH: Cache Check (Memória/Disco)
|
|
288
|
+
# ---------------------------------------------------------
|
|
289
|
+
# Se o cache diz que está bloqueado, abortamos imediatamente.
|
|
290
|
+
# Isso economiza 99% das queries de SELECT durante um ataque (fuzzing).
|
|
291
|
+
if waf_cache.get(f"blocked_{ip}"):
|
|
292
|
+
abort(self.block_code)
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------
|
|
295
|
+
# 2. SLOW PATH: Database Check
|
|
296
|
+
# ---------------------------------------------------------
|
|
297
|
+
# Só chegamos aqui se o IP não estiver no cache.
|
|
298
|
+
# Pode ser um IP limpo OU um IP bloqueado cujo cache expirou (TTL).
|
|
299
|
+
|
|
300
|
+
session = req.session # Reutiliza a sessão da request (Fundamental!)
|
|
301
|
+
|
|
102
302
|
try:
|
|
103
|
-
client_blocked = session.query(Blocked).filter_by(
|
|
104
|
-
|
|
105
|
-
).first()
|
|
303
|
+
client_blocked = session.query(Blocked).filter_by(ip=ip).first()
|
|
304
|
+
|
|
106
305
|
if client_blocked:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
306
|
+
# Normalização de fuso horário
|
|
307
|
+
now = datetime.now(timezone.utc) if client_blocked.blocked_until.tzinfo else datetime.utcnow()
|
|
308
|
+
|
|
309
|
+
# Caso 1: Ainda está bloqueado
|
|
310
|
+
if client_blocked.blocked_until > now:
|
|
311
|
+
# RE-AQUECIMENTO DO CACHE:
|
|
312
|
+
# O bloqueio ainda é válido no banco, então renovamos o cache por mais 60s.
|
|
313
|
+
# Assim, as próximas requisições desse IP vão cair no Fast Path acima.
|
|
314
|
+
waf_cache.set(f"blocked_{ip}", True, expire=60)
|
|
112
315
|
abort(self.block_code)
|
|
113
|
-
|
|
316
|
+
|
|
317
|
+
# Caso 2: O bloqueio expirou (Desbloqueio)
|
|
318
|
+
else:
|
|
319
|
+
try:
|
|
320
|
+
session.delete(client_blocked)
|
|
321
|
+
session.commit()
|
|
322
|
+
self.log.info(f"[UNBLOCKED] Bloqueio do IP {ip} expirou.")
|
|
323
|
+
|
|
324
|
+
# Garante que não sobrou lixo no cache
|
|
325
|
+
waf_cache.delete(f"blocked_{ip}")
|
|
326
|
+
# Remove travas de bloqueio antigas se existirem
|
|
327
|
+
waf_cache.delete(f"blocking_lock_{ip}")
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
# Se der erro de concorrência (outro thread já deletou), apenas ignora
|
|
331
|
+
session.rollback()
|
|
332
|
+
|
|
333
|
+
except OperationalError:
|
|
334
|
+
# Se o banco estiver travado/ocupado, abortamos por segurança (Fail Closed)
|
|
114
335
|
session.rollback()
|
|
115
336
|
abort(self.block_code)
|
|
116
|
-
|
|
117
337
|
|
|
118
338
|
def block_ip_address(self, ip, user_agent=None):
|
|
119
|
-
if self.block_ip:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
339
|
+
if not self.block_ip:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
# 1. TRAVA DE CACHE (A Salvação do Fuzzing)
|
|
343
|
+
# Verifica se já existe um processo de bloqueio rodando para este IP.
|
|
344
|
+
# Isso impede que 50 threads do ffuf tentem fazer INSERT ao mesmo tempo.
|
|
345
|
+
cache_key = f"blocking_lock_{ip}"
|
|
346
|
+
|
|
347
|
+
if waf_cache.get(cache_key):
|
|
348
|
+
return # Já está sendo bloqueado por outra thread, aborta.
|
|
349
|
+
|
|
350
|
+
# Cria a trava por 5 segundos (tempo mais que suficiente para o insert ocorrer)
|
|
351
|
+
waf_cache.set(cache_key, True, expire=5)
|
|
352
|
+
|
|
353
|
+
# 2. REUTILIZAÇÃO DE SESSÃO
|
|
354
|
+
# Usamos a sessão que já está aberta na requisição atual.
|
|
355
|
+
session = req.session
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
# Verifica se já existe na tabela (Query leve)
|
|
359
|
+
exists_block = session.query(Blocked).filter_by(ip=ip).first()
|
|
360
|
+
|
|
361
|
+
if not exists_block:
|
|
362
|
+
now = datetime.now(timezone.utc)
|
|
363
|
+
until = now + timedelta(minutes=self.block_durantion)
|
|
364
|
+
|
|
365
|
+
# Atenção ao formato do blocked_at se o seu Model esperar String
|
|
366
|
+
# Se no model for DateTime, use 'now'. Se for String, use 'now.strftime...'
|
|
367
|
+
new_block = Blocked(
|
|
368
|
+
ip=ip,
|
|
369
|
+
user_agent=user_agent or "unknown",
|
|
370
|
+
blocked_at=now.strftime("%H:%M:%S"),
|
|
371
|
+
blocked_until=until
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
session.add(new_block)
|
|
124
375
|
session.commit()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
self.log.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
376
|
+
|
|
377
|
+
# Log no arquivo/console
|
|
378
|
+
self.log.warning(f"[BLOCKED] IP: {ip} bloqueado por {self.block_durantion} min.")
|
|
379
|
+
|
|
380
|
+
# 3. PRÉ-AQUECIMENTO DE CACHE
|
|
381
|
+
# Já avisa o cache que este IP está bloqueado.
|
|
382
|
+
# A próxima requisição vai bater no verify_client_blocked, ler o cache e ser barrada sem tocar no banco.
|
|
383
|
+
waf_cache.set(f"blocked_{ip}", True, expire=60)
|
|
384
|
+
|
|
385
|
+
except Exception as e:
|
|
386
|
+
session.rollback()
|
|
387
|
+
self.log.error(f"Erro ao persistir bloqueio: {e}")
|
|
388
|
+
# Se deu erro, removemos a trava para tentar novamente na próxima
|
|
389
|
+
waf_cache.delete(cache_key)
|
|
390
|
+
|
|
391
|
+
# IMPORTANTE:
|
|
392
|
+
# Não usamos 'finally: session.close()' aqui!
|
|
393
|
+
# Quem fecha é o @app.teardown_request no final do ciclo.
|
|
133
394
|
|
|
134
395
|
def verify_rate_limit(self, req) -> None:
|
|
135
396
|
if self.rate_limit:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
397
|
+
ip = req.remote_addr
|
|
398
|
+
ua = req.headers.get("User-Agent", "unknown")
|
|
399
|
+
|
|
400
|
+
if limiter.is_rate_limited(ip, ua):
|
|
401
|
+
self.log_attack(
|
|
402
|
+
req=req,
|
|
403
|
+
attack_type="RATE LIMIT",
|
|
404
|
+
payload="Too Many Requests",
|
|
405
|
+
attack_local="Rate Limiter"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
self.log.warning(f"[RATE LIMIT] IP: {ip} exceeded limit.")
|
|
409
|
+
|
|
410
|
+
if self.monitor_mode:
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
if self.block_ip:
|
|
414
|
+
self.block_ip_address(ip, ua)
|
|
415
|
+
abort(self.block_code)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def check_whitelist(self, req) -> bool:
|
|
419
|
+
ip_str = req.remote_addr
|
|
420
|
+
|
|
421
|
+
# 1. CACHE (Velocidade Extrema)
|
|
422
|
+
# Se esse IP já foi validado antes (seja por faixa ou exato), libera.
|
|
423
|
+
if waf_cache.get(f"whitelist_{ip_str}"):
|
|
424
|
+
return True
|
|
425
|
+
|
|
426
|
+
session = req.session
|
|
427
|
+
try:
|
|
428
|
+
# 2. Busca Exata (Para IPs unitários como 8.8.8.8)
|
|
429
|
+
# É muito rápido.
|
|
430
|
+
if session.query(Whitelist).filter_by(ip=ip_str).first():
|
|
431
|
+
waf_cache.set(f"whitelist_{ip_str}", True, expire=3600)
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
# 3. Busca por Faixas (CIDR)
|
|
435
|
+
# Só executamos isso se não achou match exato.
|
|
436
|
+
# Trazemos apenas as faixas que contêm "/" para não trazer IPs soltos
|
|
437
|
+
cidr_ranges = session.query(Whitelist.ip).filter(Whitelist.ip.like('%/%')).all()
|
|
438
|
+
|
|
439
|
+
if not cidr_ranges:
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
user_ip = ipaddress.ip_address(ip_str)
|
|
443
|
+
|
|
444
|
+
for row in cidr_ranges:
|
|
445
|
+
try:
|
|
446
|
+
# Verifica matematicamente se o IP está na rede
|
|
447
|
+
network = ipaddress.ip_network(row.ip, strict=False)
|
|
448
|
+
if user_ip in network:
|
|
449
|
+
# ACHOU!
|
|
450
|
+
# Salva o IP DO USUÁRIO no cache.
|
|
451
|
+
# Na próxima requisição, ele cai no passo 1 e nem passa por aqui.
|
|
452
|
+
waf_cache.set(f"whitelist_{ip_str}", True, expire=3600)
|
|
453
|
+
return True
|
|
454
|
+
except ValueError:
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
except Exception as e:
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
return False
|
|
143
461
|
|
|
144
|
-
def parse_req(self, req, payload, attack_local=None) -> str:
|
|
462
|
+
def parse_req(self, req, payload, attack_local=None, attack_type=None) -> str:
|
|
145
463
|
ip = req.remote_addr
|
|
146
464
|
user_agent = req.headers.get("User-Agent", "unknown")
|
|
147
465
|
path = req.path
|
|
148
466
|
method = req.method
|
|
149
467
|
attack_local = attack_local or "unknown"
|
|
150
|
-
|
|
151
|
-
return msg
|
|
468
|
+
return f"[ATTACK] Attack_type: {attack_type}, IP: {ip}, User-Agent: {user_agent}, Path: {path}, Method: {method}, Payload: {unquote(payload)}, attack_local: {attack_local}"
|
wafaHell/mock.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
# Configuração do alvo
|
|
4
|
+
BASE_URL = "http://127.0.0.1:5000/hello"
|
|
5
|
+
|
|
6
|
+
# Definição dos payloads de teste (SQLi e XSS clássicos)
|
|
7
|
+
payloads = {
|
|
8
|
+
"URL Query": {"url": f"{BASE_URL}?id=1' OR '1'='1"},
|
|
9
|
+
"Form Data": {"method": "POST", "data": {"user": "<script>alert(1)</script>"}},
|
|
10
|
+
"JSON Body": {"method": "POST", "json": {"search": "SELECT * FROM users"}},
|
|
11
|
+
"Cookies": {"method": "POST", "cookies": {"session": "UNION SELECT NULL,NULL--"}},
|
|
12
|
+
"Custom Header": {"method": "POST", "headers": {"User-Agent": "'; DROP TABLE logs;--"}},
|
|
13
|
+
"Multipart File": {"method": "POST", "files": {"file": ("README.md", "payload: <script>alert('XSS')</script>")}},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def run_mock():
|
|
17
|
+
print(f"🚀 Iniciando Mock de Testes WafaHell no alvo: {BASE_URL}\n")
|
|
18
|
+
print(f"{'VETOR DE TESTE':<20} | {'STATUS':<10} | {'RESULTADO'}")
|
|
19
|
+
print("-" * 55)
|
|
20
|
+
|
|
21
|
+
for test_name, config in payloads.items():
|
|
22
|
+
try:
|
|
23
|
+
# Prepara a requisição
|
|
24
|
+
url = config.get("url", BASE_URL)
|
|
25
|
+
method = config.get("method", "GET")
|
|
26
|
+
|
|
27
|
+
# Executa a requisição com os parâmetros dinâmicos
|
|
28
|
+
response = requests.request(
|
|
29
|
+
method=method,
|
|
30
|
+
url=url,
|
|
31
|
+
data=config.get("data"),
|
|
32
|
+
json=config.get("json"),
|
|
33
|
+
cookies=config.get("cookies"),
|
|
34
|
+
headers=config.get("headers"),
|
|
35
|
+
files=config.get("files"),
|
|
36
|
+
timeout=5
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Validação: 403 significa que o WAF bloqueou (Sucesso no teste)
|
|
40
|
+
if response.status_code == 403:
|
|
41
|
+
status_txt = "✅ BLOQUEADO"
|
|
42
|
+
result_txt = "Sucesso (WAF Ativo)"
|
|
43
|
+
else:
|
|
44
|
+
status_txt = f"❌ {response.status_code}"
|
|
45
|
+
result_txt = "Falha (Vulnerável!)"
|
|
46
|
+
|
|
47
|
+
print(f"{test_name:<20} | {status_txt:<10} | {result_txt}")
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"{test_name:<20} | ⚠️ ERRO | {str(e)}")
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
run_mock()
|