Feature/IPP: Fix Retry-TLS and Collect All IPP Attributes (#143)
* Correctly enables TLS only during retry grab when retry-tls flag is set. * Fixes evaluation which caused too many arguments error in IPP integration test. * Updates IPP zgrab2 schema to reflect storing all attributes in response. * Adds Attributes member to ScanResults * Ensures tryReadAttributes only reads attributes in the case of a postive detection. Cleans up isIPP * Reads all attributes in IPP response * Detects invalid length errors when reading IPP attributes. * Returns the correct amount of uris, ipp versions, and cups versions in ScanResults.
This commit is contained in:
parent
db6bf4c8b6
commit
c11be290dc
@ -36,7 +36,7 @@ function test_cups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function test_cups_tls() {
|
function test_cups_tls() {
|
||||||
echo "ipp/test: Tests runner for ipp_cups"
|
echo "ipp/test: Tests runner for ipp_cups-tls"
|
||||||
|
|
||||||
CONTAINER_NAME="zgrab_ipp_cups-tls" $ZGRAB_ROOT/docker-runner/docker-run.sh ipp --timeout 3s --ipps --verbose > "$OUTPUT_ROOT/cups-tls.json"
|
CONTAINER_NAME="zgrab_ipp_cups-tls" $ZGRAB_ROOT/docker-runner/docker-run.sh ipp --timeout 3s --ipps --verbose > "$OUTPUT_ROOT/cups-tls.json"
|
||||||
# FIXME: No good reason to use a tmp file & saved file, b/c I'm not testing any failure states yet
|
# FIXME: No good reason to use a tmp file & saved file, b/c I'm not testing any failure states yet
|
||||||
@ -60,7 +60,7 @@ function test_cups_tls() {
|
|||||||
echo "ipp/test: Incorrect CUPS version. Expected CUPS/2.1, got $cups"
|
echo "ipp/test: Incorrect CUPS version. Expected CUPS/2.1, got $cups"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ $tls = "null" ]; then
|
if [ "$tls" = "null" ]; then
|
||||||
echo "ipp/test: No TLS handshake logged"
|
echo "ipp/test: No TLS handshake logged"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// TODO: Describe module, the flags, the probe, the output, etc.
|
// TODO: Describe module, the flags, the probe, the output, etc.
|
||||||
package ipp
|
package ipp
|
||||||
|
|
||||||
//TODO: Clean up these imports
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@ -43,6 +42,7 @@ var (
|
|||||||
ErrVersionNotSupported = errors.New("IPP version not supported")
|
ErrVersionNotSupported = errors.New("IPP version not supported")
|
||||||
|
|
||||||
Versions = []version{{Major: 2, Minor: 1}, {Major: 2, Minor: 0}, {Major: 1, Minor: 1}, {Major: 1, Minor: 0}}
|
Versions = []version{{Major: 2, Minor: 1}, {Major: 2, Minor: 0}, {Major: 1, Minor: 1}, {Major: 1, Minor: 0}}
|
||||||
|
AttributesCharset = []byte{0x47, 0x00, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74}
|
||||||
)
|
)
|
||||||
|
|
||||||
type scan struct {
|
type scan struct {
|
||||||
@ -51,6 +51,7 @@ type scan struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
results ScanResults
|
results ScanResults
|
||||||
url string
|
url string
|
||||||
|
tls bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Tag relevant results and exlain in comments
|
//TODO: Tag relevant results and exlain in comments
|
||||||
@ -62,21 +63,21 @@ type ScanResults struct {
|
|||||||
|
|
||||||
// RedirectResponseChain is non-empty if the scanner follows a redirect.
|
// RedirectResponseChain is non-empty if the scanner follows a redirect.
|
||||||
// It contains all redirect responses prior to the final response.
|
// It contains all redirect responses prior to the final response.
|
||||||
RedirectResponseChain []*http.Response `json:"redirect_response_chain,omitempty" zgrab:"debug"`
|
RedirectResponseChain []*http.Response `json:"redirect_response_chain,omitempty"`
|
||||||
|
|
||||||
MajorVersion *int8 `json:"version_major,omitempty"`
|
MajorVersion *int8 `json:"version_major,omitempty"`
|
||||||
MinorVersion *int8 `json:"version_minor,omitempty"`
|
MinorVersion *int8 `json:"version_minor,omitempty"`
|
||||||
VersionString string `json:"version_string,omitempty"`
|
VersionString string `json:"version_string,omitempty"`
|
||||||
CUPSVersion string `json:"cups_version,omitempty"`
|
CUPSVersion string `json:"cups_version,omitempty"`
|
||||||
|
|
||||||
|
Attributes []*Attribute `json:"attributes,omitempty" zgrab:"debug"`
|
||||||
AttributeCUPSVersion string `json:"attr_cups_version,omitempty"`
|
AttributeCUPSVersion string `json:"attr_cups_version,omitempty"`
|
||||||
AttributeIPPVersions []string `json:"attr_ipp_versions,omitempty"`
|
AttributeIPPVersions []string `json:"attr_ipp_versions,omitempty"`
|
||||||
AttributePrinterURI string `json:"attr_printer_uri,omitempty"`
|
AttributePrinterURIs []string `json:"attr_printer_uris,omitempty"`
|
||||||
|
|
||||||
TLSLog *zgrab2.TLSLog `json:"tls,omitempty"`
|
TLSLog *zgrab2.TLSLog `json:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Annotate every flag thoroughly
|
|
||||||
// TODO: Add more protocol-specific flags as needed
|
|
||||||
// Flags holds the command-line configuration for the ipp scan module.
|
// Flags holds the command-line configuration for the ipp scan module.
|
||||||
// Populated by the framework.
|
// Populated by the framework.
|
||||||
type Flags struct {
|
type Flags struct {
|
||||||
@ -84,7 +85,7 @@ type Flags struct {
|
|||||||
zgrab2.TLSFlags
|
zgrab2.TLSFlags
|
||||||
Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"`
|
Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"`
|
||||||
|
|
||||||
//FIXME: Borrowed from http module
|
//FIXME: Borrowed from http module, determine whether this is all needed
|
||||||
MaxSize int `long:"max-size" default:"256" description:"Max kilobytes to read in response to an IPP request"`
|
MaxSize int `long:"max-size" default:"256" description:"Max kilobytes to read in response to an IPP request"`
|
||||||
MaxRedirects int `long:"max-redirects" default:"0" description:"Max number of redirects to follow"`
|
MaxRedirects int `long:"max-redirects" default:"0" description:"Max number of redirects to follow"`
|
||||||
UserAgent string `long:"user-agent" default:"Mozilla/5.0 zgrab/0.x" description:"Set a custom user agent"`
|
UserAgent string `long:"user-agent" default:"Mozilla/5.0 zgrab/0.x" description:"Set a custom user agent"`
|
||||||
@ -182,24 +183,6 @@ func (scanner *Scanner) GetPort() uint {
|
|||||||
return scanner.config.Port
|
return scanner.config.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasContentType(resp *http.Response, contentType string) (bool, error) {
|
|
||||||
// TODO: Capture parameters and report them in ScanResults?
|
|
||||||
// Parameters can be ignored, since there are no required or optional parameters
|
|
||||||
// IPP parameters specified at https://www.iana.org/assignments/media-types/application/ipp
|
|
||||||
mediatype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
|
||||||
// TODO: See if empty media type is sufficient as failure indicator,
|
|
||||||
// there could be other states where reading mediatype screwed up, but isn't empty (ie: corrupted/malformed)
|
|
||||||
if mediatype == "" && err != nil {
|
|
||||||
//TODO: Handle errors in a weird way, since media type is still returned
|
|
||||||
// if there's an error when parsing optional parameters
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
// FIXME: Maybe pass the error along, maybe not. We got what we wanted.
|
|
||||||
return mediatype == contentType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Cleaner to write this code, possibly slower than copy-pasted version
|
|
||||||
// FIXME: Quite possibly not easier to read ("What does storeBody do? Where does it store it?")
|
|
||||||
// FIXME: Add some error handling somewhere in here, unless errors should just be ignored and we get what we get
|
// FIXME: Add some error handling somewhere in here, unless errors should just be ignored and we get what we get
|
||||||
func storeBody(res *http.Response, scanner *Scanner) {
|
func storeBody(res *http.Response, scanner *Scanner) {
|
||||||
b := bufferFromBody(res, scanner)
|
b := bufferFromBody(res, scanner)
|
||||||
@ -224,83 +207,201 @@ func bufferFromBody(res *http.Response, scanner *Scanner) *bytes.Buffer {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: This will read the wrong section of the body if a substring matches the attribute name passed in
|
type Value struct {
|
||||||
// TODO: Support reading from multiple instances of the same attribute in a response
|
Bytes []byte `json:"raw,omitempty"`
|
||||||
func readAttributeFromBody(attrString string, body *[]byte) ([][]byte, error) {
|
|
||||||
attr := []byte(attrString)
|
|
||||||
interims := bytes.Split(*body, attr)
|
|
||||||
if len(interims) > 1 {
|
|
||||||
valueTag := interims[0][len(interims[0])-3]
|
|
||||||
var vals [][]byte
|
|
||||||
buf := bytes.NewBuffer(interims[1])
|
|
||||||
// This reading occurs in a loop because some attributes can have type "1 setOf <type>"
|
|
||||||
// where same attribute has a set of values, rather than one
|
|
||||||
for tag, nameLength := valueTag, int16(0); tag == valueTag && nameLength == 0; {
|
|
||||||
var length int16
|
|
||||||
if err := binary.Read(buf, binary.BigEndian, &length); err != nil {
|
|
||||||
//Couldn't read length of content
|
|
||||||
return vals, err
|
|
||||||
}
|
|
||||||
val := make([]byte, length)
|
|
||||||
if err := binary.Read(buf, binary.BigEndian, &val); err != nil {
|
|
||||||
//Couldn't read content
|
|
||||||
vals = append(vals, val)
|
|
||||||
return vals, err
|
|
||||||
}
|
|
||||||
vals = append(vals, val)
|
|
||||||
if err := binary.Read(buf, binary.BigEndian, &tag); err != nil {
|
|
||||||
//Couldn't read next valueTag
|
|
||||||
return vals, err
|
|
||||||
}
|
|
||||||
// FIXME: Only try to read next namelength if previous valueTag wasn't end-of-attributes-tag
|
|
||||||
if err := binary.Read(buf, binary.BigEndian, &nameLength); err != nil {
|
|
||||||
//Couldn't read next nameLength
|
|
||||||
return vals, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vals, nil
|
|
||||||
}
|
|
||||||
//The attribute was not present
|
|
||||||
return nil, errors.New("Attribute \"" + attrString + "\" not present.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (scan *scan) tryReadAttributes(body string) {
|
type Attribute struct {
|
||||||
bodyBytes := []byte(body)
|
Name string `json:"name,omitempty"`
|
||||||
// Write reported CUPS version to results object
|
Values []Value `json:"values,omitempty"`
|
||||||
if scan.results.AttributeCUPSVersion == "" {
|
ValueTag byte `json:"tag,omitempty"`
|
||||||
if cupsVersions, err := readAttributeFromBody(CupsVersion, &bodyBytes); err != nil {
|
}
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"error": err,
|
func shouldReturnAttrs(length, soFar, size, upperBound int) (bool, error) {
|
||||||
"attribute": CupsVersion,
|
if soFar + length > size {
|
||||||
}).Debug("Failed to read attribute.")
|
// Size should never exceed upperBound in practice because of truncation, but this is more general
|
||||||
} else if len(cupsVersions) > 0 {
|
if size >= upperBound {
|
||||||
scan.results.AttributeCUPSVersion = string(cupsVersions[0])
|
return true, nil
|
||||||
}
|
}
|
||||||
|
return true, zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("Reported field length runs out of bounds."))
|
||||||
|
|
||||||
}
|
}
|
||||||
// Write reported IPP versions to results object
|
return false, nil
|
||||||
if len(scan.results.AttributeIPPVersions) == 0 {
|
}
|
||||||
if ippVersions, err := readAttributeFromBody(VersionsSupported, &bodyBytes); err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
func detectReadBodyError(err error) error {
|
||||||
"error": err,
|
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||||
"attribute": VersionsSupported,
|
return zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("Fewer body bytes read than expected."))
|
||||||
}).Debug("Failed to read attribute.")
|
}
|
||||||
|
return zgrab2.NewScanError(zgrab2.TryGetScanStatus(err), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An IPP response contains the following data (as specified in RFC 8010 Section 3.1.8
|
||||||
|
https://tools.ietf.org/html/rfc8010#section-3.1.8)
|
||||||
|
bytes name
|
||||||
|
----------------------------
|
||||||
|
2 version-number
|
||||||
|
2 status-code
|
||||||
|
4 request-id
|
||||||
|
|
||||||
|
(0 or more instances of the following pair of fields)
|
||||||
|
1 delimiter-tag OR value-tag
|
||||||
|
x empty if delimiter-tag to begin a group OR rest of attribute if value-tag
|
||||||
|
|
||||||
|
1 end-of-attributes-tag
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Those x bytes of any given attribute consist of the following (as specified in RFC 8010 Section 3.1.4
|
||||||
|
https://tools.ietf.org/html/rfc8010#section-3.1.4)
|
||||||
|
----------------------------
|
||||||
|
2 name-length = u
|
||||||
|
u name
|
||||||
|
2 value-length = v
|
||||||
|
v value
|
||||||
|
----------------------------
|
||||||
|
*/
|
||||||
|
func readAllAttributes(body []byte, scanner *Scanner) ([]*Attribute, error) {
|
||||||
|
var attrs []*Attribute
|
||||||
|
bytesRead := 0
|
||||||
|
buf := bytes.NewBuffer(body)
|
||||||
|
// Each field of this struct is exported to avoid binary.Read panicking
|
||||||
|
var start struct {
|
||||||
|
Version int16
|
||||||
|
StatusCode int16
|
||||||
|
ReqID int32
|
||||||
|
}
|
||||||
|
// Read in pre-attribute part of body to ignore it
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &start); err != nil {
|
||||||
|
return attrs, detectReadBodyError(err)
|
||||||
|
}
|
||||||
|
bytesRead += 8
|
||||||
|
// Read in first delimiter tag, usually a begin-attribute-group-tag (which is equal to 1)
|
||||||
|
var tag byte
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &tag); err != nil {
|
||||||
|
return attrs, detectReadBodyError(err)
|
||||||
|
}
|
||||||
|
bytesRead++
|
||||||
|
var lastTag byte
|
||||||
|
// Until encountering end-of-attributes-tag (which is equal to 3):
|
||||||
|
for tag != 0x03 {
|
||||||
|
// If tag is a delimiter-tag ([0x00, 0x05]), read the next tag, which corresponds to the first
|
||||||
|
// attribute's value-tag
|
||||||
|
if tag <= 0x05 {
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &tag); err != nil {
|
||||||
|
return attrs, detectReadBodyError(err)
|
||||||
|
}
|
||||||
|
bytesRead++
|
||||||
|
}
|
||||||
|
// TODO: Implement parsing attribute collections, since they're special
|
||||||
|
// Read in length of attribute's name, which will be used to determine whether this attribute stands alone
|
||||||
|
// or provides an additonal value for the previous attribute
|
||||||
|
var nameLength int16
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &nameLength); err != nil {
|
||||||
|
return attrs, detectReadBodyError(err)
|
||||||
|
}
|
||||||
|
bytesRead += 2
|
||||||
|
// If reading the name would entail reading past body, check whether body was truncated
|
||||||
|
if should, err := shouldReturnAttrs(int(nameLength), bytesRead, len(body), scanner.config.MaxSize * 1024); should {
|
||||||
|
// If body was truncated, return all attributes so far without error
|
||||||
|
// Otherwise, return a protocol error because name-length should indicate the
|
||||||
|
// length of the following name when obeying the protocol's encoding
|
||||||
|
return attrs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var attr *Attribute
|
||||||
|
// If sequential tags match and name-length of the latter is 0, the second attribute is
|
||||||
|
// an additional value for the former, so we read and append another value for that attr
|
||||||
|
if tag == lastTag && nameLength == 0 {
|
||||||
|
attr = attrs[len(attrs)-1]
|
||||||
|
// Otherwise, create a new attribute and read in its name
|
||||||
} else {
|
} else {
|
||||||
for _, v := range ippVersions {
|
attr = &Attribute{ValueTag: tag}
|
||||||
scan.results.AttributeIPPVersions = append(scan.results.AttributeIPPVersions, string(v))
|
attrs = append(attrs, attr)
|
||||||
|
}
|
||||||
|
// Read in name into this slice (or no name into an empty slice if nameLength == 0)
|
||||||
|
name := make([]byte, nameLength)
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &name); err != nil {
|
||||||
|
return attrs, detectReadBodyError(err)
|
||||||
|
}
|
||||||
|
bytesRead += int(nameLength)
|
||||||
|
if attr.Name == "" {
|
||||||
|
attr.Name = string(name)
|
||||||
|
}
|
||||||
|
// Determine length of current value of the current attribute
|
||||||
|
var length int16
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &length); err != nil {
|
||||||
|
return attrs, detectReadBodyError(err)
|
||||||
|
}
|
||||||
|
bytesRead += 2
|
||||||
|
// If reading the name would entail reading past body, check whether body was truncated
|
||||||
|
if should, err := shouldReturnAttrs(int(length), bytesRead, len(body), scanner.config.MaxSize * 1024); should {
|
||||||
|
// If body was truncated, return all attributes so far without error
|
||||||
|
// Otherwise, return a protocol error because name-length should indicate the
|
||||||
|
// length of the following name when obeying the protocol's encoding
|
||||||
|
return attrs, err
|
||||||
|
}
|
||||||
|
if length > 0 {
|
||||||
|
// Read and append a value to the current attribute
|
||||||
|
val := make([]byte, length)
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &val); err != nil {
|
||||||
|
return attrs, detectReadBodyError(err)
|
||||||
|
}
|
||||||
|
bytesRead += int(length)
|
||||||
|
attr.Values = append(attr.Values, Value{Bytes: val})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read in the following tag to be assessed at the next iteration's start
|
||||||
|
lastTag = tag
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &tag); err != nil {
|
||||||
|
return attrs, detectReadBodyError(err)
|
||||||
|
}
|
||||||
|
bytesRead++
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scanner *Scanner) tryReadAttributes(resp *http.Response, scan *scan) *zgrab2.ScanError {
|
||||||
|
body := []byte(resp.BodyText)
|
||||||
|
// A well-formed IPP response MUST include the required status-code field.
|
||||||
|
// "If an IPP status-code is returned, the HTTP status-code MUST be 200"
|
||||||
|
// Therefore, an HTTP Status Code other than 200 indicates the response is not a well-formed IPP response.
|
||||||
|
// RFC 8010 Section 3.4.3 Source: https://tools.ietf.org/html/rfc8010#section-3.4.3
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, errors.New("Response returned with status " + resp.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject successful responses which specify non-IPP MIME mediatype (ie: text/html)
|
||||||
|
// RFC 8010's abstract specifies that IPP uses the MIME media type "application/ipp"
|
||||||
|
if !isIPP(resp) {
|
||||||
|
return zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("IPP Content-Type not detected."))
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs, err := readAllAttributes(body, scanner)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Handle error appropriately
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
"body": resp.BodyText,
|
||||||
|
}).Debug("Failed to read attributes from body with error.")
|
||||||
|
}
|
||||||
|
scan.results.Attributes = append(scan.results.Attributes, attrs...)
|
||||||
|
|
||||||
|
for _, attr := range scan.results.Attributes {
|
||||||
|
if attr.Name == CupsVersion && scan.results.AttributeCUPSVersion == "" {
|
||||||
|
scan.results.AttributeCUPSVersion = string(attr.Values[0].Bytes)
|
||||||
|
}
|
||||||
|
if attr.Name == VersionsSupported && len(scan.results.AttributeIPPVersions) == 0 {
|
||||||
|
for _, v := range attr.Values {
|
||||||
|
scan.results.AttributeIPPVersions = append(scan.results.AttributeIPPVersions, string(v.Bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if attr.Name == PrinterURISupported {
|
||||||
// Write reported printer URI to results object
|
scan.results.AttributePrinterURIs = append(scan.results.AttributePrinterURIs, string(attr.Values[0].Bytes))
|
||||||
if scan.results.AttributePrinterURI == "" {
|
|
||||||
if uris, err := readAttributeFromBody(PrinterURISupported, &bodyBytes); err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"error": err,
|
|
||||||
"attribute": PrinterURISupported,
|
|
||||||
}).Debug("Failed to read attribute.")
|
|
||||||
} else if len(uris) > 0 {
|
|
||||||
scan.results.AttributePrinterURI = string(uris[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func versionNotSupported(body string) bool {
|
func versionNotSupported(body string) bool {
|
||||||
@ -341,7 +442,9 @@ func (scanner *Scanner) augmentWithCUPSData(scan *scan, target *zgrab2.ScanTarge
|
|||||||
return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, ErrVersionNotSupported)
|
return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, ErrVersionNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
scan.tryReadAttributes(scan.results.CUPSResponse.BodyText)
|
if err := scanner.tryReadAttributes(scan.results.CUPSResponse, scan); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,9 +485,36 @@ func sendIPPRequest(scan *scan, body *bytes.Buffer) (*http.Response, *zgrab2.Sca
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasContentType(resp *http.Response, contentType string) bool {
|
||||||
|
// Removal of everything post-comma added in response to empirical examples of Virata-EmWeb
|
||||||
|
// print servers listed with "Content-Type" of "application/ipp, public"
|
||||||
|
cType := strings.Split(resp.Header.Get("Content-Type"), ",")[0]
|
||||||
|
// Parameters can be ignored, since there are no required or optional parameters
|
||||||
|
// IPP parameters specified at https://www.iana.org/assignments/media-types/application/ipp
|
||||||
|
mediatype, _, err := mime.ParseMediaType(cType)
|
||||||
|
// Certainly doesn't have correct Content-Type if there was a malformed or empty Content-Type
|
||||||
|
if mediatype == "" && err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Check for only subtype added in resonse to empirical examples of Rapid Logic print servers
|
||||||
|
// listed with "Content-Type" of "IPP"
|
||||||
|
subType := strings.Split(contentType, "/")[1]
|
||||||
|
return strings.HasPrefix(mediatype, contentType) || strings.HasPrefix(mediatype, subType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIPP(resp *http.Response) bool {
|
||||||
|
hasIPP := hasContentType(resp, ContentType)
|
||||||
|
body := []byte(resp.BodyText)
|
||||||
|
// If Content-Type header doesn't clearly indicate IPP, but "attributes-charset"
|
||||||
|
// attribute is specified in the correct format for IPP, still indicate a positive detection
|
||||||
|
// This is in response to empirical evidence of all false negatives specifying "attributes-charset"
|
||||||
|
// in the correct format.
|
||||||
|
return resp.StatusCode == 200 && (hasIPP || bytes.Contains(body, AttributesCharset))
|
||||||
|
}
|
||||||
|
|
||||||
func (scanner *Scanner) Grab(scan *scan, target *zgrab2.ScanTarget, version *version) *zgrab2.ScanError {
|
func (scanner *Scanner) Grab(scan *scan, target *zgrab2.ScanTarget, version *version) *zgrab2.ScanError {
|
||||||
// Send get-printer-attributes request to the host, preferably a print server
|
// Send get-printer-attributes request to the host, preferably a print server
|
||||||
body := getPrinterAttributesRequest(version.Major, version.Minor, scan.url, scanner.config.IPPSecure)
|
body := getPrinterAttributesRequest(version.Major, version.Minor, scan.url, scan.tls)
|
||||||
// TODO: Log any weird errors coming out of this
|
// TODO: Log any weird errors coming out of this
|
||||||
resp, err := sendIPPRequest(scan, body)
|
resp, err := sendIPPRequest(scan, body)
|
||||||
//Store response regardless of error in request, because we may have gotten something back
|
//Store response regardless of error in request, because we may have gotten something back
|
||||||
@ -431,29 +561,21 @@ func (scanner *Scanner) Grab(scan *scan, target *zgrab2.ScanTarget, version *ver
|
|||||||
}
|
}
|
||||||
if strings.HasPrefix(strings.ToUpper(p), "CUPS/") {
|
if strings.HasPrefix(strings.ToUpper(p), "CUPS/") {
|
||||||
scan.results.CUPSVersion = p
|
scan.results.CUPSVersion = p
|
||||||
err := scanner.augmentWithCUPSData(scan, target, version)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"error": err,
|
|
||||||
}).Debug("Failed to augment with CUPS-get-printers request.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Cite RFC justification for this
|
if err := scanner.tryReadAttributes(scan.results.Response, scan); err != nil {
|
||||||
// Reject successful responses which specify non-IPP MIME mediatype (ie: text/html)
|
return err
|
||||||
if isIPP, _ := hasContentType(resp, ContentType);
|
|
||||||
resp.StatusCode == 200 && resp.Header.Get("Content-Type") != "" && !isIPP {
|
|
||||||
// TODO: Log error if any
|
|
||||||
return zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("application/ipp not present in Content-Type header."))
|
|
||||||
}
|
}
|
||||||
|
if scan.results.CUPSVersion != "" {
|
||||||
if resp.StatusCode != 200 {
|
err := scanner.augmentWithCUPSData(scan, target, version)
|
||||||
return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, errors.New("Response returned with status " + resp.Status))
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Debug("Failed to augment with CUPS-get-printers request.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scan.tryReadAttributes(scan.results.Response.BodyText)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,7 +649,7 @@ func getHTTPURL(https bool, host string, port uint16, endpoint string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adapted from newHTTPScan in zgrab2 http module
|
// Adapted from newHTTPScan in zgrab2 http module
|
||||||
func (scanner *Scanner) newIPPScan(target *zgrab2.ScanTarget) *scan {
|
func (scanner *Scanner) newIPPScan(target *zgrab2.ScanTarget, tls bool) *scan {
|
||||||
newScan := scan{
|
newScan := scan{
|
||||||
client: http.MakeNewClient(),
|
client: http.MakeNewClient(),
|
||||||
}
|
}
|
||||||
@ -544,6 +666,7 @@ func (scanner *Scanner) newIPPScan(target *zgrab2.ScanTarget) *scan {
|
|||||||
newScan.client.UserAgent = scanner.config.UserAgent
|
newScan.client.UserAgent = scanner.config.UserAgent
|
||||||
newScan.client.Transport = transport
|
newScan.client.Transport = transport
|
||||||
newScan.client.Jar = nil // Don't transfer cookies FIXME: Stolen from HTTP, unclear if needed
|
newScan.client.Jar = nil // Don't transfer cookies FIXME: Stolen from HTTP, unclear if needed
|
||||||
|
newScan.tls = tls
|
||||||
host := target.Domain
|
host := target.Domain
|
||||||
if host == "" {
|
if host == "" {
|
||||||
// FIXME: I only know this works for sure for IPv4, uri string might get weird w/ IPv6
|
// FIXME: I only know this works for sure for IPv4, uri string might get weird w/ IPv6
|
||||||
@ -555,14 +678,24 @@ func (scanner *Scanner) newIPPScan(target *zgrab2.ScanTarget) *scan {
|
|||||||
return &newScan
|
return &newScan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup closes any connections that have been opened during the scan
|
||||||
|
func (scan *scan) Cleanup() {
|
||||||
|
if scan.connections != nil {
|
||||||
|
for _, conn := range scan.connections {
|
||||||
|
defer conn.Close()
|
||||||
|
}
|
||||||
|
scan.connections = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Do you want to retry with TLS for all versions? Just one's you've already tried? Haven't tried? Just the same version?
|
// TODO: Do you want to retry with TLS for all versions? Just one's you've already tried? Haven't tried? Just the same version?
|
||||||
func (scanner *Scanner) tryGrabForVersions(target *zgrab2.ScanTarget, versions *[]version) (*scan, *zgrab2.ScanError) {
|
func (scanner *Scanner) tryGrabForVersions(target *zgrab2.ScanTarget, versions []version, tls bool) (*scan, *zgrab2.ScanError) {
|
||||||
scan := scanner.newIPPScan(target)
|
scan := scanner.newIPPScan(target, tls)
|
||||||
// TODO: Implement scan.Cleanup()
|
defer scan.Cleanup()
|
||||||
var err *zgrab2.ScanError
|
var err *zgrab2.ScanError
|
||||||
for i := 0; i < len(*versions); i++ {
|
for i := 0; i < len(versions); i++ {
|
||||||
err = scanner.Grab(scan, target, &(*versions)[i])
|
err = scanner.Grab(scan, target, &versions[i])
|
||||||
if err != nil && err.Err == ErrVersionNotSupported && i < len(*versions)-1 {
|
if err != nil && err.Err == ErrVersionNotSupported && i < len(versions)-1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -575,7 +708,7 @@ func (scanner *Scanner) tryGrabForVersions(target *zgrab2.ScanTarget, versions *
|
|||||||
func (scan *scan) shouldReportResult(scanner *Scanner) bool {
|
func (scan *scan) shouldReportResult(scanner *Scanner) bool {
|
||||||
if scan.results.Response != nil {
|
if scan.results.Response != nil {
|
||||||
return true
|
return true
|
||||||
} else if scanner.config.IPPSecure {
|
} else if scan.tls {
|
||||||
l := scan.results.TLSLog
|
l := scan.results.TLSLog
|
||||||
return l != nil && l.HandshakeLog != nil && l.HandshakeLog.ServerHello != nil
|
return l != nil && l.HandshakeLog != nil && l.HandshakeLog.ServerHello != nil
|
||||||
}
|
}
|
||||||
@ -587,7 +720,7 @@ func (scan *scan) shouldReportResult(scanner *Scanner) bool {
|
|||||||
//2. Take in that response & read out version numbers
|
//2. Take in that response & read out version numbers
|
||||||
func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) {
|
func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) {
|
||||||
// Try all known IPP versions from newest to oldest until we reach a supported version
|
// Try all known IPP versions from newest to oldest until we reach a supported version
|
||||||
scan, err := scanner.tryGrabForVersions(&target, &Versions)
|
scan, err := scanner.tryGrabForVersions(&target, Versions, scanner.config.IPPSecure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If versionNotSupported error was confirmed, the scanner was connecting w/o TLS, so don't retry
|
// If versionNotSupported error was confirmed, the scanner was connecting w/o TLS, so don't retry
|
||||||
// Same goes for a protocol error of any kind. It means we got something back but it didn't conform.
|
// Same goes for a protocol error of any kind. It means we got something back but it didn't conform.
|
||||||
@ -595,13 +728,16 @@ func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, inter
|
|||||||
return err.Unpack(&scan.results)
|
return err.Unpack(&scan.results)
|
||||||
}
|
}
|
||||||
if scanner.config.RetryTLS && !scanner.config.IPPSecure {
|
if scanner.config.RetryTLS && !scanner.config.IPPSecure {
|
||||||
scanner.config.IPPSecure = true
|
retry, retryErr := scanner.tryGrabForVersions(&target, Versions, true)
|
||||||
retry, retryErr := scanner.tryGrabForVersions(&target, &Versions)
|
|
||||||
if retryErr != nil {
|
if retryErr != nil {
|
||||||
if retry.shouldReportResult(scanner) {
|
if retry.shouldReportResult(scanner) {
|
||||||
return err.Unpack(&retry.results)
|
return retryErr.Unpack(&retry.results)
|
||||||
}
|
}
|
||||||
return zgrab2.TryGetScanStatus(err), nil, err
|
// Use original result as a fallback when retry result shouldn't be returned
|
||||||
|
if scan.shouldReportResult(scanner) {
|
||||||
|
return err.Unpack(&scan.results)
|
||||||
|
}
|
||||||
|
return zgrab2.TryGetScanStatus(retryErr), nil, retryErr
|
||||||
}
|
}
|
||||||
return zgrab2.SCAN_SUCCESS, &retry.results, nil
|
return zgrab2.SCAN_SUCCESS, &retry.results, nil
|
||||||
}
|
}
|
||||||
|
@ -147,15 +147,56 @@ http_response_full = SubRecord({
|
|||||||
"request": http_request_full
|
"request": http_request_full
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# TODO: Determine whether value-tag types with same underlying form should have a different name in this mapping
|
||||||
|
# TODO: Add method to decode these values appropriately. Encoding of different tag's attribute values specified
|
||||||
|
# in Table 7 of RFC 8010 Section 3.9 (https://tools.ietf.org/html/rfc8010#section-3.9)
|
||||||
|
# "value-tag" values which specify the interpretation of an attribute's value:
|
||||||
|
# From RFC 8010 Section 3.5.2 (https://tools.ietf.org/html/rfc8010#section-3.5.2)
|
||||||
|
# Note: value-tag values are camelCase because the names are specified that way in RFC
|
||||||
|
ipp_attribute_value = SubRecord({
|
||||||
|
"raw": Binary(),
|
||||||
|
"integer": Signed32BitInteger(),
|
||||||
|
"boolean": Boolean(),
|
||||||
|
"enum": String(),
|
||||||
|
"octetString": Binary(),
|
||||||
|
"dateTime": DateTime(),
|
||||||
|
# TODO: Determine appropriate type for resolution
|
||||||
|
"resolution": Binary(),
|
||||||
|
# TODO: Determine appropriate type for range of Integers (probably {min, max} pair)
|
||||||
|
"rangeOfInteger": Binary(),
|
||||||
|
# TODO: Determine appropriate type for beginning of attribute collection
|
||||||
|
"begCollection": Binary(),
|
||||||
|
"textWithLanguage": String(),
|
||||||
|
"nameWithLanguage": String(),
|
||||||
|
# TODO: Determine appropriate type for end of attribute collection
|
||||||
|
"endCollection": Binary(),
|
||||||
|
"textWithoutLanguage": String(),
|
||||||
|
"nameWithoutLanguage": String(),
|
||||||
|
"keyword": String(),
|
||||||
|
"uri": String(),
|
||||||
|
"uriScheme": String(),
|
||||||
|
"charset": String(),
|
||||||
|
"naturalLanguage": String(),
|
||||||
|
"mimeMediaType": String(),
|
||||||
|
"memberAttrName": String(),
|
||||||
|
})
|
||||||
|
|
||||||
|
ipp_attribute = SubRecord({
|
||||||
|
"name": String(),
|
||||||
|
"values": ListOf(ipp_attribute_value),
|
||||||
|
"tag": Unsigned8BitInteger(),
|
||||||
|
})
|
||||||
|
|
||||||
ipp_scan_response = SubRecord({
|
ipp_scan_response = SubRecord({
|
||||||
"result": SubRecord({
|
"result": SubRecord({
|
||||||
"version_major": Signed8BitInteger(),
|
"version_major": Signed8BitInteger(),
|
||||||
"version_minor": Signed8BitInteger(),
|
"version_minor": Signed8BitInteger(),
|
||||||
"version_string": String(),
|
"version_string": String(),
|
||||||
"cups_version": String(),
|
"cups_version": String(),
|
||||||
|
"attributes": ListOf(ipp_attribute),
|
||||||
"attr_cups_version": String(),
|
"attr_cups_version": String(),
|
||||||
"attr_ipp_versions": ListOf(String()),
|
"attr_ipp_versions": ListOf(String()),
|
||||||
"attr_printer_uri": String(),
|
"attr_printer_uris": ListOf(String()),
|
||||||
"response": http_response_full,
|
"response": http_response_full,
|
||||||
"cups_response": http_response_full,
|
"cups_response": http_response_full,
|
||||||
"tls": zgrab2.tls_log,
|
"tls": zgrab2.tls_log,
|
||||||
|
Loading…
Reference in New Issue
Block a user