Add extension enumeration detection and comprehensive SIP protection
Major features: - Extension enumeration detection with 3 detection algorithms: - Max unique extensions threshold (default: 20 in 5 min) - Sequential pattern detection (e.g., 100,101,102...) - Rapid-fire detection (many extensions in short window) - Prometheus metrics for all SIP Guardian operations - SQLite persistent storage for bans and attack history - Webhook notifications for ban/unban/suspicious events - GeoIP-based country blocking with continent shortcuts - Per-method rate limiting with token bucket algorithm Bug fixes: - Fix whitelist count always reporting zero in stats - Fix whitelisted connections metric never incrementing - Fix Caddyfile config not being applied to shared guardian New files: - enumeration.go: Extension enumeration detector - enumeration_test.go: 14 comprehensive unit tests - metrics.go: Prometheus metrics handler - storage.go: SQLite persistence layer - webhooks.go: Webhook notification system - geoip.go: MaxMind GeoIP integration - ratelimit.go: Per-method rate limiting Testing: - sandbox/ contains complete Docker Compose test environment - All 14 enumeration tests pass
This commit is contained in:
parent
0b0fb53c9c
commit
c73fa9d3d1
19 changed files with 4630 additions and 544 deletions
107
sandbox/Caddyfile
Normal file
107
sandbox/Caddyfile
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Sandbox Caddyfile for SIP Guardian Testing
|
||||
#
|
||||
# This configuration showcases all the new features:
|
||||
# - Prometheus metrics endpoint
|
||||
# - Rate limiting per method (built-in defaults)
|
||||
# - Suspicious pattern detection
|
||||
#
|
||||
# Note: Storage and webhooks are configured in JSON config mode,
|
||||
# as the L4 handler uses the shared global guardian instance
|
||||
|
||||
{
|
||||
debug
|
||||
|
||||
admin 0.0.0.0:2019
|
||||
|
||||
layer4 {
|
||||
# SIP over UDP
|
||||
udp/:5060 {
|
||||
@sip sip {
|
||||
methods REGISTER INVITE OPTIONS ACK BYE CANCEL INFO NOTIFY SUBSCRIBE MESSAGE
|
||||
}
|
||||
|
||||
route @sip {
|
||||
sip_guardian {
|
||||
max_failures 3 # Lower for faster testing
|
||||
find_time 2m # Shorter window
|
||||
ban_time 5m # Short bans for testing
|
||||
|
||||
# Whitelist legitimate test clients
|
||||
whitelist 10.55.0.50/32 # client container
|
||||
whitelist 10.55.0.51/32 # linphone container
|
||||
|
||||
# Enumeration detection (low thresholds for testing)
|
||||
enumeration {
|
||||
max_extensions 10
|
||||
extension_window 2m
|
||||
sequential_threshold 5
|
||||
rapid_fire_count 8
|
||||
rapid_fire_window 10s
|
||||
ban_time 10m
|
||||
exempt_extensions 100 200
|
||||
}
|
||||
}
|
||||
proxy udp/{$SIP_UPSTREAM_HOST}:{$SIP_UPSTREAM_PORT}
|
||||
}
|
||||
|
||||
# Unmatched traffic - drop silently
|
||||
route {
|
||||
}
|
||||
}
|
||||
|
||||
# SIP over TCP
|
||||
tcp/:5060 {
|
||||
@sip sip
|
||||
|
||||
route @sip {
|
||||
sip_guardian {
|
||||
max_failures 3
|
||||
find_time 2m
|
||||
ban_time 5m
|
||||
whitelist 10.55.0.50/32
|
||||
whitelist 10.55.0.51/32
|
||||
}
|
||||
proxy tcp/{$SIP_UPSTREAM_HOST}:{$SIP_UPSTREAM_PORT}
|
||||
}
|
||||
}
|
||||
|
||||
# SIP over TLS
|
||||
tcp/:5061 {
|
||||
@sip sip
|
||||
|
||||
route @sip {
|
||||
sip_guardian {
|
||||
max_failures 3
|
||||
find_time 2m
|
||||
ban_time 5m
|
||||
whitelist 10.55.0.50/32
|
||||
whitelist 10.55.0.51/32
|
||||
}
|
||||
proxy tcp/{$SIP_UPSTREAM_HOST}:{$SIP_UPSTREAM_TLS_PORT}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Admin API and Metrics
|
||||
:2020 {
|
||||
# SIP Guardian admin endpoints
|
||||
handle /api/sip-guardian/* {
|
||||
sip_guardian_admin
|
||||
}
|
||||
|
||||
# Prometheus metrics endpoint
|
||||
handle /metrics {
|
||||
sip_guardian_metrics
|
||||
}
|
||||
|
||||
# Health check
|
||||
handle /health {
|
||||
respond "OK" 200
|
||||
}
|
||||
|
||||
# Stats (alias for backwards compatibility)
|
||||
handle /stats {
|
||||
sip_guardian_admin
|
||||
}
|
||||
}
|
||||
219
sandbox/docker-compose.yml
Normal file
219
sandbox/docker-compose.yml
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# SIP Guardian Testing Sandbox
|
||||
#
|
||||
# This provides a complete testing environment with:
|
||||
# - FreePBX (real PBX for testing)
|
||||
# - Caddy with SIP Guardian (the proxy under test)
|
||||
# - Attack containers (sipvicious, custom scripts)
|
||||
# - Valid client containers (pjsip for legitimate traffic)
|
||||
#
|
||||
# Usage:
|
||||
# docker compose up -d
|
||||
# docker compose logs -f caddy
|
||||
# docker compose run --rm attacker sipvicious_svwar -e100-200 caddy
|
||||
# docker compose run --rm client pjsua --registrar=sip:caddy
|
||||
|
||||
services:
|
||||
# ============================================
|
||||
# FreePBX - The Protected PBX
|
||||
# ============================================
|
||||
freepbx:
|
||||
image: tiredofit/freepbx:latest
|
||||
container_name: sandbox-freepbx
|
||||
hostname: freepbx
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
environment:
|
||||
- VIRTUAL_HOST=pbx.sandbox.local
|
||||
- VIRTUAL_NETWORK=sandbox
|
||||
- HTTP_PORT=80
|
||||
- HTTPS_PORT=443
|
||||
- UCP_FIRST_RUN=true
|
||||
- DB_HOST=mariadb
|
||||
- DB_PORT=3306
|
||||
- DB_NAME=asterisk
|
||||
- DB_USER=asterisk
|
||||
- DB_PASS=asteriskpass
|
||||
- ENABLE_FAIL2BAN=FALSE # We're replacing this!
|
||||
- TZ=UTC
|
||||
volumes:
|
||||
- freepbx_data:/data
|
||||
- freepbx_logs:/var/log
|
||||
- freepbx_www:/var/www/html
|
||||
depends_on:
|
||||
- mariadb
|
||||
networks:
|
||||
sandbox:
|
||||
ipv4_address: 10.55.0.10
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.11
|
||||
container_name: sandbox-mariadb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=rootpass
|
||||
- MYSQL_DATABASE=asterisk
|
||||
- MYSQL_USER=asterisk
|
||||
- MYSQL_PASSWORD=asteriskpass
|
||||
volumes:
|
||||
- mariadb_data:/var/lib/mysql
|
||||
networks:
|
||||
sandbox:
|
||||
ipv4_address: 10.55.0.11
|
||||
|
||||
# ============================================
|
||||
# Caddy with SIP Guardian - The Proxy
|
||||
# ============================================
|
||||
caddy:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
container_name: sandbox-caddy
|
||||
hostname: caddy
|
||||
restart: unless-stopped
|
||||
# Override default command to use explicit Caddyfile (ENTRYPOINT is "caddy")
|
||||
command: ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
ports:
|
||||
# Expose SIP ports to host for external testing
|
||||
- "5060:5060/udp"
|
||||
- "5060:5060/tcp"
|
||||
- "5061:5061/tcp"
|
||||
# Admin API
|
||||
- "2020:2020"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
environment:
|
||||
- SIP_UPSTREAM_HOST=freepbx
|
||||
- SIP_UPSTREAM_PORT=5060
|
||||
- SIP_UPSTREAM_TLS_PORT=5061
|
||||
networks:
|
||||
sandbox:
|
||||
ipv4_address: 10.55.0.2
|
||||
depends_on:
|
||||
- freepbx
|
||||
|
||||
# ============================================
|
||||
# Attack Simulation Containers
|
||||
# ============================================
|
||||
|
||||
# SIPVicious scanner - for testing scanner detection
|
||||
attacker:
|
||||
image: python:3.11-slim
|
||||
container_name: sandbox-attacker
|
||||
hostname: attacker
|
||||
profiles:
|
||||
- testing
|
||||
command: >
|
||||
bash -c "
|
||||
pip install sipvicious pysip3 &&
|
||||
echo 'SIPVicious and tools installed. Use: sipvicious_svwar, sipvicious_svcrack, sipvicious_svmap' &&
|
||||
tail -f /dev/null
|
||||
"
|
||||
networks:
|
||||
sandbox:
|
||||
ipv4_address: 10.55.0.100
|
||||
depends_on:
|
||||
- caddy
|
||||
|
||||
# Brute force simulation
|
||||
bruteforcer:
|
||||
image: python:3.11-slim
|
||||
container_name: sandbox-bruteforcer
|
||||
hostname: bruteforcer
|
||||
profiles:
|
||||
- testing
|
||||
volumes:
|
||||
- ./scripts:/scripts:ro
|
||||
command: >
|
||||
bash -c "
|
||||
pip install sipsimple requests &&
|
||||
echo 'Brute force tools ready. Run: python /scripts/bruteforce.py' &&
|
||||
tail -f /dev/null
|
||||
"
|
||||
networks:
|
||||
sandbox:
|
||||
ipv4_address: 10.55.0.101
|
||||
depends_on:
|
||||
- caddy
|
||||
|
||||
# ============================================
|
||||
# Legitimate Client Containers
|
||||
# ============================================
|
||||
|
||||
# PJSIP-based SIP client
|
||||
client:
|
||||
image: alpine:latest
|
||||
container_name: sandbox-client
|
||||
hostname: client
|
||||
profiles:
|
||||
- testing
|
||||
command: >
|
||||
sh -c "
|
||||
apk add --no-cache pjsua netcat-openbsd curl jq &&
|
||||
echo 'PJSIP client ready. Use pjsua for SIP registration.' &&
|
||||
tail -f /dev/null
|
||||
"
|
||||
networks:
|
||||
sandbox:
|
||||
ipv4_address: 10.55.0.50
|
||||
depends_on:
|
||||
- caddy
|
||||
|
||||
# Linphone CLI client
|
||||
linphone:
|
||||
image: alpine:latest
|
||||
container_name: sandbox-linphone
|
||||
hostname: linphone
|
||||
profiles:
|
||||
- testing
|
||||
command: >
|
||||
sh -c "
|
||||
apk add --no-cache linphone netcat-openbsd curl &&
|
||||
echo 'Linphone ready.' &&
|
||||
tail -f /dev/null
|
||||
"
|
||||
networks:
|
||||
sandbox:
|
||||
ipv4_address: 10.55.0.51
|
||||
depends_on:
|
||||
- caddy
|
||||
|
||||
# ============================================
|
||||
# Monitoring & Debugging
|
||||
# ============================================
|
||||
|
||||
# Network sniffer for SIP traffic analysis
|
||||
tcpdump:
|
||||
image: alpine:latest
|
||||
container_name: sandbox-tcpdump
|
||||
hostname: tcpdump
|
||||
profiles:
|
||||
- debug
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
network_mode: "service:caddy"
|
||||
command: >
|
||||
sh -c "
|
||||
apk add --no-cache tcpdump &&
|
||||
tcpdump -i any -n 'udp port 5060 or tcp port 5060 or tcp port 5061' -vvv
|
||||
"
|
||||
depends_on:
|
||||
- caddy
|
||||
|
||||
volumes:
|
||||
freepbx_data:
|
||||
freepbx_logs:
|
||||
freepbx_www:
|
||||
mariadb_data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
|
||||
networks:
|
||||
sandbox:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 10.55.0.0/24
|
||||
gateway: 10.55.0.1
|
||||
123
sandbox/scripts/bruteforce.py
Normal file
123
sandbox/scripts/bruteforce.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
SIP Brute Force Simulation for SIP Guardian Testing
|
||||
|
||||
Simulates authentication failures to test rate limiting and banning.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import time
|
||||
import argparse
|
||||
import random
|
||||
import string
|
||||
|
||||
def generate_call_id():
|
||||
"""Generate a random SIP Call-ID"""
|
||||
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
def generate_branch():
|
||||
"""Generate a random Via branch parameter"""
|
||||
return 'z9hG4bK' + ''.join(random.choices(string.ascii_letters + string.digits, k=16))
|
||||
|
||||
def create_register_request(target_host: str, target_port: int, extension: str, from_ip: str) -> bytes:
|
||||
"""Create a SIP REGISTER request"""
|
||||
call_id = generate_call_id()
|
||||
branch = generate_branch()
|
||||
tag = ''.join(random.choices(string.digits, k=8))
|
||||
|
||||
request = f"""REGISTER sip:{target_host}:{target_port} SIP/2.0\r
|
||||
Via: SIP/2.0/UDP {from_ip}:5060;branch={branch}\r
|
||||
Max-Forwards: 70\r
|
||||
From: <sip:{extension}@{target_host}>;tag={tag}\r
|
||||
To: <sip:{extension}@{target_host}>\r
|
||||
Call-ID: {call_id}@{from_ip}\r
|
||||
CSeq: 1 REGISTER\r
|
||||
Contact: <sip:{extension}@{from_ip}:5060>\r
|
||||
Expires: 3600\r
|
||||
User-Agent: BruteForcer/1.0\r
|
||||
Content-Length: 0\r
|
||||
\r
|
||||
"""
|
||||
return request.encode()
|
||||
|
||||
def send_register(sock: socket.socket, target: tuple, request: bytes) -> str:
|
||||
"""Send REGISTER and receive response"""
|
||||
try:
|
||||
sock.sendto(request, target)
|
||||
sock.settimeout(2.0)
|
||||
response, _ = sock.recvfrom(4096)
|
||||
return response.decode()
|
||||
except socket.timeout:
|
||||
return "TIMEOUT"
|
||||
except Exception as e:
|
||||
return f"ERROR: {e}"
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='SIP Brute Force Simulator')
|
||||
parser.add_argument('target', help='Target host (Caddy proxy)')
|
||||
parser.add_argument('-p', '--port', type=int, default=5060, help='Target port')
|
||||
parser.add_argument('-e', '--extensions', default='100-105', help='Extension range (e.g., 100-200)')
|
||||
parser.add_argument('-c', '--count', type=int, default=10, help='Attempts per extension')
|
||||
parser.add_argument('-d', '--delay', type=float, default=0.1, help='Delay between attempts')
|
||||
parser.add_argument('--udp', action='store_true', default=True, help='Use UDP (default)')
|
||||
parser.add_argument('--tcp', action='store_true', help='Use TCP')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse extension range
|
||||
if '-' in args.extensions:
|
||||
start, end = map(int, args.extensions.split('-'))
|
||||
extensions = list(range(start, end + 1))
|
||||
else:
|
||||
extensions = [int(args.extensions)]
|
||||
|
||||
# Get our IP
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.connect((args.target, args.port))
|
||||
from_ip = sock.getsockname()[0]
|
||||
sock.close()
|
||||
|
||||
print(f"[*] Starting brute force simulation")
|
||||
print(f"[*] Target: {args.target}:{args.port}")
|
||||
print(f"[*] Source IP: {from_ip}")
|
||||
print(f"[*] Extensions: {extensions}")
|
||||
print(f"[*] Attempts per extension: {args.count}")
|
||||
print()
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
target = (args.target, args.port)
|
||||
|
||||
attempts = 0
|
||||
blocked_at = None
|
||||
|
||||
for ext in extensions:
|
||||
for i in range(args.count):
|
||||
request = create_register_request(args.target, args.port, str(ext), from_ip)
|
||||
response = send_register(sock, target, request)
|
||||
attempts += 1
|
||||
|
||||
if 'TIMEOUT' in response:
|
||||
if blocked_at is None:
|
||||
blocked_at = attempts
|
||||
print(f"[!] BLOCKED after {attempts} attempts (extension {ext}, attempt {i+1})")
|
||||
print(f"[+] SIP Guardian is working! Blocked after {blocked_at} attempts")
|
||||
return
|
||||
elif '401' in response or '407' in response:
|
||||
print(f"[*] Auth required: ext={ext} attempt={i+1} total={attempts}")
|
||||
elif '403' in response:
|
||||
print(f"[!] FORBIDDEN: ext={ext} - Connection blocked")
|
||||
if blocked_at is None:
|
||||
blocked_at = attempts
|
||||
else:
|
||||
# Print first line of response
|
||||
first_line = response.split('\r\n')[0] if response else 'No response'
|
||||
print(f"[?] Response: {first_line} (ext={ext})")
|
||||
|
||||
time.sleep(args.delay)
|
||||
|
||||
print(f"\n[*] Completed {attempts} attempts without being blocked")
|
||||
print("[!] SIP Guardian may not be working correctly")
|
||||
|
||||
sock.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
168
sandbox/scripts/valid_register.py
Normal file
168
sandbox/scripts/valid_register.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Valid SIP Registration Test
|
||||
|
||||
Tests that legitimate registrations pass through SIP Guardian successfully.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import time
|
||||
import argparse
|
||||
import random
|
||||
import string
|
||||
import hashlib
|
||||
|
||||
def generate_call_id():
|
||||
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
|
||||
def generate_branch():
|
||||
return 'z9hG4bK' + ''.join(random.choices(string.ascii_letters + string.digits, k=16))
|
||||
|
||||
def compute_digest_response(username: str, password: str, realm: str, nonce: str, uri: str, method: str = 'REGISTER') -> str:
|
||||
"""Compute SIP Digest authentication response"""
|
||||
ha1 = hashlib.md5(f"{username}:{realm}:{password}".encode()).hexdigest()
|
||||
ha2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest()
|
||||
response = hashlib.md5(f"{ha1}:{nonce}:{ha2}".encode()).hexdigest()
|
||||
return response
|
||||
|
||||
def create_register_with_auth(target_host: str, target_port: int, extension: str,
|
||||
password: str, from_ip: str, realm: str, nonce: str) -> bytes:
|
||||
"""Create an authenticated SIP REGISTER request"""
|
||||
call_id = generate_call_id()
|
||||
branch = generate_branch()
|
||||
tag = ''.join(random.choices(string.digits, k=8))
|
||||
uri = f"sip:{target_host}:{target_port}"
|
||||
|
||||
response = compute_digest_response(extension, password, realm, nonce, uri)
|
||||
|
||||
request = f"""REGISTER sip:{target_host}:{target_port} SIP/2.0\r
|
||||
Via: SIP/2.0/UDP {from_ip}:5060;branch={branch}\r
|
||||
Max-Forwards: 70\r
|
||||
From: <sip:{extension}@{target_host}>;tag={tag}\r
|
||||
To: <sip:{extension}@{target_host}>\r
|
||||
Call-ID: {call_id}@{from_ip}\r
|
||||
CSeq: 2 REGISTER\r
|
||||
Contact: <sip:{extension}@{from_ip}:5060>\r
|
||||
Authorization: Digest username="{extension}",realm="{realm}",nonce="{nonce}",uri="{uri}",response="{response}",algorithm=MD5\r
|
||||
Expires: 3600\r
|
||||
User-Agent: ValidClient/1.0\r
|
||||
Content-Length: 0\r
|
||||
\r
|
||||
"""
|
||||
return request.encode()
|
||||
|
||||
def create_initial_register(target_host: str, target_port: int, extension: str, from_ip: str) -> bytes:
|
||||
"""Create initial REGISTER without auth (to get challenge)"""
|
||||
call_id = generate_call_id()
|
||||
branch = generate_branch()
|
||||
tag = ''.join(random.choices(string.digits, k=8))
|
||||
|
||||
request = f"""REGISTER sip:{target_host}:{target_port} SIP/2.0\r
|
||||
Via: SIP/2.0/UDP {from_ip}:5060;branch={branch}\r
|
||||
Max-Forwards: 70\r
|
||||
From: <sip:{extension}@{target_host}>;tag={tag}\r
|
||||
To: <sip:{extension}@{target_host}>\r
|
||||
Call-ID: {call_id}@{from_ip}\r
|
||||
CSeq: 1 REGISTER\r
|
||||
Contact: <sip:{extension}@{from_ip}:5060>\r
|
||||
Expires: 3600\r
|
||||
User-Agent: ValidClient/1.0\r
|
||||
Content-Length: 0\r
|
||||
\r
|
||||
"""
|
||||
return request.encode()
|
||||
|
||||
def parse_www_authenticate(response: str) -> tuple:
|
||||
"""Parse WWW-Authenticate header to get realm and nonce"""
|
||||
for line in response.split('\r\n'):
|
||||
if line.lower().startswith('www-authenticate:'):
|
||||
# Extract realm
|
||||
realm_start = line.find('realm="') + 7
|
||||
realm_end = line.find('"', realm_start)
|
||||
realm = line[realm_start:realm_end] if realm_start > 6 else ''
|
||||
|
||||
# Extract nonce
|
||||
nonce_start = line.find('nonce="') + 7
|
||||
nonce_end = line.find('"', nonce_start)
|
||||
nonce = line[nonce_start:nonce_end] if nonce_start > 6 else ''
|
||||
|
||||
return realm, nonce
|
||||
return '', ''
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Valid SIP Registration Test')
|
||||
parser.add_argument('target', help='Target host (Caddy proxy)')
|
||||
parser.add_argument('-p', '--port', type=int, default=5060, help='Target port')
|
||||
parser.add_argument('-e', '--extension', default='100', help='Extension to register')
|
||||
parser.add_argument('-s', '--secret', default='password123', help='Extension password')
|
||||
parser.add_argument('-r', '--repeat', type=int, default=1, help='Number of registration cycles')
|
||||
args = parser.parse_args()
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.connect((args.target, args.port))
|
||||
from_ip = sock.getsockname()[0]
|
||||
sock.close()
|
||||
|
||||
print(f"[*] Valid Registration Test")
|
||||
print(f"[*] Target: {args.target}:{args.port}")
|
||||
print(f"[*] Extension: {args.extension}")
|
||||
print(f"[*] Source IP: {from_ip}")
|
||||
print()
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
target = (args.target, args.port)
|
||||
|
||||
for cycle in range(args.repeat):
|
||||
print(f"[*] Registration cycle {cycle + 1}/{args.repeat}")
|
||||
|
||||
# Send initial REGISTER
|
||||
request = create_initial_register(args.target, args.port, args.extension, from_ip)
|
||||
sock.sendto(request, target)
|
||||
|
||||
try:
|
||||
sock.settimeout(5.0)
|
||||
response, _ = sock.recvfrom(4096)
|
||||
response = response.decode()
|
||||
except socket.timeout:
|
||||
print("[!] TIMEOUT - Connection may be blocked")
|
||||
continue
|
||||
|
||||
first_line = response.split('\r\n')[0]
|
||||
print(f"[*] Initial response: {first_line}")
|
||||
|
||||
if '401' in response or '407' in response:
|
||||
# Parse auth challenge
|
||||
realm, nonce = parse_www_authenticate(response)
|
||||
print(f"[*] Got challenge: realm={realm}")
|
||||
|
||||
# Send authenticated REGISTER
|
||||
auth_request = create_register_with_auth(
|
||||
args.target, args.port, args.extension, args.secret,
|
||||
from_ip, realm, nonce
|
||||
)
|
||||
sock.sendto(auth_request, target)
|
||||
|
||||
try:
|
||||
response, _ = sock.recvfrom(4096)
|
||||
response = response.decode()
|
||||
first_line = response.split('\r\n')[0]
|
||||
print(f"[*] Auth response: {first_line}")
|
||||
|
||||
if '200' in response:
|
||||
print("[+] SUCCESS - Registration completed!")
|
||||
else:
|
||||
print(f"[!] Failed: {first_line}")
|
||||
except socket.timeout:
|
||||
print("[!] TIMEOUT after auth - may be blocked")
|
||||
elif '200' in response:
|
||||
print("[+] SUCCESS - Already registered or no auth required")
|
||||
else:
|
||||
print(f"[?] Unexpected response: {first_line}")
|
||||
|
||||
if cycle < args.repeat - 1:
|
||||
time.sleep(2)
|
||||
|
||||
sock.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue