local datetime = require "datetime" local http = require "http" local nmap = require "nmap" local stdnse = require "stdnse" local string = require "string" local tab = require "tab" local table = require "table" _ENV = stdnse.module("ipp", stdnse.seeall) --- -- -- A small CUPS ipp (Internet Printing Protocol) library implementation -- -- @author Patrik Karlsson -- -- The IPP layer IPP = { StatusCode = { OK = 0, }, State = { IPP_JOB_PENDING = 3, IPP_JOB_HELD = 4, IPP_JOB_PROCESSING = 5, IPP_JOB_STOPPED = 6, IPP_JOB_CANCELED = 7, IPP_JOB_ABORTED = 8, IPP_JOB_COMPLETED = 9, }, StateName = { [3] = "Pending", [4] = "Held", [5] = "Processing", [6] = "Stopped", [7] = "Canceled", [8] = "Aborted", [9] = "Completed", }, OperationID = { IPP_CANCEL_JOB = 0x0008, IPP_GET_JOB_ATTRIBUTES = 0x0009, IPP_GET_JOBS = 0x000a, CUPS_GET_PRINTERS = 0x4002, CUPS_GET_DOCUMENT = 0x4027 }, PrinterState = { IPP_PRINTER_IDLE = 3, IPP_PRINTER_PROCESSING = 4, IPP_PRINTER_STOPPED = 5, }, Attribute = { IPP_TAG_OPERATION = 0x01, IPP_TAG_JOB = 0x02, IPP_TAG_END = 0x03, IPP_TAG_PRINTER = 0x04, IPP_TAG_INTEGER = 0x21, IPP_TAG_ENUM = 0x23, IPP_TAG_NAME = 0x42, IPP_TAG_KEYWORD = 0x44, IPP_TAG_URI = 0x45, IPP_TAG_CHARSET = 0x47, IPP_TAG_LANGUAGE = 0x48, new = function(self, tag, name, value) local o = { tag = tag, name = name, value = value } setmetatable(o, self) self.__index = self return o end, parse = function(data, pos) local attrib = IPP.Attribute:new() local val attrib.tag, attrib.name, val, pos = string.unpack(">Bs2s2", data, pos) attrib.value = {} table.insert(attrib.value, { tag = attrib.tag, val = val }) repeat local tag, name_len, val if ( #data < pos + 3 ) then break end tag, name_len, pos = string.unpack(">BI2", data, pos) if ( name_len == 0 ) then val, pos = string.unpack(">s2", data, pos) table.insert(attrib.value, { tag = tag, val = val }) else pos = pos - 3 end until( name_len ~= 0 ) -- do minimal decoding for i=1, #attrib.value do if ( attrib.value[i].tag == IPP.Attribute.IPP_TAG_INTEGER ) then attrib.value[i].val = string.unpack(">I4", attrib.value[i].val) elseif ( attrib.value[i].tag == IPP.Attribute.IPP_TAG_ENUM ) then attrib.value[i].val = string.unpack(">I4", attrib.value[i].val) end end if ( 1 == #attrib.value ) then attrib.value = attrib.value[1].val end --print(attrib.name, attrib.value, stdnse.tohex(val)) return pos, attrib end, __tostring = function(self) if ( "string" == type(self.value) ) then return string.pack(">Bs2s2", self.tag, self.name, self.value) else local data = {string.pack(">Bs2s2", self.tag, self.name, self.value[1].val)} for i=2, #self.value do data[#data+1] = string.pack(">BI2s2", self.value[i].tag, 0, self.value[i].val) end return table.concat(data) end end }, -- An attribute group, groups several attributes AttributeGroup = { new = function(self, tag, attribs) local o = { tag = tag, attribs = attribs or {} } setmetatable(o, self) self.__index = self return o end, addAttribute = function(self, attrib) table.insert(self.attribs, attrib) end, -- -- Gets the first attribute matching name and optionally tag from the -- attribute group. -- -- @param name string containing the attribute name -- @param tag number containing the attribute tag getAttribute = function(self, name, tag) for _, attrib in ipairs(self.attribs) do if ( attrib.name == name ) then if ( not(tag) ) then return attrib elseif ( tag and attrib.tag == tag ) then return attrib end end end end, getAttributeValue = function(self, name, tag) for _, attrib in ipairs(self.attribs) do if ( attrib.name == name ) then if ( not(tag) ) then return attrib.value elseif ( tag and attrib.tag == tag ) then return attrib.value end end end end, __tostring = function(self) local data = {string.pack("B", self.tag)} for _, attrib in ipairs(self.attribs) do data[#data+1] = tostring(attrib) end return table.concat(data) end }, -- The IPP request Request = { new = function(self, opid, reqid) local o = { version = 0x0101, opid = opid, reqid = reqid, attrib_groups = {}, } setmetatable(o, self) self.__index = self return o end, addAttributeGroup = function(self, group) table.insert( self.attrib_groups, group ) end, __tostring = function(self) local data = {string.pack(">I2I2I4", self.version, self.opid, self.reqid )} for _, group in ipairs(self.attrib_groups) do data[#data+1] = tostring(group) end data[#data+1] = string.pack("B", IPP.Attribute.IPP_TAG_END) return table.concat(data) end, }, -- A class to handle responses from the server Response = { -- Creates a new instance of response new = function(self) local o = {} setmetatable(o, self) self.__index = self return o end, getAttributeGroups = function(self, tag) local groups = {} for _, v in ipairs(self.attrib_groups or {}) do if ( v.tag == tag ) then table.insert(groups, v) end end return groups end, parse = function(data) local resp = IPP.Response:new() local pos resp.version, resp.status, resp.reqid, pos = string.unpack(">I2I2I4", data) resp.attrib_groups = {} local group = nil repeat local tag = data:byte(pos, pos) if ( tag == IPP.Attribute.IPP_TAG_OPERATION or tag == IPP.Attribute.IPP_TAG_JOB or tag == IPP.Attribute.IPP_TAG_PRINTER or tag == IPP.Attribute.IPP_TAG_END ) then if group then table.insert(resp.attrib_groups, group) end if tag ~= IPP.Attribute.IPP_TAG_END then group = IPP.AttributeGroup:new(tag) else group = nil end pos = pos + 1 else if not group then stdnse.debug2("Unexpected tag: %d", tag) return end local attrib pos, attrib = IPP.Attribute.parse(data, pos) group:addAttribute(attrib) end until pos > #data return resp end, }, } HTTP = { Request = function(host, port, request) local headers = { ['Content-Type'] = 'application/ipp', ['User-Agent'] = 'CUPS/1.5.1', } port.version.service_tunnel = "ssl" local http_resp = http.post(host, port, '/', { header = headers }, nil, tostring(request)) if ( http_resp.status ~= 200 ) then return false, "Unexpected response from server" end local response = IPP.Response.parse(http_resp.body) if ( not(response) ) then return false, "Failed to parse response" end return true, response end, } Helper = { new = function(self, host, port, options) local o = { host = host, port = port, options = options or {} } setmetatable(o, self) self.__index = self return o end, connect = function(self) self.socket = nmap.new_socket() self.socket:set_timeout(self.options.timeout or 10000) return self.socket:connect(self.host, self.port) end, getPrinters = function(self) local attribs = { IPP.Attribute:new(IPP.Attribute.IPP_TAG_CHARSET, "attributes-charset", "utf-8" ), IPP.Attribute:new(IPP.Attribute.IPP_TAG_LANGUAGE, "attributes-natural-language", "en"), } local ag = IPP.AttributeGroup:new(IPP.Attribute.IPP_TAG_OPERATION, attribs) local request = IPP.Request:new(IPP.OperationID.CUPS_GET_PRINTERS, 1) request:addAttributeGroup(ag) local status, response = HTTP.Request( self.host, self.port, tostring(request) ) if ( not(response) ) then return status, response end local printers = {} for _, ag in ipairs(response:getAttributeGroups(IPP.Attribute.IPP_TAG_PRINTER)) do local attrib = { ["printer-name"] = "name", ["printer-location"] = "location", ["printer-make-and-model"] = "model", ["printer-state"] = "state", ["queued-job-count"] = "queue_count", ["printer-dns-sd-name"] = "dns_sd_name", } local printer = {} for k, v in pairs(attrib) do if ( ag:getAttributeValue(k) ) then printer[v] = ag:getAttributeValue(k) end end table.insert(printers, printer) end return true, printers end, getQueueInfo = function(self, uri) local uri = uri or ("ipp://%s/"):format(self.host.ip) local attribs = { IPP.Attribute:new(IPP.Attribute.IPP_TAG_CHARSET, "attributes-charset", "utf-8" ), IPP.Attribute:new(IPP.Attribute.IPP_TAG_LANGUAGE, "attributes-natural-language", "en-us"), IPP.Attribute:new(IPP.Attribute.IPP_TAG_URI, "printer-uri", uri), IPP.Attribute:new(IPP.Attribute.IPP_TAG_KEYWORD, "requested-attributes", { -- { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "job-originating-host-name"}, { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "com.apple.print.JobInfo.PMJobName"}, { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "com.apple.print.JobInfo.PMJobOwner"}, { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "job-id" }, { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "job-k-octets" }, { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "job-name" }, { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "job-state" }, { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "printer-uri" }, -- { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "job-originating-user-name" }, -- { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "job-printer-state-message" }, -- { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "job-printer-uri" }, { tag = IPP.Attribute.IPP_TAG_KEYWORD, val = "time-at-creation" } } ), IPP.Attribute:new(IPP.Attribute.IPP_TAG_KEYWORD, "which-jobs", "not-completed" ) } local ag = IPP.AttributeGroup:new(IPP.Attribute.IPP_TAG_OPERATION, attribs) local request = IPP.Request:new(IPP.OperationID.IPP_GET_JOBS, 1) request:addAttributeGroup(ag) local status, response = HTTP.Request( self.host, self.port, tostring(request) ) if ( not(response) ) then return status, response end local results = {} for _, ag in ipairs(response:getAttributeGroups(IPP.Attribute.IPP_TAG_JOB)) do local uri = ag:getAttributeValue("printer-uri") local printer = uri:match(".*/(.*)$") or "Unknown" -- some jobs have multiple state attributes, so far the ENUM ones have been correct local state = ag:getAttributeValue("job-state", IPP.Attribute.IPP_TAG_ENUM) or ag:getAttributeValue("job-state") -- some jobs have multiple id tag, so far the INTEGER type have shown the correct ID local id = ag:getAttributeValue("job-id", IPP.Attribute.IPP_TAG_INTEGER) or ag:getAttributeValue("job-id") local attr = ag:getAttribute("time-at-creation") local tm = ag:getAttributeValue("time-at-creation") local size = ag:getAttributeValue("job-k-octets") .. "k" local jobname = ag:getAttributeValue("com.apple.print.JobInfo.PMJobName") or "Unknown" local owner = ag:getAttributeValue("com.apple.print.JobInfo.PMJobOwner") or "Unknown" results[printer] = results[printer] or {} table.insert(results[printer], { id = id, time = datetime.format_timestamp(tm), state = ( IPP.StateName[tonumber(state)] or "Unknown" ), size = size, owner = owner, jobname = jobname }) end local output = {} for name, entries in pairs(results) do local t = tab.new(5) tab.addrow(t, "id", "time", "state", "size (kb)", "owner", "jobname") for _, entry in ipairs(entries) do tab.addrow(t, entry.id, entry.time, entry.state, entry.size, entry.owner, entry.jobname) end if ( 1<#t ) then table.insert(output, { name = name, tab.dump(t) }) end end return output end, close = function(self) return self.socket:close() end, } return _ENV;