Add local PS5 guide DNS gateway

This commit is contained in:
2026-09-29 23:41:52 +01:00
parent bbb25074c4
commit 0017ea8fd4
8 changed files with 306 additions and 28 deletions
+1
View File
@@ -10,5 +10,6 @@ app/.codex
app/LICENSE app/LICENSE
app/README.md app/README.md
app/serve.py app/serve.py
docs
**/__pycache__ **/__pycache__
**/*.pyc **/*.pyc
+4
View File
@@ -21,6 +21,7 @@ before including them in an image.
- `Dockerfile`: runtime image and payload-integrity checks. - `Dockerfile`: runtime image and payload-integrity checks.
- `compose.yaml`: hardened local deployment. - `compose.yaml`: hardened local deployment.
- `docker/server.py`: static HTTP server and security headers. - `docker/server.py`: static HTTP server and security headers.
- `docker/guide_gateway.py`: restricted DNS plus HTTP/HTTPS User Guide redirect.
- `payloads.sha256`: approved payload digests for the pinned submodule commit. - `payloads.sha256`: approved payload digests for the pinned submodule commit.
- `docs/PS5_SETUP.md`: user and operator guide. - `docs/PS5_SETUP.md`: user and operator guide.
- `docs/HANDOFF.md`: current infrastructure state and durable session handoff. - `docs/HANDOFF.md`: current infrastructure state and durable session handoff.
@@ -69,6 +70,9 @@ Then perform a disposable smoke test and verify:
- `/.git/config` returns 404; - `/.git/config` returns 404;
- the process runs as UID/GID `65532:65532`; - the process runs as UID/GID `65532:65532`;
- the configured security headers remain present. - the configured security headers remain present.
- `manuals.playstation.net` resolves to the configured gateway address;
- unrelated DNS names return NXDOMAIN;
- HTTP and HTTPS User Guide requests redirect to the local exploit URL.
Do not leave smoke-test containers running. Do not leave smoke-test containers running.
+13
View File
@@ -1,4 +1,15 @@
ARG PYTHON_IMAGE=python:3.14-alpine ARG PYTHON_IMAGE=python:3.14-alpine
FROM ${PYTHON_IMAGE} AS certificates
RUN apk add --no-cache openssl \
&& install -d -m 0755 /certificates \
&& openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 3650 \
-subj "/CN=manuals.playstation.net" \
-addext "subjectAltName=DNS:manuals.playstation.net" \
-keyout /certificates/manuals.key \
-out /certificates/manuals.crt \
&& chmod 0600 /certificates/manuals.key
FROM ${PYTHON_IMAGE} FROM ${PYTHON_IMAGE}
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
@@ -13,6 +24,8 @@ COPY app/src /srv/public/src
COPY app/offsets /srv/public/offsets COPY app/offsets /srv/public/offsets
COPY app/payloads /srv/public/payloads COPY app/payloads /srv/public/payloads
COPY docker/server.py /srv/server.py COPY docker/server.py /srv/server.py
COPY docker/guide_gateway.py /srv/guide_gateway.py
COPY --from=certificates /certificates /srv/certificates
RUN cd /srv/public && sha256sum -c /tmp/payloads.sha256 \ RUN cd /srv/public && sha256sum -c /tmp/payloads.sha256 \
&& rm /tmp/payloads.sha256 \ && rm /tmp/payloads.sha256 \
+4
View File
@@ -4,6 +4,10 @@ Container packaging for [PS5 Relapse Exploit](https://github.com/ntfargo/Relapse
The upstream project is pinned as a Git submodule, and the image build verifies the The upstream project is pinned as a Git submodule, and the image build verifies the
SHA-256 digest of every executable payload before including it. SHA-256 digest of every executable payload before including it.
The Compose stack also includes a local PS5 User Guide gateway: authoritative DNS
for `manuals.playstation.net`, HTTP/HTTPS redirect to the local exploit, and NXDOMAIN
for every other domain so the console cannot resolve PSN or update services.
Use this only on consoles you own or are authorized to test. The exploit can panic Use this only on consoles you own or are authorized to test. The exploit can panic
the console, corrupt kernel state, expose administrative services, and execute the console, corrupt kernel state, expose administrative services, and execute
arbitrary code with elevated privileges. arbitrary code with elevated privileges.
+26
View File
@@ -13,3 +13,29 @@ services:
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
restart: unless-stopped restart: unless-stopped
ps5-guide-gateway:
image: relapse-exploit:local
command: ["python", "/srv/guide_gateway.py"]
environment:
GATEWAY_IP: "${GATEWAY_IP:-192.168.20.30}"
REDIRECT_URL: "${GUIDE_REDIRECT_URL:-http://192.168.20.30:8000/}"
ports:
- "${GATEWAY_BIND_IP:-192.168.20.30}:53:8053/udp"
- "${GATEWAY_BIND_IP:-192.168.20.30}:53:8053/tcp"
- "${GATEWAY_BIND_IP:-192.168.20.30}:80:8081/tcp"
- "${GATEWAY_BIND_IP:-192.168.20.30}:443:8443/tcp"
read_only: true
tmpfs:
- /tmp:size=1m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8081/_health', timeout=2).close()"]
interval: 30s
timeout: 3s
start_period: 5s
retries: 3
restart: unless-stopped
+204
View File
@@ -0,0 +1,204 @@
import http.server
import ipaddress
import os
import signal
import socket
import socketserver
import ssl
import struct
import threading
DNS_PORT = int(os.environ.get("DNS_PORT", "8053"))
HTTP_PORT = int(os.environ.get("HTTP_PORT", "8081"))
HTTPS_PORT = int(os.environ.get("HTTPS_PORT", "8443"))
GATEWAY_IP = ipaddress.IPv4Address(os.environ.get("GATEWAY_IP", "192.168.20.30"))
REDIRECT_URL = os.environ.get("REDIRECT_URL", f"http://{GATEWAY_IP}:8000/")
TARGET_DOMAIN = "manuals.playstation.net"
CERTIFICATE = "/srv/certificates/manuals.crt"
PRIVATE_KEY = "/srv/certificates/manuals.key"
def parse_question(packet):
if len(packet) < 17:
raise ValueError("short DNS query")
labels = []
offset = 12
while True:
if offset >= len(packet):
raise ValueError("unterminated DNS name")
length = packet[offset]
offset += 1
if length == 0:
break
if length & 0xC0 or length > 63 or offset + length > len(packet):
raise ValueError("unsupported DNS name")
labels.append(packet[offset : offset + length].decode("ascii"))
offset += length
if offset + 4 > len(packet):
raise ValueError("short DNS question")
qtype, qclass = struct.unpack_from("!HH", packet, offset)
return ".".join(labels).lower(), qtype, qclass, offset + 4
def dns_response(packet):
if len(packet) < 12:
return b""
transaction_id, request_flags, question_count = struct.unpack_from("!HHH", packet, 0)
response_flags = 0x8400 | (request_flags & 0x0100) # response, authoritative, preserve RD
try:
name, qtype, qclass, question_end = parse_question(packet)
if question_count != 1:
raise ValueError("expected one DNS question")
except (UnicodeDecodeError, ValueError):
return struct.pack("!HHHHHH", transaction_id, response_flags | 1, 0, 0, 0, 0)
question = packet[12:question_end]
if name != TARGET_DOMAIN:
return struct.pack("!HHHHHH", transaction_id, response_flags | 3, 1, 0, 0, 0) + question
if qclass == 1 and qtype in (1, 255):
answer = (
b"\xc0\x0c"
+ struct.pack("!HHIH", 1, 1, 60, 4)
+ GATEWAY_IP.packed
)
return struct.pack("!HHHHHH", transaction_id, response_flags, 1, 1, 0, 0) + question + answer
# NOERROR with no data prevents IPv6 or unsupported record fallback.
return struct.pack("!HHHHHH", transaction_id, response_flags, 1, 0, 0, 0) + question
class ThreadingUDPServer(socketserver.ThreadingUDPServer):
allow_reuse_address = True
daemon_threads = True
class ThreadingTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
class UDPDNSHandler(socketserver.BaseRequestHandler):
def handle(self):
packet, connection = self.request
try:
name, qtype, _, _ = parse_question(packet)
print(f"DNS UDP {self.client_address[0]} {name} type={qtype}", flush=True)
except (UnicodeDecodeError, ValueError):
print(f"DNS UDP {self.client_address[0]} malformed", flush=True)
response = dns_response(packet)
if response:
connection.sendto(response, self.client_address)
class TCPDNSHandler(socketserver.BaseRequestHandler):
def handle(self):
length_bytes = self._receive_exactly(2)
if length_bytes is None:
return
wanted = struct.unpack("!H", length_bytes)[0]
packet = self._receive_exactly(wanted)
if packet is None:
return
try:
name, qtype, _, _ = parse_question(packet)
print(f"DNS TCP {self.client_address[0]} {name} type={qtype}", flush=True)
except (UnicodeDecodeError, ValueError):
print(f"DNS TCP {self.client_address[0]} malformed", flush=True)
response = dns_response(packet)
if response:
self.request.sendall(struct.pack("!H", len(response)) + response)
def _receive_exactly(self, wanted):
chunks = []
received = 0
while received < wanted:
chunk = self.request.recv(wanted - received)
if not chunk:
return None
chunks.append(chunk)
received += len(chunk)
return b"".join(chunks)
class RedirectHandler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if self.path == "/_health":
body = b"ok\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
self.send_response(302)
self.send_header("Location", REDIRECT_URL)
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", "0")
self.end_headers()
def do_HEAD(self):
self.send_response(302)
self.send_header("Location", REDIRECT_URL)
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, message, *args):
user_agent = self.headers.get("User-Agent", "-")
print(
f"HTTP {self.client_address[0]} {message % args} user-agent={user_agent!r}",
flush=True,
)
class HTTPServer(http.server.ThreadingHTTPServer):
allow_reuse_address = True
daemon_threads = True
def serve(server, name):
thread = threading.Thread(target=server.serve_forever, name=name, daemon=True)
thread.start()
return thread
if __name__ == "__main__":
dns_udp = ThreadingUDPServer(("0.0.0.0", DNS_PORT), UDPDNSHandler)
dns_tcp = ThreadingTCPServer(("0.0.0.0", DNS_PORT), TCPDNSHandler)
http = HTTPServer(("0.0.0.0", HTTP_PORT), RedirectHandler)
https = HTTPServer(("0.0.0.0", HTTPS_PORT), RedirectHandler)
tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
tls.load_cert_chain(CERTIFICATE, PRIVATE_KEY)
https.socket = tls.wrap_socket(https.socket, server_side=True)
servers = [dns_udp, dns_tcp, http, https]
serve(dns_udp, "dns-udp")
serve(dns_tcp, "dns-tcp")
serve(http, "http-redirect")
serve(https, "https-redirect")
print(
f"PS5 guide gateway: {TARGET_DOMAIN} -> {GATEWAY_IP}; redirect -> {REDIRECT_URL}",
flush=True,
)
stopped = threading.Event()
def stop(*_):
stopped.set()
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
stopped.wait()
for server in servers:
server.shutdown()
server.server_close()
+14 -8
View File
@@ -68,6 +68,11 @@ kadinho <[email protected]>
- container port: `8080/TCP`; - container port: `8080/TCP`;
- restart policy: `unless-stopped`. - restart policy: `unless-stopped`.
The same Compose project also runs `ps5-guide-gateway`, bound specifically to
`192.168.20.30` on DNS UDP/TCP 53 and HTTP/HTTPS TCP 80/443. It resolves only
`manuals.playstation.net`, returns NXDOMAIN for every other name, and redirects the
User Guide to `http://192.168.20.30:8000/`.
Port `8080` on the host is already used by `unifi-network-application`; do not move Port `8080` on the host is already used by `unifi-network-application`; do not move
this service there. At the last verification, the Relapse container was healthy, this service there. At the last verification, the Relapse container was healthy,
ran as `65532:65532`, used a read-only root filesystem, and returned HTTP 200 over ran as `65532:65532`, used a read-only root filesystem, and returned HTTP 200 over
@@ -133,15 +138,16 @@ The local HTTP endpoint is available and verified at:
http://192.168.20.30:8000/ http://192.168.20.30:8000/
``` ```
The upstream project recommends public primary DNS `45.56.67.85`. The user reported The upstream public DNS `45.56.67.85` was used initially, but the PS5 did not contact
successful access through the User Guide. No DNS container was installed on the local container. The local gateway was therefore added. Configure the PS5 with
`net-utils01`, and this repository's Compose publishes no DNS service. primary DNS `192.168.20.30` and secondary DNS `0.0.0.0`, then restart the console and
accept the self-signed certificate warning in the User Guide.
A local DNS service and an HTTP/HTTPS reverse proxy for the PS5 User Guide are The gateway intentionally provides no recursive DNS forwarding. Only
possible future improvements, but must be designed separately. DNS only maps names `manuals.playstation.net` resolves; all other names receive NXDOMAIN. This blocks
to IP addresses; it cannot redirect the User Guide from ports 80/443 to port 8000. PSN and update resolution and keeps the setup independent of a public DNS operator.
Before binding ports 53, 80, or 443 on `net-utils01`, inspect for conflicts and agree The PS5 was identified as `192.168.10.26`; requests from `192.168.10.6` seen earlier
on the desired DNS forwarding and logging policy. were from a different client.
Full PS5 instructions and DNS options are in `docs/PS5_SETUP.md`. Full PS5 instructions and DNS options are in `docs/PS5_SETUP.md`.
+40 -20
View File
@@ -18,6 +18,8 @@ Na rede onde este projeto foi inicialmente instalado:
- imagem: `relapse-exploit:local`; - imagem: `relapse-exploit:local`;
- porta interna: `8080`; - porta interna: `8080`;
- porta publicada: `8000`. - porta publicada: `8000`.
- DNS local: `192.168.20.30:53` (UDP e TCP);
- gateway do Guia: `192.168.20.30:80` e `192.168.20.30:443`.
A porta 8080 do host já pertence ao UniFi e não deve ser reutilizada. A porta 8080 do host já pertence ao UniFi e não deve ser reutilizada.
@@ -127,15 +129,17 @@ Um DNS público implica confiar no respetivo operador. Pode ficar indisponível,
alterar o destino do Guia do Utilizador ou ser contornado por um ISP que intercepte alterar o destino do Guia do Utilizador ou ser contornado por um ISP que intercepte
DNS. Confirmar sempre a página e o endereço apresentados antes de executar. DNS. Confirmar sempre a página e o endereço apresentados antes de executar.
### Opção 3: DNS local ### Opção 3: DNS local (configuração instalada)
A solução preferível a longo prazo é um DNS controlado na LAN, por exemplo no O Compose inclui um DNS controlado no `net-utils01` que:
`net-utils01`, que:
- bloqueie domínios PlayStation/PSN e servidores de atualização; - bloqueie domínios PlayStation/PSN e servidores de atualização;
- encaminhe as restantes consultas apenas se isso for desejado; - responda `manuals.playstation.net` com `192.168.20.30`;
- redirecione `manuals.playstation.net` para o host de exploit apropriado; - devolva `NXDOMAIN` para todos os restantes nomes, bloqueando PSN, updates,
- registe consultas para permitir auditoria. telemetria e acesso geral à Internet por DNS;
- registe as consultas nos logs do container;
- apresente um gateway HTTPS local e redirecione o browser para o exploit em
`http://192.168.20.30:8000/`.
Nesse cenário, a PS5 usaria: Nesse cenário, a PS5 usaria:
@@ -144,14 +148,15 @@ DNS primário: 192.168.20.30
DNS secundário: 0.0.0.0 DNS secundário: 0.0.0.0
``` ```
O serviço DNS não faz parte deste Compose e deve ser instalado e auditado Depois de guardar o DNS na PS5, reiniciar completamente a consola se o Guia continuar
separadamente. Também é possível bloquear o acesso WAN da PS5 no router, mantendo a usar uma resolução antiga. Abrir o Guia e aceitar o aviso de certificado não
apenas a comunicação LAN; esta é uma camada de proteção mais forte do que DNS. confiável: o certificado é self-signed para `manuals.playstation.net` e existe apenas
para permitir que o Guia chegue ao redirecionamento local.
DNS só traduz nomes para endereços IP; não redireciona portas. Para servir o Guia do O gateway liga-se por defeito apenas ao endereço `192.168.20.30`. Para outra rede,
Utilizador a partir do host local, é também necessário disponibilizar o site nas definir `GATEWAY_BIND_IP`, `GATEWAY_IP` e `GUIDE_REDIRECT_URL` antes de executar o
portas HTTP/HTTPS esperadas pelo manual ou colocar um reverse proxy à frente da Compose. Também é possível bloquear o acesso WAN da PS5 no router, mantendo apenas
porta 8000. O Compose atual publica apenas HTTP em `8000`. a comunicação LAN; esta continua a ser uma camada adicional mais forte do que DNS.
## Abrir pelo Guia do Utilizador ## Abrir pelo Guia do Utilizador
@@ -161,11 +166,16 @@ Com um DNS que redirecione o manual:
2. abrir `Guia do Utilizador, saúde e segurança e outras informações`; 2. abrir `Guia do Utilizador, saúde e segurança e outras informações`;
3. abrir `Guia do Utilizador`; 3. abrir `Guia do Utilizador`;
4. confirmar que aparece a página Relapse esperada; 4. confirmar que aparece a página Relapse esperada;
5. se for importante usar a instalação local, confirmar que o destino é 5. aceitar o aviso de certificado self-signed, se aparecer;
`192.168.20.30:8000` e não um host público inesperado. 6. confirmar nos logs que o cliente `192.168.10.26` consultou o DNS e abriu o
gateway;
7. confirmar que o destino final é `192.168.20.30:8000`.
O DNS público pode abrir diretamente uma página de exploit própria. Isso não prova Ver os logs do gateway:
que a página veio do container local.
```sh
docker compose logs -f ps5-guide-gateway
```
## Executar o exploit ## Executar o exploit
@@ -193,6 +203,8 @@ uma mensagem genérica de conclusão prova que todas as etapas terminaram corret
## Portas e serviços posteriores ## Portas e serviços posteriores
- `8000/TCP` no servidor: página Relapse; - `8000/TCP` no servidor: página Relapse;
- `53/UDP` e `53/TCP` no servidor: DNS local restritivo;
- `80/TCP` e `443/TCP` no servidor: gateway do Guia do Utilizador;
- `9021/TCP` na PS5: ELF loader; - `9021/TCP` na PS5: ELF loader;
- `10101/TCP` na PS5: API HTTP do ShadowMount+, por defeito em loopback. - `10101/TCP` na PS5: API HTTP do ShadowMount+, por defeito em loopback.
@@ -259,9 +271,17 @@ impede comunicação com `192.168.20.30`.
### O Guia do Utilizador abre a página oficial ou outro host ### O Guia do Utilizador abre a página oficial ou outro host
O DNS configurado não está a redirecionar `manuals.playstation.net` para o destino Confirmar que o DNS primário da PS5 é `192.168.20.30`, que não existe um DNS
esperado, ou o ISP/router está a interferir com DNS. Isto é independente do container secundário normal e que o gateway está saudável:
HTTP. Usar o URL local diretamente ou instalar um DNS local controlado.
```sh
docker compose ps
docker compose logs --tail=100 ps5-guide-gateway
```
Depois reiniciar completamente a consola para limpar resoluções antigas. Se não
existir qualquer consulta proveniente do IP da PS5, verificar regras entre as VLANs
`192.168.10.0/24` e `192.168.20.0/24`, incluindo UDP/TCP 53 e TCP 80/443/8000.
### O build falha nos hashes ### O build falha nos hashes