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)