ipspoof 2.0.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.
ipspoof/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """ipspoof — HTTP header/IP allowlist bypass discovery tool."""
2
+
3
+ __version__ = "2.0.0"
4
+ __author__ = "Exript (S. Engin ÖZLEM)"
5
+ __license__ = "MIT"
6
+
7
+ from .cli import main
8
+
9
+ __all__ = ["main", "__version__"]
ipspoof/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
ipspoof/cli.py ADDED
@@ -0,0 +1,613 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ipspoof — HTTP header/IP allowlist bypass discovery tool.
4
+
5
+ Authorized security testing only. Use against systems you own or have
6
+ explicit permission to test.
7
+
8
+ Quick start:
9
+ ipspoof -u http://target/login.php --follow
10
+ ipspoof -u http://target/ --body-regex "dashboard|welcome"
11
+ ipspoof -i
12
+ """
13
+
14
+ import argparse
15
+ import hashlib
16
+ import json as jsonlib
17
+ import re
18
+ import socket
19
+ import sys
20
+ import threading
21
+ import time
22
+ from concurrent.futures import ThreadPoolExecutor, as_completed
23
+ from dataclasses import dataclass
24
+
25
+ import requests
26
+ import urllib3
27
+
28
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
29
+
30
+ # ── ANSI ─────────────────────────────────────────────────
31
+ G = "\033[92m"; R = "\033[91m"; Y = "\033[93m"; C = "\033[96m"
32
+ DIM = "\033[2m"; B = "\033[1m"; RST = "\033[0m"
33
+
34
+ # ═════════════════════════════════════════════════════════
35
+ # HEADER WORDLIST
36
+ # ═════════════════════════════════════════════════════════
37
+ HEADERS = sorted(set([
38
+ "X-Forwarded-For", "X_FORWARDED_FOR", "X-Forwarded", "X_FORWARDED",
39
+ "X-Forwarded-For-Original", "X-Forwarded-For-IP", "X_FORWARDED_FOR_IP",
40
+ "X-Forwarded-By", "X-Forwarded-Server", "X-Forwarded-Host",
41
+ "Forwarded", "FORWARDED", "Forwarded-For", "FORWARDED-FOR",
42
+ "Forwarded-For-IP", "FORWARDED-FOR-IP", "Forward-For", "FORWARD_FOR",
43
+ "X-Real-IP", "X_REAL_IP", "X-Real-IP-Original", "Real-IP",
44
+ "X-Client-IP", "CLIENT_IP", "Client-IP", "HTTP-CLIENT-IP",
45
+ "X-Originating-IP", "X-Original-IP", "X-Original-Remote-Addr",
46
+ "X-Originally-Forwarded-For", "X-From-IP", "X-From",
47
+ "X-Remote-IP", "X-Remote-Addr", "PC_REMOTE_ADDR", "HTTP-PC-REMOTE-ADDR",
48
+ "Remote-Addr", "REMOTE_ADDR",
49
+ "X-True-Client-IP", "True-Client-IP", "X_True-Client-IP",
50
+ "CF-Connecting-IP", "CF_CONNECTING_IP", "True-Client-IP-CF",
51
+ "Fastly-Client-IP", "X-Azure-ClientIP", "X-Appengine-Remote-Addr",
52
+ "X-Envoy-External-Address",
53
+ "X-ProxyUser-IP", "X-ProxyUser-Ip", "X-Proxy-IP", "X-ProxyMesh-IP",
54
+ "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP-XROXY-CONNECTION",
55
+ "XROXY_CONNECTION", "X-Proxy-Connection", "Proxy-Connection",
56
+ "PROXY_CONNECTION", "CONNECT_VIA_IP", "XPROXY",
57
+ "XONNECTION", "X_Connection",
58
+ "Via", "VIA", "HTTP-VIA", "X-BlueCoat-Via",
59
+ "X-Original-URL", "X-Override-URL", "X-Rewrite-URL", "X-Original-Host",
60
+ "X-Host", "X-Gateway-Host", "X-Backend-Host",
61
+ "X-Ip", "Source-IP", "COMING_FROM", "X_COMING_FROM",
62
+ "X-Delegating-Remote-Host", "X_DELEGATE_REMOTE_HOST",
63
+ "X-Imforwards", "X_IMFORWARDS", "HTTP-X-IMFORWARDS",
64
+ "X-Locking", "X_LOCKING", "X-Looking", "X_LOOKING",
65
+ "X-Cluster-Client-IP", "X_CLUSTER_CLIENT_IP", "Cluster-Client-IP",
66
+ "HTTP-X-FORWARDED-FOR-IP", "HTTP-FORWARDED-FOR-IP", "HTTP-FORWARDED-FOR",
67
+ "Z-Forwarded-For",
68
+ "X-Cache-Info", "ZCACHE_CONTROL", "CACHE_INFO",
69
+ "Pragma", "PRAGMA", "Proxy-Authorization", "PROXY_AUTHORIZATION",
70
+ "X-Server-IP", "X-Forwarded-Server-IP",
71
+ "X-Forwarded-Proto", "Front-End-Https",
72
+ ]))
73
+
74
+ TRUSTED_IPS = [
75
+ "127.0.0.1", "localhost", "0.0.0.0", "::1",
76
+ "10.0.0.1", "10.10.10.1", "10.10.10.10",
77
+ "172.16.0.1", "192.168.0.1", "192.168.1.1",
78
+ "169.254.169.254",
79
+ ]
80
+
81
+ CHAINABLE_KEYWORDS = ("forwarded", "real", "client", "remote", "originating",
82
+ "x-ip", "proxy", "via")
83
+
84
+
85
+ # ═════════════════════════════════════════════════════════
86
+ # HELPERS
87
+ # ═════════════════════════════════════════════════════════
88
+ @dataclass
89
+ class Result:
90
+ header: str
91
+ value: str
92
+ status: int
93
+ size: int
94
+ reason: str
95
+
96
+
97
+ _thread_local = threading.local()
98
+
99
+
100
+ def get_session(cookies=None):
101
+ if not hasattr(_thread_local, "session"):
102
+ s = requests.Session()
103
+ if cookies:
104
+ for k, v in cookies.items():
105
+ s.cookies.set(k, v)
106
+ _thread_local.session = s
107
+ return _thread_local.session
108
+
109
+
110
+ class RateLimiter:
111
+ def __init__(self, rate):
112
+ self.rate = rate
113
+ self.interval = 1.0 / rate if rate > 0 else 0.0
114
+ self.last = 0.0
115
+ self.lock = threading.Lock()
116
+
117
+ def wait(self):
118
+ if self.rate <= 0:
119
+ return
120
+ with self.lock:
121
+ now = time.time()
122
+ delta = self.interval - (now - self.last)
123
+ if delta > 0:
124
+ time.sleep(delta)
125
+ self.last = time.time()
126
+ else:
127
+ self.last = now
128
+
129
+
130
+ class ProxyRotator:
131
+ def __init__(self, proxies=None, use_tor=False, tor_new_every=0):
132
+ self.proxies = list(proxies or [])
133
+ self.use_tor = use_tor
134
+ self.tor_new_every = tor_new_every
135
+ self.counter = 0
136
+ self.index = 0
137
+ self.lock = threading.Lock()
138
+
139
+ def next(self):
140
+ with self.lock:
141
+ self.counter += 1
142
+ if self.use_tor:
143
+ if self.tor_new_every and self.counter % self.tor_new_every == 0:
144
+ self._signal_newnym()
145
+ return {"http": "socks5h://127.0.0.1:9050",
146
+ "https": "socks5h://127.0.0.1:9050"}
147
+ if not self.proxies:
148
+ return None
149
+ p = self.proxies[self.index % len(self.proxies)]
150
+ self.index += 1
151
+ return {"http": p, "https": p}
152
+
153
+ def _signal_newnym(self):
154
+ try:
155
+ s = socket.create_connection(("127.0.0.1", 9051), timeout=3)
156
+ s.sendall(b'AUTHENTICATE ""\r\nSIGNAL NEWNYM\r\nQUIT\r\n')
157
+ s.close()
158
+ print(f"\n{Y}[*] Tor identity rotated (NEWNYM){RST}")
159
+ except Exception as e:
160
+ print(f"\n{Y}[!] Tor NEWNYM failed: {e}{RST}")
161
+
162
+
163
+ def probe(url, headers, timeout, follow, cookies, proxy, verify,
164
+ method="GET", data=None, json_data=None, limiter=None):
165
+ if limiter:
166
+ limiter.wait()
167
+ session = get_session(cookies)
168
+ kwargs = dict(headers=headers, timeout=timeout,
169
+ allow_redirects=follow, proxies=proxy, verify=verify)
170
+ try:
171
+ m = method.upper()
172
+ if m == "GET":
173
+ r = session.get(url, **kwargs)
174
+ elif m == "POST":
175
+ r = session.post(url, json=json_data, data=data, **kwargs)
176
+ elif m == "PUT":
177
+ r = session.put(url, json=json_data, data=data, **kwargs)
178
+ elif m == "PATCH":
179
+ r = session.patch(url, json=json_data, data=data, **kwargs)
180
+ elif m == "DELETE":
181
+ r = session.delete(url, **kwargs)
182
+ elif m == "HEAD":
183
+ r = session.head(url, **kwargs)
184
+ else:
185
+ r = session.request(m, url, json=json_data, data=data, **kwargs)
186
+ content = r.content
187
+ return r.status_code, len(content), hashlib.sha256(content).hexdigest(), content, None
188
+ except requests.exceptions.RequestException as e:
189
+ return None, None, None, None, str(e)
190
+
191
+
192
+ def is_anomaly(base, current, args):
193
+ b_status, b_size, b_hash, b_body = base
194
+ status, size, h, body, err = current
195
+ if status is None:
196
+ return None
197
+
198
+ reasons = []
199
+
200
+ if args.body_regex:
201
+ try:
202
+ now_match = re.search(args.body_regex, body.decode("utf-8", "ignore"))
203
+ base_match = re.search(args.body_regex, b_body.decode("utf-8", "ignore"))
204
+ if now_match and not base_match:
205
+ reasons.append("regex")
206
+ except re.error as e:
207
+ print(f"{R}[!] Regex error: {e}{RST}")
208
+ sys.exit(1)
209
+ return reasons or None
210
+
211
+ if status != b_status:
212
+ reasons.append(f"status({b_status}->{status})")
213
+ elif b_size == 0 and size > 0:
214
+ reasons.append(f"size(0->{size})")
215
+ elif b_size > 0 and abs(size - b_size) / max(b_size, size, 1) > args.size_tol:
216
+ reasons.append(f"size({b_size}->{size})")
217
+
218
+ if args.body_hash and h != b_hash:
219
+ reasons.append("hash")
220
+
221
+ return reasons or None
222
+
223
+
224
+ # ═════════════════════════════════════════════════════════
225
+ # BASELINE
226
+ # ═════════════════════════════════════════════════════════
227
+ def baseline(url, args, proxy, cookies):
228
+ print(f"{C}[*] Fetching baseline: {url}{RST}")
229
+ status, size, h, body, err = probe(
230
+ url, {}, args.timeout, args.follow, cookies, proxy, False,
231
+ method=args.method, data=args.data, json_data=args.json_data,
232
+ )
233
+ if err:
234
+ print(f"{R}[!] Baseline error: {err}{RST}")
235
+ sys.exit(1)
236
+ print(f"{C}[*] Baseline -> status={status} size={size} hash={h[:12]}{RST}\n")
237
+ return status, size, h, body
238
+
239
+
240
+ # ═════════════════════════════════════════════════════════
241
+ # PHASE 1 — header discovery
242
+ # ═════════════════════════════════════════════════════════
243
+ def phase1(url, base, args, proxy_rotator, cookies):
244
+ print(f"{C}[*] Phase 1: Header discovery ({len(HEADERS)} headers x {len(args.trusted_ips)} IPs){RST}")
245
+
246
+ tasks = [(h, ip) for ip in args.trusted_ips for h in HEADERS]
247
+ total = len(tasks)
248
+ hits = []
249
+ done = 0
250
+ t0 = time.time()
251
+
252
+ def worker(h, ip):
253
+ proxy = proxy_rotator.next()
254
+ res = probe(url, {h: ip}, args.timeout, args.follow, cookies, proxy, False,
255
+ method=args.method, data=args.data, json_data=args.json_data,
256
+ limiter=args._limiter)
257
+ return h, ip, res
258
+
259
+ with ThreadPoolExecutor(max_workers=args.threads) as ex:
260
+ futs = [ex.submit(worker, h, ip) for h, ip in tasks]
261
+ for f in as_completed(futs):
262
+ h, ip, res = f.result()
263
+ done += 1
264
+ if done % 50 == 0 or done == total:
265
+ print(f"{DIM} ... {done}/{total}{RST}", end="\r")
266
+ reasons = is_anomaly(base, res, args)
267
+ if reasons:
268
+ r = Result(h, ip, res[0], res[1], ",".join(reasons))
269
+ hits.append(r)
270
+ print(f"\n{G}[+] HIT: {h}: {ip} -> status={res[0]} size={res[1]} [{r.reason}]{RST}")
271
+
272
+ print(f"\n{C}[*] Phase 1 complete in {time.time()-t0:.1f}s. {len(hits)} anomalies.{RST}\n")
273
+ return hits
274
+
275
+
276
+ # ═════════════════════════════════════════════════════════
277
+ # PHASE 2 — IP fuzz (+ optional chain)
278
+ # ═════════════════════════════════════════════════════════
279
+ def build_test_values(header, ip, chain):
280
+ values = [ip]
281
+ if chain and any(k in header.lower() for k in CHAINABLE_KEYWORDS):
282
+ values += [f"1.2.3.4, {ip}", f"{ip}, 1.2.3.4", f"{ip}, {ip}", f"for={ip}"]
283
+ return values
284
+
285
+
286
+ def phase2(url, hits, base, args, proxy_rotator, cookies):
287
+ if not args.ip_pattern:
288
+ print(f"{Y}[!] --ip-pattern not set, skipping Phase 2.{RST}")
289
+ return []
290
+
291
+ ip_range = parse_range(args.ip_range)
292
+ headers_to_try = sorted({h.header for h in hits})
293
+ print(f"{C}[*] Phase 2: IP fuzz ({len(headers_to_try)} headers x {len(ip_range)} IPs "
294
+ f"{'+ chain' if args.chain else ''}){RST}")
295
+ print(f"{DIM} Headers: {', '.join(headers_to_try)}{RST}")
296
+
297
+ tasks = []
298
+ for h in headers_to_try:
299
+ for n in ip_range:
300
+ ip = args.ip_pattern.format(n=n)
301
+ for v in build_test_values(h, ip, args.chain):
302
+ tasks.append((h, v, ip))
303
+
304
+ results = []
305
+ total = len(tasks)
306
+ done = 0
307
+ t0 = time.time()
308
+
309
+ def worker(h, value, ip):
310
+ proxy = proxy_rotator.next()
311
+ res = probe(url, {h: value}, args.timeout, args.follow, cookies, proxy, False,
312
+ method=args.method, data=args.data, json_data=args.json_data,
313
+ limiter=args._limiter)
314
+ return h, value, ip, res
315
+
316
+ with ThreadPoolExecutor(max_workers=args.threads) as ex:
317
+ futs = [ex.submit(worker, h, v, ip) for h, v, ip in tasks]
318
+ for f in as_completed(futs):
319
+ h, value, ip, res = f.result()
320
+ done += 1
321
+ if done % 50 == 0 or done == total:
322
+ print(f"{DIM} ... {done}/{total}{RST}", end="\r")
323
+ reasons = is_anomaly(base, res, args)
324
+ if reasons:
325
+ r = Result(h, value, res[0], res[1], ",".join(reasons))
326
+ results.append(r)
327
+ print(f"\n{G}[+] HIT: {h}: {value} -> status={res[0]} size={res[1]} [{r.reason}]{RST}")
328
+
329
+ print(f"\n{C}[*] Phase 2 complete in {time.time()-t0:.1f}s. {len(results)} anomalies.{RST}\n")
330
+ return results
331
+
332
+
333
+ def parse_range(s):
334
+ if "-" in s:
335
+ a, b = s.split("-", 1)
336
+ return list(range(int(a), int(b) + 1))
337
+ return [int(x) for x in s.split(",") if x.strip()]
338
+
339
+
340
+ # ═════════════════════════════════════════════════════════
341
+ # OUTPUT
342
+ # ═════════════════════════════════════════════════════════
343
+ def save_output(path, base, phase1_hits, phase2_hits, args):
344
+ data = {
345
+ "url": args.url,
346
+ "method": args.method,
347
+ "baseline": {"status": base[0], "size": base[1], "hash": base[2]},
348
+ "phase1": [h.__dict__ for h in phase1_hits],
349
+ "phase2": [h.__dict__ for h in phase2_hits],
350
+ "config": {
351
+ "follow": args.follow,
352
+ "threads": args.threads,
353
+ "chain": args.chain,
354
+ "body_regex": args.body_regex,
355
+ "body_hash": args.body_hash,
356
+ "tor": args.tor,
357
+ "rate": args.rate,
358
+ },
359
+ }
360
+ with open(path, "w") as f:
361
+ jsonlib.dump(data, f, indent=2)
362
+ print(f"{G}[+] Results saved: {path}{RST}")
363
+
364
+
365
+ # ═════════════════════════════════════════════════════════
366
+ # INTERACTIVE MODE
367
+ # ═════════════════════════════════════════════════════════
368
+ def interactive_setup(base_args):
369
+ print(f"{C}=== Interactive Mode ==={RST}")
370
+ url = input("Target URL: ").strip()
371
+ if not url:
372
+ print(f"{R}[!] URL is required.{RST}")
373
+ sys.exit(1)
374
+
375
+ threads = input("Threads [30]: ").strip() or "30"
376
+ follow = input("Follow redirects? [y/N]: ").strip().lower() == "y"
377
+ method = (input("Method [GET]: ").strip() or "GET").upper()
378
+
379
+ data = None
380
+ json_data = None
381
+ if method in ("POST", "PUT", "PATCH"):
382
+ d = input("POST data (key=val&key2=val2, empty to skip): ").strip()
383
+ if d:
384
+ data = d
385
+
386
+ cookie = input("Cookie (empty to skip): ").strip() or None
387
+ body_regex = input("Body regex (empty to skip): ").strip() or None
388
+
389
+ advanced = input("Advanced options (Tor/proxy/rate)? [y/N]: ").strip().lower() == "y"
390
+ proxy_file = None
391
+ tor = False
392
+ rate = 0
393
+ tor_new_every = 0
394
+ if advanced:
395
+ pf = input("Proxy file (empty to skip): ").strip()
396
+ if pf:
397
+ proxy_file = pf
398
+ tor = input("Use Tor? [y/N]: ").strip().lower() == "y"
399
+ if tor:
400
+ tne = input("Rotate Tor identity every N requests [0=never]: ").strip() or "0"
401
+ tor_new_every = int(tne)
402
+ rate = int(input("Rate limit (req/s, 0=unlimited): ").strip() or "0")
403
+
404
+ phase2_choice = input("Run Phase 2 (IP fuzz)? [y/N]: ").strip().lower() == "y"
405
+ ip_pattern = "10.10.10.{n}"
406
+ ip_range = "1-254"
407
+ if phase2_choice:
408
+ ip_pattern = input("IP pattern [10.10.10.{n}]: ").strip() or ip_pattern
409
+ ip_range = input("IP range [1-254]: ").strip() or "1-254"
410
+
411
+ base_args.url = url
412
+ base_args.threads = int(threads)
413
+ base_args.follow = follow
414
+ base_args.method = method
415
+ base_args.data = data
416
+ base_args.json_data = json_data
417
+ base_args.cookie = cookie
418
+ base_args.body_regex = body_regex
419
+ base_args.proxy_file = proxy_file
420
+ base_args.tor = tor
421
+ base_args.tor_new_every = tor_new_every
422
+ base_args.rate = rate
423
+ base_args.phase2 = phase2_choice
424
+ base_args.ip_pattern = ip_pattern
425
+ base_args.ip_range = ip_range
426
+ return base_args
427
+
428
+
429
+ # ═════════════════════════════════════════════════════════
430
+ # MAIN
431
+ # ═════════════════════════════════════════════════════════
432
+ EPILOG = f"""
433
+ {B}ADVANCED FEATURES (opt-in):{RST}
434
+ --proxy-file FILE Rotate through a list of proxies
435
+ --tor Route through Tor SOCKS5 (127.0.0.1:9050)
436
+ --tor-new-every N Rotate Tor identity every N requests (control port 9051)
437
+ --rate N Max N requests/second (avoid bans)
438
+ --chain XFF chain variations (1.2.3.4, <ip> / <ip>, 1.2.3.4)
439
+ --body-hash Treat different body hash as HIT
440
+ --body-regex PATTERN Only treat body regex match as HIT
441
+ --size-tol X Size difference tolerance (default 0.05 = 5%)
442
+ -o/--output FILE Save results as JSON
443
+
444
+ {B}EXAMPLES:{RST}
445
+ # Basic
446
+ ipspoof -u http://target/login.php --follow
447
+
448
+ # POST login attempt
449
+ ipspoof -u http://target/login.php -X POST -d "user=a&pass=b"
450
+
451
+ # Precise detection: only pages containing dashboard/welcome
452
+ ipspoof -u http://target/ --body-regex "dashboard|welcome"
453
+
454
+ # Tor with periodic identity rotation
455
+ ipspoof -u http://target/ --tor --tor-new-every 10
456
+
457
+ # Proxy list + rate-limit
458
+ ipspoof -u http://target/ --proxy-file proxies.txt --rate 30
459
+
460
+ # Interactive
461
+ ipspoof -i
462
+ """
463
+
464
+
465
+ def build_argparser():
466
+ ap = argparse.ArgumentParser(
467
+ prog="ipspoof",
468
+ description="HTTP header/IP allowlist bypass discovery tool for authorized security testing.",
469
+ formatter_class=argparse.RawDescriptionHelpFormatter,
470
+ epilog=EPILOG,
471
+ )
472
+ ap.add_argument("-u", "--url", help="Target URL (required unless -i)")
473
+ ap.add_argument("-i", "--interactive", action="store_true", help="Interactive mode")
474
+ ap.add_argument("-t", "--threads", type=int, default=30, help="Concurrent threads (default 30)")
475
+ ap.add_argument("--timeout", type=float, default=8.0, help="Request timeout (default 8s)")
476
+ ap.add_argument("--follow", action="store_true", help="Follow redirects")
477
+ ap.add_argument("-c", "--cookie", help="Cookie string")
478
+
479
+ ap.add_argument("-X", "--method", default="GET",
480
+ help="HTTP method (GET/POST/PUT/... — default GET)")
481
+ ap.add_argument("-d", "--data", help="Form data (for POST)")
482
+ ap.add_argument("--json", dest="json_data", help="JSON body (for POST)")
483
+ ap.add_argument("-H", "--header", action="append", default=[],
484
+ help="Extra static header (repeatable: -H 'A: b')")
485
+
486
+ ap.add_argument("--proxy", help="Single proxy (http://127.0.0.1:8080)")
487
+ ap.add_argument("--proxy-file", help="Proxy list file (one per line)")
488
+ ap.add_argument("--tor", action="store_true", help="Route through Tor SOCKS5")
489
+ ap.add_argument("--tor-new-every", type=int, default=0,
490
+ help="Rotate Tor identity every N requests")
491
+
492
+ ap.add_argument("--rate", type=int, default=0, help="Max req/s (0=unlimited)")
493
+
494
+ ap.add_argument("--body-regex", help="Only treat body regex match as HIT")
495
+ ap.add_argument("--body-hash", action="store_true", help="Treat hash change as HIT")
496
+ ap.add_argument("--size-tol", type=float, default=0.05, help="Size tolerance (default 0.05)")
497
+ ap.add_argument("--chain", action="store_true", help="Try XFF chain variations")
498
+
499
+ ap.add_argument("--ip", action="append", default=None,
500
+ help="IPs to try in Phase 1 (repeatable)")
501
+ ap.add_argument("--phase2", action="store_true", help="Run IP fuzz phase")
502
+ ap.add_argument("--ip-pattern", default="10.10.10.{n}", help="Phase 2 IP pattern")
503
+ ap.add_argument("--ip-range", default="1-254", help="Phase 2 IP range")
504
+ ap.add_argument("--show-all", action="store_true", help="Debug: show all responses")
505
+ ap.add_argument("-o", "--output", help="Save results as JSON file")
506
+ return ap
507
+
508
+
509
+ def main(argv=None):
510
+ ap = build_argparser()
511
+ args = ap.parse_args(argv)
512
+
513
+ # Interactive only when explicitly requested
514
+ if args.interactive:
515
+ args = interactive_setup(args)
516
+ elif not args.url:
517
+ ap.print_help()
518
+ sys.exit(2)
519
+
520
+ print(f"{C}┌──────────────────────────────────────────────┐")
521
+ print(f"│ ipspoof v2.0.0 — header/IP allowlist bypass │")
522
+ print(f"└──────────────────────────────────────────────┘{RST}")
523
+ print(f" URL : {args.url}")
524
+ print(f" Method : {args.method}")
525
+ print(f" Threads : {args.threads}")
526
+ print(f" Follow : {args.follow}")
527
+ if args.body_regex:
528
+ print(f" Body regex: {args.body_regex}")
529
+ if args.body_hash:
530
+ print(f" Body hash : on")
531
+ if args.chain:
532
+ print(f" Chain : on")
533
+ if args.proxy_file:
534
+ print(f" Proxy file: {args.proxy_file}")
535
+ if args.tor:
536
+ print(f" Tor : on (rotate every {args.tor_new_every})")
537
+ if args.rate:
538
+ print(f" Rate : {args.rate} req/s")
539
+ print()
540
+
541
+ # Proxy setup
542
+ proxies_list = []
543
+ if args.proxy_file:
544
+ try:
545
+ with open(args.proxy_file) as f:
546
+ proxies_list = [l.strip() for l in f if l.strip() and not l.startswith("#")]
547
+ print(f"{G}[+] Loaded {len(proxies_list)} proxies.{RST}")
548
+ except FileNotFoundError:
549
+ print(f"{R}[!] Proxy file not found: {args.proxy_file}{RST}")
550
+ sys.exit(1)
551
+ elif args.proxy:
552
+ proxies_list = [args.proxy]
553
+
554
+ proxy_rotator = ProxyRotator(
555
+ proxies=proxies_list,
556
+ use_tor=args.tor,
557
+ tor_new_every=args.tor_new_every,
558
+ )
559
+
560
+ cookies = {}
561
+ if args.cookie:
562
+ for kv in args.cookie.split(";"):
563
+ if "=" in kv:
564
+ k, v = kv.strip().split("=", 1)
565
+ cookies[k] = v
566
+
567
+ static_headers = {}
568
+ for h in args.header:
569
+ if ":" in h:
570
+ k, v = h.split(":", 1)
571
+ static_headers[k.strip()] = v.strip()
572
+
573
+ args.trusted_ips = args.ip if args.ip else TRUSTED_IPS
574
+ args._limiter = RateLimiter(args.rate) if args.rate > 0 else None
575
+
576
+ if args.json_data:
577
+ try:
578
+ args.json_data = jsonlib.loads(args.json_data)
579
+ except jsonlib.JSONDecodeError:
580
+ print(f"{R}[!] Invalid JSON: {args.json_data}{RST}")
581
+ sys.exit(1)
582
+
583
+ base = baseline(args.url, args, proxy_rotator.next(), cookies)
584
+ args._static_headers = static_headers
585
+
586
+ hits = phase1(args.url, base, args, proxy_rotator, cookies)
587
+ if not hits:
588
+ print(f"{R}[!] No anomalies found.{RST}")
589
+ print(f"{Y} Tips: try --ip with more values, add --follow, "
590
+ f"switch detection with --body-regex/--body-hash.{RST}")
591
+ if args.output:
592
+ save_output(args.output, base, [], [], args)
593
+ return
594
+
595
+ print(f"{C}[*] Phase 1 summary:{RST}")
596
+ for r in hits:
597
+ print(f" {G}{r.header:<30}{RST} value={r.value:<20} "
598
+ f"status={r.status} size={r.size} [{r.reason}]")
599
+
600
+ phase2_hits = []
601
+ if args.phase2:
602
+ phase2_hits = phase2(args.url, hits, base, args, proxy_rotator, cookies)
603
+
604
+ if args.output:
605
+ save_output(args.output, base, hits, phase2_hits, args)
606
+
607
+
608
+ if __name__ == "__main__":
609
+ try:
610
+ main()
611
+ except KeyboardInterrupt:
612
+ print(f"\n{Y}[!] Interrupted by user.{RST}")
613
+ sys.exit(130)
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: ipspoof
3
+ Version: 2.0.0
4
+ Summary: HTTP header/IP allowlist bypass discovery tool for authorized security testing
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourname/ipspoof
8
+ Project-URL: Repository, https://github.com/yourname/ipspoof
9
+ Project-URL: Issues, https://github.com/yourname/ipspoof/issues
10
+ Project-URL: Changelog, https://github.com/yourname/ipspoof/blob/main/CHANGELOG.md
11
+ Keywords: security,pentest,bugbounty,ip-spoof,header-bypass,xff,x-forwarded-for,allowlist
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Security
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: requests>=2.28
28
+ Requires-Dist: urllib3>=1.26
29
+ Provides-Extra: tor
30
+ Requires-Dist: requests[socks]>=2.28; extra == "tor"
31
+ Provides-Extra: dev
32
+ Requires-Dist: build; extra == "dev"
33
+ Requires-Dist: twine; extra == "dev"
34
+ Requires-Dist: pytest; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # ipspoof
38
+
39
+ > HTTP header/IP allowlist bypass discovery tool for authorized security testing.
40
+
41
+ [![PyPI](https://img.shields.io/badge/pypi-v2.0.0-blue)](https://pypi.org/project/ipspoof/)
42
+ [![Python](https://img.shields.io/badge/python-3.8+-blue)](https://www.python.org/)
43
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
44
+ [![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macOS%20%7C%20windows-lightgrey)]()
45
+
46
+ `ipspoof` helps pentesters and bug bounty hunters quickly identify which HTTP header a web application trusts for client-IP-based access control, and which IP range bypasses the allowlist. Common in labs, CTFs, and real-world engagements where you see messages like:
47
+
48
+ ```
49
+ Your IP is not allowed to use this webservice. Only 10.10.10.x is allowed
50
+ <img width="1210" height="477" alt="resim" src="https://github.com/user-attachments/assets/b3868ae3-1499-401d-bb17-a171ddc822b3" />
51
+
52
+ ```
53
+ ![ipspoof demo](docs/images/1.jpeg)
54
+
55
+ ---
56
+
57
+ ## Features
58
+
59
+ - **Header discovery** — tests 100+ client-IP headers (`X-Forwarded-For`, `X-Real-IP`, `Forwarded`, `Client-IP`, `CF-Connecting-IP`, etc.) with a curated set of trusted IP values.
60
+ - **IP fuzzing** — once a working header is found, brute-forces the allowed IP range (e.g. `10.10.10.1-254`).
61
+ - **Smart anomaly detection** — catches bypasses by status change, size change, body hash, or body regex.
62
+ - **XFF chain variations** — `1.2.3.4, <ip>`, `<ip>, 1.2.3.4`, `for=<ip>`.
63
+ - **POST/PUT/PATCH support** — works against login forms and API endpoints.
64
+ - **Proxy rotation** — round-robin through a proxy list.
65
+ - **Tor support** — route through SOCKS5 with automatic identity rotation.
66
+ - **Rate limiting** — avoid bans during brute-force.
67
+ - **JSON output** — machine-readable results for reporting.
68
+ - **Interactive mode** — guided prompts for quick runs.
69
+ ![ipspoof Feature](docs/images/2.jpeg)
70
+ ---
71
+
72
+ ## Installation
73
+
74
+ ### pipx (recommended)
75
+
76
+ ```bash
77
+ pipx install ipspoof
78
+ ```
79
+
80
+ ### pip
81
+
82
+ ```bash
83
+ pip install ipspoof
84
+ ```
85
+
86
+ ### From source
87
+
88
+ ```bash
89
+ git clone https://github.com/Exript/ipspoof.git
90
+ cd ipspoof
91
+ pipx install .
92
+ ```
93
+
94
+ For Tor support:
95
+
96
+ ```bash
97
+ pipx install "ipspoof[tor]"
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Usage
103
+
104
+ ### Basic
105
+
106
+ ```bash
107
+ ipspoof -u http://target/login.php --follow
108
+ ```
109
+
110
+ Output:
111
+
112
+ ![ipspoof Output](docs/images/3.jpeg)
113
+
114
+
115
+ ### Full pipeline (header discovery + IP fuzz)
116
+
117
+ ```bash
118
+ ipspoof -u http://target/login.php \
119
+ --phase2 --ip-pattern "10.10.10.{n}" --ip-range 1-254 \
120
+ --follow
121
+ ```
122
+
123
+ ### POST login attempt
124
+
125
+ ```bash
126
+ ipspoof -u http://target/login.php \
127
+ -X POST -d "username=admin&password=admin" \
128
+ --body-regex "dashboard|welcome" \
129
+ --follow
130
+ ```
131
+
132
+ ### Tor + periodic identity rotation
133
+
134
+ ```bash
135
+ ipspoof -u http://target/ --tor --tor-new-every 10
136
+ ```
137
+
138
+ ### Proxy list + rate limit
139
+
140
+ ```bash
141
+ ipspoof -u http://target/ --proxy-file proxies.txt --rate 30
142
+ ```
143
+
144
+ ### Interactive
145
+
146
+ ```bash
147
+ ipspoof -i
148
+ ```
149
+ Output:
150
+
151
+ ![ipspoof Output](docs/images/4.jpeg)
152
+
153
+ ---
154
+
155
+ ## Options
156
+
157
+ | Flag | Description |
158
+ |------|-------------|
159
+ | `-u, --url` | Target URL |
160
+ | `-i, --interactive` | Interactive mode |
161
+ | `-t, --threads` | Concurrent threads (default 30) |
162
+ | `--timeout` | Request timeout (default 8s) |
163
+ | `--follow` | Follow redirects |
164
+ | `-c, --cookie` | Cookie string |
165
+ | `-X, --method` | HTTP method (default GET) |
166
+ | `-d, --data` | Form data (for POST) |
167
+ | `--json` | JSON body (for POST) |
168
+ | `-H, --header` | Extra static header (repeatable) |
169
+ | `--proxy` | Single proxy |
170
+ | `--proxy-file` | Proxy list file |
171
+ | `--tor` | Route through Tor SOCKS5 |
172
+ | `--tor-new-every N` | Rotate Tor identity every N requests |
173
+ | `--rate N` | Max requests per second |
174
+ | `--body-regex` | Only treat body regex match as HIT |
175
+ | `--body-hash` | Treat hash change as HIT |
176
+ | `--size-tol` | Size tolerance (default 0.05) |
177
+ | `--chain` | Try XFF chain variations |
178
+ | `--ip` | IPs for Phase 1 (repeatable) |
179
+ | `--phase2` | Run IP fuzz phase |
180
+ | `--ip-pattern` | IP pattern (default `10.10.10.{n}`) |
181
+ | `--ip-range` | IP range (default `1-254`) |
182
+ | `-o, --output` | Save results as JSON |
183
+
184
+ ---
185
+
186
+ ## Tor setup (optional)
187
+
188
+ For `--tor-new-every`, add to `/etc/tor/torrc`:
189
+
190
+ ```
191
+ ControlPort 9051
192
+ CookieAuthentication 0
193
+ ```
194
+
195
+ Then:
196
+
197
+ ```bash
198
+ sudo systemctl restart tor
199
+ ```
200
+
201
+ Without this, `--tor` still works but `--tor-new-every` will only print a warning.
202
+
203
+ ---
204
+
205
+ ## How it works
206
+
207
+ 1. **Baseline** — sends a request with no spoof headers. Records status, size, and body hash of the "deny" response.
208
+ 2. **Phase 1** — for each (header, trusted IP) pair, sends a request and compares the response against baseline. Any response with a different status, size, hash, or regex match is flagged as a HIT.
209
+ 3. **Phase 2** — takes the working header(s) from Phase 1 and brute-forces the IP range you specify.
210
+
211
+ The tool never sends anything malicious — it only adds HTTP headers to ordinary requests.
212
+
213
+ ---
214
+
215
+ ## Legal
216
+
217
+ This tool is for **authorized security testing only**. Use it against:
218
+
219
+ - Systems you own
220
+ - Systems you have explicit written permission to test
221
+ - CTF/lab environments designed for testing
222
+
223
+ Unauthorized use against third-party systems is illegal in most jurisdictions. The author takes no responsibility for misuse.
224
+
225
+ ---
226
+
227
+ ## Contributing
228
+
229
+ PRs welcome. Please open an issue first for major changes.
230
+
231
+ ```bash
232
+ git clone https://github.com/Exript/ipspoof.git
233
+ cd ipspoof
234
+ pip install -e ".[dev]"
235
+ ```
236
+
237
+ ---
238
+
239
+ ## License
240
+
241
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,9 @@
1
+ ipspoof/__init__.py,sha256=iOFUjcGpDJfv5So21y_NUeJe_uPYniC1o0p93harwkw,207
2
+ ipspoof/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ ipspoof/cli.py,sha256=bMcgOmU19yTdWH5RXlIFGegRTozQ_mtbjY9nn1wFJyM,24669
4
+ ipspoof-2.0.0.dist-info/licenses/LICENSE,sha256=v2spsd7N1pKFFh2G8wGP_45iwe5S0DYiJzG4im8Rupc,1066
5
+ ipspoof-2.0.0.dist-info/METADATA,sha256=0rbP9FJHNup5IFMw9Ep6SlSMINNhV7O3a_-3pCXuSJ4,6881
6
+ ipspoof-2.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ ipspoof-2.0.0.dist-info/entry_points.txt,sha256=OvqFj-sDC9k0hwjHEqM17EuY3kkiKXpq3eqzCjUIcxM,45
8
+ ipspoof-2.0.0.dist-info/top_level.txt,sha256=ET7cPYu8gD5GTyHcee8cG2G88mArP5M_mGpT2ML5jO4,8
9
+ ipspoof-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ipspoof = ipspoof.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ipspoof