diff --git a/README.md b/README.md index 5c53846..ef53d01 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,15 @@ This will download, compile, and install the Go package for your local system. T # The following command will listen for incoming connections on 127.0.0.1:2222 ./Terrapin-Scanner --listen 2222 ``` + +The scanner supports outputting the scan result as json. To do so, provide the `--json` flag when calling the scanner. The output is structured as follows: + +```json +{ + "Banner": "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.5", + "SupportsChaCha20": true, + "SupportsCbcEtm": false, + "SupportsStrictKex": true, + "Vulnerable": false +} +``` diff --git a/go.mod b/go.mod index 24ccead..16af75b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module github.com/RUB-NDS/Terrapin-Scanner go 1.21 + +require github.com/fatih/color v1.16.0 + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.14.0 // indirect +) diff --git a/go.sum b/go.sum index e69de29..7f13de9 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go index 0b21757..4562ccf 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,10 @@ package main import ( "bufio" "encoding/binary" + "encoding/json" "flag" "fmt" + "github.com/fatih/color" "io" "net" "os" @@ -62,6 +64,16 @@ func (report *TerrapinVulnerabilityReport) IsVulnerable() bool { return (report.SupportsChaCha20 || report.SupportsCbcEtm) && !report.SupportsStrictKex } +func (report *TerrapinVulnerabilityReport) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + TerrapinVulnerabilityReport + Vulnerable bool + }{ + *report, + report.IsVulnerable(), + }) +} + // Reads a single incoming, unencrypted binary packet from the provided connection. // Does not support reading encrypted binary packets. func readSinglePacket(connrw *bufio.ReadWriter) (*BinaryPacket, error) { @@ -200,7 +212,7 @@ func performVulnerabilityScan(address string, scanMode ScanMode) (*TerrapinVulne return nil, err } defer listener.Close() - fmt.Println("Listening for incoming client connection on", address) + fmt.Fprintln(os.Stderr, "Listening for incoming client connection on", address) if conn, err = listener.Accept(); err != nil { return nil, err @@ -260,26 +272,54 @@ func formatAddress(address string, mode ScanMode) string { return formatted } +func printColoredBoolean(value bool, ifTrue color.Attribute, ifFalse color.Attribute) { + if value { + color.Set(ifTrue) + } else { + color.Set(ifFalse) + } + fmt.Printf("%t\n", value) + color.Unset() +} + // Prints the report to stdout -func printReport(report *TerrapinVulnerabilityReport) { - fmt.Println("================================================================================") - fmt.Println("==================================== Report ====================================") - fmt.Println("================================================================================") - fmt.Println() - fmt.Printf("Remote Banner: %s\n", report.Banner) - fmt.Println() - fmt.Printf("ChaCha20-Poly1305 support: %t\n", report.SupportsChaCha20) - fmt.Printf("CBC-EtM support: %t\n", report.SupportsCbcEtm) - fmt.Println() - fmt.Printf("Strict key exchange support: %t\n", report.SupportsStrictKex) - fmt.Println() - if report.IsVulnerable() { - fmt.Println("==> The scanned peer is VULNERABLE to Terrapin.") +func printReport(report *TerrapinVulnerabilityReport, outputJson bool) error { + if !outputJson { + color.Set(color.FgBlue) + fmt.Println("================================================================================") + fmt.Println("==================================== Report ====================================") + fmt.Println("================================================================================") + color.Unset() + fmt.Println() + fmt.Printf("Remote Banner: %s\n", report.Banner) + fmt.Println() + fmt.Print("ChaCha20-Poly1305 support: ") + printColoredBoolean(report.SupportsChaCha20, color.FgYellow, color.FgGreen) + fmt.Print("CBC-EtM support: ") + printColoredBoolean(report.SupportsCbcEtm, color.FgYellow, color.FgGreen) + fmt.Println() + fmt.Print("Strict key exchange support: ") + printColoredBoolean(report.SupportsStrictKex, color.FgGreen, color.FgRed) + fmt.Println() + if report.IsVulnerable() { + color.Set(color.FgRed) + fmt.Println("The scanned peer is VULNERABLE to Terrapin.") + color.Unset() + } else { + color.Set(color.FgGreen) + fmt.Println("The scanned peer supports Terrapin mitigations and can establish") + fmt.Println("connections that are NOT VULNERABLE to Terrapin. Glad to see this.") + fmt.Println("For strict key exchange to take effect, both peers must support it.") + color.Unset() + } } else { - fmt.Println("==> The scanned peer supports Terrapin mitigations and can establish") - fmt.Println(" connections that are NOT VULNERABLE to Terrapin. Glad to see this.") - fmt.Println(" For strict key exchange to take effect, both peers must support it.") + marshalledReport, err := json.MarshalIndent(report, "", " ") + if err != nil { + return err + } + fmt.Println(string(marshalledReport)) } + return nil } // Prints a short disclaimer to stdout @@ -303,11 +343,20 @@ func main() { "listen", "", "Address to bind to for client-side scans. Format: [host:]") + jsonPtr := flag.Bool( + "json", + false, + "Outputs the scan result as json. Can be useful when calling the scanner from a script.") + noColor := flag.Bool( + "no-color", + false, + "Disables colored output.") helpPtr := flag.Bool( "help", false, "Prints this usage help to the user.") flag.Parse() + color.NoColor = *noColor if (*connectPtr == "" && *listenPtr == "") || *helpPtr { flag.Usage() printDisclaimer() @@ -330,6 +379,10 @@ func main() { panic(err) } } - printReport(report) - printDisclaimer() + if err := printReport(report, *jsonPtr); err != nil { + panic(err) + } + if !*jsonPtr { + printDisclaimer() + } }