Merge branch 'master' into ah/trigger

This commit is contained in:
Alex Halderman 2018-06-26 17:53:20 -07:00 committed by GitHub
commit 0c597e5a8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1060 additions and 189 deletions

@ -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

@ -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"]

@ -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

@ -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"]

@ -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)