diff --git a/integration_tests/ipp/cleanup.sh b/integration_tests/ipp/cleanup.sh index 0c4aeba..8bfd2bb 100755 --- a/integration_tests/ipp/cleanup.sh +++ b/integration_tests/ipp/cleanup.sh @@ -2,8 +2,12 @@ set +e +versions="cups cups-tls" + echo "ipp/cleanup: Tests cleanup for ipp" -CONTAINER_NAME=zgrab_ipp +for version in $versions; do + CONTAINER_NAME="zgrab_ipp_$version" -docker stop $CONTAINER_NAME + docker stop $CONTAINER_NAME +done \ No newline at end of file diff --git a/integration_tests/ipp/container-cups-tls/Dockerfile b/integration_tests/ipp/container-cups-tls/Dockerfile new file mode 100755 index 0000000..6335e4b --- /dev/null +++ b/integration_tests/ipp/container-cups-tls/Dockerfile @@ -0,0 +1,16 @@ +FROM zgrab2_service_base:latest + +RUN apt-get update && apt-get install -y \ + cups \ + cups-pdf +WORKDIR /etc/cups +COPY cupsssl.conf cupsd.conf + +RUN service cups stop +RUN update-rc.d -f cupsd remove + +WORKDIR / +COPY entrypoint.sh . +RUN chmod a+x ./entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/integration_tests/ipp/container-cups-tls/cupsssl.conf b/integration_tests/ipp/container-cups-tls/cupsssl.conf new file mode 100644 index 0000000..c352098 --- /dev/null +++ b/integration_tests/ipp/container-cups-tls/cupsssl.conf @@ -0,0 +1,152 @@ +# +# Configuration file for the CUPS scheduler. See "man cupsd.conf" for a +# complete description of this file. +# + +# Log general information in error_log - change "warn" to "debug" +# for troubleshooting... +LogLevel warn +PageLogFormat + +# Deactivate CUPS' internal logrotating, as we provide a better one, especially +# LogLevel debug2 gets usable now +MaxLogSize 0 + +# Only listen for connections from the local machine. +Listen /var/run/cups/cups.sock + +# Show shared printers on the local network. +Browsing Off +BrowseLocalProtocols dnssd + +# Default authentication type, when authentication is required... +DefaultAuthType Basic + +# Web interface setting... +WebInterface Yes + +# Restrict access to the server... + + Order allow,deny + # Allow any host to access the print server + Allow all + + +# Restrict access to the admin pages... + + Order allow,deny + + +# Restrict access to configuration files... + + AuthType Default + Require user @SYSTEM + Order allow,deny + + +# Restrict access to log files... + + AuthType Default + Require user @SYSTEM + Order allow,deny + + +# Set the default printer/job policies... + + # Job/subscription privacy... + JobPrivateAccess default + JobPrivateValues default + SubscriptionPrivateAccess default + SubscriptionPrivateValues default + + # Job-related operations must be done by the owner or an administrator... + + Order deny,allow + + + + Require user @OWNER @SYSTEM + Order deny,allow + + + # All administration operations require an administrator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # All printer operations require a printer operator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # Only the owner or an administrator can cancel or authenticate a job... + + Require user @OWNER @SYSTEM + Order deny,allow + + + + Order deny,allow + + + +# Set the authenticated printer/job policies... + + # Job/subscription privacy... + JobPrivateAccess default + JobPrivateValues default + SubscriptionPrivateAccess default + SubscriptionPrivateValues default + + # Job-related operations must be done by the owner or an administrator... + + AuthType Default + Order deny,allow + + + + AuthType Default + Require user @OWNER @SYSTEM + Order deny,allow + + + # All administration operations require an administrator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # All printer operations require a printer operator to authenticate... + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + # Only the owner or an administrator can cancel or authenticate a job... + + AuthType Default + Require user @OWNER @SYSTEM + Order deny,allow + + + + Order deny,allow + + +# Allows access to print server from all valid names +ServerAlias * +# Let the server print to cups-pdf +FileServer yes +# cups creates these files upon install +# Configure certificate for TLS +ServerCertificate /etc/cups/ssl/server.crt +# Configure private key for TLS +ServerKey /etc/cups/ssl/server.key +# Specify port on which to listen for TLS connections +SSLListen 631 \ No newline at end of file diff --git a/integration_tests/ipp/container/entrypoint.sh b/integration_tests/ipp/container-cups-tls/entrypoint.sh similarity index 100% rename from integration_tests/ipp/container/entrypoint.sh rename to integration_tests/ipp/container-cups-tls/entrypoint.sh diff --git a/integration_tests/ipp/container-cups/Dockerfile b/integration_tests/ipp/container-cups/Dockerfile new file mode 100755 index 0000000..0991bd6 --- /dev/null +++ b/integration_tests/ipp/container-cups/Dockerfile @@ -0,0 +1,16 @@ +FROM zgrab2_service_base:latest + +RUN apt-get update && apt-get install -y \ + cups \ + cups-pdf +WORKDIR /etc/cups +COPY cupsd.conf cupsd.conf + +RUN service cups stop +RUN update-rc.d -f cupsd remove + +WORKDIR / +COPY entrypoint.sh . +RUN chmod a+x ./entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/integration_tests/ipp/container/cupsd.conf b/integration_tests/ipp/container-cups/cupsd.conf similarity index 100% rename from integration_tests/ipp/container/cupsd.conf rename to integration_tests/ipp/container-cups/cupsd.conf diff --git a/integration_tests/ipp/container-cups/entrypoint.sh b/integration_tests/ipp/container-cups/entrypoint.sh new file mode 100755 index 0000000..3901281 --- /dev/null +++ b/integration_tests/ipp/container-cups/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -x + +while true; do +#FIXME: Determine whether -f or -F is ideal, and whether any other options are needed + if ! /usr/sbin/cupsd -f; then + echo "cupsd exited unexpectedly. Restarting..." + sleep 1 + fi +done diff --git a/integration_tests/ipp/container/Dockerfile b/integration_tests/ipp/container/Dockerfile deleted file mode 100755 index 5f1c372..0000000 --- a/integration_tests/ipp/container/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM zgrab2_service_base:latest - -RUN apt-get update && apt-get install -y \ - cups \ - cups-pdf -# TODO: Provide a pre-built cupsd.conf rather than relying on modifying the default config file -WORKDIR /etc/cups -COPY cupsd.conf cupsd.conf -# TODO: Provide a pre-built cups-pdf.conf -RUN service cups restart - -# TODO: Actually stop service; see why this works without stopping it -#RUN service cupsd stop -# TODO: Actually prevent cups from being started automatically -#RUN update-rc.d -f cupsd remove - -WORKDIR / -COPY entrypoint.sh . -RUN chmod a+x ./entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/integration_tests/ipp/setup.sh b/integration_tests/ipp/setup.sh index 1aba714..cd77b24 100755 --- a/integration_tests/ipp/setup.sh +++ b/integration_tests/ipp/setup.sh @@ -2,24 +2,29 @@ echo "ipp/setup: Tests setup for ipp" +versions="cups cups-tls" + CONTAINER_TAG="zgrab_ipp" -CONTAINER_NAME="zgrab_ipp" +for version in $versions; do + CONTAINER_NAME="zgrab_ipp_$version" -# If the container is already running, use it. -if docker ps --filter "name=$CONTAINER_NAME" | grep -q $CONTAINER_NAME; then - echo "ipp/setup: Container $CONTAINER_NAME already running -- nothing to setup" - exit 0 -fi + echo "ipp/setup: Setting up $CONTAINER_NAME" -DOCKER_RUN_FLAGS="--rm --name $CONTAINER_NAME -td" + DOCKER_RUN_FLAGS="--rm --name $CONTAINER_NAME -td" -# If it is not running, try launching it -- on success, use that. -echo "ipp/setup: Trying to launch $CONTAINER_NAME..." -if ! docker run $DOCKER_RUN_FLAGS $CONTAINER_TAG; then - echo "ipp/setup: Building docker image $CONTAINER_TAG..." - # If it fails, build it from ./container/Dockerfile - docker build -t $CONTAINER_TAG ./container - # Try again - echo "ipp/setup: Launching $CONTAINER_NAME..." - docker run $DOCKER_RUN_FLAGS $CONTAINER_TAG -fi + # If the container is already running, use it. + if docker ps --filter "name=$CONTAINER_NAME" | grep -q $CONTAINER_NAME; then + echo "ipp/setup: Container $CONTAINER_NAME already running -- nothing to setup" + else + if ! docker run $DOCKER_RUN_FLAGS "$CONTAINER_TAG:$version"; then + echo "ipp/setup: Building docker image $CONTAINER_TAG..." + # If it fails, build it from ./container/Dockerfile + docker build -t "$CONTAINER_TAG:$version" ./container-$version + # Try again + echo "ipp/setup: Launching $CONTAINER_NAME..." + docker run $DOCKER_RUN_FLAGS $CONTAINER_TAG:$version + fi + fi + # Add file printer so that CUPS-get-printers response is populated + docker exec $CONTAINER_NAME lpadmin -p null -E -v file:/dev/null +done \ No newline at end of file diff --git a/integration_tests/ipp/test.sh b/integration_tests/ipp/test.sh index 88aae69..7e70ac5 100755 --- a/integration_tests/ipp/test.sh +++ b/integration_tests/ipp/test.sh @@ -5,32 +5,91 @@ MODULE_DIR=$(dirname $0) ZGRAB_ROOT=$MODULE_DIR/../.. ZGRAB_OUTPUT=$ZGRAB_ROOT/zgrab-output +OUTPUT_ROOT=$ZGRAB_OUTPUT/ipp + mkdir -p $ZGRAB_OUTPUT/ipp -CONTAINER_NAME=zgrab_ipp +versions="cups cups-tls" -OUTPUT_FILE=$ZGRAB_OUTPUT/ipp/ipp.json +function test_cups() { + echo "ipp/test: Tests runner for ipp_cups" -echo "ipp/test: Testing IPP on $CONTAINER_NAME..." -# TODO FIXME: Add any necessary flags or additional tests -CONTAINER_NAME=$CONTAINER_NAME $ZGRAB_ROOT/docker-runner/docker-run.sh ipp > $OUTPUT_FILE -# TODO: Add version with TLS flag when that's implemented + CONTAINER_NAME="zgrab_ipp_cups" $ZGRAB_ROOT/docker-runner/docker-run.sh ipp --timeout 3 --verbose > "$OUTPUT_ROOT/cups.json" + # FIXME: No good reason to use a tmp file & saved file, b/c I'm not testing any failure states yet + #CONTAINER_NAME="zgrab_ipp_cups" $ZGRAB_ROOT/docker-runner/docker-run.sh ipp --timeout 3 --verbose > out.tmp + major=$($ZGRAB_ROOT/jp -u data.ipp.result.version_major < "$OUTPUT_ROOT/cups.json") + minor=$($ZGRAB_ROOT/jp -u data.ipp.result.version_minor < "$OUTPUT_ROOT/cups.json") + cups=$($ZGRAB_ROOT/jp -u data.ipp.result.cups_version < "$OUTPUT_ROOT/cups.json") + rm -f out.tmp + if ! [ $major = "2" ]; then + echo "ipp/test: Incorrect major version. Expected 2, got $major" + exit 1 + fi + if ! [ $minor = "1" ]; then + echo "ipp/test: Incorrect minor version. Expected 1, got $minor" + exit 1 + fi + if ! [ $cups = "CUPS/2.1" ]; then + echo "ipp/test: Incorrect CUPS version. Expected CUPS/2.1, got $cups" + exit 1 + fi +} -# Dump the docker logs -echo "ipp/test: BEGIN docker logs from $CONTAINER_NAME [{(" -docker logs --tail all $CONTAINER_NAME -echo ")}] END docker logs from $CONTAINER_NAME" +function test_cups_tls() { + echo "ipp/test: Tests runner for ipp_cups" -# TODO: If there are any other relevant log files, dump those to stdout here. -# FIXME: Only dump these logs if they exist -#echo "ipp/test: BEGIN cups logs from $CONTAINER_NAME [{(" -#docker exec -t $CONTAINER_NAME cat //var/log/cups/access_log -#echo ")}] END cups logs from $CONTAINER_NAME" + CONTAINER_NAME="zgrab_ipp_cups-tls" $ZGRAB_ROOT/docker-runner/docker-run.sh ipp --timeout 3 --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 + #CONTAINER_NAME="zgrab_ipp_cups-tls" $ZGRAB_ROOT/docker-runner/docker-run.sh ipp --timeout 3 --ipps --verbose > out.tmp + major=$($ZGRAB_ROOT/jp -u data.ipp.result.version_major < "$OUTPUT_ROOT/cups-tls.json") + minor=$($ZGRAB_ROOT/jp -u data.ipp.result.version_minor < "$OUTPUT_ROOT/cups-tls.json") + response=$($ZGRAB_ROOT/jp -u data.ipp.result.response < "$OUTPUT_ROOT/cups-tls.json") + cups=$($ZGRAB_ROOT/jp -u data.ipp.result.cups_version < "$OUTPUT_ROOT/cups-tls.json") + # TODO: Check for a particular field in the tls object, since it may be safer + tls=$($ZGRAB_ROOT/jp -u data.ipp.result.tls < "$OUTPUT_ROOT/cups-tls.json") + #rm -f out.tmp + if ! [ $major = "2" ]; then + echo "ipp/test: Incorrect major version. Expected 2, got $major" + exit 1 + fi + if ! [ $minor = "1" ]; then + echo "ipp/test: Incorrect minor version. Expected 1, got $minor" + exit 1 + fi + if ! [ $cups = "CUPS/2.1" ]; then + echo "ipp/test: Incorrect CUPS version. Expected CUPS/2.1, got $cups" + exit 1 + fi + if [ $tls = "null" ]; then + echo "ipp/test: No TLS handshake logged" + exit 1 + fi +} -#echo "ipp/test: BEGIN cups logs from $CONTAINER_NAME [{(" -#docker exec -t $CONTAINER_NAME cat //var/log/cups/error_log -#echo ")}] END cups logs from $CONTAINER_NAME" +echo "ipp/test: Testing IPP..." +test_cups +test_cups_tls -#echo "ipp/test: BEGIN cups logs from $CONTAINER_NAME [{(" -#docker exec -t $CONTAINER_NAME cat //var/log/cups/page_log -#echo ")}] END cups logs from $CONTAINER_NAME" \ No newline at end of file + +for version in $versions; do + CONTAINER_NAME="zgrab_ipp_$version" + + # Dump the docker logs + echo "ipp/test: BEGIN docker logs from $CONTAINER_NAME [{(" + docker logs --tail all $CONTAINER_NAME + echo ")}] END docker logs from $CONTAINER_NAME" + + # TODO: If there are any other relevant log files, dump those to stdout here. + # FIXME: Only dump these 3 logs if they exist + #echo "ipp/test: BEGIN cups logs from $CONTAINER_NAME [{(" + #docker exec -t $CONTAINER_NAME cat //var/log/cups/access_log + #echo ")}] END cups logs from $CONTAINER_NAME" + + #echo "ipp/test: BEGIN cups logs from $CONTAINER_NAME [{(" + #docker exec -t $CONTAINER_NAME cat //var/log/cups/error_log + #echo ")}] END cups logs from $CONTAINER_NAME" + + #echo "ipp/test: BEGIN cups logs from $CONTAINER_NAME [{(" + #docker exec -t $CONTAINER_NAME cat //var/log/cups/page_log + #echo ")}] END cups logs from $CONTAINER_NAME" +done \ No newline at end of file diff --git a/modules/ipp/ipp.go b/modules/ipp/ipp.go index 4fda365..837dd68 100644 --- a/modules/ipp/ipp.go +++ b/modules/ipp/ipp.go @@ -3,56 +3,114 @@ package ipp import ( "bytes" "encoding/binary" - //"io" - "net" + "errors" + "math" + "strings" + "net/url" + + log "github.com/sirupsen/logrus" ) -type Connection struct { - Conn net.Conn -} - -//func ReadResponse(body *io.ReadCloser) *ScanResults { -// result := &ScanResults{} -// -//} - -// Returns a byte-encoded "attribute-with-one-value" with the provided "value-tag", "name", and "value" +// Writes an "attribute-with-one-value" with the provided "value-tag", "name", and "value" to provided buffer // attribute-with-one-value encoding described at https://tools.ietf.org/html/rfc8010#section-3.1.4 // Example (runnable from ipp_test.go): // Input: 0x47, "attributes-charset", "us-ascii" // Output: [71 0 18 97 116 116 114 105 98 117 116 101 115 45 99 104 97 114 115 101 116 0 8 117 115 45 97 115 99 105 105] -// TODO: Should return an error when fed an invalid valueTag? -// TODO: Determine whether this should remain public. Currently is for Testable Example -func AttributeByteString(valueTag byte, name string, value string) []byte { +// TODO: Switch output and Example function to use hex.Dump() +// TODO: Should return an error when fed an invalid valueTag +func AttributeByteString(valueTag byte, name string, value string, target *bytes.Buffer) error { //special byte denoting value syntax - b := []byte{valueTag} + binary.Write(target, binary.BigEndian, valueTag) - //append 16-bit signed int denoting name length - l := new(bytes.Buffer) - binary.Write(l, binary.BigEndian, int16(len(name))) - b = append(b, l.Bytes()...) + if len(name) <= math.MaxInt16 && len(name) >= 0 { + //append 16-bit signed int denoting name length + binary.Write(target, binary.BigEndian, int16(len(name))) - //append name - b = append(b, []byte(name)...) + //append name + binary.Write(target, binary.BigEndian, []byte(name)) + } else { + // TODO: Log error somewhere + return errors.New("Name wrong length to encode.") + } - //append 16-bit signed int denoting value length - l = new(bytes.Buffer) - binary.Write(l, binary.BigEndian, int16(len(value))) - b = append(b, l.Bytes()...) + if len(value) <= math.MaxInt16 && len(value) >= 0 { + //append 16-bit signed int denoting value length + binary.Write(target, binary.BigEndian, int16(len(value))) - //append value - b = append(b, []byte(value)...) - return b + //append value + binary.Write(target, binary.BigEndian, []byte(value)) + } else { + // TODO: Log error somewhere + return errors.New("Value wrong length to encode.") + } + + return nil } +// TODO: Eventually handle scheme-less urls, even though getHTTPURL will never construct one (we can use regex) +// TODO: RFC claims that literal IP addresses are not valid IPP uri's, but Wireshark IPP Capture example uses them +// (Source: https://wiki.wireshark.org/SampleCaptures?action=AttachFile&do=view&target=ipp.pcap) +func ConvertURIToIPP(uriString string, tls bool) string { + uri, err := url.Parse(uriString) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "url": uriString, + }).Debug("Failed to parse URL from string") + } + // TODO: Create a better condition than uri.Scheme == "" b/c url.Parse doesn't know whether there's a scheme + if uri.Scheme == "" || uri.Scheme == "http" || uri.Scheme == "https" { + if tls { + uri.Scheme = "ipps" + } else { + uri.Scheme = "ipp" + } + } + if !strings.Contains(uri.Host, ":") { + uri.Host += ":631" + } + return uri.String() +} -// IPP request encoding described at https://tools.ietf.org/html/rfc8010#section-3.1.1 -//TODO: Store everything except uri statically? -//Construct a minimal request that an IPP server will respond to -func getPrinterAttributesRequest(uri string) bytes.Buffer { +func getPrintersRequest(major, minor int8) *bytes.Buffer { var b bytes.Buffer - //version 2.1 (newest as of 2018) - b.Write([]byte{2, 1}) + // Sending too new a version leads to a version-not-supported error, so we'll just send newest + //version + b.Write([]byte{byte(major), byte(minor)}) + //operation-id = get-printer-attributes + b.Write([]byte{0x40, 2}) + //request-id = 1 + b.Write([]byte{0, 0, 0, 1}) + //operation-attributes-tag = 1 (begins an attribute-group) + b.Write([]byte{1}) + + // TODO: Handle error ocurring in any AttributeByteString call + //attributes-charset + AttributeByteString(0x47, "attributes-charset", "utf-8", &b) + //attributes-natural-language + AttributeByteString(0x48, "attributes-natural-language", "en-us", &b) + + //end-of-attributes-tag = 3 + b.Write([]byte{3}) + + return &b +} + +// TODO: Store everything except uri statically? +// Construct a minimal request that an IPP server will respond to +// IPP request encoding described at https://tools.ietf.org/html/rfc8010#section-3.1.1 +func getPrinterAttributesRequest(major, minor int8, uri string, tls bool) *bytes.Buffer { + var b bytes.Buffer + // Using newest version number, because we must provide a supported major version number + // Object must reply to unsupported major version with + // "'server-error-version-not-supported' along with the closest version number that + // is supported" RFC 8011 4.1.8 https://tools.ietf.org/html/rfc8011#4.1.8 + // "In all cases, the IPP object MUST return the "version-number" value that it supports + // that is closest to the version number supplied by the Client in the request." + // CUPS behavior defies the RFC. The response to a request with a bad version number should encode + // the closest supported version number per RFC 8011 Section Appendix B.1.5.4 https://tools.ietf.org/html/rfc8011#appendix-B.1.5.4 + //version + b.Write([]byte{byte(major), byte(minor)}) //operation-id = get-printer-attributes b.Write([]byte{0, 0xb}) //request-id = 1 @@ -60,17 +118,18 @@ func getPrinterAttributesRequest(uri string) bytes.Buffer { //operation-attributes-tag = 1 (begins an attribute-group) b.Write([]byte{1}) + // TODO: Handle error ocurring in any AttributeByteString call //attributes-charset - b.Write(AttributeByteString(0x47, "attributes-charset", "utf-8")) + AttributeByteString(0x47, "attributes-charset", "utf-8", &b) //attributes-natural-language - b.Write(AttributeByteString(0x48, "attributes-natural-language", "en-us")) + AttributeByteString(0x48, "attributes-natural-language", "en-us", &b) //printer-uri - b.Write(AttributeByteString(0x45, "printer-uri", uri)) + AttributeByteString(0x45, "printer-uri", ConvertURIToIPP(uri, tls), &b) //requested-attributes - b.Write(AttributeByteString(0x44, "requested-attributes", "all")) + AttributeByteString(0x44, "requested-attributes", "all", &b) //end-of-attributes-tag = 3 b.Write([]byte{3}) - return b -} \ No newline at end of file + return &b +} diff --git a/modules/ipp/ipp_test.go b/modules/ipp/ipp_test.go index 5270fa5..77cc838 100644 --- a/modules/ipp/ipp_test.go +++ b/modules/ipp/ipp_test.go @@ -1,11 +1,33 @@ -package ipp_test +package ipp import ( + "bytes" "fmt" - "github.com/zmap/zgrab2/modules/ipp" ) func ExampleAttributeByteString() { - fmt.Println(ipp.AttributeByteString(0x47, "attributes-charset", "us-ascii")) + var buf bytes.Buffer + if err := AttributeByteString(0x47, "attributes-charset", "us-ascii", &buf); err == nil { + fmt.Println(buf.Bytes()) + } // Output: [71 0 18 97 116 116 114 105 98 117 116 101 115 45 99 104 97 114 115 101 116 0 8 117 115 45 97 115 99 105 105] +} + +func ExampleConvertURIToIPP() { + fmt.Println(ConvertURIToIPP("http://www.google.com:631/ipp", false)) + fmt.Println(ConvertURIToIPP("https://www.google.com:631/ipp", true)) + fmt.Println(ConvertURIToIPP("http://www.google.com/ipp", false)) + fmt.Println(ConvertURIToIPP("https://www.google.com/ipp", true)) + fmt.Println(ConvertURIToIPP("http://www.google.com:631", false)) + fmt.Println(ConvertURIToIPP("https://www.google.com:631", true)) + // TODO: Eventually test for scheme-less urls, but getHTTPURL will never construct one + //fmt.Println(ConvertURIToIPP("www.google.com:631/ipp", false)) + //fmt.Println(ConvertURIToIPP("www.google.com:631/ipp", true)) + // Output: + // ipp://www.google.com:631/ipp + // ipps://www.google.com:631/ipp + // ipp://www.google.com:631/ipp + // ipps://www.google.com:631/ipp + // ipp://www.google.com:631 + // ipps://www.google.com:631 } \ No newline at end of file diff --git a/modules/ipp/scanner.go b/modules/ipp/scanner.go index edebcde..f5006ed 100644 --- a/modules/ipp/scanner.go +++ b/modules/ipp/scanner.go @@ -4,40 +4,74 @@ package ipp //TODO: Clean up these imports import ( - //"bytes" + "bytes" + "crypto/sha256" "encoding/binary" - //"errors" + "errors" //"fmt" - //"io" - "net/http" + "io" + "io/ioutil" + "mime" + "net" + "net/url" "strconv" "strings" - //"net" - //"net/url" - //"time" + "time" log "github.com/sirupsen/logrus" "github.com/zmap/zgrab2" + "github.com/zmap/zgrab2/lib/http" ) const ( - ContentType string = "application/ipp" + ContentType string = "application/ipp" + VersionsSupported string = "ipp-versions-supported" + CupsVersion string = "cups-version" + PrinterURISupported string = "printer-uri-supported" ) +var ( + // ErrRedirLocalhost is returned when an HTTP redirect points to localhost, + // unless FollowLocalhostRedirects is set. + ErrRedirLocalhost = errors.New("Redirecting to localhost") + + // ErrTooManyRedirects is returned when the number of HTTP redirects exceeds + // MaxRedirects. + ErrTooManyRedirects = errors.New("Too many redirects") + + // TODO: Explain this error + 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},} +) + +type scan struct { + connections []net.Conn + transport *http.Transport + client *http.Client + results ScanResults + url string +} + //TODO: Tag relevant results and exlain in comments // ScanResults instances are returned by the module's Scan function. type ScanResults struct { //TODO: ?Include the request sent as well?? - //TODO: Include a full response or at least a blob in the data (at least in verbose mode) - //Response *http.Response `json:"response,omitempty"` + Response *http.Response `json:"response,omitempty" zgrab:"debug"` + CUPSResponse *http.Response `json:"cups_response,omitempty" zgrab:"debug"` - MajorVersion int8 `json:"version_major"` - MinorVersion int8 `json:"version_minor"` + // RedirectResponseChain is non-empty if the scanner follows a redirect. + // It contains all redirect responses prior to the final response. + RedirectResponseChain []*http.Response `json:"redirect_response_chain,omitempty" zgrab:"debug"` + MajorVersion *int8 `json:"version_major,omitempty"` + MinorVersion *int8 `json:"version_minor,omitempty"` VersionString string `json:"version_string,omitempty"` CUPSVersion string `json:"cups_version,omitempty"` - - TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` + AttributeCUPSVersion string `json:"attr_cups_version,omitempty"` + AttributeIPPVersions []string `json:"attr_ipp_versions,omitempty"` + AttributePrinterURI string `json:"attr_printer_uri,omitempty"` + TLSLog *zgrab2.TLSLog `json:"tls,omitempty"` } // TODO: Annotate every flag thoroughly @@ -46,13 +80,21 @@ type ScanResults struct { // Populated by the framework. type Flags struct { zgrab2.BaseFlags - //FIXME: Borrowed from http module - MaxRead int `long:"max-size" default:"256" description:"Max kilobytes to read in response to an HTTP request"` - // TODO: Protocols that support TLS should include zgrab2.TLSFlags (do once implemented) - // TODO: Maybe implement both an ipps connection and upgrade to https - IPPSecure bool `long:"ipps" description:"Perform a TLS handshake immediately upon connecting."` - + zgrab2.TLSFlags Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"` + + //FIXME: Borrowed from http module + 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"` + UserAgent string `long:"user-agent" default:"Mozilla/5.0 zgrab/0.x" description:"Set a custom user agent"` + RetryTLS bool `long:"retry-tls" description:"If the initial request fails, reconnect and try using TLS."` + + // FollowLocalhostRedirects overrides the default behavior to return + // ErrRedirLocalhost whenever a redirect points to localhost. + FollowLocalhostRedirects bool `long:"follow-localhost-redirects" description:"Follow HTTP redirects to localhost"` + + // TODO: Maybe separately implement both an ipps connection and upgrade to https + IPPSecure bool `long:"ipps" description:"Perform a TLS handshake immediately upon connecting."` } // Module implements the zgrab2.Module interface. @@ -60,6 +102,11 @@ type Module struct { // TODO: Add any module-global state if necessary } +type version struct { + Major int8 + Minor int8 +} + // Scanner implements the zgrab2.Scanner interface. type Scanner struct { config *Flags @@ -102,7 +149,10 @@ func (flags *Flags) Help() string { func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error { f, _ := flags.(*Flags) scanner.config = f - //TODO: Take action in response to flags which were set + // TODO: Remove debug logging for unexpected behavior after 1% scan + if f.Verbose { + log.SetLevel(log.DebugLevel) + } return nil } @@ -131,8 +181,356 @@ func (scanner *Scanner) GetPort() uint { return scanner.config.Port } -//FIXME: Maybe switch to ipp/ipps schemes, at least optionally -func getIPPURL(https bool, host string, port uint16, endpoint string) string { +func ippInContentType(resp http.Response) (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")) + // FIXME: 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 +func storeBody(res *http.Response, scanner *Scanner) { + b := bufferFromBody(res, scanner) + res.BodyText = b.String() + if len(res.BodyText) > 0 { + m := sha256.New() + m.Write(b.Bytes()) + res.BodySHA256 = m.Sum(nil) + } +} + +func bufferFromBody(res *http.Response, scanner *Scanner) *bytes.Buffer { + b := new(bytes.Buffer) + maxReadLen := int64(scanner.config.MaxSize) * 1024 + readLen := maxReadLen + if res.ContentLength >= 0 && res.ContentLength < maxReadLen { + readLen = res.ContentLength + } + io.CopyN(b, res.Body, readLen) + res.Body.Close() + res.Body = ioutil.NopCloser(b) + return b +} + +// FIXME: This will read the wrong section of the body if a substring matches the attribute name passed in +// TODO: Support reading from multiple instances of the same attribute in a response +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 " + // 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 versionNotSupported(body string) bool { + if body != "" { + buf := bytes.NewBuffer([]byte(body)) + // Ignore first two bytes, read second two for status code + var reader struct { + _ uint16 + StatusCode uint16 + } + err := binary.Read(buf, binary.BigEndian, &reader) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "body": body, + }).Debug("Failed to read statusCode from body.") + return false + } + // 0x0503 in the second two bytes of the body denotes server-error-version-not-supported + // Source: RFC 8011 Section 4.1.8 (https://tools.ietf.org/html/rfc8011#4.1.8) + return reader.StatusCode == 0x0503 + } + return false +} + +func (scanner *Scanner) augmentWithCUPSData(scan *scan, target *zgrab2.ScanTarget, version *version) *zgrab2.ScanError { + cupsBody := getPrintersRequest(version.Major, version.Minor) + cupsReq, err := http.NewRequest("POST", scan.url, cupsBody) + if err != nil { + return zgrab2.DetectScanError(err) + } + cupsReq.Header.Set("Accept", "*/*") + cupsReq.Header.Set("Content-Type", ContentType) + cupsResp, err := scan.client.Do(cupsReq) + scan.results.CUPSResponse = cupsResp + + // FIXME: This block is copy-pasted directly from Grab() + if err != nil { + //If error is a url.Error (a struct), unwrap it + if urlError, ok := err.(*url.Error); ok { + err = urlError.Err + } + } + if err != nil { + switch err { + case ErrRedirLocalhost: + break + case ErrTooManyRedirects: + return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, err) + default: + return zgrab2.DetectScanError(err) + } + } + + if cupsResp != nil && cupsResp.Body != nil { + defer cupsResp.Body.Close() + } else { + if cupsResp == nil { + return zgrab2.NewScanError(zgrab2.SCAN_CONNECTION_TIMEOUT, errors.New("No HTTP response")) + } + if cupsResp.Body == nil { + return zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("Empty body.")) + } + // resp == nil or resp.Body == nil + // Empty response/body is not allowed in IPP because a response has required parameter + // Source: RFC 8011 Section 4.1.1 https://tools.ietf.org/html/rfc8011#section-4.1.1 + // Still returns the response, if any, because assignment occurs before this else block + // TODO: Examine whether an empty response overall is a protocol error, I'd think of it as another kind of error entirely, + // and later conditions might handle that case; see RFC 8011 Section 4.2.5.2? + } + // Store data into BodyText and BodySHA256 of cupsResp + storeBody(cupsResp, scanner) + if versionNotSupported(scan.results.CUPSResponse.BodyText) { + return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, ErrVersionNotSupported) + } + + bodyBytes := []byte(cupsResp.BodyText) + // Write reported CUPS version to results object + if cupsVersions, err := readAttributeFromBody(CupsVersion, &bodyBytes); err != nil { + log.WithFields(log.Fields{ + "error": err, + "attribute": CupsVersion, + }).Debug("Failed to read attribute.") + } else if len(cupsVersions) > 0 { + scan.results.AttributeCUPSVersion = string(cupsVersions[0]) + } + // Write reported IPP versions to results object + if ippVersions, err := readAttributeFromBody(VersionsSupported, &bodyBytes); err != nil { + log.WithFields(log.Fields{ + "error": err, + "attribute": VersionsSupported, + }).Debug("Failed to read attribute.") + } else { + for _, v := range ippVersions { + scan.results.AttributeIPPVersions = append(scan.results.AttributeIPPVersions, string(v)) + } + } + // Write reported printer URI to results object + 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 (scanner *Scanner) Grab(scan *scan, target *zgrab2.ScanTarget, version *version) *zgrab2.ScanError { + // Send get-printer-attributes request to the host, preferably a print server + body := getPrinterAttributesRequest(version.Major, version.Minor, scan.url, scanner.config.IPPSecure) + request, err := http.NewRequest("POST", scan.url, body) + if err != nil { + return zgrab2.DetectScanError(err) + } + request.Header.Set("Accept", "*/*") + request.Header.Set("Content-Type", ContentType) + resp, err := scan.client.Do(request) + //Store response regardless of error in request, because we may have gotten something back + scan.results.Response = resp + if err != nil { + //If error is a url.Error (a struct), unwrap it + if urlError, ok := err.(*url.Error); ok { + err = urlError.Err + } + } + if err != nil { + switch err { + case ErrRedirLocalhost: + break + case ErrTooManyRedirects: + return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, err) + default: + return zgrab2.DetectScanError(err) + } + } + + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } else { + if resp == nil { + return zgrab2.NewScanError(zgrab2.SCAN_CONNECTION_TIMEOUT, errors.New("No HTTP response")) + } + if resp.Body == nil { + return zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("Empty body.")) + } + // resp == nil or resp.Body == nil + // Empty response/body is not allowed in IPP because a response has required parameter + // Source: RFC 8011 Section 4.1.1 https://tools.ietf.org/html/rfc8011#section-4.1.1 + // Still returns the response, if any, because assignment occurs before this else block + // TODO: Examine whether an empty response overall is a protocol error, I'd think of it as another kind of error entirely, + // and later conditions might handle that case; see RFC 8011 Section 4.2.5.2? + } + storeBody(resp, scanner) + if versionNotSupported(scan.results.Response.BodyText) { + return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, ErrVersionNotSupported) + } + + // TODO: Check to make sure that the repsonse received is actually IPP + //Content-Type header matches is sufficient + //HTTP on port 631 is sufficient + //Still record data in the case of protocol error to see what that data looks like + + protocols := strings.Split(resp.Header.Get("Server"), " ") + for _, p := range protocols { + if strings.HasPrefix(strings.ToUpper(p), "IPP/") { + scan.results.VersionString = p + protocol := strings.Split(p, "/")[1] + components := strings.Split(protocol, ".") + // Reads in signed integers because "every integer MUST be encoded as a signed integer" + // (Source: https://tools.ietf.org/html/rfc8010#section-3) + var major, minor int8 + if len(components) >= 1 { + if val, err := strconv.Atoi(components[0]); err != nil { + log.WithFields(log.Fields{ + "error": err, + "string": components[0], + }).Debug("Failed to read major version from string.") + } else { + major = int8(val) + scan.results.MajorVersion = &major + } + } + if len(components) >= 2 { + if val, err := strconv.Atoi(components[1]); err != nil { + log.WithFields(log.Fields{ + "error": err, + "string": components[1], + }).Debug("Failed to read minor version from string.") + } else { + minor = int8(val) + scan.results.MinorVersion = &minor + } + } + } + if strings.HasPrefix(strings.ToUpper(p), "CUPS/") { + 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.") + } + } + } + + return nil +} + +//Taken from zgrab/zlib/grabber.go -- check if the URL points to localhost +func redirectsToLocalhost(host string) bool { + if i := net.ParseIP(host); i != nil { + return i.IsLoopback() || i.Equal(net.IPv4zero) + } + if host == "localhost" { + return true + } + + if addrs, err := net.LookupHost(host); err == nil { + for _, i := range addrs { + if ip := net.ParseIP(i); ip != nil { + if ip.IsLoopback() || ip.Equal(net.IPv4zero) { + return true + } + } + } + } + return false +} + +// Taken from zgrab/zlib/grabber.go -- get a CheckRedirect callback that uses redirectToLocalhost and MaxRedirects config +func (scan *scan) getCheckRedirect(scanner *Scanner) func(*http.Request, *http.Response, []*http.Request) error { + return func(req *http.Request, res *http.Response, via []*http.Request) error { + if !scanner.config.FollowLocalhostRedirects && redirectsToLocalhost(req.URL.Hostname()) { + return ErrRedirLocalhost + } + scan.results.RedirectResponseChain = append(scan.results.RedirectResponseChain, res) + storeBody(res, scanner) + + if len(via) > scanner.config.MaxRedirects { + return ErrTooManyRedirects + } + + return nil + } +} + +// Taken from zgrab2 http library, slightly modified to use slightly leaner scan object +func (scan *scan) getTLSDialer(scanner *Scanner) func(net, addr string) (net.Conn, error) { + return func(net, addr string) (net.Conn, error) { + outer, err := zgrab2.DialTimeoutConnection(net, addr, time.Second*time.Duration(scanner.config.BaseFlags.Timeout)) + if err != nil { + return nil, err + } + scan.connections = append(scan.connections, outer) + tlsConn, err := scanner.config.TLSFlags.GetTLSConnection(outer) + if err != nil { + return nil, err + } + // lib/http/transport.go fills in the TLSLog in the http.Request instance(s) + err = tlsConn.Handshake() + scan.results.TLSLog = tlsConn.GetLog() + return tlsConn, err + } +} + +// This doesn't use ipp(s) scheme, because http doesn't recognize them, so we need http scheme +// We convert as needed later in convertURIToIPP +func getHTTPURL(https bool, host string, port uint16, endpoint string) string { var proto string if https { proto = "https" @@ -142,75 +540,78 @@ func getIPPURL(https bool, host string, port uint16, endpoint string) string { return proto + "://" + host + ":" + strconv.FormatUint(uint64(port), 10) + endpoint } -// FIXME: Ensure that an actual content type is being used with some library -func ippInContentType(resp http.Response) bool { - return strings.Contains(resp.Header.Get("Content-Type"), ContentType) -} - -// FIXME: Change to instead return (*ScanResults, *zgrab2.ScanError) -// TODO: Doesn't support TLS at all right now -func (scanner *Scanner) grab(target zgrab2.ScanTarget) (int8, int8, *zgrab2.ScanError) { - //FIXME: This is not where this hostname assignment logic should live +// Adapted from newHTTPScan in zgrab2 http module +func (scanner *Scanner) newIPPScan(target *zgrab2.ScanTarget) *scan { + newScan := scan{ + client: http.MakeNewClient(), + } + newScan.results = ScanResults{} + transport := &http.Transport{ + Proxy: nil, // TODO: implement proxying + DisableKeepAlives: false, + DisableCompression: false, + MaxIdleConnsPerHost: scanner.config.MaxRedirects, + } + transport.DialTLS = newScan.getTLSDialer(scanner) + transport.DialContext = zgrab2.GetTimeoutConnectionDialer(time.Duration(scanner.config.Timeout) * time.Second).DialContext + newScan.client.CheckRedirect = newScan.getCheckRedirect(scanner) + newScan.client.UserAgent = scanner.config.UserAgent + newScan.client.Transport = transport + newScan.client.Jar = nil // Don't transfer cookies FIXME: Stolen from HTTP, unclear if needed host := target.Domain if host == "" { + // FIXME: I only know this works for sure for IPv4, uri string might get weird w/ IPv6 + // FIXME: Change this, since ipp uri's cannot contain an IP address. Still valid for HTTP host = target.IP.String() } - //FIXME: ?Should just use endpoint "/", since we get the same response as "/ipp" on CUPS?? - uri := getIPPURL(scanner.config.IPPSecure, host, uint16(scanner.config.BaseFlags.Port), "/ipp") - b := getPrinterAttributesRequest(uri) - - resp, err := http.Post(uri, ContentType, &b) - if err != nil { - return 0, 0, zgrab2.DetectScanError(err) - } - if resp != nil && resp.Body != nil { - defer resp.Body.Close() - } else { - // FIXME: Is empty body allowed in IPP? - // Cite RFC!! - // Empty body is not allowed in valid IPP - return 0, 0, zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, nil) - } - - // FIXME: Maybe add something to handle redirects - // FIXME: Probably return the whole response for further inspection by ztag, rather - // than grabbing first 2 bytes. In that case, implement maxRead like http module - - //Check to make sure that the repsonse received is actually IPP - //Content-Type header matches is sufficient - //HTTP on port 631 is sufficient - //Still record data in the case of protocol error to see what that data looks like - - // TODO: Record server-header version numbers - //protocols := resp.Header.Get("Server") - // TODO: Change this to perform two separate binary.Reads - var version uint16 - // TODO: Determine whether errors other than protocol (ie: too few bytes) can be triggered here - if err := binary.Read(resp.Body, binary.BigEndian, &version); err != nil { - // FIXME: Determine whether sending fewer than 2 bytes is a protocol or application error - // I believe it's protocol, since the version must be specified (iirc) - // FIXME: Cite RFC!! - return 0, 0, zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, err) - } - // Returns signed integers because "every integer MUST be encoded as a signed integer" - // (Source: https://tools.ietf.org/html/rfc8010#section-3) - return int8(version >> 8), int8(version & 0xff), nil + // FIXME: ?Should just use endpoint "/", since we get the same response as "/ipp" on CUPS?? + newScan.url = getHTTPURL(scanner.config.IPPSecure, host, uint16(scanner.config.BaseFlags.Port), "/ipp") + return &newScan } -// Scan TODO: describe how scan operates +// Scan TODO: describe how scan operates in appropriate detail //1. Send a request (currently get-printer-attributes) //2. Take in that response & read out version numbers func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) { - // TODO: use Connection again, at least when implementing TLS - major, minor, err := scanner.grab(target) - results := &ScanResults{} - results.MajorVersion = major - results.MinorVersion = minor - // FIXME: Triggering even though error IS nil - // FIXME: This is a sloppy bodge to handle the issue - if major == 0 && minor == 0 && err != nil { - // TODO: Consider mimicking HTTP Scan's retryHTTPS functionality - return zgrab2.TryGetScanStatus(err), results, err + scan := scanner.newIPPScan(&target) + //defer scan.Cleanup() + var err *zgrab2.ScanError + // Try all known IPP versions from newest to oldest until version is supported + for i := 0; i < len(Versions); i++ { + err = scanner.Grab(scan, &target, &Versions[i]) + if err == nil || (err != nil && err.Err != ErrVersionNotSupported) { + break + } + if i == len(Versions) - 1 && err.Err == ErrVersionNotSupported { + return zgrab2.SCAN_APPLICATION_ERROR, &scan.results, err.Err + } } - return zgrab2.SCAN_SUCCESS, results, nil + if err != nil { + // Adapted from http module's RetryHTTPS logic + if scanner.config.RetryTLS && !scanner.config.IPPSecure { + //scan.Cleanup() + scanner.config.IPPSecure = true + // TODO: ?Refactor this to just call Scan again?? + retry := scanner.newIPPScan(&target) + //defer retry.Cleanup() + var retryErr *zgrab2.ScanError + // Try all known IPP versions from newest to oldest until version is supported + // TODO: Figure out why retry-TLS is working worse than w/ or w/o TLS in the first place + for i := 0; i < len(Versions); i++ { + retryErr = scanner.Grab(retry, &target, &Versions[i]) + if err == nil || (err != nil && err.Err != ErrVersionNotSupported) { + break + } + if i == len(Versions) - 1 && err.Err == ErrVersionNotSupported { + return zgrab2.SCAN_APPLICATION_ERROR, &scan.results, err.Err + } + } + if retryErr != nil { + return retryErr.Unpack(retry.results) + } + return zgrab2.SCAN_SUCCESS, retry.results, nil + } + return zgrab2.TryGetScanStatus(err), &scan.results, err + } + return zgrab2.SCAN_SUCCESS, &scan.results, nil } diff --git a/zgrab2_schemas/zgrab2/ipp.py b/zgrab2_schemas/zgrab2/ipp.py index 4e8434f..bb795fb 100644 --- a/zgrab2_schemas/zgrab2/ipp.py +++ b/zgrab2_schemas/zgrab2/ipp.py @@ -7,13 +7,160 @@ import zschema.registry import zcrypto_schemas.zcrypto as zcrypto import zgrab2 +# FIXME: Copy-pasted from http schema except for ipp_scan_response +# lib/http/header.go: knownHeaders +http_known_headers = [ + "access_control_allow_origin", + "accept_patch", + "accept_ranges", + "age", + "allow", + "alt_svc", + "alternate_protocol", + "cache_control", + "connection", + "content_disposition", + "content_encoding", + "content_language", + "content_length", + "content_location", + "content_md5", + "content_range", + "content_type", + "expires", + "last_modified", + "link", + "location", + "p3p", + "pragma", + "proxy_agent", + "proxy_authenticate", + "public_key_pins", + "referer", + "refresh", + "retry_after", + "server", + "set_cookie", + "status", + "strict_transport_security", + "trailer", + "transfer_encoding", + "upgrade", + "vary", + "via", + "warning", + "www_authenticate", + "x_frame_options", + "x_xss_protection", + "content_security_policy", + "x_content_security_policy", + "x_webkit_csp", + "x_content_type_options", + "x_powered_by", + "x_ua_compatible", + "x_content_duration", + "x_real_ip", + "x_forwarded_for", +] + +http_unknown_headers = ListOf(SubRecord({ + "key": String(), + "value": ListOf(String()) +})) + +_http_headers = dict( + (header_name, ListOf(String())) + for header_name in http_known_headers +) +_http_headers["unknown"] = http_unknown_headers + +# Format from the custom JSON Marshaller in lib/http/header.go +http_headers = SubRecord(_http_headers) + +# net.url: type Values map[string][]string +http_form_values = SubRecord({}) # TODO FIXME: unconstrained dict + +# lib/http/request.go: URLWrapper +http_url_wrapper = SubRecord({ + "scheme": String(), + "opaque": String(), + "host": String(), + "path": String(), + "raw_path": String(), + "raw_query": String(), + "fragment": String() +}) + +# modules/http.go: HTTPRequest +http_request = SubRecord({ + "method": String(), + "endpoint": String(), + "user_agent": String(), + "body": String() +}) + +# modules/http.go: HTTPResponse +http_response = SubRecord({ + "version_major": Signed32BitInteger(), + "version_minor": Signed32BitInteger(), + "status_code": Signed32BitInteger(), + "status_line": String(), + "headers": http_headers, + "body": String(), + "body_sha256": String() +}) + +# lib/http/request.go: http.Request +http_request_full = SubRecord({ + "url": http_url_wrapper, + "method": String(), + "headers": http_headers, + "body": String(), + "content_length": Signed64BitInteger(), + "transfer_encoding": ListOf(String()), + "close": Boolean(), + "host": String(), + "form": http_form_values, + "post_form": http_form_values, + "multipart_form": http_form_values, + "trailers": http_headers, + # The new field tls_log contains the zgrab2 TLS logs. + "tls_log": zgrab2.tls_log +}) + +# lib/http/response.go: http.Response +http_response_full = SubRecord({ + "status_line": String(), + "status_code": Unsigned32BitInteger(), + # lib/http/protocol.go: http.Protocol + "protocol": SubRecord({ + "name": String(), + "major": Unsigned32BitInteger(), + "minor": Unsigned32BitInteger(), + }), + "headers": http_headers, + "body": String(), + "body_sha256": Binary(), + "content_length": Signed64BitInteger(), + "transfer_encoding": ListOf(String()), + "trailers": http_headers, + "request": http_request_full +}) + +# TODO: Re-work to use most of schema from http module, rather than copy-pasting ipp_scan_response = SubRecord({ "result": SubRecord({ "version_major": Signed8BitInteger(), "version_minor": Signed8BitInteger(), "version_string": String(), "cups_version": String(), + "attr_cups_version": String(), + "attr_ipp_versions": ListOf(String()), + "attr_printer_uri": String(), + "response": http_response_full, + "cups_response": http_response_full, "tls": zgrab2.tls_log, + "redirect_response_chain": ListOf(http_response_full), }) }, extends=zgrab2.base_scan_response)