2018-06-07 19:13:01 +00:00
|
|
|
// Package ipp provides a zgrab2 module that scans for ipp.
|
|
|
|
// TODO: Describe module, the flags, the probe, the output, etc.
|
|
|
|
package ipp
|
|
|
|
|
|
|
|
import (
|
2018-06-26 16:00:27 +00:00
|
|
|
"bytes"
|
|
|
|
"crypto/sha256"
|
2018-06-11 16:02:42 +00:00
|
|
|
"encoding/binary"
|
2018-06-26 16:00:27 +00:00
|
|
|
"errors"
|
2018-06-12 13:27:45 +00:00
|
|
|
//"fmt"
|
2018-06-26 16:00:27 +00:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"mime"
|
|
|
|
"net"
|
|
|
|
"net/url"
|
2018-06-11 16:02:42 +00:00
|
|
|
"strconv"
|
2018-06-12 13:27:45 +00:00
|
|
|
"strings"
|
2018-06-07 19:13:01 +00:00
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/zmap/zgrab2"
|
2018-06-26 16:00:27 +00:00
|
|
|
"github.com/zmap/zgrab2/lib/http"
|
2018-06-07 19:13:01 +00:00
|
|
|
)
|
|
|
|
|
2018-06-12 13:27:45 +00:00
|
|
|
const (
|
2018-06-26 16:00:27 +00:00
|
|
|
ContentType string = "application/ipp"
|
|
|
|
VersionsSupported string = "ipp-versions-supported"
|
|
|
|
CupsVersion string = "cups-version"
|
|
|
|
PrinterURISupported string = "printer-uri-supported"
|
2018-06-12 13:27:45 +00:00
|
|
|
)
|
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
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")
|
|
|
|
|
2018-06-28 18:58:40 +00:00
|
|
|
Versions = []version{{Major: 2, Minor: 1}, {Major: 2, Minor: 0}, {Major: 1, Minor: 1}, {Major: 1, Minor: 0}}
|
2018-07-09 18:39:54 +00:00
|
|
|
AttributesCharset = []byte{0x47, 0x00, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74}
|
2018-06-26 16:00:27 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type scan struct {
|
|
|
|
connections []net.Conn
|
|
|
|
transport *http.Transport
|
|
|
|
client *http.Client
|
|
|
|
results ScanResults
|
|
|
|
url string
|
2018-07-09 18:39:54 +00:00
|
|
|
tls bool
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
|
|
|
|
2018-06-07 19:13:01 +00:00
|
|
|
//TODO: Tag relevant results and exlain in comments
|
|
|
|
// ScanResults instances are returned by the module's Scan function.
|
|
|
|
type ScanResults struct {
|
2018-06-12 21:00:52 +00:00
|
|
|
//TODO: ?Include the request sent as well??
|
2018-06-26 16:00:27 +00:00
|
|
|
Response *http.Response `json:"response,omitempty" zgrab:"debug"`
|
|
|
|
CUPSResponse *http.Response `json:"cups_response,omitempty" zgrab:"debug"`
|
2018-06-07 19:13:01 +00:00
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
// RedirectResponseChain is non-empty if the scanner follows a redirect.
|
|
|
|
// It contains all redirect responses prior to the final response.
|
2018-07-12 20:47:26 +00:00
|
|
|
RedirectResponseChain []*http.Response `json:"redirect_response_chain,omitempty" zgrab:"debug"`
|
2018-06-11 16:02:42 +00:00
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
MajorVersion *int8 `json:"version_major,omitempty"`
|
|
|
|
MinorVersion *int8 `json:"version_minor,omitempty"`
|
2018-06-12 13:27:45 +00:00
|
|
|
VersionString string `json:"version_string,omitempty"`
|
2018-06-26 17:51:10 +00:00
|
|
|
CUPSVersion string `json:"cups_version,omitempty"`
|
2018-07-09 18:39:54 +00:00
|
|
|
|
2018-07-11 15:52:13 +00:00
|
|
|
Attributes []*Attribute `json:"attributes,omitempty"`
|
2018-06-28 18:58:40 +00:00
|
|
|
AttributeCUPSVersion string `json:"attr_cups_version,omitempty"`
|
2018-06-26 16:00:27 +00:00
|
|
|
AttributeIPPVersions []string `json:"attr_ipp_versions,omitempty"`
|
2018-07-09 18:39:54 +00:00
|
|
|
AttributePrinterURIs []string `json:"attr_printer_uris,omitempty"`
|
2018-06-07 19:13:01 +00:00
|
|
|
|
2018-06-28 18:58:40 +00:00
|
|
|
TLSLog *zgrab2.TLSLog `json:"tls,omitempty"`
|
2018-06-07 19:13:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Flags holds the command-line configuration for the ipp scan module.
|
|
|
|
// Populated by the framework.
|
|
|
|
type Flags struct {
|
|
|
|
zgrab2.BaseFlags
|
2018-06-26 16:00:27 +00:00
|
|
|
zgrab2.TLSFlags
|
|
|
|
Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"`
|
|
|
|
|
2018-07-09 18:39:54 +00:00
|
|
|
//FIXME: Borrowed from http module, determine whether this is all needed
|
2018-06-26 16:00:27 +00:00
|
|
|
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"`
|
2018-07-11 14:38:42 +00:00
|
|
|
TLSRetry bool `long:"ipps-retry" description:"If the initial request using TLS fails, reconnect and try using plaintext IPP."`
|
2018-06-07 19:13:01 +00:00
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
// 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."`
|
2018-06-07 19:13:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Module implements the zgrab2.Module interface.
|
|
|
|
type Module struct {
|
2018-06-12 13:27:45 +00:00
|
|
|
// TODO: Add any module-global state if necessary
|
2018-06-07 19:13:01 +00:00
|
|
|
}
|
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
type version struct {
|
|
|
|
Major int8
|
|
|
|
Minor int8
|
|
|
|
}
|
|
|
|
|
2018-06-07 19:13:01 +00:00
|
|
|
// Scanner implements the zgrab2.Scanner interface.
|
|
|
|
type Scanner struct {
|
|
|
|
config *Flags
|
2018-06-12 13:27:45 +00:00
|
|
|
// TODO: Add scan state if any is necessary
|
2018-06-07 19:13:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// RegisterModule registers the zgrab2 module.
|
|
|
|
func RegisterModule() {
|
|
|
|
var module Module
|
|
|
|
_, err := zgrab2.AddCommand("ipp", "ipp", "Probe for ipp", 631, &module)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFlags returns a default Flags object.
|
|
|
|
func (module *Module) NewFlags() interface{} {
|
|
|
|
return new(Flags)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewScanner returns a new Scanner instance.
|
|
|
|
func (module *Module) NewScanner() zgrab2.Scanner {
|
|
|
|
return new(Scanner)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate checks that the flags are valid.
|
|
|
|
// On success, returns nil.
|
|
|
|
// On failure, returns an error instance describing the error.
|
|
|
|
func (flags *Flags) Validate(args []string) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Help returns the module's help string.
|
|
|
|
func (flags *Flags) Help() string {
|
|
|
|
//TODO: Write a help string
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// Init initializes the Scanner.
|
|
|
|
func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error {
|
|
|
|
f, _ := flags.(*Flags)
|
|
|
|
scanner.config = f
|
2018-06-26 16:00:27 +00:00
|
|
|
// TODO: Remove debug logging for unexpected behavior after 1% scan
|
|
|
|
if f.Verbose {
|
|
|
|
log.SetLevel(log.DebugLevel)
|
|
|
|
}
|
2018-06-07 19:13:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// InitPerSender initializes the scanner for a given sender.
|
|
|
|
func (scanner *Scanner) InitPerSender(senderID int) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetName returns the Scanner name defined in the Flags.
|
|
|
|
func (scanner *Scanner) GetName() string {
|
|
|
|
return scanner.config.Name
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:51:10 +00:00
|
|
|
// GetTrigger returns the Trigger defined in the Flags.
|
|
|
|
func (scanner *Scanner) GetTrigger() string {
|
|
|
|
return scanner.config.Trigger
|
|
|
|
}
|
|
|
|
|
2018-06-07 19:13:01 +00:00
|
|
|
// Protocol returns the protocol identifier of the scan.
|
|
|
|
func (scanner *Scanner) Protocol() string {
|
|
|
|
return "ipp"
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPort returns the port being scanned.
|
|
|
|
func (scanner *Scanner) GetPort() uint {
|
|
|
|
return scanner.config.Port
|
|
|
|
}
|
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
// 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)
|
2018-06-11 16:02:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
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
|
2018-06-12 13:27:45 +00:00
|
|
|
}
|
|
|
|
|
2018-07-09 18:39:54 +00:00
|
|
|
type Value struct {
|
|
|
|
Bytes []byte `json:"raw,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Attribute struct {
|
|
|
|
Name string `json:"name,omitempty"`
|
|
|
|
Values []Value `json:"values,omitempty"`
|
|
|
|
ValueTag byte `json:"tag,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func shouldReturnAttrs(length, soFar, size, upperBound int) (bool, error) {
|
|
|
|
if soFar + length > size {
|
|
|
|
// Size should never exceed upperBound in practice because of truncation, but this is more general
|
|
|
|
if size >= upperBound {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
return true, zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("Reported field length runs out of bounds."))
|
|
|
|
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func detectReadBodyError(err error) error {
|
|
|
|
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
|
|
return zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("Fewer body bytes read than expected."))
|
|
|
|
}
|
|
|
|
return zgrab2.NewScanError(zgrab2.TryGetScanStatus(err), err)
|
|
|
|
}
|
|
|
|
|
|
|
|
/* An IPP response contains the following data (as specified in RFC 8010 Section 3.1.8
|
|
|
|
https://tools.ietf.org/html/rfc8010#section-3.1.8)
|
|
|
|
bytes name
|
|
|
|
----------------------------
|
|
|
|
2 version-number
|
|
|
|
2 status-code
|
|
|
|
4 request-id
|
|
|
|
|
|
|
|
(0 or more instances of the following pair of fields)
|
|
|
|
1 delimiter-tag OR value-tag
|
|
|
|
x empty if delimiter-tag to begin a group OR rest of attribute if value-tag
|
|
|
|
|
|
|
|
1 end-of-attributes-tag
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
Those x bytes of any given attribute consist of the following (as specified in RFC 8010 Section 3.1.4
|
|
|
|
https://tools.ietf.org/html/rfc8010#section-3.1.4)
|
|
|
|
----------------------------
|
|
|
|
2 name-length = u
|
|
|
|
u name
|
|
|
|
2 value-length = v
|
|
|
|
v value
|
|
|
|
----------------------------
|
|
|
|
*/
|
|
|
|
func readAllAttributes(body []byte, scanner *Scanner) ([]*Attribute, error) {
|
|
|
|
var attrs []*Attribute
|
|
|
|
bytesRead := 0
|
|
|
|
buf := bytes.NewBuffer(body)
|
|
|
|
// Each field of this struct is exported to avoid binary.Read panicking
|
|
|
|
var start struct {
|
|
|
|
Version int16
|
|
|
|
StatusCode int16
|
|
|
|
ReqID int32
|
|
|
|
}
|
|
|
|
// Read in pre-attribute part of body to ignore it
|
|
|
|
if err := binary.Read(buf, binary.BigEndian, &start); err != nil {
|
|
|
|
return attrs, detectReadBodyError(err)
|
|
|
|
}
|
|
|
|
bytesRead += 8
|
|
|
|
// Read in first delimiter tag, usually a begin-attribute-group-tag (which is equal to 1)
|
|
|
|
var tag byte
|
|
|
|
if err := binary.Read(buf, binary.BigEndian, &tag); err != nil {
|
|
|
|
return attrs, detectReadBodyError(err)
|
|
|
|
}
|
|
|
|
bytesRead++
|
|
|
|
var lastTag byte
|
|
|
|
// Until encountering end-of-attributes-tag (which is equal to 3):
|
|
|
|
for tag != 0x03 {
|
|
|
|
// If tag is a delimiter-tag ([0x00, 0x05]), read the next tag, which corresponds to the first
|
|
|
|
// attribute's value-tag
|
|
|
|
if tag <= 0x05 {
|
2018-07-10 19:16:30 +00:00
|
|
|
lastTag = tag
|
2018-07-09 18:39:54 +00:00
|
|
|
if err := binary.Read(buf, binary.BigEndian, &tag); err != nil {
|
|
|
|
return attrs, detectReadBodyError(err)
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
2018-07-09 18:39:54 +00:00
|
|
|
bytesRead++
|
2018-07-10 19:16:30 +00:00
|
|
|
// Start a new iteration after reading this tag, since the next tag could be another
|
|
|
|
// delimiter to be caught by this same if block
|
|
|
|
continue
|
2018-07-09 18:39:54 +00:00
|
|
|
}
|
|
|
|
// TODO: Implement parsing attribute collections, since they're special
|
|
|
|
// Read in length of attribute's name, which will be used to determine whether this attribute stands alone
|
|
|
|
// or provides an additonal value for the previous attribute
|
|
|
|
var nameLength int16
|
|
|
|
if err := binary.Read(buf, binary.BigEndian, &nameLength); err != nil {
|
|
|
|
return attrs, detectReadBodyError(err)
|
|
|
|
}
|
|
|
|
bytesRead += 2
|
|
|
|
// If reading the name would entail reading past body, check whether body was truncated
|
|
|
|
if should, err := shouldReturnAttrs(int(nameLength), bytesRead, len(body), scanner.config.MaxSize * 1024); should {
|
|
|
|
// If body was truncated, return all attributes so far without error
|
|
|
|
// Otherwise, return a protocol error because name-length should indicate the
|
|
|
|
// length of the following name when obeying the protocol's encoding
|
|
|
|
return attrs, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var attr *Attribute
|
|
|
|
// If sequential tags match and name-length of the latter is 0, the second attribute is
|
|
|
|
// an additional value for the former, so we read and append another value for that attr
|
|
|
|
if tag == lastTag && nameLength == 0 {
|
|
|
|
attr = attrs[len(attrs)-1]
|
|
|
|
// Otherwise, create a new attribute and read in its name
|
|
|
|
} else {
|
|
|
|
attr = &Attribute{ValueTag: tag}
|
|
|
|
attrs = append(attrs, attr)
|
|
|
|
}
|
|
|
|
// Read in name into this slice (or no name into an empty slice if nameLength == 0)
|
|
|
|
name := make([]byte, nameLength)
|
|
|
|
if err := binary.Read(buf, binary.BigEndian, &name); err != nil {
|
|
|
|
return attrs, detectReadBodyError(err)
|
|
|
|
}
|
|
|
|
bytesRead += int(nameLength)
|
|
|
|
if attr.Name == "" {
|
|
|
|
attr.Name = string(name)
|
|
|
|
}
|
|
|
|
// Determine length of current value of the current attribute
|
|
|
|
var length int16
|
|
|
|
if err := binary.Read(buf, binary.BigEndian, &length); err != nil {
|
|
|
|
return attrs, detectReadBodyError(err)
|
|
|
|
}
|
|
|
|
bytesRead += 2
|
|
|
|
// If reading the name would entail reading past body, check whether body was truncated
|
|
|
|
if should, err := shouldReturnAttrs(int(length), bytesRead, len(body), scanner.config.MaxSize * 1024); should {
|
|
|
|
// If body was truncated, return all attributes so far without error
|
|
|
|
// Otherwise, return a protocol error because name-length should indicate the
|
|
|
|
// length of the following name when obeying the protocol's encoding
|
|
|
|
return attrs, err
|
|
|
|
}
|
|
|
|
if length > 0 {
|
|
|
|
// Read and append a value to the current attribute
|
2018-06-26 16:00:27 +00:00
|
|
|
val := make([]byte, length)
|
|
|
|
if err := binary.Read(buf, binary.BigEndian, &val); err != nil {
|
2018-07-09 18:39:54 +00:00
|
|
|
return attrs, detectReadBodyError(err)
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
2018-07-09 18:39:54 +00:00
|
|
|
bytesRead += int(length)
|
|
|
|
attr.Values = append(attr.Values, Value{Bytes: val})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read in the following tag to be assessed at the next iteration's start
|
|
|
|
lastTag = tag
|
|
|
|
if err := binary.Read(buf, binary.BigEndian, &tag); err != nil {
|
|
|
|
return attrs, detectReadBodyError(err)
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
2018-07-09 18:39:54 +00:00
|
|
|
bytesRead++
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
2018-07-09 18:39:54 +00:00
|
|
|
|
|
|
|
return attrs, nil
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-09 18:39:54 +00:00
|
|
|
func (scanner *Scanner) tryReadAttributes(resp *http.Response, scan *scan) *zgrab2.ScanError {
|
|
|
|
body := []byte(resp.BodyText)
|
|
|
|
// A well-formed IPP response MUST include the required status-code field.
|
|
|
|
// "If an IPP status-code is returned, the HTTP status-code MUST be 200"
|
|
|
|
// Therefore, an HTTP Status Code other than 200 indicates the response is not a well-formed IPP response.
|
|
|
|
// RFC 8010 Section 3.4.3 Source: https://tools.ietf.org/html/rfc8010#section-3.4.3
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, errors.New("Response returned with status " + resp.Status))
|
2018-06-28 18:58:40 +00:00
|
|
|
}
|
2018-07-09 18:39:54 +00:00
|
|
|
|
|
|
|
// Reject successful responses which specify non-IPP MIME mediatype (ie: text/html)
|
|
|
|
// RFC 8010's abstract specifies that IPP uses the MIME media type "application/ipp"
|
|
|
|
if !isIPP(resp) {
|
|
|
|
return zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("IPP Content-Type not detected."))
|
|
|
|
}
|
|
|
|
|
|
|
|
attrs, err := readAllAttributes(body, scanner)
|
|
|
|
if err != nil {
|
|
|
|
// TODO: Handle error appropriately
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"error": err,
|
|
|
|
"body": resp.BodyText,
|
|
|
|
}).Debug("Failed to read attributes from body with error.")
|
|
|
|
}
|
|
|
|
scan.results.Attributes = append(scan.results.Attributes, attrs...)
|
|
|
|
|
|
|
|
for _, attr := range scan.results.Attributes {
|
2018-07-11 18:57:22 +00:00
|
|
|
if attr.Name == CupsVersion && scan.results.AttributeCUPSVersion == "" && len(attr.Values) > 0 {
|
2018-07-09 18:39:54 +00:00
|
|
|
scan.results.AttributeCUPSVersion = string(attr.Values[0].Bytes)
|
|
|
|
}
|
|
|
|
if attr.Name == VersionsSupported && len(scan.results.AttributeIPPVersions) == 0 {
|
|
|
|
for _, v := range attr.Values {
|
|
|
|
scan.results.AttributeIPPVersions = append(scan.results.AttributeIPPVersions, string(v.Bytes))
|
2018-06-28 18:58:40 +00:00
|
|
|
}
|
|
|
|
}
|
2018-07-11 18:57:22 +00:00
|
|
|
if attr.Name == PrinterURISupported && len(attr.Values) > 0 {
|
2018-07-09 18:39:54 +00:00
|
|
|
scan.results.AttributePrinterURIs = append(scan.results.AttributePrinterURIs, string(attr.Values[0].Bytes))
|
2018-06-28 18:58:40 +00:00
|
|
|
}
|
|
|
|
}
|
2018-07-09 18:39:54 +00:00
|
|
|
|
|
|
|
return nil
|
2018-06-28 18:58:40 +00:00
|
|
|
}
|
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
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 {
|
2018-06-28 18:58:40 +00:00
|
|
|
_ uint16
|
2018-06-26 16:00:27 +00:00
|
|
|
StatusCode uint16
|
|
|
|
}
|
|
|
|
err := binary.Read(buf, binary.BigEndian, &reader)
|
|
|
|
if err != nil {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"error": err,
|
2018-06-28 18:58:40 +00:00
|
|
|
"body": body,
|
2018-06-26 16:00:27 +00:00
|
|
|
}).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
|
|
|
|
}
|
|
|
|
|
2018-06-28 18:58:40 +00:00
|
|
|
// TODO: Genericize this with passed-in getIPPRequest function and *http.Response for some result field to store into
|
2018-06-26 16:00:27 +00:00
|
|
|
func (scanner *Scanner) augmentWithCUPSData(scan *scan, target *zgrab2.ScanTarget, version *version) *zgrab2.ScanError {
|
|
|
|
cupsBody := getPrintersRequest(version.Major, version.Minor)
|
2018-06-28 18:58:40 +00:00
|
|
|
cupsResp, err := sendIPPRequest(scan, cupsBody)
|
|
|
|
//Store response regardless of error in request, because we may have gotten something back
|
2018-06-26 16:00:27 +00:00
|
|
|
scan.results.CUPSResponse = cupsResp
|
|
|
|
if err != nil {
|
2018-06-28 18:58:40 +00:00
|
|
|
return err
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2018-07-09 18:39:54 +00:00
|
|
|
if err := scanner.tryReadAttributes(scan.results.CUPSResponse, scan); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-06-26 16:00:27 +00:00
|
|
|
return nil
|
|
|
|
}
|
2018-06-12 21:00:52 +00:00
|
|
|
|
2018-06-28 18:58:40 +00:00
|
|
|
// TODO: Let this receive generic *io.Reader rather than *bytes.Buffer in particular
|
|
|
|
func sendIPPRequest(scan *scan, body *bytes.Buffer) (*http.Response, *zgrab2.ScanError) {
|
2018-06-26 16:00:27 +00:00
|
|
|
request, err := http.NewRequest("POST", scan.url, body)
|
|
|
|
if err != nil {
|
2018-06-28 18:58:40 +00:00
|
|
|
// TODO: Log the error to see what exactly went wrong
|
|
|
|
return nil, zgrab2.DetectScanError(err)
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
|
|
|
request.Header.Set("Accept", "*/*")
|
|
|
|
request.Header.Set("Content-Type", ContentType)
|
|
|
|
resp, err := scan.client.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
if urlError, ok := err.(*url.Error); ok {
|
|
|
|
err = urlError.Err
|
|
|
|
}
|
|
|
|
}
|
2018-06-07 19:13:01 +00:00
|
|
|
if err != nil {
|
2018-06-26 16:00:27 +00:00
|
|
|
switch err {
|
|
|
|
case ErrRedirLocalhost:
|
|
|
|
break
|
|
|
|
case ErrTooManyRedirects:
|
2018-06-28 18:58:40 +00:00
|
|
|
return resp, zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, err)
|
2018-06-26 16:00:27 +00:00
|
|
|
default:
|
2018-06-28 18:58:40 +00:00
|
|
|
return resp, zgrab2.DetectScanError(err)
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
2018-06-07 19:13:01 +00:00
|
|
|
}
|
2018-06-28 18:58:40 +00:00
|
|
|
// TODO: Examine whether an empty response overall is a connection error; see RFC 8011 Section 4.2.5.2
|
|
|
|
if resp == nil {
|
|
|
|
return resp, zgrab2.NewScanError(zgrab2.SCAN_CONNECTION_TIMEOUT, errors.New("No HTTP response"))
|
|
|
|
}
|
|
|
|
// Empty 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)
|
|
|
|
if resp.Body == nil {
|
|
|
|
return resp, zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("Empty body."))
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|
2018-06-26 16:00:27 +00:00
|
|
|
|
2018-07-09 18:39:54 +00:00
|
|
|
func hasContentType(resp *http.Response, contentType string) bool {
|
|
|
|
// Removal of everything post-comma added in response to empirical examples of Virata-EmWeb
|
|
|
|
// print servers listed with "Content-Type" of "application/ipp, public"
|
|
|
|
cType := strings.Split(resp.Header.Get("Content-Type"), ",")[0]
|
|
|
|
// Parameters can be ignored, since there are no required or optional parameters
|
|
|
|
// IPP parameters specified at https://www.iana.org/assignments/media-types/application/ipp
|
|
|
|
mediatype, _, err := mime.ParseMediaType(cType)
|
|
|
|
// Certainly doesn't have correct Content-Type if there was a malformed or empty Content-Type
|
|
|
|
if mediatype == "" && err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
// Check for only subtype added in resonse to empirical examples of Rapid Logic print servers
|
|
|
|
// listed with "Content-Type" of "IPP"
|
|
|
|
subType := strings.Split(contentType, "/")[1]
|
|
|
|
return strings.HasPrefix(mediatype, contentType) || strings.HasPrefix(mediatype, subType)
|
|
|
|
}
|
|
|
|
|
|
|
|
func isIPP(resp *http.Response) bool {
|
|
|
|
hasIPP := hasContentType(resp, ContentType)
|
|
|
|
body := []byte(resp.BodyText)
|
|
|
|
// If Content-Type header doesn't clearly indicate IPP, but "attributes-charset"
|
|
|
|
// attribute is specified in the correct format for IPP, still indicate a positive detection
|
|
|
|
// This is in response to empirical evidence of all false negatives specifying "attributes-charset"
|
|
|
|
// in the correct format.
|
|
|
|
return resp.StatusCode == 200 && (hasIPP || bytes.Contains(body, AttributesCharset))
|
|
|
|
}
|
|
|
|
|
2018-06-28 18:58:40 +00:00
|
|
|
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
|
2018-07-09 18:39:54 +00:00
|
|
|
body := getPrinterAttributesRequest(version.Major, version.Minor, scan.url, scan.tls)
|
2018-06-28 18:58:40 +00:00
|
|
|
// TODO: Log any weird errors coming out of this
|
|
|
|
resp, err := sendIPPRequest(scan, body)
|
|
|
|
//Store response regardless of error in request, because we may have gotten something back
|
|
|
|
scan.results.Response = resp
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
|
|
|
storeBody(resp, scanner)
|
|
|
|
if versionNotSupported(scan.results.Response.BodyText) {
|
|
|
|
return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, ErrVersionNotSupported)
|
2018-06-11 16:02:42 +00:00
|
|
|
}
|
2018-06-12 21:00:52 +00:00
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
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{
|
2018-06-28 18:58:40 +00:00
|
|
|
"error": err,
|
2018-06-26 16:00:27 +00:00
|
|
|
"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{
|
2018-06-28 18:58:40 +00:00
|
|
|
"error": err,
|
2018-06-26 16:00:27 +00:00
|
|
|
"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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-09 18:39:54 +00:00
|
|
|
if err := scanner.tryReadAttributes(scan.results.Response, scan); err != nil {
|
|
|
|
return err
|
2018-06-28 18:58:40 +00:00
|
|
|
}
|
2018-07-09 18:39:54 +00:00
|
|
|
if scan.results.CUPSVersion != "" {
|
|
|
|
err := scanner.augmentWithCUPSData(scan, target, version)
|
|
|
|
if err != nil {
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"error": err,
|
|
|
|
}).Debug("Failed to augment with CUPS-get-printers request.")
|
|
|
|
}
|
2018-06-28 18:58:40 +00:00
|
|
|
}
|
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
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) {
|
2018-10-16 17:51:06 +00:00
|
|
|
outer, err := zgrab2.DialTimeoutConnection(net, addr, scanner.config.BaseFlags.Timeout, 0)
|
2018-06-26 16:00:27 +00:00
|
|
|
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"
|
|
|
|
} else {
|
|
|
|
proto = "http"
|
2018-06-11 16:02:42 +00:00
|
|
|
}
|
2018-06-26 16:00:27 +00:00
|
|
|
return proto + "://" + host + ":" + strconv.FormatUint(uint64(port), 10) + endpoint
|
|
|
|
}
|
|
|
|
|
|
|
|
// Adapted from newHTTPScan in zgrab2 http module
|
2018-07-09 18:39:54 +00:00
|
|
|
func (scanner *Scanner) newIPPScan(target *zgrab2.ScanTarget, tls bool) *scan {
|
2018-06-26 16:00:27 +00:00
|
|
|
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)
|
2018-07-10 18:25:25 +00:00
|
|
|
transport.DialContext = zgrab2.GetTimeoutConnectionDialer(scanner.config.Timeout).DialContext
|
2018-06-26 16:00:27 +00:00
|
|
|
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
|
2018-07-09 18:39:54 +00:00
|
|
|
newScan.tls = tls
|
2018-06-26 16:00:27 +00:00
|
|
|
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??
|
2018-07-10 20:07:28 +00:00
|
|
|
newScan.url = getHTTPURL(tls, host, uint16(scanner.config.BaseFlags.Port), "/ipp")
|
2018-06-26 16:00:27 +00:00
|
|
|
return &newScan
|
2018-06-11 16:02:42 +00:00
|
|
|
}
|
|
|
|
|
2018-07-09 18:39:54 +00:00
|
|
|
// Cleanup closes any connections that have been opened during the scan
|
|
|
|
func (scan *scan) Cleanup() {
|
|
|
|
if scan.connections != nil {
|
|
|
|
for _, conn := range scan.connections {
|
|
|
|
defer conn.Close()
|
|
|
|
}
|
|
|
|
scan.connections = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-28 18:58:40 +00:00
|
|
|
// TODO: Do you want to retry with TLS for all versions? Just one's you've already tried? Haven't tried? Just the same version?
|
2018-07-09 18:39:54 +00:00
|
|
|
func (scanner *Scanner) tryGrabForVersions(target *zgrab2.ScanTarget, versions []version, tls bool) (*scan, *zgrab2.ScanError) {
|
|
|
|
scan := scanner.newIPPScan(target, tls)
|
|
|
|
defer scan.Cleanup()
|
2018-06-28 18:58:40 +00:00
|
|
|
var err *zgrab2.ScanError
|
2018-07-09 18:39:54 +00:00
|
|
|
for i := 0; i < len(versions); i++ {
|
|
|
|
err = scanner.Grab(scan, target, &versions[i])
|
|
|
|
if err != nil && err.Err == ErrVersionNotSupported && i < len(versions)-1 {
|
2018-06-28 18:58:40 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
return scan, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Incorporate status into this? I don't think so, b/c with certain statuses, we should return
|
|
|
|
// early, so special casing seems to make sense
|
|
|
|
func (scan *scan) shouldReportResult(scanner *Scanner) bool {
|
|
|
|
if scan.results.Response != nil {
|
|
|
|
return true
|
2018-07-09 18:39:54 +00:00
|
|
|
} else if scan.tls {
|
2018-06-28 18:58:40 +00:00
|
|
|
l := scan.results.TLSLog
|
|
|
|
return l != nil && l.HandshakeLog != nil && l.HandshakeLog.ServerHello != nil
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-06-26 16:00:27 +00:00
|
|
|
// Scan TODO: describe how scan operates in appropriate detail
|
2018-06-12 13:27:45 +00:00
|
|
|
//1. Send a request (currently get-printer-attributes)
|
|
|
|
//2. Take in that response & read out version numbers
|
2018-06-11 16:02:42 +00:00
|
|
|
func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) {
|
2018-06-28 18:58:40 +00:00
|
|
|
// Try all known IPP versions from newest to oldest until we reach a supported version
|
2018-07-10 20:07:28 +00:00
|
|
|
scan, err := scanner.tryGrabForVersions(&target, Versions, scanner.config.TLSRetry || scanner.config.IPPSecure)
|
2018-06-26 16:00:27 +00:00
|
|
|
if err != nil {
|
2018-06-28 18:58:40 +00:00
|
|
|
// If versionNotSupported error was confirmed, the scanner was connecting w/o TLS, so don't retry
|
|
|
|
// Same goes for a protocol error of any kind. It means we got something back but it didn't conform.
|
2018-07-11 18:45:46 +00:00
|
|
|
if err.Err == ErrVersionNotSupported || err.Status == zgrab2.SCAN_PROTOCOL_ERROR {
|
2018-06-28 18:58:40 +00:00
|
|
|
return err.Unpack(&scan.results)
|
|
|
|
}
|
2018-07-10 20:07:28 +00:00
|
|
|
if scanner.config.TLSRetry && !scanner.config.IPPSecure {
|
|
|
|
retry, retryErr := scanner.tryGrabForVersions(&target, Versions, false)
|
2018-06-26 16:00:27 +00:00
|
|
|
if retryErr != nil {
|
2018-06-28 18:58:40 +00:00
|
|
|
if retry.shouldReportResult(scanner) {
|
2018-07-09 18:39:54 +00:00
|
|
|
return retryErr.Unpack(&retry.results)
|
|
|
|
}
|
|
|
|
// Use original result as a fallback when retry result shouldn't be returned
|
|
|
|
if scan.shouldReportResult(scanner) {
|
|
|
|
return err.Unpack(&scan.results)
|
2018-06-28 18:58:40 +00:00
|
|
|
}
|
2018-07-09 18:39:54 +00:00
|
|
|
return zgrab2.TryGetScanStatus(retryErr), nil, retryErr
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
2018-06-28 18:58:40 +00:00
|
|
|
return zgrab2.SCAN_SUCCESS, &retry.results, nil
|
|
|
|
}
|
|
|
|
if scan.shouldReportResult(scanner) {
|
|
|
|
return err.Unpack(&scan.results)
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
2018-06-28 18:58:40 +00:00
|
|
|
return zgrab2.TryGetScanStatus(err), nil, err
|
2018-06-26 16:00:27 +00:00
|
|
|
}
|
|
|
|
return zgrab2.SCAN_SUCCESS, &scan.results, nil
|
2018-06-07 19:13:01 +00:00
|
|
|
}
|