← Zurück zur Hauptseite Technisches Post-Mortem

LNbits-Vorfall vom 8. November 2026 – technische Analyse

Wie die Wallets geleert wurden – aufbereitet, damit andere Betreiber:innen und das LNbits-Projekt daraus lernen können.

Betroffene Software: LNbits v1.6.2 (self-hosted). Stand der Analyse: 10. November 2026. Alle Zeiten in UTC.

Verantwortungsvolle Offenlegung. Die zugrunde liegenden Fehler stecken im quelloffenen LNbits-Code und wurden auf mehreren Servern aktiv ausgenutzt; das LNbits-Projekt hat Betreiber weltweit zum Abschalten aufgefordert. Dieser Bericht beschreibt die Methodik auf Konzeptebene, damit man die Lücken erkennen, prüfen und schliessen kann – es ist bewusst kein fertiges Angriffswerkzeug und enthält keine lauffähigen Payload-Skripte.

Kurzfassung (TL;DR)

Inhalt 1. Die Angriffskette im Überblick 2. Schwachstelle A – TPoS IDOR (Wallet-Übernahme) 3. Schwachstelle B – Paywall Local-File-Read / SSRF 4. Schwachstelle C – user-id-only als Credential 5. Verschärfende Konfiguration 6. Zeitstrahl 7. Indicators of Compromise 8. Bin ich betroffen? – Detektion 9. Gegenmassnahmen & Härtung

1. Die Angriffskette im Überblick

Ausgangspunkt ist ein ganz normaler Nutzer-Account, wie ihn eine Multi-User-Instanz regulär und gewollt vergibt – also kein privilegierter Zugang. Von dort greifen die drei Fehler ineinander; jeder für sich ist schon schlimm, zusammen ergeben sie die Totalübernahme:

  1. Schwachstelle A (TPoS): Mit einem eigenen „TPoS“-Terminal lässt sich per Update die Wallet-Zuordnung auf ein fremdes Wallet umbiegen und dieses dann über die ATM-/Withdraw-Funktion leeren. So wurde zuerst das Service-Fee-Wallet (es gehört dem Superuser) angezapft.
  2. Schwachstelle B (Paywall): Eine Paywall vom Typ file lädt eine frei wählbare URL per urllib. file:// wird akzeptiert → beliebige lokale Dateien lesbar, u. a. die LNbits-Datenbank (alle Keys), die Datei .super_user und die gemountete LND-Macaroon.
  3. Schwachstelle C (Auth): Mit der ausgelesenen Superuser-ID genügt ein user-id-only-Login, um Admin zu werden.
  4. Plünderung: Als Superuser alle Nutzer auflisten, deren Admin-Keys abrufen und per POST /api/v1/payments jedes Wallet an den eigenen Node ausschütten.

2. Schwachstelle A — TPoS IDOR (Wallet-Übernahme)

IDOR / Broken Access Control   Extension tpos, Endpoint PUT /tpos/api/v1/tposs/{tpos_id}.

Das Datenmodell des Update-Requests enthält ein frei setzbares Feld wallet:

class CreateTposData(BaseModel):
    wallet: str | None        # <- vom Client bestimmbar
    name: str | None
    ...

Der Handler prüft die Besitzrechte nur gegen den aktuellen Stand und schreibt danach alle gelieferten Felder ungefiltert per setattr zurück:

@tpos_api_router.put("/api/v1/tposs/{tpos_id}")
async def api_tpos_update(data, tpos_id,
        wallet = Depends(require_admin_key)):
    tpos = await get_tpos(tpos_id)
    ...
    if wallet.wallet.id != tpos.wallet:          # Besitzprüfung
        raise HTTPException(403, "Not your TPoS.")
    ...
    for field, value in update_payload.items():
        setattr(tpos, field, value)              # <- 'wallet' wird überschrieben
    tpos = await update_tpos(tpos)

Der Denkfehler: Die Prüfung bestätigt nur, dass das TPoS jetzt dem Aufrufer gehört. Unmittelbar danach darf derselbe Request die wallet-Zuordnung auf ein beliebiges fremdes Wallet setzen. Es gibt keine zweite Prüfung, ob das neue Ziel-Wallet dem Aufrufer gehört.

Ablauf (konzeptionell)

  1. Eigenes TPoS anlegen → wallet = eigenes Wallet (Besitzprüfung besteht).
  2. PUT …/tposs/{id} mit wallet = fremde Wallet-ID → Zuordnung umgebogen.
  3. ATM-/Withdraw-Funktion des TPoS auslösen. Das System zieht jetzt vom fremden Wallet. In den Logs verrät sich das durch Fehlermeldungen wie Insufficient balance. Your balance is 398313 sats – obwohl das eigene Wallet 0 sat hatte.

Voraussetzung ist die Kenntnis einer fremden Wallet-ID. Für das Service-Fee-Wallet lässt sie sich indirekt gewinnen, weil Service-Fee-Buchungen denselben payment_hash wie die auslösende Zahlung tragen. Auf diesem Server wurden so 47 Angreifer-TPoS mit withdraw_limit = 100000000 auf das Fee-Wallet gelegt.

3. Schwachstelle B — Paywall Local-File-Read / SSRF

LFI / SSRF   Extension paywall (v1.2.0), Download-Endpoint der Paywall.

Eine Paywall vom Typ file speichert in file_config.url eine vom Nutzer gesetzte URL. Beim Download wird diese ungefiltert an urllib übergeben:

