Merge branch 'master' into ah/trigger
This commit is contained in:
commit
0c597e5a8d
@ -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
|
16
integration_tests/ipp/container-cups-tls/Dockerfile
Executable file
16
integration_tests/ipp/container-cups-tls/Dockerfile
Executable file
@ -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"]
|
152
integration_tests/ipp/container-cups-tls/cupsssl.conf
Normal file
152
integration_tests/ipp/container-cups-tls/cupsssl.conf
Normal file
@ -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...
|
||||
<Location />
|
||||
Order allow,deny
|
||||
# Allow any host to access the print server
|
||||
Allow all
|
||||
</Location>
|
||||
|
||||
# Restrict access to the admin pages...
|
||||
<Location /admin>
|
||||
Order allow,deny
|
||||
</Location>
|
||||
|
||||
# Restrict access to configuration files...
|
||||
<Location /admin/conf>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order allow,deny
|
||||
</Location>
|
||||
|
||||
# Restrict access to log files...
|
||||
<Location /admin/log>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order allow,deny
|
||||
</Location>
|
||||
|
||||
# Set the default printer/job policies...
|
||||
<Policy default>
|
||||
# Job/subscription privacy...
|
||||
JobPrivateAccess default
|
||||
JobPrivateValues default
|
||||
SubscriptionPrivateAccess default
|
||||
SubscriptionPrivateValues default
|
||||
|
||||
# Job-related operations must be done by the owner or an administrator...
|
||||
<Limit Create-Job Print-Job Print-URI Validate-Job>
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
|
||||
Require user @OWNER @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
# All administration operations require an administrator to authenticate...
|
||||
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default CUPS-Get-Devices>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
# All printer operations require a printer operator to authenticate...
|
||||
<Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After Cancel-Jobs CUPS-Accept-Jobs CUPS-Reject-Jobs>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
# Only the owner or an administrator can cancel or authenticate a job...
|
||||
<Limit Cancel-Job CUPS-Authenticate-Job>
|
||||
Require user @OWNER @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
<Limit All>
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
</Policy>
|
||||
|
||||
# Set the authenticated printer/job policies...
|
||||
<Policy authenticated>
|
||||
# Job/subscription privacy...
|
||||
JobPrivateAccess default
|
||||
JobPrivateValues default
|
||||
SubscriptionPrivateAccess default
|
||||
SubscriptionPrivateValues default
|
||||
|
||||
# Job-related operations must be done by the owner or an administrator...
|
||||
<Limit Create-Job Print-Job Print-URI Validate-Job>
|
||||
AuthType Default
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
|
||||
AuthType Default
|
||||
Require user @OWNER @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
# All administration operations require an administrator to authenticate...
|
||||
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
# All printer operations require a printer operator to authenticate...
|
||||
<Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After Cancel-Jobs CUPS-Accept-Jobs CUPS-Reject-Jobs>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
# Only the owner or an administrator can cancel or authenticate a job...
|
||||
<Limit Cancel-Job CUPS-Authenticate-Job>
|
||||
AuthType Default
|
||||
Require user @OWNER @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
<Limit All>
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
</Policy>
|
||||
# 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
|
16
integration_tests/ipp/container-cups/Dockerfile
Executable file
16
integration_tests/ipp/container-cups/Dockerfile
Executable file
@ -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"]
|
11
integration_tests/ipp/container-cups/entrypoint.sh
Executable file
11
integration_tests/ipp/container-cups/entrypoint.sh
Executable file
@ -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
|
@ -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"]
|
@ -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
|
@ -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"
|
||||
|
||||
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
|
@ -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
|
||||
}
|
||||
return &b
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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 <type>"
|
||||
// where same attribute has a set of values, rather than one
|
||||
for tag, nameLength := valueTag, int16(0); tag == valueTag && nameLength == 0; {
|
||||
var length int16
|
||||
if err := binary.Read(buf, binary.BigEndian, &length); err != nil {
|
||||
//Couldn't read length of content
|
||||
return vals, err
|
||||
}
|
||||
val := make([]byte, length)
|
||||
if err := binary.Read(buf, binary.BigEndian, &val); err != nil {
|
||||
//Couldn't read content
|
||||
vals = append(vals, val)
|
||||
return vals, err
|
||||
}
|
||||
vals = append(vals, val)
|
||||
if err := binary.Read(buf, binary.BigEndian, &tag); err != nil {
|
||||
//Couldn't read next valueTag
|
||||
return vals, err
|
||||
}
|
||||
// FIXME: Only try to read next namelength if previous valueTag wasn't end-of-attributes-tag
|
||||
if err := binary.Read(buf, binary.BigEndian, &nameLength); err != nil {
|
||||
//Couldn't read next nameLength
|
||||
return vals, err
|
||||
}
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
//The attribute was not present
|
||||
return nil, errors.New("Attribute \"" + attrString + "\" not present.")
|
||||
}
|
||||
|
||||
func 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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user