![justinbastress](/assets/img/avatar_default.png)
* remove unnecessary indirection on net.Conn * Ignore *.pyc * fix NPE on nil handshake * refactoring -- move status to status.go; add Open() methods for ScanTarget * cherry-pick .gitignore fix * pull in TLS fix * status.go comments * trim over-generalizations * use /usr/bin/env bash instead of absolute path * remove debug tcpwrap * add integration tests for postgres * hack for cleanup.sh to work on mingw -- use //var/lib instead of /var/lib * cleanup should actually stop the process though * comments / rearrange * Bump up timeout in postgres tests; only pass user if explicitly requested to do so * add schema stubs to new.sh * Integration test fixes -- use /usr/bin/env bash; log all validation failures * add postgres schemas * fill out zcrypto.client_hello schema * handle early get of TLSLog * postgres: return SCAN_SUCCESS on success * cleanup * fix new.sh * fix typo * postgres container cleanup * build.sh docs * standardize container/image names * add not to check for success * shift mysql's connection management to ScanTarget.Open(); wrap Read/Write methods returned by ScanTarget.Open() to enforce timeouts * catch schematically-valid but non-successful scans * postgres: clean up output format; more scanning * cleanup; better error handling; get detailed protocol version error * refactor modules * clean up dangling connections * split gigantic postgres.go * remove unused * ServerParams gets its own type * refactor integration tests: run zgrab2 in its own container, which is linked to the service containers, so that we don't need to keep track of unique ports on the host any more * rename entrypoint; remove duplicate postgres tests * comments for postgres schema * Use param expansion to check for env variable [minor] This is a *very* minor change to `docker-runner/docker-run.sh` checks to see if the environment variable required to run the script has been set to a non-empty string. If not, the script exits with a non-zero status code and displays a default message: ``` ❯ docker-runner/docker-run.sh docker-runner/docker-run.sh: line 7: CONTAINER_NAME: parameter null or not set ``` This was the behavior before, but just uses a one-liner declarative bash idiom. For further reading on parameter expansion, see https://stackoverflow.com/a/307735. @justinbastress can tell me if I did something wrong and broke the intent of the script :-) * Add integration_test targets to makefile; use makefile instead of directly calling go build everywhere; run postgres schema through PEP8 linter * use make in docker-runner entrypoint * add .integration_test_setup to .gitignore * more .gitignore items * Makefile updates: Windows support; add docker-runner target; better cleanup. * docker-runner Dockerfile: start from zgrab2_runner_base image * cleanup postgres setup * make travis use make * add .gitattributes, try to prevent it from overriding lfs with crlfs in shell scripts at least * fix folder name in Makefile * update go (one of our dependencies now works only with >= 1.9) * From travis: `I don't have any idea what to do with '1.9.0'.` * explicit clean make * fix dep order * fix build.sh location * popd * use make to ensure zgrab2_runner exists * Make docker-runner an order-dependency for integration-test-cleanup; don't do a cleanup after each integration test * use explicit tag name for zgrab2_runner * Add container-clean target to Makefile, to remove cyclic dependency on docker; use .id files to track docker images; add servce-base image; use Make to build / track images * use LF in Makefiles; update .gitignore; use zgrab_service_base image in ssh container; fix line endings (?) * remove overzealous cleanup * let setup continue even if some containers are already running * zgrab depends on *.go * docker-runner depends on zgrab2 binary * clean output before running integration tests
275 lines
9.0 KiB
Go
275 lines
9.0 KiB
Go
package zgrab2
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/zmap/zcrypto/tls"
|
|
)
|
|
|
|
// Shared code for TLS scans.
|
|
// Example usage:
|
|
// (include TLSFlags in ScanFlags implementation)
|
|
// (in scanning code, where you would call tls.Client()):
|
|
// tlsConnection, err := myScanFlags.TLSFlags.GetTLSConnection(myModule.netConn)
|
|
// err := tlsConnection.Handshake()
|
|
// myModule.netConn = tlsConnection
|
|
// result.tls = tlsConnection.GetLog()
|
|
|
|
// Common flags for TLS configuration -- include this in your module's ScanFlags implementation to use the common TLS code
|
|
// Adapted from modules/ssh.go
|
|
type TLSFlags struct {
|
|
Heartbleed bool `long:"heartbleed" description:"Check if server is vulnerable to Heartbleed"`
|
|
|
|
SessionTicket bool `long:"session-ticket" description:"Send support for TLS Session Tickets and output ticket if presented" json:"session"`
|
|
ExtendedMasterSecret bool `long:"extended-master-secret" description:"Offer RFC 7627 Extended Master Secret extension" json:"extended"`
|
|
ExtendedRandom bool `long:"extended-random" description:"Send TLS Extended Random Extension" json:"extran"`
|
|
NoSNI bool `long:"no-sni" description:"Do not send domain name in TLS Handshake regardless of whether known" json:"sni"`
|
|
SCTExt bool `long:"sct" description:"Request Signed Certificate Timestamps during TLS Handshake" json:"sct"`
|
|
|
|
// TODO: Do we just lump this with Verbose (and put Verbose in TLSFlags)?
|
|
KeepClientLogs bool `long:"keep-client-logs" description:"Include the client-side logs in the TLS handshake"`
|
|
|
|
Time string `long:"time" description:"Explicit request time to use, instead of clock. YYYYMMDDhhmmss format."`
|
|
// TODO: directory? glob? How to map server name -> certificate?
|
|
Certificates string `long:"certificates" description:"Set of certificates to present to the server"`
|
|
// TODO: re-evaluate this, or at least specify the file format
|
|
CertificateMap string `long:"certificate-map" description:"A file mapping server names to certificates"`
|
|
// TODO: directory? glob?
|
|
RootCAs string `long:"root-cas" description:"Set of certificates to use when verifying server certificates"`
|
|
// TODO: format?
|
|
NextProtos string `long:"next-protos" description:"A list of supported application-level protocols"`
|
|
ServerName string `long:"server-name" description:"Server name used for certificate verification and (optionally) SNI"`
|
|
VerifyServerCertificate bool `long:"verify-server-certificate" description:"If set, the scan will fail if the server certificate does not match the server-name, or does not chain to a trusted root."`
|
|
// TODO: format? mapping? zgrab1 had flags like ChromeOnly, FirefoxOnly, etc...
|
|
CipherSuite string `long:"cipher-suite" description:"A list of cipher suites to use."`
|
|
MinVersion int `long:"min-version" description:"The minimum SSL/TLS version that is acceptable. 0 means that SSLv3 is the minimum."`
|
|
MaxVersion int `long:"max-version" description:"The maximum SSL/TLS version that is acceptable. 0 means use the highest supported value."`
|
|
CurvePreferences string `long:"curve-preferences" description:"A list of elliptic curves used in an ECDHE handshake, in order of preference."`
|
|
NoECDHE bool `long:"no-ecdhe" description:"Do not allow ECDHE handshakes"`
|
|
// TODO: format?
|
|
SignatureAlgorithms string `long:"signature-algorithms" description:"Signature and hash algorithms that are acceptable"`
|
|
HeartbeatEnabled bool `long:"heartbeat-enabled" description:"If set, include the heartbeat extension"`
|
|
DSAEnabled bool `long:"dsa-enabled" description:"Accept server DSA keys"`
|
|
// TODO: format?
|
|
ClientRandom string `long:"client-random" description:"Set an explicit Client Random (base64 encoded)"`
|
|
// TODO: format?
|
|
ClientHello string `long:"client-hello" description:"Set an explicit ClientHello (base64 encoded)"`
|
|
}
|
|
|
|
func getCSV(arg string) []string {
|
|
// TODO: Find standard way to pass array-valued options
|
|
reader := csv.NewReader(strings.NewReader(arg))
|
|
ret, err := reader.ReadAll()
|
|
if err != nil {
|
|
log.Fatalf("Error parsing CSV argument '%s': %s", arg, err)
|
|
}
|
|
if len(ret) != 1 {
|
|
log.Fatalf("Bad CSV -- must have exactly one row (%s)", arg)
|
|
}
|
|
for i, v := range ret[0] {
|
|
ret[0][i] = strings.Trim(v, " \t")
|
|
}
|
|
return ret[0]
|
|
}
|
|
|
|
func (t *TLSFlags) GetTLSConfig() (*tls.Config, error) {
|
|
var err error
|
|
|
|
// TODO: Find standard names
|
|
cipherMap := map[string][]uint16{
|
|
"dhe-only": tls.DHECiphers,
|
|
"ecdhe-only": tls.ECDHECiphers,
|
|
"exports-dh-only": tls.DHEExportCiphers,
|
|
"chrome-only": tls.ChromeCiphers,
|
|
"chrome-no-dhe": tls.ChromeNoDHECiphers,
|
|
"firefox-only": tls.FirefoxCiphers,
|
|
"firefox-no-dhe": tls.FirefoxNoDHECiphers,
|
|
"safari-only": tls.SafariCiphers,
|
|
"safari-no-dhe": tls.SafariNoDHECiphers,
|
|
}
|
|
|
|
ret := tls.Config{}
|
|
|
|
if t.Time != "" {
|
|
// TODO: Find standard time format
|
|
var baseTime time.Time
|
|
baseTime, err = time.Parse("20060102150405Z", t.Time)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error parsing time '%s': %s", t.Time, err)
|
|
}
|
|
startTime := time.Now()
|
|
ret.Time = func() time.Time {
|
|
offset := time.Now().Sub(startTime)
|
|
// Return (now - startTime) + baseTime
|
|
return baseTime.Add(offset)
|
|
}
|
|
}
|
|
if t.Certificates != "" {
|
|
// TODO FIXME: Implement
|
|
log.Fatalf("--certificates not implemented")
|
|
}
|
|
if t.CertificateMap != "" {
|
|
// TODO FIXME: Implement
|
|
log.Fatalf("--certificate-map not implemented")
|
|
}
|
|
if t.RootCAs != "" {
|
|
// TODO FIXME: Implement
|
|
log.Fatalf("--root-cas not implemented")
|
|
}
|
|
if t.NextProtos != "" {
|
|
// TODO: Different format?
|
|
ret.NextProtos = getCSV(t.NextProtos)
|
|
}
|
|
if t.ServerName != "" {
|
|
// TODO: In the original zgrab, this was only set of NoSNI was not set (though in that case, it set it to the scanning host name)
|
|
ret.ServerName = t.ServerName
|
|
}
|
|
if t.VerifyServerCertificate {
|
|
ret.InsecureSkipVerify = false
|
|
} else {
|
|
ret.InsecureSkipVerify = true
|
|
}
|
|
|
|
if t.CipherSuite != "" {
|
|
ret.CipherSuites = cipherMap[t.CipherSuite]
|
|
if ret.CipherSuites == nil {
|
|
return nil, fmt.Errorf("%s is not a valid cipher suite", t.CipherSuite)
|
|
}
|
|
}
|
|
|
|
if t.MinVersion != 0 {
|
|
ret.MinVersion = uint16(t.MinVersion)
|
|
}
|
|
|
|
if t.MaxVersion != 0 {
|
|
ret.MaxVersion = uint16(t.MaxVersion)
|
|
}
|
|
|
|
if t.CurvePreferences != "" {
|
|
// TODO FIXME: Implement (how to map curveName to CurveID? Or are there standard 'suites' like we use for cipher suites?)
|
|
log.Fatalf("--curve-preferences not implemented")
|
|
}
|
|
|
|
if t.NoECDHE {
|
|
ret.ExplicitCurvePreferences = true
|
|
ret.CurvePreferences = nil
|
|
}
|
|
|
|
if t.SignatureAlgorithms != "" {
|
|
// TODO FIXME: Implement (none of the signatureAndHash functions/consts are exported from common.go...?)
|
|
log.Fatalf("--signature-algorithms not implemented")
|
|
}
|
|
|
|
if t.HeartbeatEnabled {
|
|
ret.HeartbeatEnabled = true
|
|
} else {
|
|
ret.HeartbeatEnabled = false
|
|
}
|
|
|
|
if t.DSAEnabled {
|
|
ret.ClientDSAEnabled = true
|
|
} else {
|
|
ret.ClientDSAEnabled = false
|
|
}
|
|
|
|
if t.ExtendedRandom {
|
|
ret.ExtendedRandom = true
|
|
} else {
|
|
ret.ExtendedRandom = false
|
|
}
|
|
|
|
if t.SessionTicket {
|
|
ret.ForceSessionTicketExt = true
|
|
} else {
|
|
ret.ForceSessionTicketExt = false
|
|
}
|
|
|
|
if t.ExtendedMasterSecret {
|
|
ret.ExtendedMasterSecret = true
|
|
} else {
|
|
ret.ExtendedMasterSecret = false
|
|
}
|
|
|
|
if t.SCTExt {
|
|
ret.SignedCertificateTimestampExt = true
|
|
} else {
|
|
ret.SignedCertificateTimestampExt = false
|
|
}
|
|
|
|
if t.ClientRandom != "" {
|
|
ret.ClientRandom, err = base64.StdEncoding.DecodeString(t.ClientRandom)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error decoding --client-random value '%s': %s", t.ClientRandom, err)
|
|
}
|
|
}
|
|
|
|
if t.ClientHello != "" {
|
|
ret.ExternalClientHello, err = base64.StdEncoding.DecodeString(t.ClientHello)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error decoding --client-hello value '%s': %s", t.ClientHello, err)
|
|
}
|
|
}
|
|
|
|
return &ret, nil
|
|
}
|
|
|
|
type TLSConnection struct {
|
|
tls.Conn
|
|
flags *TLSFlags
|
|
log *TLSLog
|
|
}
|
|
|
|
type TLSLog struct {
|
|
// TODO include TLSFlags?
|
|
HandshakeLog *tls.ServerHandshake `json:"handshake_log"`
|
|
// This will be nil if heartbleed is not checked because of client configuration flags
|
|
HeartbleedLog *tls.Heartbleed `json:"heartbleed_log,omitempty"`
|
|
}
|
|
|
|
func (z *TLSConnection) GetLog() *TLSLog {
|
|
if z.log == nil {
|
|
z.log = &TLSLog{}
|
|
}
|
|
|
|
return z.log
|
|
}
|
|
|
|
func (z *TLSConnection) Handshake() error {
|
|
log := z.GetLog()
|
|
if z.flags.Heartbleed {
|
|
buf := make([]byte, 256)
|
|
defer func() {
|
|
log.HandshakeLog = z.Conn.GetHandshakeLog()
|
|
log.HeartbleedLog = z.Conn.GetHeartbleedLog()
|
|
}()
|
|
// TODO - CheckHeartbleed does not bubble errors from Handshake
|
|
_, err := z.CheckHeartbleed(buf)
|
|
return err
|
|
} else {
|
|
defer func() {
|
|
log.HandshakeLog = z.Conn.GetHandshakeLog()
|
|
log.HeartbleedLog = nil
|
|
}()
|
|
return z.Conn.Handshake()
|
|
}
|
|
}
|
|
|
|
func (t *TLSFlags) GetTLSConnection(conn net.Conn) (*TLSConnection, error) {
|
|
cfg, err := t.GetTLSConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error getting TLSConfig for options: %s", err)
|
|
}
|
|
tlsClient := tls.Client(conn, cfg)
|
|
wrappedClient := TLSConnection{
|
|
Conn: *tlsClient,
|
|
flags: t,
|
|
}
|
|
return &wrappedClient, nil
|
|
}
|