← warez.sl0p.foo

Linux tgt (scsi-tgt) iSCSI — Unauthenticated Remote Root RCE

2026-08-02 · CRITICAL · affects fujita/tgt (tgtd) ≤ 1.0.97 / HEAD · write primitive shipped since 2007 · 0day — no CVE, no patch

iSCSHELL — Remote Code Execution
iSCSHELL — plug the cable in, get a shell out. Initiator → Target → Shell; the middle step was supposed to be harder.

*The job of an iSCSI target is to write the bytes you send it to the block you name. tgtd does exactly that — it just never checks that the block is on the disk. Name an offset past the buffer and it writes your bytes into the heap instead. It has done this since 2007, it listens on 3260, and it runs as root.*

Summary

tgtd, the daemon of the Linux SCSI Target Framework (fujita/tgt — shipped as the tgt / scsi-target-utils package by essentially every distribution, and the storage backend behind countless SAN appliances, libvirt iSCSI pools, Ceph/OpenStack gateways and home-lab NAS boxes), contains an unauthenticated, remote, arbitrary heap write reachable with a single iSCSI data transfer.

The iSCSI Data-Out PDU carries a 32-bit Buffer Offset telling the target where in the command's data buffer to place this chunk. tgtd computes the destination as task->data + offset and receives the segment there with no bounds check whatsoever. Point the offset past the buffer and you write attacker-controlled bytes at an attacker-controlled distance into the heap. This code has been in the tree since 2007.

Two further bugs — also in the shipped binary, also unfixed — turn that write into full RCE by defeating ASLR entirely off the wire: an R2T response leaks a live heap pointer (since 2006), and the same unchecked-length logic lets a READ over-read adjacent heap, disclosing the program (.text) and libc bases. With heap + image + libc known, we overwrite a victim connection's transport function table, point it at a buffer we filled, and ride setcontext into system().

tgtd runs as root in every default deployment, and iSCSI is unauthenticated unless CHAP is explicitly configured (it is not, by default). The result is remote unauthenticated root code execution against any exposed target.

Affected

SoftwareLinux SCSI Target Framework — tgt / tgtd (fujita/tgt)
VulnerableAll versions through HEAD — v1.0.97 (commit f0dc199). Write primitive present since 2007; pointer leak since 2006.
Fixed inNot fixed — 0day at time of publication. No CVE, no vendor advisory.
Auth requiredNo — unauthenticated whenever CHAP is not configured (the default). Gated only by the initiator-address ACL, whose documented/common value is ALL.
Privilegetgtd runs as root → remote unauthenticated root
Tested onArch Linux tgt 1.0.97-3 (/usr/bin/tgtd), glibc 2.44, ASLR on — end-to-end RCE, reliable
CVENone assigned at time of publication

Bug 1 — Data-Out Buffer Offset → Arbitrary Heap Write

A SCSI WRITE can deliver its payload in one or more Data-Out PDUs. Each Data-Out carries a Buffer Offset — where in the command's buffer this segment belongs. iscsi_data_out_rx_start() resolves the destination pointer like this, and the incoming data segment is then read straight into it:

// usr/iscsi/iscsid.c — iscsi_data_out_rx_start()  (unchanged since 2007)
found:
    conn->req.data = task->data + be32_to_cpu(req->offset);  // ← offset is attacker-controlled, unchecked

    task->offset   += ntoh24(req->dlength);
    task->r2t_count -= ntoh24(req->dlength);   // (only ever decremented; may go negative)
    conn->rx_task = task;
    return 0;

There is no check that offset — or offset + dlength — lies within task->data_len. The Buffer Offset is a full 32-bit field taken verbatim from the wire. The subsequent receive copies the PDU's data segment to task->data + offset, so an attacker writes dlength bytes of chosen content at a chosen forward distance from a heap buffer they also chose the size of — a clean, repeatable, arbitrary heap write.

The Buffer Offset had one job: say where in the buffer the data goes. tgtd reads it as a suggestion about where in the heap the data goes — and the field is a full 32 bits wide, so there are roughly four billion places to put your write, exactly one of which is the buffer.

To keep a write anchor resident, send a WRITE with the Final bit clear (F=0): the task and its task->data buffer stay alive awaiting more data, and every subsequent Data-Out re-runs the code above. With InitialR2T=No / ImmediateData=Yes (both freely negotiable by the initiator) no R2T handshake is required first.

Bug 2 — R2T Target Transfer Tag Leaks a Heap Pointer

When tgtd solicits write data it emits a Ready-To-Transfer (R2T) PDU. The Target Transfer Tag is built by casting the task object's address to 32 bits:

// usr/iscsi/iscsid.c — iscsi_r2t_build()  (unchanged since 2006)
rsp->ttt = (unsigned long) task;   // ← low 32 bits of a live heap pointer, straight onto the wire

Every R2T hands the initiator the low 32 bits of a struct iscsi_task heap address. That is the bottom half of the heap ASLR slide, for free, before authentication. It pins where our controlled objects live. They cast a live heap pointer to an integer and mail it to the attacker in a field literally named Tag — Ready To Transfer, indeed.

Bug 3 — READ Residual Over-Read Leaks .text + libc

The missing-bounds disease is systemic. The same unchecked length arithmetic lets Bug 1 corrupt a parked READ's residual transfer length (scmd.in_sdb.transfer_len). The target then builds its Data-In response from that length and reads past the end of the READ buffer, streaming ~64 KB of adjacent heap back to the initiator. That window is full of pointers:

