Vulnerability description
The binary-search path in show_pwd_r uses a cumulative Markov score to index nbparts but does not check that the next score is at most gmax_level. This differs from nb_parts, which returns zero before accessing the table when its level argument exceeds max_lvl.
while ((curchar = charsorted[pwd->password[pwd->len - 2] * 256 + k])
!= pwd->password[pwd->len - 1]) {
i -= nbparts[curchar + pwd->len * 256 +
(pwd->level + proba2[pwd->password[pwd->len - 2] *
256 + curchar]) *
256 * gmax_len];
k++;
}
The supplied stats file creates an initial password beginning with character 97 (a) at level 200. Its first sorted child is character 1 with transition score 251. The lookup at src/genmkvpwd.c:68 therefore uses level 451 even though gmax_level is 400:
index = 1 + 2 * 256 + (200 + 251) * 256 * 8
= 924,161
The allocation at src/genmkvpwd.c:274 has 256 * 401 * 9 = 923,904 uint64_t elements. Element 924,161 is 257 elements, or 2,056 bytes, past the end of the allocation. AddressSanitizer reports the resulting 8-byte heap-buffer-overflow.
Version and commit
John the Ripper 1.9.0-jumbo-1+bleeding, commit 9a336d8 (built version string: 1.9.0-jumbo-1+bleeding-9a336d800a).
Environment
-
Ubuntu 24.04.4 LTS; Linux 6.8.0-136-generic; x86_64.
-
GCC 13.3.0, glibc 2.39, and GCC AddressSanitizer.
-
The binary was configured with --without-openssl --enable-asan, debug information, and frame pointers.
Steps to reproduce
-
On Ubuntu, install the build prerequisites:
sudo apt-get update
sudo apt-get install -y build-essential git perl
-
Check out the vulnerable revision and set POC_STATS to the absolute path of the included stats artifact:
export JTR=$PWD/john
export POC_STATS=/absolute/path/to/stats
git clone https://github.com/openwall/john.git "$JTR"
git -C "$JTR" checkout 9a336d800a091bec9650c29282485145f31c9ffc
-
Build genmkvpwd with AddressSanitizer:
cd "$JTR/src"
./configure --quiet --without-openssl --enable-asan CC=gcc \
CFLAGS='-O0 -g -fno-omit-frame-pointer' \
CFLAGS_EXTRA='-O0 -g -fno-omit-frame-pointer' \
LDFLAGS='-fsanitize=address'
make -j"$(nproc)"
-
Run the PoC. AddressSanitizer terminates the process with heap-buffer-overflow:
cd "$JTR/run"
./genmkvpwd "$POC_STATS" 400 8 1 2> sanitizer_report.txt
Sanitizer report
The following is the complete, unmodified report included as sanitizer_report.txt.
allocated 7218 KB for nbparts
256 possible passwords
starting with a (1 to 256, 99.609375% of the scope)
=================================================================
==992521==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7530aa7ff808 at pc 0x587b94be235e bp 0x7ffee6c313a0 sp 0x7ffee6c31390
READ of size 8 at 0x7530aa7ff808 thread T0
#0 0x587b94be235d in show_pwd_r /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:68
#1 0x587b94be31b8 in show_pwd /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:121
#2 0x587b94be495b in main /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:301
#3 0x7530adc2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#4 0x7530adc2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x587b94be1784 in _start (/home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/run/genmkvpwd+0x4784) (BuildId: 4875d0d0571be6b5aca6c0dbdb99f1729b7a84ad)
0x7530aa7ff808 is located 2056 bytes after 7391232-byte region [0x7530aa0f2800,0x7530aa7ff000)
allocated by thread T0 here:
#0 0x7530ae0fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x587b94be6b27 in mem_alloc /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/memory.c:92
#2 0x587b94be4208 in main /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:274
#3 0x7530adc2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#4 0x7530adc2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x587b94be1784 in _start (/home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/run/genmkvpwd+0x4784) (BuildId: 4875d0d0571be6b5aca6c0dbdb99f1729b7a84ad)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:68 in show_pwd_r
Shadow bytes around the buggy address:
0x7530aa7ff580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ff600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ff680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ff700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ff780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x7530aa7ff800: fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ff880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ff900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ff980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ffa00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7530aa7ffa80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==992521==ABORTING
Potential fix
Make traversal lookups obey the same state bounds as nb_parts. The helper below returns zero for an impossible state, which is the semantic value nb_parts uses when level exceeds max_lvl. The traversal loops then skip zero-count children rather than indexing the table or emitting a nonexistent child. Initializing the local password state and checking the prefix avoids beginning a binary-search traversal with an invalid prefix.
I applied this diff to a fresh checkout of the vulnerable commit, rebuilt it with the ASan command above, and reran this PoC and the invalid-initial-level PoC. Both exited normally with no AddressSanitizer diagnostic. Two valid comparison runs, using arguments 10 8 0 1 and 400 8 0 1 with their respective stats files, produced byte-for-byte identical output before and after the change.
diff --git a/src/genmkvpwd.c b/src/genmkvpwd.c
index de62d03..4365d6e 100644
--- a/src/genmkvpwd.c
+++ b/src/genmkvpwd.c
@@ -19,22 +19,44 @@
#include "memory.h"
#include "mkvlib.h"
+/* nb_parts() defines states beyond either limit as having zero completions. */
+static uint64_t nbparts_at(unsigned char letter, unsigned int len,
+ unsigned long level)
+{
+ if (len > gmax_len || level > gmax_level)
+ return 0;
+
+ return nbparts[(size_t)letter + (size_t)len * 256 +
+ (size_t)level * 256 * gmax_len];
+}
+
static void show_pwd_rnbs(struct s_pwd * pwd)
{
uint64_t i;
+ uint64_t child_parts;
unsigned int k;
unsigned long lvl;
+ unsigned long child_level;
+
+ if (pwd->len > gmax_len || pwd->level > gmax_level)
+ return;
k=0;
- i = nbparts[pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len];
+ i = nbparts_at(pwd->password[pwd->len-1], pwd->len, pwd->level);
pwd->len++;
lvl = pwd->level;
pwd->password[pwd->len] = 0;
while(i>1)
{
pwd->password[pwd->len-1] = charsorted[ pwd->password[pwd->len-2]*256 + k ];
- pwd->level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
- i -= nbparts[ pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len ];
+ child_level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
+ child_parts = nbparts_at(pwd->password[pwd->len-1], pwd->len, child_level);
+ if (!child_parts) {
+ k++;
+ continue;
+ }
+ pwd->level = child_level;
+ i -= child_parts;
if (pwd->len<=gmax_len)
{
show_pwd_rnbs(pwd);
@@ -53,25 +75,39 @@ static void show_pwd_rnbs(struct s_pwd * pwd)
static void show_pwd_r(struct s_pwd * pwd, unsigned int bs)
{
uint64_t i;
+ uint64_t child_parts;
unsigned int k;
unsigned long lvl;
+ unsigned long child_level;
unsigned char curchar;
+ if (pwd->len > gmax_len || pwd->level > gmax_level)
+ return;
+
k=0;
- i = nbparts[pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len];
+ i = nbparts_at(pwd->password[pwd->len-1], pwd->len, pwd->level);
pwd->len++;
lvl = pwd->level;
if (bs)
{
while( (curchar=charsorted[ pwd->password[pwd->len-2]*256 + k ]) != pwd->password[pwd->len-1] )
{
- i -= nbparts[ curchar + pwd->len*256 + (pwd->level + proba2[ pwd->password[pwd->len-2]*256 + curchar ])*256*gmax_len ];
+ child_level = lvl + proba2[ pwd->password[pwd->len-2]*256 + curchar ];
+ i -= nbparts_at(curchar, pwd->len, child_level);
k++;
}
- pwd->level += proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
+ child_level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
+ child_parts = nbparts_at(pwd->password[pwd->len-1], pwd->len, child_level);
+ if (!child_parts) {
+ pwd->len--;
+ pwd->password[pwd->len] = 0;
+ pwd->level = lvl;
+ return;
+ }
+ pwd->level = child_level;
if (pwd->password[pwd->len]!=0)
show_pwd_r(pwd, 1);
- i -= nbparts[ pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len ];
+ i -= child_parts;
printf("%s\n", pwd->password);
gidx++;
k++;
@@ -80,8 +116,14 @@ static void show_pwd_r(struct s_pwd * pwd, unsigned int bs)
while(i>1)
{
pwd->password[pwd->len-1] = charsorted[ pwd->password[pwd->len-2]*256 + k ];
- pwd->level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
- i -= nbparts[ pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len ];
+ child_level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
+ child_parts = nbparts_at(pwd->password[pwd->len-1], pwd->len, child_level);
+ if (!child_parts) {
+ k++;
+ continue;
+ }
+ pwd->level = child_level;
+ i -= child_parts;
if (pwd->len<=gmax_len)
{
show_pwd_r(pwd, 0);
@@ -99,7 +141,7 @@ static void show_pwd_r(struct s_pwd * pwd, unsigned int bs)
static void show_pwd(uint64_t start, uint64_t end, unsigned int max_level, unsigned int max_len)
{
- struct s_pwd pwd;
+ struct s_pwd pwd = { 0 };
unsigned int i;
unsigned int bs;
@@ -114,10 +156,14 @@ static void show_pwd(uint64_t start, uint64_t end, unsigned int max_level, unsig
if (bs)
{
print_pwd(start, &pwd, max_level, max_len);
+ if (!pwd.password[0])
+ return;
while(charsorted[i] != pwd.password[0])
i++;
pwd.len = 1;
pwd.level = proba1[pwd.password[0]];
+ if (pwd.level > max_level)
+ return;
show_pwd_r(&pwd, 1);
printf("%s\n", pwd.password);
i++;
Artifacts
The PoC and the sanitizer report are packaged into this ZIP file.
artifacts.zip
Vulnerability description
The binary-search path in show_pwd_r uses a cumulative Markov score to index nbparts but does not check that the next score is at most gmax_level. This differs from nb_parts, which returns zero before accessing the table when its level argument exceeds max_lvl.
The supplied stats file creates an initial password beginning with character 97 (a) at level 200. Its first sorted child is character 1 with transition score 251. The lookup at src/genmkvpwd.c:68 therefore uses level 451 even though gmax_level is 400:
The allocation at src/genmkvpwd.c:274 has 256 * 401 * 9 = 923,904 uint64_t elements. Element 924,161 is 257 elements, or 2,056 bytes, past the end of the allocation. AddressSanitizer reports the resulting 8-byte heap-buffer-overflow.
Version and commit
John the Ripper 1.9.0-jumbo-1+bleeding, commit 9a336d8 (built version string: 1.9.0-jumbo-1+bleeding-9a336d800a).
Environment
Ubuntu 24.04.4 LTS; Linux 6.8.0-136-generic; x86_64.
GCC 13.3.0, glibc 2.39, and GCC AddressSanitizer.
The binary was configured with --without-openssl --enable-asan, debug information, and frame pointers.
Steps to reproduce
On Ubuntu, install the build prerequisites:
Check out the vulnerable revision and set POC_STATS to the absolute path of the included stats artifact:
Build genmkvpwd with AddressSanitizer:
Run the PoC. AddressSanitizer terminates the process with heap-buffer-overflow:
Sanitizer report
The following is the complete, unmodified report included as sanitizer_report.txt.
Potential fix
Make traversal lookups obey the same state bounds as nb_parts. The helper below returns zero for an impossible state, which is the semantic value nb_parts uses when level exceeds max_lvl. The traversal loops then skip zero-count children rather than indexing the table or emitting a nonexistent child. Initializing the local password state and checking the prefix avoids beginning a binary-search traversal with an invalid prefix.
I applied this diff to a fresh checkout of the vulnerable commit, rebuilt it with the ASan command above, and reran this PoC and the invalid-initial-level PoC. Both exited normally with no AddressSanitizer diagnostic. Two valid comparison runs, using arguments 10 8 0 1 and 400 8 0 1 with their respective stats files, produced byte-for-byte identical output before and after the change.
Artifacts
The PoC and the sanitizer report are packaged into this ZIP file.
artifacts.zip