async def _file_streamer(url, headers):
    with request.urlopen(request.Request(url=url, headers=headers)) as dl_file:
        yield dl_file.read()

Das Problem: urllib.request.urlopen akzeptiert nicht nur http(s)://, sondern auch file://. Es gibt keine Schema-Allowlist und keine Pfad-Einschränkung. Damit wird der Endpoint zu:

Verschärfend: Der LNbits-Container lief als root und hatte das Datenverzeichnis sowie das LND-Verzeichnis gemountet. Lesbar waren damit u. a.:

Der Download setzt eine bezahlte Rechnung voraus – der Betrag ist aber frei und darf 0 sein, die Hürde entfällt also. In den Logs taucht als zuletzt gespeicherte Sonde der Wert file:///data/.env auf. Welche Dateien konkret ausgelesen wurden, ist aus den Logs nicht beweisbar; der unmittelbar folgende Superuser-Login (siehe Schwachstelle C) ist aber genau damit konsistent.

Hinweis: Eine zweite Extension (fossa) erlaubt beim Anlegen ebenfalls ein fremdes Wallet (IDOR). Darüber floss nachweislich kein Geld ab, der Fehler sollte aber mitbehoben werden.

4. Schwachstelle C — user-id-only als Credential

Design / Auth   Kern-Einstellung auth_allowed_methods.

War die Methode user-id-only aktiv, genügt die Kenntnis einer User-ID, um sich als diese:r Nutzer:in einzuloggen (…/api/v1/auth?usr=<id>). Die User-ID ist damit faktisch Benutzername und Passwort in einem. Kombiniert mit dem Local-File-Read aus Schwachstelle B (Auslesen von .super_user) führt das direkt zu Admin-Rechten: Danach lieferte /admin/api/v1/settings Status 200, und die Angreiferseite listete via /users/api/v1/user?limit=100&offset=… sämtliche Konten samt Wallets und Admin-Keys auf.

5. Verschärfende Konfiguration

Diese Punkte haben den Schaden erst gross gemacht – unabhängig von den Code-Bugs:

6. Zeitstrahl (UTC)

ZeitEreignis
07.11. abendsErste Erkundung: Tests von /api/v1/auth/impersonate, /admin/…, Testregistrierung.
08.11. 09:42–09:49Mehrere Wegwerf-Accounts (u. a. „admin“). Automatisiertes Abklopfen der Extensions.
08.11. 09:48Fossa-IDOR auf fremdes Wallet getestet (vorher mit deadbeef… auf Prüfung getestet).
08.11. 10:33Phase 1 – TPoS-IDOR: Fee-Wallet wird über umgebogene TPoS geleert.
08.11. 11:52–12:27Paywall-LFI: Paywall Typ file, file:///…, mehrfacher Download.
08.11. 12:02Erster usr=-Login.
08.11. 12:31Superuser bestätigt (/admin/api/v1/settings → 200), Massen-Enumeration aller Nutzer.
08.11. 13:11–16:22Phase 2: systematisches Leeren der Wallets, ~alle 5 s eine Zahlung.
08.11. 14:13–14:16Versuche über die Node-UI (phoenixd/send, DELETE /node/…/channels) – alle gescheitert.
08.11. 16:22LNbits gestoppt → Angriff endet. Nach Neustart keine Angreifer-Aktivität mehr.

7. Indicators of Compromise (IoC)

Angreifer-bezogene Artefakte. Die IP-Adressen stammen aus dem Applikations-Log; da der Reverse-Proxy so konfiguriert war, dass er jedem X-Forwarded-For vertraute, können sie gefälscht sein und sind nur als Indiz zu verstehen.

Ziel-Node der gestohlenen Sats

021294fff596e497ad2902cd5f19673e9020953d90625d68c22e91b51a45c032d3

Nahezu alle entwendeten Sats gingen an diesen einen Node. Betreiber von Verwahr-/Börsendiensten können diesen Pubkey prüfen.

Beobachtete Quell-IPs (mit Spoofing-Vorbehalt)

66.179.142.14   64.38.116.4     45.86.210.167
186.247.35.215  186.247.40.143  216.203.38.19
94.183.234.112  104.28.0.0/16 (WARP-Bereich)

Angreifer-Artefakte in den Extension-Daten

8. Bin ich betroffen? — Detektion

Zur Prüfung einer eigenen Instanz (vorher den Dienst stoppen und auf Kopien arbeiten):

Log-Signaturen

# verdächtige Wallet-Übernahme per TPoS-Update, gefolgt von „fremdem“ Saldo
grep -nE 'PUT /tpos/api/v1/tposs/' debug.log
grep -n 'Insufficient balance. Your balance is' debug.log

# Paywall-Dateidownloads (LFI-Sonden)
grep -nE 'paywall/(download|api/v1/paywalls)' debug.log

# Login per User-ID + Admin-Zugriff
grep -nE '/api/v1/auth\?usr=|/admin/api/v1/settings|/users/api/v1/user' debug.log

In der Datenbank

9. Gegenmassnahmen & Härtung

Sofort

Konfiguration

Fix-Empfehlung an die Extension-Autor:innen


Dieser Bericht wurde bewusst anonymisiert: interne Adressen, Hostnamen, Wallet-IDs und Konten der betroffenen Instanz sind entfernt oder durch Platzhalter ersetzt. Angreifer-bezogene Artefakte sind erhalten, damit sie zur Abwehr und Zuordnung dienen können.

Stand: 10. November 2026  ·  Zurück zur Hauptseite