So all three ASLR domains — heap (Bug 2), image and libc (Bug 3) — collapse to bugs already present in the shipped binary. No brute force, no guessing, no secondary vulnerability required.

Storage daemons are supposed to be boring. This one hands an unauthenticated client a heap write, a heap pointer, and the text and libc bases — and then executes whatever it finds at the address you wrote. As root.

The Kill Chain

Log in unauthenticated (CHAP off by default) and park a WRITE whose data buffer T we own completely. Its heap address comes from the Bug 2 R2T leak.
Defeat ASLR off the wire — Bug 3's over-read discloses the image and libc bases; Bug 2 gives the heap. Every absolute address we need is now known.
Forge a transport object in T — lay down a fake iscsi_transport vtable plus a setcontext argument frame (a ucontext with RDI = &cmd, RIP = system), all filled in-bounds with ordinary Data-Out writes.
Hijack a victim connection — use Bug 1's arbitrary write to overwrite another connection's conn->tp so it points at T.
Fire — send any PDU on the victim. do_recv() performs conn->tp->ep_read(conn, …); our fake ep_read slot is a mov rdi, rax ; call [rax] gadget in tgtd itself → setcontext(T) → system(cmd). Code runs as root.

Confirmed Remote Root Execution

Against the stock Arch package /usr/bin/tgtd (tgt 1.0.97-3, glibc 2.44), ASLR on, no debugger, every address recovered live from the socket:

[leak] Data-In 188416 bytes (EDTL 0x1e000, over-read 65536)
[fp] glibc: main_arena=0x71b959819ac8 (& 0xfff = 0xac8) -> 'glibc 2.44' libc_base=0x71b959600000
[fp] tgt  : &iscsi_tcp=0x648565f11100 (& 0xfff = 0x100) -> 'arch-pkg' image_base=0x648565ec4000
[+] #6 R2T: TTT=d0b7ba25 -> low32(&W_task)=0x25bab7d0 ; T=0x585025bb1000
[+] OOB write victim->tp = 0x585025bb1000 (Buffer Offset 0x2c568); triggering
[+] RCE CONFIRMED — system('id > /tmp/PWNED_BY_TGT_RCE 2>&1') executed on the target
# id: uid=0(root) gid=0(root) groups=0(root)

The exploit is self-calibrating: it reads the libc and tgt build out of the low (ASLR-invariant) bits of the leaked pointers and selects the matching offset set, so a single binary targets a known build with no per-run tuning.

Reachability — Who Is Exposed

iSCSI targets live on TCP 3260. A tgtd is exploitable when:

tgtd is almost universally run as root (it needs raw block-device and large-page access). Compromise is immediate, total, and as uid 0. Exposure is common: iSCSI is frequently left reachable on management/storage networks under the assumption that "it's just block storage." Authentication in tgt is opt-in. Exploitation is not.

The vulnerable code is version-independent — the write, the pointer leak and the over-read are identical across the tree. The published PoC ships the offset set for the Arch tgt 1.0.97-3 build and a documented recipe for adding others; the technique itself is not tied to any particular release.

PoC

poc_tgt_iscsi_rce.py — single-file, no dependencies: wire leak → ASLR defeat → fake vtable → setcontext → system(cmd)
sha256: eeede84e6a4ccf5c1ca4970a1b0528d9c08d25ec6467c95b4936003a354383bf

Pure Python 3 standard library — no pip, no sibling files. Lists its known targets, takes an arbitrary command, and proves execution with a target-side sentinel file.

python3 poc_tgt_iscsi_rce.py --list
python3 poc_tgt_iscsi_rce.py --target arch-pkg --host 10.0.0.5 --cmd 'id > /tmp/proof 2>&1'

It logs in unauthenticated, leaks the three bases off the wire, hijacks a connection's transport pointer, and runs the command as root on the target.

Impact

On the bright side: it's always DNS — except this time it's storage, nineteen years old, running as root, and the backups are on the same box you just rooted. Somewhere a compliance checkbox reads "iSCSI restricted to the storage VLAN"; the storage VLAN is where the crown jewels live and the door is a 32-bit integer.

Remediation

The vendor will not fix this on any timeline we control, so the only reliable mitigations are operational:

The actual fixes are one-liners and we wrote them: bounds-check the Data-Out offset/length against task->data_len before computing conn->req.data; make the R2T Target Transfer Tag an opaque per-task counter instead of a cast pointer; and validate transfer_len on the READ path. None of these are hard. They have simply never been done.

Timeline

2006R2T Target-Transfer-Tag pointer leak introduced (rsp->ttt = (unsigned long) task)
2007Unchecked Data-Out Buffer Offset write introduced (task->data + be32_to_cpu(req->offset))
2026-08-02Rediscovered via automated source audit of the tgt iSCSI front-end
2026-08-02End-to-end unauthenticated root RCE confirmed on stock Arch tgt 1.0.97-3, ASLR on
—No CVE assigned. No vendor contact. No patch available. Present in HEAD (v1.0.97, f0dc199).
Disclaimer — This advisory, including all analysis, proof-of-concept code, and reproduction steps, was produced by AI coding agents (LLM-driven static analysis and automated testing). Nothing here is published on the strength of static analysis alone: every finding is validated end-to-end before publication — the proof-of-concept is run against a real, affected build and the stated impact is observed, not inferred. That validation pipeline is itself largely automated, and the surrounding root-cause analysis, affected-version ranges, and severity assessments may still contain errors. All materials are provided as-is with absolutely no warranty. Use at your own risk and validate independently before acting on any claims made herein. Published for defensive research purposes only.