feat: apk-reverse Agent Skill
Progressive-disclosure skill for Android APK reverse engineering: recon, ad removal, membership limits, dex surgical patching, repack/sign, runtime data, Frida, environment setup, and a hard-won failure catalogue.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Probe an app's HTTP API the way the app does.
|
||||
|
||||
Purpose-built for the common frustration: your request gets 403 while the app
|
||||
works fine. Almost always the app sends a header you are not sending (frequently
|
||||
one custom header), or your egress IP differs from the device's.
|
||||
|
||||
Usage
|
||||
-----
|
||||
python probe_api.py --base https://api.example.com --path /health
|
||||
python probe_api.py --base https://api.example.com --path /adverts \
|
||||
--param position=banner --header 'X-App-Name: myapp'
|
||||
python probe_api.py --base https://api.example.com \
|
||||
--path "/items?page=1" --raw
|
||||
|
||||
Options
|
||||
-------
|
||||
--base Base URL (required)
|
||||
--path Path, may already contain a query string (required)
|
||||
--param k=v Repeatable; appended as query parameters
|
||||
--header k=v Repeatable; sent as-is. Always send the app's real headers.
|
||||
--token T Convenience: adds 'Authorization: Bearer T'
|
||||
--ua UA User-Agent (default: okhttp/4.12.0)
|
||||
--no-proxy Ignore environment proxy settings (recommended)
|
||||
--show Print the first N bytes of the body (default 800)
|
||||
|
||||
Interpreting results
|
||||
--------------------
|
||||
200 + data -> alive; read the envelope shape
|
||||
200 + empty list -> valid input, nothing to return. Do NOT assume "ad removed":
|
||||
it may simply be that no item is configured.
|
||||
400 + message -> input is validated; the message often lists valid values
|
||||
401/403, no creds -> auth-gated (server-side ownership)
|
||||
401/403, forged -> token is verified server-side; client patching cannot mint one
|
||||
403 + HTML body -> a WAF/CDN refused your *request shape* or egress, not the endpoint
|
||||
404 -> exact path mismatch
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
def build_opener(no_proxy):
|
||||
if no_proxy:
|
||||
return urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
return urllib.request.build_opener()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(add_help=True)
|
||||
ap.add_argument('--base', required=True)
|
||||
ap.add_argument('--path', required=True)
|
||||
ap.add_argument('--param', action='append', default=[])
|
||||
ap.add_argument('--header', action='append', default=[])
|
||||
ap.add_argument('--token')
|
||||
ap.add_argument('--ua', default='okhttp/4.12.0')
|
||||
ap.add_argument('--no-proxy', action='store_true')
|
||||
ap.add_argument('--show', type=int, default=800)
|
||||
a = ap.parse_args()
|
||||
|
||||
url = a.base.rstrip('/') + a.path
|
||||
if a.param:
|
||||
params = []
|
||||
for p in a.param:
|
||||
if '=' not in p:
|
||||
print('[FAIL] --param expects k=v, got %r' % p)
|
||||
return 2
|
||||
k, v = p.split('=', 1)
|
||||
params.append((k, v))
|
||||
sep = '&' if '?' in url else '?'
|
||||
url += sep + urllib.parse.urlencode(params)
|
||||
|
||||
headers = {
|
||||
'User-Agent': a.ua,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
for h in a.header:
|
||||
if ':' not in h:
|
||||
print('[FAIL] --header expects "Name: value", got %r' % h)
|
||||
return 2
|
||||
k, v = h.split(':', 1)
|
||||
headers[k.strip()] = v.strip()
|
||||
if a.token:
|
||||
headers['Authorization'] = 'Bearer %s' % a.token
|
||||
|
||||
print('[req] GET %s' % url)
|
||||
for k, v in headers.items():
|
||||
shown = v if k.lower() != 'authorization' else v[:24] + '...'
|
||||
print(' %s: %s' % (k, shown))
|
||||
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
op = build_opener(a.no_proxy)
|
||||
try:
|
||||
with op.open(req, timeout=30) as r:
|
||||
body = r.read().decode('utf-8', 'replace')
|
||||
print('[res] %s %s' % (r.status, r.headers.get('Content-Type', '')))
|
||||
print(body[:a.show])
|
||||
if body.strip().startswith(('{', '[')):
|
||||
try:
|
||||
parsed = json.loads(body)
|
||||
if isinstance(parsed, dict) and 'data' in parsed:
|
||||
d = parsed['data']
|
||||
if isinstance(d, dict) and 'list' in d:
|
||||
print('[shape] data.list length=%s' % len(d['list'] or []))
|
||||
except Exception:
|
||||
pass
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode('utf-8', 'replace')
|
||||
print('[res] HTTP %s' % e.code)
|
||||
print(body[:a.show])
|
||||
except Exception as e:
|
||||
print('[ERR] %s' % e)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user