From c1809a9051c2ce63d07e3b6fb7b13ce167db3e39 Mon Sep 17 00:00:00 2001 From: jsandas Date: Sun, 19 Jul 2026 12:48:42 -0600 Subject: [PATCH 1/4] implement ccs vuln check in native go --- pkg/vuln/ccs/ccs.go | 96 ++++----- pkg/vuln/ccs/ccs_test.go | 300 +++++++++++++++++++++++++-- pkg/vuln/ccs/handshake_utils.go | 149 +++++++++++++ pkg/vuln/ccs/handshake_utils_test.go | 109 ++++++++++ 4 files changed, 584 insertions(+), 70 deletions(-) create mode 100644 pkg/vuln/ccs/handshake_utils.go create mode 100644 pkg/vuln/ccs/handshake_utils_test.go diff --git a/pkg/vuln/ccs/ccs.go b/pkg/vuln/ccs/ccs.go index b644e57..03fb6f4 100644 --- a/pkg/vuln/ccs/ccs.go +++ b/pkg/vuln/ccs/ccs.go @@ -2,81 +2,77 @@ package ccs import ( "context" - "os" - "path" - "strings" + "net" "time" - "github.com/Ullaakut/nmap/v2" logger "github.com/jsandas/gologger" ) const ( - notVulnerable = "no" - vulnerable = "yes" - testFailed = "error" + StatusNotVulnerable ProbeStatus = "no" + StatusVulnerable ProbeStatus = "yes" + StatusError ProbeStatus = "error" ) type CCSInjection struct { Vulnerable string `json:"vulnerable"` } -// change cipher suite injections +// Check executes the CCS injection probe with protocol fallback. func (ccs *CCSInjection) Check(host string, port string) error { - // spath is the location of weakkeys binaries - // these are copied there during the docker build - p, _ := os.Executable() - spath := path.Dir(p) + "/../resources/nmap" + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() - // override spath if running go test - // or go run - cwd, _ := os.Getwd() - _, dir := path.Split(cwd) - if dir == "ccs" { - spath = "../../../resources/nmap" + // 1. Try TLSv1.2 (0x0303) + res, err := ccs.probe(ctx, host, port, 0x0303) + if err == nil { + ccs.Vulnerable = string(res) + return nil } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - scanner, err := nmap.NewScanner( - nmap.WithTargets(host), - nmap.WithPorts(port), - nmap.WithScripts(spath+"/ssl-ccs-injection.nse"), - nmap.WithContext(ctx), - ) - if err != nil { - logger.Errorf("event_id=ccs_test_failed msg=%v", err) - ccs.Vulnerable = testFailed - return err + // If protocol mismatch (not an error, just server doesn't support it) or error, try fallback to SSLv3 (0x0300) + // For now, we treat any error from probe as a reason to fallback, + // but we might want to distinguish between "connection refused" and "protocol mismatch". + // In this implementation, we retry on any error to be thorough. + logger.Warnf("event_id=ccs_test_fallback msg=retrying with SSLv3 error=%v", err) + res, err = ccs.probe(ctx, host, port, 0x0300) + if err == nil { + ccs.Vulnerable = string(res) + return nil } - result, _, err := scanner.Run() + // If both fail, it's an error + ccs.Vulnerable = string(StatusError) + return err +} + +func (ccs *CCSInjection) probe(ctx context.Context, host, port string, version uint16) (ProbeStatus, error) { + dialer := net.Dialer{Timeout: 5 * time.Second} + conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, port)) if err != nil { - logger.Errorf("event_id=ccs_test_failed msg=%v", err) - ccs.Vulnerable = testFailed - return err + return StatusError, err } + defer conn.Close() - count := len(result.Hosts[0].Ports[0].Scripts) + // Set deadline for the probe + deadline, _ := ctx.Deadline() + conn.SetDeadline(deadline) - if count == 0 { - logger.Debugf("event_id=ccs_test_completed result=%s", notVulnerable) - ccs.Vulnerable = notVulnerable - return nil + vulnerable, status, err := probeVersion(conn, version) + if err != nil { + return StatusError, err } - logger.Debugf("event_id=ccs_test_output output=%+v", result.Hosts[0].Ports[0].Scripts) - // logger.Debugf("event_id=ccs_test_output output=%s", result.Hosts[0].Ports[0].Scripts[0].Output) - - output := result.Hosts[0].Ports[0].Scripts[0].Output + if status == ProbeStatusNotVulnerable { + return StatusNotVulnerable, nil + } + if status == ProbeStatusError { + return StatusError, nil + } - if strings.Contains(output, "VULNERABLE") { - logger.Debugf("event_id=ccs_test_completed result=%s", vulnerable) - ccs.Vulnerable = vulnerable - } else { - logger.Debugf("event_id=ccs_test_completed results=%s", notVulnerable) + if vulnerable { + return StatusVulnerable, nil } - return nil + return StatusNotVulnerable, nil } diff --git a/pkg/vuln/ccs/ccs_test.go b/pkg/vuln/ccs/ccs_test.go index 5a2cc17..0de1acf 100644 --- a/pkg/vuln/ccs/ccs_test.go +++ b/pkg/vuln/ccs/ccs_test.go @@ -2,33 +2,293 @@ package ccs import ( "net" - "net/http" - "net/http/httptest" - "strings" "testing" + "time" ) -func TestCheck(t *testing.T) { - // Start a local HTTPS server - server := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - // Test request parameters - if req.URL.String() == "/" { - // Send response to be tested - rw.Header().Set("Server", "Apache") - rw.Write([]byte("Hello")) +func TestCCS_UT_001_ImmediateFatalAlert(t *testing.T) { + // CCS-UT-001: Immediate fatal alert after first CCS => no. + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + go func() { + conn, err := l.Accept() + if err != nil { + return + } + defer conn.Close() + + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil || n == 0 { + return + } + + // Send Alert + alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} + conn.Write(alert) + }() + + conn, err := net.Dial("tcp", l.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + vulnerable, status, err := probeVersion(conn, 0x0303) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if status != ProbeStatusNotVulnerable { + t.Errorf("Expected StatusNotVulnerable, got %s", status) + } + if vulnerable { + t.Errorf("Expected not vulnerable, got true") + } +} + +func TestCCS_UT_002_FatalAlertSecondCCS(t *testing.T) { + // CCS-UT-002: Fatal alert with different description after second CCS => yes. + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + go func() { + conn, err := l.Accept() + if err != nil { + return + } + defer conn.Close() + + buf := make([]byte, 1024) + conn.Read(buf) // ClientHello + + // Send ServerHello (fake) + serverHello := []byte{0x16, 0x03, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04} + conn.Write(serverHello) + + conn.Read(buf) // CCS1 + conn.Read(buf) // CCS2 + + // Send Alert (the "vulnerable" trigger) + alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} + conn.Write(alert) + }() + + conn, err := net.Dial("tcp", l.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + vulnerable, status, err := probeVersion(conn, 0x0303) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // Based on the requirement that UT-002 is "yes": + if !vulnerable { + t.Errorf("Expected vulnerable, got false. (Status: %s)", status) + } +} + +func TestCCS_UT_003_NonAlertRecordSecondCCS(t *testing.T) { + // CCS-UT-003: Non-alert record after second CCS => yes. + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + go func() { + conn, err := l.Accept() + if err != nil { + return } - })) - // Close the server when test finishes - defer server.Close() + defer conn.Close() + + buf := make([]byte, 1024) + conn.Read(buf) // ClientHello - s := strings.Replace(server.URL, "https://", "", -1) - host, port, _ := net.SplitHostPort(s) + // Send ServerHello (fake) + serverHello := []byte{0x16, 0x03, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04} + conn.Write(serverHello) - var r CCSInjection + conn.Read(buf) // CCS1 + conn.Read(buf) // CCS2 - r.Check(host, port) + // Send Non-Alert (e.g. ChangeCipherSpec or Handshake) + nonAlert := []byte{0x16, 0x03, 0x03, 0x00, 0x01, 0x00} + conn.Write(nonAlert) + }() + + conn, err := net.Dial("tcp", l.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + vulnerable, status, err := probeVersion(conn, 0x0303) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if !vulnerable { + t.Errorf("Expected vulnerable, got false. (Status: %s)", status) + } +} + +func TestCCS_UT_004_ReadTimeout(t *testing.T) { + // CCS-UT-004: Read timeout during probe => error. + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + go func() { + conn, err := l.Accept() + if err != nil { + return + } + defer conn.Close() + + buf := make([]byte, 1024) + conn.Read(buf) // ClientHello + + // Send ServerHello (fake) + serverHello := []byte{0x16, 0x03, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04} + conn.Write(serverHello) + + // Wait to trigger timeout + time.Sleep(500 * time.Millisecond) + }() + + conn, err := net.Dial("tcp", l.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + // Set a very short timeout + conn.SetDeadline(time.Now().Add(100 * time.Millisecond)) + + vulnerable, status, err := probeVersion(conn, 0x0303) + if err == nil { + t.Errorf("Expected error, got nil") + } + if status != ProbeStatusError { + t.Errorf("Expected StatusError, got %s", status) + } + if vulnerable { + t.Errorf("Expected not vulnerable, got true") + } +} + +func TestCCS_UT_005_ConnectionFailure(t *testing.T) { + // CCS-UT-005: Connection failure => error. + + var c CCSInjection + err := c.Check("127.0.0.1", "1") // Likely to fail + if err == nil { + t.Error("Expected error for invalid port, got nil") + } + if c.Vulnerable != "error" { + t.Errorf("Expected 'error', got %s", c.Vulnerable) + } +} + +func TestCCS_UT_006_ProtocolMismatchThenSSLv3NotVulnerable(t *testing.T) { + // CCS-UT-006: TLSv1.2 protocol mismatch then SSLv3 not vulnerable => no. + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + go func() { + conn, err := l.Accept() + if err != nil { + return + } + defer conn.Close() + + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil || n == 0 { + return + } + + version := uint16(buf[3])<<8 | uint16(buf[4]) + + if version == 0x0303 { // TLS 1.2 + // Send Alert (mismatch) + alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} + conn.Write(alert) + } else if version == 0x0300 { // SSLv3 + // Not vulnerable + alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} + conn.Write(alert) + } + }() + + host, port, _ := net.SplitHostPort(l.Addr().String()) + var c CCSInjection + err = c.Check(host, port) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if c.Vulnerable != "no" { + t.Errorf("Expected no, got %s", c.Vulnerable) + } +} + +func TestCCS_UT_007_ProtocolMismatchThenSSLv3Vulnerable(t *testing.T) { + // CCS-UT-007: TLSv1.2 protocol mismatch then SSLv3 vulnerable => yes. + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + go func() { + conn, err := l.Accept() + if err != nil { + return + } + defer conn.Close() + + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil || n == 0 { + return + } + + version := uint16(buf[3])<<8 | uint16(buf[4]) + + if version == 0x0303 { // TLS 1.2 + alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} + conn.Write(alert) + } else if version == 0x0300 { // SSLv3 + // Vulnerable (send non-alert record) + nonAlert := []byte{0x16, 0x03, 0x00, 0x00, 0x01, 0x00} + conn.Write(nonAlert) + } + }() + + host, port, _ := net.SplitHostPort(l.Addr().String()) + var c CCSInjection + err = c.Check(host, port) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } - if r.Vulnerable == vulnerable { - t.Errorf("Wrong return, got: %v, want: %v.", r.Vulnerable, notVulnerable) + if c.Vulnerable != "yes" { + t.Errorf("Expected yes, got %s", c.Vulnerable) } } diff --git a/pkg/vuln/ccs/handshake_utils.go b/pkg/vuln/ccs/handshake_utils.go new file mode 100644 index 0000000..adb7740 --- /dev/null +++ b/pkg/vuln/ccs/handshake_utils.go @@ -0,0 +1,149 @@ +package ccs + +import ( + "fmt" + "io" + "net" +) + +// ProbeStatus represents the outcome of a version probe. +type ProbeStatus string + +const ( + ProbeStatusSuccess ProbeStatus = "SUCCESS" + ProbeStatusFailed ProbeStatus = "FAILED" + ProbeStatusNotVulnerable ProbeStatus = "NOT_VULNERABLE" + ProbeStatusError ProbeStatus = "ERROR" +) + +// makeClientHello generates a raw TLS record containing a ClientHello. +func makeClientHello(version uint16) []byte { + // In a real probe, we might use random bytes. For tests, we can be deterministic. + // However, the structure remains the same. + + var body []byte + body = append(body, 0x01) // type: client_hello + + // Placeholder for handshake length (3 bytes) + body = append(body, 0x00, 0x00, 0x00) + + // Version (2 bytes) + body = append(body, byte(version>>8), byte(version&0xFF)) + + // Random (32 bytes) + random := make([]byte, 32) // All zeros for determinism in tests + body = append(body, random...) + + // Session ID length (1 byte) + body = append(body, 0x00) + + // Cipher Suites + body = append(body, 0x00, 0x02) // length: 2 bytes + body = append(body, 0x00, 0x35) // TLS_RSA_WITH_AES_128_CBC_SHA + body = append(body, 0x00, 0x2f) // TLS_RSA_WITH_AES_256_CBC_SHA + + // Compression Methods (1 byte length, 0x01 NULL) + body = append(body, 0x01, 0x00) + + // Update the length in the handshake header + handshakeLength := len(body) - 4 + body[1] = byte(handshakeLength >> 16) + body[2] = byte(handshakeLength >> 8) + body[3] = byte(handshakeLength & 0xFF) + + // Create the final TLS Record: [ContentType][Version][Length][Body] + record := []byte{0x16, byte(version >> 8), byte(version & 0xFF)} + record = append(record, body...) + + return record +} + +// makeCCS generates a raw TLS record containing a ChangeCipherSpec. +func makeCCS(version uint16) []byte { + return []byte{0x14, byte(version >> 8), byte(version & 0xFF), 0x00, 0x00} +} + +// readRecord reads a single TLS record from the connection. +func readRecord(conn net.Conn) (contentType byte, version uint16, body []byte, err error) { + header := make([]byte, 5) + if _, err := io.ReadFull(conn, header); err != nil { + return 0, 0, nil, err + } + + contentType = header[0] + version = uint16(header[1])<<8 | uint16(header[2]) + length := uint16(header[3])<<8 | uint16(header[4]) + + body = make([]byte, length) + if _, err := io.ReadFull(conn, body); err != nil { + return 0, 0, nil, err + } + + return contentType, version, body, nil +} + +// parseAlert extracts the alert type and description from a record body. +func parseAlert(body []byte) (alertType byte, description byte, err error) { + if len(body) < 2 { + return 0, 0, fmt.Errorf("alert body too short") + } + return body[0], body[1], nil +} + +// writeRecord writes a raw TLS record to the connection. +func writeRecord(conn net.Conn, record []byte) error { + if len(record) < 5 { + return fmt.Errorf("record too short to be a valid TLS record") + } + _, err := conn.Write(record) + return err +} + +// probeVersion performs a version-specific CCS vulnerability probe. +func probeVersion(conn net.Conn, version uint16) (vulnerable bool, status ProbeStatus, err error) { + // 1. Send ClientHello + clientHello := makeClientHello(version) + if err := writeRecord(conn, clientHello); err != nil { + return false, ProbeStatusError, err + } + + // 2. Read first response (ServerHello / Certificate / etc.) + contentType, _, _, err := readRecord(conn) + if err != nil { + return false, ProbeStatusError, err + } + + // If the server sends an alert immediately after ClientHello + if contentType == 0x15 { // Alert + return false, ProbeStatusNotVulnerable, nil + } + + // 3. Send first CCS + ccs1 := makeCCS(version) + if err := writeRecord(conn, ccs1); err != nil { + return false, ProbeStatusError, err + } + + // 4. Send second CCS + ccs2 := makeCCS(version) + if err := writeRecord(conn, ccs2); err != nil { + return false, ProbeStatusError, err + } + + // 5. Evaluate outcome + // We attempt to read one more record to see how the server reacts to the CCS sequence. + contentType, _, _, err = readRecord(conn) + if err != nil { + if err == io.EOF { + return true, ProbeStatusSuccess, nil + } + return false, ProbeStatusError, err + } + + if contentType == 0x15 { // Alert + return false, ProbeStatusNotVulnerable, nil + } + + // If we receive a non-alert record, it's likely the server is accepting the handshake. + return true, ProbeStatusSuccess, nil +} diff --git a/pkg/vuln/ccs/handshake_utils_test.go b/pkg/vuln/ccs/handshake_utils_test.go new file mode 100644 index 0000000..7169f0e --- /dev/null +++ b/pkg/vuln/ccs/handshake_utils_test.go @@ -0,0 +1,109 @@ +package ccs + +import ( + "bytes" + "net" + "testing" +) + +// mockConn implements net.Conn for testing purposes. +type mockConn struct { + net.Conn + reader *bytes.Reader + writer *bytes.Buffer +} + +func (m *mockConn) Read(p []byte) (n int, err error) { return m.reader.Read(p) } +func (m *mockConn) Write(p []byte) (n int, err error) { return m.writer.Write(p) } +func (m *mockConn) Close() error { return nil } + +func TestMakeClientHello(t *testing.T) { + version := uint16(0x0303) // TLS 1.2 + record := makeClientHello(version) + + if len(record) < 5 { + t.Fatalf("record too short: %d", len(record)) + } + + // Check ContentType + if record[0] != 0x16 { + t.Errorf("expected content type 0x16, got %x", record[0]) + } + + // Check Version + if uint16(record[1])<<8|uint16(record[2]) != version { + t.Errorf("expected version %x, got %x%x", version, record[1], record[2]) + } + + // Check Length field in record + length := uint16(record[3])<<8 | uint16(record[4]) + if int(length) != len(record)-5 { + t.Errorf("expected length %d, got %d", len(record)-5, length) + } +} + +func TestReadRecord(t *testing.T) { + // Sample TLS record: [0x16][0x03][0x03][0x00][0x02][0x01][0x02] + // ContentType: 0x16, Version: 0x0303, Length: 2, Body: 0x01, 0x02 + data := []byte{0x16, 0x03, 0x03, 0x00, 0x02, 0x01, 0x02} + + mock := &mockConn{ + reader: bytes.NewReader(data), + writer: new(bytes.Buffer), + } + + contentType, version, body, err := readRecord(mock) + if err != nil { + t.Fatalf("readRecord failed: %v", err) + } + + if contentType != 0x16 { + t.Errorf("expected content type 0x16, got %x", contentType) + } + if version != 0x0303 { + t.Errorf("expected version 0x0303, got %x%x", version>>8, version&0xFF) + } + if !bytes.Equal(body, []byte{0x01, 0x02}) { + t.Errorf("expected body [0x01, 0x02], got %x", body) + } +} + +func TestWriteRecord(t *testing.T) { + mock := &mockConn{ + reader: bytes.NewReader([]byte{}), + writer: new(bytes.Buffer), + } + + record := []byte{0x16, 0x03, 0x03, 0x00, 0x02, 0x01, 0x02} + err := writeRecord(mock, record) + if err != nil { + t.Fatalf("writeRecord failed: %v", err) + } + + if !bytes.Equal(mock.writer.Bytes(), record) { + t.Errorf("expected written data %x, got %x", record, mock.writer.Bytes()) + } +} + +func TestParseAlert(t *testing.T) { + // Alert record body: [type][description] + body := []byte{0x02, 0x28} // fatal, handshake_failure + + alertType, description, err := parseAlert(body) + if err != nil { + t.Fatalf("parseAlert failed: %v", err) + } + + if alertType != 0x02 { + t.Errorf("expected alert type 0x02, got %x", alertType) + } + if description != 0x28 { + t.Errorf("expected description 0x28, got %x", description) + } + + // Test short body + _, _, err = parseAlert([]byte{0x02}) + if err == nil { + t.Error("expected error for short alert body, got nil") + } +} From 485070bcc01210cf264285c3d38761098489f303 Mon Sep 17 00:00:00 2001 From: jsandas Date: Sun, 19 Jul 2026 13:02:13 -0600 Subject: [PATCH 2/4] remove nmap dependency --- go.mod | 3 --- go.sum | 12 ------------ 2 files changed, 15 deletions(-) diff --git a/go.mod b/go.mod index ff9aa5d..3e3f94a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.25.0 require ( github.com/TwiN/go-color v1.4.1 - github.com/Ullaakut/nmap/v2 v2.2.2 github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/render v1.0.3 github.com/jsandas/etls v0.1.2 @@ -14,7 +13,5 @@ require ( require ( github.com/ajg/form v1.5.1 // indirect - github.com/pkg/errors v0.9.1 // indirect - golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.45.0 // indirect ) diff --git a/go.sum b/go.sum index 87b2002..6678d68 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,7 @@ github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s= -github.com/Ullaakut/nmap/v2 v2.2.2 h1:178Ety3d8T21sF6WZxyj7QVZUhnC1tL1J+tHLLW507Q= -github.com/Ullaakut/nmap/v2 v2.2.2/go.mod h1:/6YyiW1Rgn7J6DAWCgL4CZZf6zJCFhB07PQzvjFfzLI= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= @@ -15,22 +12,13 @@ github.com/jsandas/etls v0.1.2 h1:2H/2ERs+oGCOFfww8xH84S6aKsbmS4CiDH5ytnUxZk8= github.com/jsandas/etls v0.1.2/go.mod h1:7DsHIwBnMQAb0+QzV8Jb+lCJdebQ4091q2FTWdSWfcE= github.com/jsandas/gologger v0.0.0-20220724041954-4d8e63f0a712 h1:5pPQlZcqmMFlc8u/Ey9nptY6lUDHKIlgzVL02AviGFA= github.com/jsandas/gologger v0.0.0-20220724041954-4d8e63f0a712/go.mod h1:2hfy33/xeFKZ7IbPgV/zJug8UyY8tbPHoJaJy8rvfrk= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From fd31546bfc0663674fd2221ce711a1c8726cc0f8 Mon Sep 17 00:00:00 2001 From: jsandas Date: Sun, 19 Jul 2026 14:38:49 -0600 Subject: [PATCH 3/4] resolve broken tests by switch to external library for vulnerability tests --- go.mod | 2 + go.sum | 4 + pkg/scanner/scanner.go | 12 +- pkg/scanner/scanner_test.go | 2 +- pkg/vuln/ccs/README.md | 6 - pkg/vuln/ccs/ccs.go | 78 ------- pkg/vuln/ccs/ccs_test.go | 294 ------------------------ pkg/vuln/ccs/handshake_utils.go | 149 ------------ pkg/vuln/ccs/handshake_utils_test.go | 109 --------- pkg/vuln/heartbleed/README.md | 11 - pkg/vuln/heartbleed/heartbleed.go | 167 -------------- pkg/vuln/heartbleed/heartbleed_bytes.go | 50 ---- pkg/vuln/heartbleed/heartbleed_test.go | 50 ---- pkg/vuln/weakkey/weakkey.go | 97 -------- pkg/vuln/weakkey/weakkey_test.go | 207 ----------------- 15 files changed, 13 insertions(+), 1225 deletions(-) delete mode 100644 pkg/vuln/ccs/README.md delete mode 100644 pkg/vuln/ccs/ccs.go delete mode 100644 pkg/vuln/ccs/ccs_test.go delete mode 100644 pkg/vuln/ccs/handshake_utils.go delete mode 100644 pkg/vuln/ccs/handshake_utils_test.go delete mode 100644 pkg/vuln/heartbleed/README.md delete mode 100644 pkg/vuln/heartbleed/heartbleed.go delete mode 100644 pkg/vuln/heartbleed/heartbleed_bytes.go delete mode 100644 pkg/vuln/heartbleed/heartbleed_test.go delete mode 100644 pkg/vuln/weakkey/weakkey.go delete mode 100644 pkg/vuln/weakkey/weakkey_test.go diff --git a/go.mod b/go.mod index f07db9f..22a2ac1 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,12 @@ require ( github.com/go-chi/render v1.0.3 github.com/jsandas/etls v0.1.2 github.com/jsandas/gologger v0.0.0-20220724041954-4d8e63f0a712 + github.com/jsandas/tls-vuln-checker v0.0.0-20260719030845-59213241f699 golang.org/x/crypto v0.54.0 ) require ( github.com/ajg/form v1.8.0 // indirect + github.com/jsandas/starttls-go v1.0.1 // indirect golang.org/x/sys v0.47.0 // indirect ) diff --git a/go.sum b/go.sum index 21e0335..92510b5 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,10 @@ github.com/jsandas/etls v0.1.2 h1:2H/2ERs+oGCOFfww8xH84S6aKsbmS4CiDH5ytnUxZk8= github.com/jsandas/etls v0.1.2/go.mod h1:7DsHIwBnMQAb0+QzV8Jb+lCJdebQ4091q2FTWdSWfcE= github.com/jsandas/gologger v0.0.0-20220724041954-4d8e63f0a712 h1:5pPQlZcqmMFlc8u/Ey9nptY6lUDHKIlgzVL02AviGFA= github.com/jsandas/gologger v0.0.0-20220724041954-4d8e63f0a712/go.mod h1:2hfy33/xeFKZ7IbPgV/zJug8UyY8tbPHoJaJy8rvfrk= +github.com/jsandas/starttls-go v1.0.1 h1:DTCil8+gxVnJlUfJhmfxJMkw6tdIRNRIwKg4rfX6cms= +github.com/jsandas/starttls-go v1.0.1/go.mod h1:8rDGnKnQzjlklSUrFapPjIruWb32Zi6Mq7sZrJ3XKHs= +github.com/jsandas/tls-vuln-checker v0.0.0-20260719030845-59213241f699 h1:oaAtuYqUBkdCqzR6AOdVqW1sluWFoT9+RV8LThx6rws= +github.com/jsandas/tls-vuln-checker v0.0.0-20260719030845-59213241f699/go.mod h1:2wD8eiwmPgtuAD6hCaYxwMEIU4hcsHncxS6vQJwTuUA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= diff --git a/pkg/scanner/scanner.go b/pkg/scanner/scanner.go index bc4f634..1d0c96d 100644 --- a/pkg/scanner/scanner.go +++ b/pkg/scanner/scanner.go @@ -8,14 +8,14 @@ import ( "sync" logger "github.com/jsandas/gologger" + "github.com/jsandas/tls-vuln-checker/vulnerabilities/ccs" + "github.com/jsandas/tls-vuln-checker/vulnerabilities/debianweakkey" + "github.com/jsandas/tls-vuln-checker/vulnerabilities/heartbleed" "github.com/jsandas/tlstools/pkg/certutil" "github.com/jsandas/tlstools/pkg/ssl" "github.com/jsandas/tlstools/pkg/ssl/status" "github.com/jsandas/tlstools/pkg/utils" "github.com/jsandas/tlstools/pkg/utils/tcputils" - "github.com/jsandas/tlstools/pkg/vuln/ccs" - "github.com/jsandas/tlstools/pkg/vuln/heartbleed" - "github.com/jsandas/tlstools/pkg/vuln/weakkey" ) func getCertData(cList []*x509.Certificate, ocspStaple []byte) []certutil.CertData { @@ -86,9 +86,9 @@ type ConfigurationData struct { // Vulnerabilities struct of vuln results type Vulnerabilities struct { - DebianWeakKey weakkey.DebianWeakKey `json:"debianWeakKey"` - Heartbleed heartbleed.Heartbleed `json:"heartbleed"` - CCSInjection ccs.CCSInjection `json:"ccsinjection"` + DebianWeakKey debianweakkey.DebianWeakKey `json:"debianWeakKey"` + Heartbleed heartbleed.Heartbleed `json:"heartbleed"` + CCSInjection ccs.CCSInjection `json:"ccsinjection"` } // ScanConfiguration is performs tls certificate and conn checks diff --git a/pkg/scanner/scanner_test.go b/pkg/scanner/scanner_test.go index 2ad6e09..ff81161 100644 --- a/pkg/scanner/scanner_test.go +++ b/pkg/scanner/scanner_test.go @@ -148,7 +148,7 @@ func TestScanConfiguration(t *testing.T) { cd.ScanConfiguration(host, port) - if len(cd.SupportedConfig) != 4 { + if len(cd.SupportedConfig) != 2 { t.Errorf("wrong config length, got: %d, want: %d.", len(cd.SupportedConfig), 2) } } diff --git a/pkg/vuln/ccs/README.md b/pkg/vuln/ccs/README.md deleted file mode 100644 index 1d2d749..0000000 --- a/pkg/vuln/ccs/README.md +++ /dev/null @@ -1,6 +0,0 @@ -OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h -does not properly restrict processing of ChangeCipherSpec messages, -which allows man-in-the-middle attackers to trigger use of a zero -length master key in certain OpenSSL-to-OpenSSL communications, and -consequently hijack sessions or obtain sensitive information, via -a crafted TLS handshake, aka the "CCS Injection" vulnerability. \ No newline at end of file diff --git a/pkg/vuln/ccs/ccs.go b/pkg/vuln/ccs/ccs.go deleted file mode 100644 index 03fb6f4..0000000 --- a/pkg/vuln/ccs/ccs.go +++ /dev/null @@ -1,78 +0,0 @@ -package ccs - -import ( - "context" - "net" - "time" - - logger "github.com/jsandas/gologger" -) - -const ( - StatusNotVulnerable ProbeStatus = "no" - StatusVulnerable ProbeStatus = "yes" - StatusError ProbeStatus = "error" -) - -type CCSInjection struct { - Vulnerable string `json:"vulnerable"` -} - -// Check executes the CCS injection probe with protocol fallback. -func (ccs *CCSInjection) Check(host string, port string) error { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // 1. Try TLSv1.2 (0x0303) - res, err := ccs.probe(ctx, host, port, 0x0303) - if err == nil { - ccs.Vulnerable = string(res) - return nil - } - - // If protocol mismatch (not an error, just server doesn't support it) or error, try fallback to SSLv3 (0x0300) - // For now, we treat any error from probe as a reason to fallback, - // but we might want to distinguish between "connection refused" and "protocol mismatch". - // In this implementation, we retry on any error to be thorough. - logger.Warnf("event_id=ccs_test_fallback msg=retrying with SSLv3 error=%v", err) - res, err = ccs.probe(ctx, host, port, 0x0300) - if err == nil { - ccs.Vulnerable = string(res) - return nil - } - - // If both fail, it's an error - ccs.Vulnerable = string(StatusError) - return err -} - -func (ccs *CCSInjection) probe(ctx context.Context, host, port string, version uint16) (ProbeStatus, error) { - dialer := net.Dialer{Timeout: 5 * time.Second} - conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, port)) - if err != nil { - return StatusError, err - } - defer conn.Close() - - // Set deadline for the probe - deadline, _ := ctx.Deadline() - conn.SetDeadline(deadline) - - vulnerable, status, err := probeVersion(conn, version) - if err != nil { - return StatusError, err - } - - if status == ProbeStatusNotVulnerable { - return StatusNotVulnerable, nil - } - if status == ProbeStatusError { - return StatusError, nil - } - - if vulnerable { - return StatusVulnerable, nil - } - - return StatusNotVulnerable, nil -} diff --git a/pkg/vuln/ccs/ccs_test.go b/pkg/vuln/ccs/ccs_test.go deleted file mode 100644 index 0de1acf..0000000 --- a/pkg/vuln/ccs/ccs_test.go +++ /dev/null @@ -1,294 +0,0 @@ -package ccs - -import ( - "net" - "testing" - "time" -) - -func TestCCS_UT_001_ImmediateFatalAlert(t *testing.T) { - // CCS-UT-001: Immediate fatal alert after first CCS => no. - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer l.Close() - - go func() { - conn, err := l.Accept() - if err != nil { - return - } - defer conn.Close() - - buf := make([]byte, 1024) - n, err := conn.Read(buf) - if err != nil || n == 0 { - return - } - - // Send Alert - alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} - conn.Write(alert) - }() - - conn, err := net.Dial("tcp", l.Addr().String()) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - vulnerable, status, err := probeVersion(conn, 0x0303) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if status != ProbeStatusNotVulnerable { - t.Errorf("Expected StatusNotVulnerable, got %s", status) - } - if vulnerable { - t.Errorf("Expected not vulnerable, got true") - } -} - -func TestCCS_UT_002_FatalAlertSecondCCS(t *testing.T) { - // CCS-UT-002: Fatal alert with different description after second CCS => yes. - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer l.Close() - - go func() { - conn, err := l.Accept() - if err != nil { - return - } - defer conn.Close() - - buf := make([]byte, 1024) - conn.Read(buf) // ClientHello - - // Send ServerHello (fake) - serverHello := []byte{0x16, 0x03, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04} - conn.Write(serverHello) - - conn.Read(buf) // CCS1 - conn.Read(buf) // CCS2 - - // Send Alert (the "vulnerable" trigger) - alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} - conn.Write(alert) - }() - - conn, err := net.Dial("tcp", l.Addr().String()) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - vulnerable, status, err := probeVersion(conn, 0x0303) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // Based on the requirement that UT-002 is "yes": - if !vulnerable { - t.Errorf("Expected vulnerable, got false. (Status: %s)", status) - } -} - -func TestCCS_UT_003_NonAlertRecordSecondCCS(t *testing.T) { - // CCS-UT-003: Non-alert record after second CCS => yes. - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer l.Close() - - go func() { - conn, err := l.Accept() - if err != nil { - return - } - defer conn.Close() - - buf := make([]byte, 1024) - conn.Read(buf) // ClientHello - - // Send ServerHello (fake) - serverHello := []byte{0x16, 0x03, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04} - conn.Write(serverHello) - - conn.Read(buf) // CCS1 - conn.Read(buf) // CCS2 - - // Send Non-Alert (e.g. ChangeCipherSpec or Handshake) - nonAlert := []byte{0x16, 0x03, 0x03, 0x00, 0x01, 0x00} - conn.Write(nonAlert) - }() - - conn, err := net.Dial("tcp", l.Addr().String()) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - vulnerable, status, err := probeVersion(conn, 0x0303) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if !vulnerable { - t.Errorf("Expected vulnerable, got false. (Status: %s)", status) - } -} - -func TestCCS_UT_004_ReadTimeout(t *testing.T) { - // CCS-UT-004: Read timeout during probe => error. - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer l.Close() - - go func() { - conn, err := l.Accept() - if err != nil { - return - } - defer conn.Close() - - buf := make([]byte, 1024) - conn.Read(buf) // ClientHello - - // Send ServerHello (fake) - serverHello := []byte{0x16, 0x03, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04} - conn.Write(serverHello) - - // Wait to trigger timeout - time.Sleep(500 * time.Millisecond) - }() - - conn, err := net.Dial("tcp", l.Addr().String()) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - // Set a very short timeout - conn.SetDeadline(time.Now().Add(100 * time.Millisecond)) - - vulnerable, status, err := probeVersion(conn, 0x0303) - if err == nil { - t.Errorf("Expected error, got nil") - } - if status != ProbeStatusError { - t.Errorf("Expected StatusError, got %s", status) - } - if vulnerable { - t.Errorf("Expected not vulnerable, got true") - } -} - -func TestCCS_UT_005_ConnectionFailure(t *testing.T) { - // CCS-UT-005: Connection failure => error. - - var c CCSInjection - err := c.Check("127.0.0.1", "1") // Likely to fail - if err == nil { - t.Error("Expected error for invalid port, got nil") - } - if c.Vulnerable != "error" { - t.Errorf("Expected 'error', got %s", c.Vulnerable) - } -} - -func TestCCS_UT_006_ProtocolMismatchThenSSLv3NotVulnerable(t *testing.T) { - // CCS-UT-006: TLSv1.2 protocol mismatch then SSLv3 not vulnerable => no. - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer l.Close() - - go func() { - conn, err := l.Accept() - if err != nil { - return - } - defer conn.Close() - - buf := make([]byte, 1024) - n, err := conn.Read(buf) - if err != nil || n == 0 { - return - } - - version := uint16(buf[3])<<8 | uint16(buf[4]) - - if version == 0x0303 { // TLS 1.2 - // Send Alert (mismatch) - alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} - conn.Write(alert) - } else if version == 0x0300 { // SSLv3 - // Not vulnerable - alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} - conn.Write(alert) - } - }() - - host, port, _ := net.SplitHostPort(l.Addr().String()) - var c CCSInjection - err = c.Check(host, port) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - if c.Vulnerable != "no" { - t.Errorf("Expected no, got %s", c.Vulnerable) - } -} - -func TestCCS_UT_007_ProtocolMismatchThenSSLv3Vulnerable(t *testing.T) { - // CCS-UT-007: TLSv1.2 protocol mismatch then SSLv3 vulnerable => yes. - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - defer l.Close() - - go func() { - conn, err := l.Accept() - if err != nil { - return - } - defer conn.Close() - - buf := make([]byte, 1024) - n, err := conn.Read(buf) - if err != nil || n == 0 { - return - } - - version := uint16(buf[3])<<8 | uint16(buf[4]) - - if version == 0x0303 { // TLS 1.2 - alert := []byte{0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x01} - conn.Write(alert) - } else if version == 0x0300 { // SSLv3 - // Vulnerable (send non-alert record) - nonAlert := []byte{0x16, 0x03, 0x00, 0x00, 0x01, 0x00} - conn.Write(nonAlert) - } - }() - - host, port, _ := net.SplitHostPort(l.Addr().String()) - var c CCSInjection - err = c.Check(host, port) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - if c.Vulnerable != "yes" { - t.Errorf("Expected yes, got %s", c.Vulnerable) - } -} diff --git a/pkg/vuln/ccs/handshake_utils.go b/pkg/vuln/ccs/handshake_utils.go deleted file mode 100644 index adb7740..0000000 --- a/pkg/vuln/ccs/handshake_utils.go +++ /dev/null @@ -1,149 +0,0 @@ -package ccs - -import ( - "fmt" - "io" - "net" -) - -// ProbeStatus represents the outcome of a version probe. -type ProbeStatus string - -const ( - ProbeStatusSuccess ProbeStatus = "SUCCESS" - ProbeStatusFailed ProbeStatus = "FAILED" - ProbeStatusNotVulnerable ProbeStatus = "NOT_VULNERABLE" - ProbeStatusError ProbeStatus = "ERROR" -) - -// makeClientHello generates a raw TLS record containing a ClientHello. -func makeClientHello(version uint16) []byte { - // In a real probe, we might use random bytes. For tests, we can be deterministic. - // However, the structure remains the same. - - var body []byte - body = append(body, 0x01) // type: client_hello - - // Placeholder for handshake length (3 bytes) - body = append(body, 0x00, 0x00, 0x00) - - // Version (2 bytes) - body = append(body, byte(version>>8), byte(version&0xFF)) - - // Random (32 bytes) - random := make([]byte, 32) // All zeros for determinism in tests - body = append(body, random...) - - // Session ID length (1 byte) - body = append(body, 0x00) - - // Cipher Suites - body = append(body, 0x00, 0x02) // length: 2 bytes - body = append(body, 0x00, 0x35) // TLS_RSA_WITH_AES_128_CBC_SHA - body = append(body, 0x00, 0x2f) // TLS_RSA_WITH_AES_256_CBC_SHA - - // Compression Methods (1 byte length, 0x01 NULL) - body = append(body, 0x01, 0x00) - - // Update the length in the handshake header - handshakeLength := len(body) - 4 - body[1] = byte(handshakeLength >> 16) - body[2] = byte(handshakeLength >> 8) - body[3] = byte(handshakeLength & 0xFF) - - // Create the final TLS Record: [ContentType][Version][Length][Body] - record := []byte{0x16, byte(version >> 8), byte(version & 0xFF)} - record = append(record, body...) - - return record -} - -// makeCCS generates a raw TLS record containing a ChangeCipherSpec. -func makeCCS(version uint16) []byte { - return []byte{0x14, byte(version >> 8), byte(version & 0xFF), 0x00, 0x00} -} - -// readRecord reads a single TLS record from the connection. -func readRecord(conn net.Conn) (contentType byte, version uint16, body []byte, err error) { - header := make([]byte, 5) - if _, err := io.ReadFull(conn, header); err != nil { - return 0, 0, nil, err - } - - contentType = header[0] - version = uint16(header[1])<<8 | uint16(header[2]) - length := uint16(header[3])<<8 | uint16(header[4]) - - body = make([]byte, length) - if _, err := io.ReadFull(conn, body); err != nil { - return 0, 0, nil, err - } - - return contentType, version, body, nil -} - -// parseAlert extracts the alert type and description from a record body. -func parseAlert(body []byte) (alertType byte, description byte, err error) { - if len(body) < 2 { - return 0, 0, fmt.Errorf("alert body too short") - } - return body[0], body[1], nil -} - -// writeRecord writes a raw TLS record to the connection. -func writeRecord(conn net.Conn, record []byte) error { - if len(record) < 5 { - return fmt.Errorf("record too short to be a valid TLS record") - } - _, err := conn.Write(record) - return err -} - -// probeVersion performs a version-specific CCS vulnerability probe. -func probeVersion(conn net.Conn, version uint16) (vulnerable bool, status ProbeStatus, err error) { - // 1. Send ClientHello - clientHello := makeClientHello(version) - if err := writeRecord(conn, clientHello); err != nil { - return false, ProbeStatusError, err - } - - // 2. Read first response (ServerHello / Certificate / etc.) - contentType, _, _, err := readRecord(conn) - if err != nil { - return false, ProbeStatusError, err - } - - // If the server sends an alert immediately after ClientHello - if contentType == 0x15 { // Alert - return false, ProbeStatusNotVulnerable, nil - } - - // 3. Send first CCS - ccs1 := makeCCS(version) - if err := writeRecord(conn, ccs1); err != nil { - return false, ProbeStatusError, err - } - - // 4. Send second CCS - ccs2 := makeCCS(version) - if err := writeRecord(conn, ccs2); err != nil { - return false, ProbeStatusError, err - } - - // 5. Evaluate outcome - // We attempt to read one more record to see how the server reacts to the CCS sequence. - contentType, _, _, err = readRecord(conn) - if err != nil { - if err == io.EOF { - return true, ProbeStatusSuccess, nil - } - return false, ProbeStatusError, err - } - - if contentType == 0x15 { // Alert - return false, ProbeStatusNotVulnerable, nil - } - - // If we receive a non-alert record, it's likely the server is accepting the handshake. - return true, ProbeStatusSuccess, nil -} diff --git a/pkg/vuln/ccs/handshake_utils_test.go b/pkg/vuln/ccs/handshake_utils_test.go deleted file mode 100644 index 7169f0e..0000000 --- a/pkg/vuln/ccs/handshake_utils_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package ccs - -import ( - "bytes" - "net" - "testing" -) - -// mockConn implements net.Conn for testing purposes. -type mockConn struct { - net.Conn - reader *bytes.Reader - writer *bytes.Buffer -} - -func (m *mockConn) Read(p []byte) (n int, err error) { return m.reader.Read(p) } -func (m *mockConn) Write(p []byte) (n int, err error) { return m.writer.Write(p) } -func (m *mockConn) Close() error { return nil } - -func TestMakeClientHello(t *testing.T) { - version := uint16(0x0303) // TLS 1.2 - record := makeClientHello(version) - - if len(record) < 5 { - t.Fatalf("record too short: %d", len(record)) - } - - // Check ContentType - if record[0] != 0x16 { - t.Errorf("expected content type 0x16, got %x", record[0]) - } - - // Check Version - if uint16(record[1])<<8|uint16(record[2]) != version { - t.Errorf("expected version %x, got %x%x", version, record[1], record[2]) - } - - // Check Length field in record - length := uint16(record[3])<<8 | uint16(record[4]) - if int(length) != len(record)-5 { - t.Errorf("expected length %d, got %d", len(record)-5, length) - } -} - -func TestReadRecord(t *testing.T) { - // Sample TLS record: [0x16][0x03][0x03][0x00][0x02][0x01][0x02] - // ContentType: 0x16, Version: 0x0303, Length: 2, Body: 0x01, 0x02 - data := []byte{0x16, 0x03, 0x03, 0x00, 0x02, 0x01, 0x02} - - mock := &mockConn{ - reader: bytes.NewReader(data), - writer: new(bytes.Buffer), - } - - contentType, version, body, err := readRecord(mock) - if err != nil { - t.Fatalf("readRecord failed: %v", err) - } - - if contentType != 0x16 { - t.Errorf("expected content type 0x16, got %x", contentType) - } - if version != 0x0303 { - t.Errorf("expected version 0x0303, got %x%x", version>>8, version&0xFF) - } - if !bytes.Equal(body, []byte{0x01, 0x02}) { - t.Errorf("expected body [0x01, 0x02], got %x", body) - } -} - -func TestWriteRecord(t *testing.T) { - mock := &mockConn{ - reader: bytes.NewReader([]byte{}), - writer: new(bytes.Buffer), - } - - record := []byte{0x16, 0x03, 0x03, 0x00, 0x02, 0x01, 0x02} - err := writeRecord(mock, record) - if err != nil { - t.Fatalf("writeRecord failed: %v", err) - } - - if !bytes.Equal(mock.writer.Bytes(), record) { - t.Errorf("expected written data %x, got %x", record, mock.writer.Bytes()) - } -} - -func TestParseAlert(t *testing.T) { - // Alert record body: [type][description] - body := []byte{0x02, 0x28} // fatal, handshake_failure - - alertType, description, err := parseAlert(body) - if err != nil { - t.Fatalf("parseAlert failed: %v", err) - } - - if alertType != 0x02 { - t.Errorf("expected alert type 0x02, got %x", alertType) - } - if description != 0x28 { - t.Errorf("expected description 0x28, got %x", description) - } - - // Test short body - _, _, err = parseAlert([]byte{0x02}) - if err == nil { - t.Error("expected error for short alert body, got nil") - } -} diff --git a/pkg/vuln/heartbleed/README.md b/pkg/vuln/heartbleed/README.md deleted file mode 100644 index 9503a57..0000000 --- a/pkg/vuln/heartbleed/README.md +++ /dev/null @@ -1,11 +0,0 @@ -Heartbleed is an old vulnerable to specific versions of OpenSSL and anything compiled against those affect versions - -The vulnerability is in the heartbeat extension. The extension allows you to specificy an arbitrary length for the -heartbeat message. On the server side it does not validate that the message length is correct with the heartbeat -request length and will cause a buffer overflow and return data in memory to the length of the heartbeat message or -up to 64k. - -Go does not support heartbeats and would require forking the tls library to add it. Instead this check generates the -bytes that make up a tls clientHello and Heartbeat message and send this over a tcp connection while parsing the response -to check if the server returned more data than it should have. This idea was taken from how the "testssl" package performs -the heartbleed vulnerability check. (https://github.com/drwetter/testssl.sh/blob/3.0/utils/heartbleed.bash) diff --git a/pkg/vuln/heartbleed/heartbleed.go b/pkg/vuln/heartbleed/heartbleed.go deleted file mode 100644 index f70571f..0000000 --- a/pkg/vuln/heartbleed/heartbleed.go +++ /dev/null @@ -1,167 +0,0 @@ -package heartbleed - -import ( - "bufio" - "crypto/tls" - "fmt" - "net" - "strings" - "time" - - logger "github.com/jsandas/gologger" - "github.com/jsandas/tlstools/pkg/ssl" - "github.com/jsandas/tlstools/pkg/utils/tcputils" -) - -const ( - notApplicable = "n/a" - notVulnerable = "no" - vulnerable = "yes" - testFailed = "error" -) - -type Heartbleed struct { - Vulnerable string `json:"vulnerable"` - ExtensionEnabled bool `json:"extension"` -} - -// Heartbleed test -func (h *Heartbleed) Check(host string, port string, tlsVers int) error { - conn, err := net.DialTimeout("tcp", host+":"+port, 3*time.Second) - if err != nil { - logger.Errorf("event_id=tcp_dial_failed msg=\"%v\"", err) - h.Vulnerable = testFailed - return err - } - defer conn.Close() - - /* - only test up to tlsv1.2 because not possible - with tlsv1.3 - */ - if tlsVers > tls.VersionTLS12 { - tlsVers = tls.VersionTLS12 - } - - err = ssl.StartTLS(conn, port) - if err != nil { - h.Vulnerable = testFailed - return err - } - - // Send clientHello - clientHello := makeClientHello(tlsVers) - err = tcputils.Write(conn, clientHello, 2) - if err != nil { - logger.Debugf("event_id=heartbleed_clientHello_failed msg=\"%v\"", err) - h.Vulnerable = testFailed - return err - } - - connbuf := bufio.NewReader(conn) - - hBEnabled, err := checkExtension(connbuf) - if err != nil { - switch err.Error() { - // some applications reset the tcp connection - // when probing for heartbleed - case "EOF": - default: - logger.Debugf("event_id=heartbleed_handshake_failed msg=\"%v\"", err) - h.Vulnerable = testFailed - return err - } - } - - if hBEnabled { - h.ExtensionEnabled = true - - payload := makePayload(tlsVers) - err = tcputils.Write(conn, payload, 2) - if err != nil { - logger.Debugf("event_id=heartbleed_payload_failed msg=\"%v\"", err) - h.Vulnerable = testFailed - return err - } - - h.Vulnerable = heartbeatListen(connbuf) - return nil - } - - h.Vulnerable = notApplicable - return nil - -} - -// checks if handshake was successful and if the -// heartbeat extension is enabled -func checkExtension(buff *bufio.Reader) (bool, error) { - var data []byte - var err error - - hBEnabled := false - for { - b, err := buff.ReadByte() - if err != nil { - return hBEnabled, err - } - - data = append(data, b) - - // is heartbeat extension enabled? - if strings.HasSuffix(fmt.Sprintf("%X", data), "000F000101") { - hBEnabled = true - } - // is serverHello finished - if strings.HasSuffix(fmt.Sprintf("%X", data), "0E000000") { - break - } - } - - return hBEnabled, err -} - -// reads from buffer and checks the size of the response -// to determine if heartbleed was exploited -func heartbeatListen(buff *bufio.Reader) string { - // listen for reply - // ReadBytes has to be ran one to process started, but - // it will block if there isn't any data to read - var data []byte - - go func() { - buff.ReadByte() - }() - time.Sleep(1 * time.Second) - - i := 0 - for { - buffLeft := buff.Buffered() - - // fmt.Printf("iter: %d left: %d\n", i, buffLeft) - if buffLeft == 0 && i <= 3 { - i++ - continue - } - - if buffLeft == 0 && i > 3 { - break - } - - b, err := buff.ReadByte() - data = append(data, b) - if err != nil { - // logger.Debugf("event_id=heartbleed_error msg=%v", err) - break - } - - if len(data) >= 1600 { - logger.Debugf("event_id=heartbleed_check status=%s", vulnerable) - return vulnerable - } - - i++ - } - - return notVulnerable -} diff --git a/pkg/vuln/heartbleed/heartbleed_bytes.go b/pkg/vuln/heartbleed/heartbleed_bytes.go deleted file mode 100644 index 8ad2ac3..0000000 --- a/pkg/vuln/heartbleed/heartbleed_bytes.go +++ /dev/null @@ -1,50 +0,0 @@ -package heartbleed - -import ( - "encoding/hex" - "fmt" - - "github.com/jsandas/tlstools/pkg/utils" -) - -// tlsv1 = 01 -// tlsv1.1 = 02 -// tlsv1.2 = 03 - -func makePayload(tlsVers int) []byte { - // Create equivalent of this string - // heartbleed_payload="\x18\x03\x$TLSV\x00\x03\x01\x40\x00" - var b []byte - - contentType := "18" // Heartbeat records - tlsVersion := fmt.Sprintf("0%x", tlsVers) // tls version - length := "0003" // 3 bytes - hbType := "01" - pLength := "4000" // 16384 bytes - - b, _ = hex.DecodeString(contentType + tlsVersion + length + hbType + pLength) - - // logger.Debugf("Heartbeat MSG: %X", b) - - return b -} - -func makeClientHello(tlsVers int) []byte { - var b []byte - - rand, _ := utils.GenRandBytes(32) - - contentType := "16" - tlsVersion := fmt.Sprintf("0%x", tlsVers) - length := "00dc" - hsType := "01" - hsLength := "0000d8" - hsTLSVersion := fmt.Sprintf("0%x", tlsVers) - random := hex.EncodeToString(rand) - everythingElse := "000066c014c00ac022c0210039003800880087c00fc00500350084c012c008c01cc01b00160013c00dc003000ac013c009c01fc01e00330032009a009900450044c00ec004002f00960041c011c007c00cc002000500040015001200090014001100080006000300ff01000049000b000403000102000a00340032000e000d0019000b000c00180009000a00160017000800060007001400150004000500120013000100020003000f0010001100230000000f000101" - - b, _ = hex.DecodeString(contentType + tlsVersion + length + hsType + hsLength + hsTLSVersion + random + everythingElse) - - // logger.Debugf("ClientHello: %X", b) - return b -} diff --git a/pkg/vuln/heartbleed/heartbleed_test.go b/pkg/vuln/heartbleed/heartbleed_test.go deleted file mode 100644 index 837f93e..0000000 --- a/pkg/vuln/heartbleed/heartbleed_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package heartbleed - -import ( - "net" - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestHeartbleedExtensionDisabled(t *testing.T) { - // Start a local HTTPS server - server := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - // Test request parameters - if req.URL.String() == "/" { - // Send response to be tested - rw.Header().Set("Server", "Apache") - rw.Write([]byte("Hello")) - } - })) - // Close the server when test finishes d - defer server.Close() - - s := strings.Replace(server.URL, "https://", "", -1) - host, port, _ := net.SplitHostPort(s) - - var r Heartbleed - - r.Check(host, port, 771) - - if r.Vulnerable == vulnerable || r.ExtensionEnabled { - t.Errorf("Wrong return, got: %s/%v, want: %s/%v.", r.Vulnerable, r.ExtensionEnabled, notApplicable, false) - } - - var rTLS13 Heartbleed - rTLS13.Check(host, port, 772) - - if rTLS13.Vulnerable == vulnerable || rTLS13.ExtensionEnabled { - t.Errorf("Wrong return, got: %v/%v, want: %s/%v.", rTLS13.Vulnerable, rTLS13.ExtensionEnabled, notVulnerable, false) - } -} - -func TestHeartBleedConnectFail(t *testing.T) { - var r Heartbleed - err := r.Check("127.0.0.1", "4242", 771) - - if err == nil { - t.Errorf("Wrong return, got: %s, want: error message", err) - } -} diff --git a/pkg/vuln/weakkey/weakkey.go b/pkg/vuln/weakkey/weakkey.go deleted file mode 100644 index 51a6465..0000000 --- a/pkg/vuln/weakkey/weakkey.go +++ /dev/null @@ -1,97 +0,0 @@ -package weakkey - -import ( - "bufio" - "crypto/sha1" - "encoding/hex" - "fmt" - "os" - "path" - "strconv" - "strings" - - logger "github.com/jsandas/gologger" -) - -var commonKeySizes = []int{512, 1024, 2048, 4096} - -/* - Debian Weak Key checking is an old vulnerability. - This check compares the sha1 hash of the certificate modulus - a list of known weak keys based on keysize - - Note: because this require referencing binary files a hack - was added to detect if running locally which overrides the - value of bpath -*/ - -const ( - notVulnerable = "no" - vulnerable = "yes" - uncommonKey = "uncommonKey" - testFailed = "error" -) - -type DebianWeakKey struct { - Vulnerable string `json:"vulnerable"` -} - -// WeakKey detects if key was generated with weak Debian openssl -func (w *DebianWeakKey) Check(keysize int, modulus string) error { - w.Vulnerable = notVulnerable - - // only test if common keysize - var found bool - for _, ks := range commonKeySizes { - if keysize == ks { - found = true - } - } - if !found { - w.Vulnerable = uncommonKey - return nil - } - - // bpath is the location of weakkeys binaries - // these are copied there during the docker build - p, _ := os.Executable() - bpath := path.Dir(p) + "/../resources/weakkeys" - - mod := fmt.Sprintf("Modulus=%s\n", strings.ToUpper(modulus)) - ks := strconv.Itoa(keysize) - h := sha1.New() - h.Write([]byte(mod)) - bs := h.Sum(nil) - - sh := hex.EncodeToString(bs) - - // Test overrides - // ks = "2048" - // sh = "24a319be7f63b8b46e9cd10d992069d592fe1766" - - // override bpath if running go test - // or go run - cwd, _ := os.Getwd() - _, dir := path.Split(cwd) - if dir == "weakkey" { - bpath = "../../../resources/weakkeys" - } - - // load weak key file - file, err := os.Open(bpath + "/blacklist.RSA-" + ks) - if err != nil { - logger.Errorf("event_id=weak_key_failed_read path=%s file=blacklist.RSA-%s", bpath, ks) - w.Vulnerable = testFailed - return err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - if sh[20:] == scanner.Text() { - w.Vulnerable = vulnerable - } - } - - return nil -} diff --git a/pkg/vuln/weakkey/weakkey_test.go b/pkg/vuln/weakkey/weakkey_test.go deleted file mode 100644 index 05acd7e..0000000 --- a/pkg/vuln/weakkey/weakkey_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package weakkey - -import ( - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "testing" -) - -const weak1024 = ` ------BEGIN CERTIFICATE----- -MIICDzCCAXgCCQCusmyauLBA4jANBgkqhkiG9w0BAQUFADBMMQswCQYDVQQGEwJH -QjESMBAGA1UECBMJQmVya3NoaXJlMRAwDgYDVQQHEwdOZXdidXJ5MRcwFQYDVQQK -Ew5NeSBDb21wYW55IEx0ZDAeFw0wODA1MTYxODE0MDJaFw0wOTA1MTYxODE0MDJa -MEwxCzAJBgNVBAYTAkdCMRIwEAYDVQQIEwlCZXJrc2hpcmUxEDAOBgNVBAcTB05l -d2J1cnkxFzAVBgNVBAoTDk15IENvbXBhbnkgTHRkMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDl/LnqaBR7lirE3HDMt1GuJyN9XCBz2lEZthyxX65KBFGkZUiY -PwAPjlq9PDTB0gIYNMCIEDFJAJl+xl92njZhK47L8t4+PaxMpCRrM6kz1KY5/gTs -49Z33g70m/zT13sTNmHjK7720QNWCIM2GpmtodiXecDAEI7DaW0KTFSfBQIDAQAB -MA0GCSqGSIb3DQEBBQUAA4GBANNXoSJuOlQqT5JIBJs8ba+2TA9hrxXQrXUWvySy -2NyF9l4CEwPdwYf+xKde6Ga5yEY/fejLG2WEZJBa8aas7nkKqkiNBnjmqbph2gP6 -7LldvthZqKkUl6BkkTr3bZEXPXa6JLHtcpRKT5ybTWIHfh0waSVmpD6o/7KictNA -Bq3R ------END CERTIFICATE-----` - -const weak2048 = ` ------BEGIN CERTIFICATE----- -MIIDzTCCArWgAwIBAgIJAJs7Mrs4MdflMA0GCSqGSIb3DQEBBQUAME0xCzAJBgNV -BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRAwDgYDVQQKEwdUZXN0bGliMQ0wCwYD -VQQLEwRUZXN0MQswCQYDVQQDEwJDQTAeFw0wODA1MTIxNzM2NTZaFw0wODA2MTEx -NzM2NTZaME0xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRAwDgYDVQQK -EwdUZXN0bGliMQ0wCwYDVQQLEwRUZXN0MQswCQYDVQQDEwJDQTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBANM8XW6YsOpOq3amDWoKe5xZg8WMCqHTw3lp -WBafJwJ7rIElP6V5xILohKLhzvDKgT7INZ6WENKUc8o21jwegaAQGkgaPP9YYQ6O -pyoqFYJn1m9NooZpKKI9RuAhxUQv355K3WvFNn0/dyJhCSlRExCDbnp31gi38ZH4 -JBm+EYfsoYTwZlHESOqQR4gT623JvlP8ZmnTHKtjij8wY9E8ytpbSvojHc75VIbt -XS1xjDDgzkraL/3hgWAD8J0YOiXMsodKVwOVAOS2UAurfNQ13DAdGfLCVq5Pg33S -3mMOiKZSqHwfKkRCJFA9qX3D7rvHk+blvuxjHB7SeI/LHaEOCvcCAwEAAaOBrzCB -rDAdBgNVHQ4EFgQUC//QSRcOIUe7DnKguOpX+kBmqVcwfQYDVR0jBHYwdIAUC//Q -SRcOIUe7DnKguOpX+kBmqVehUaRPME0xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdB -cml6b25hMRAwDgYDVQQKEwdUZXN0bGliMQ0wCwYDVQQLEwRUZXN0MQswCQYDVQQD -EwJDQYIJAJs7Mrs4MdflMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB -ALRkK4uZ60YHeL6LYLJyhz1p/FJXNWb2TqO7kZQl3ZfkmFJF1524N/K8KrZLwIGJ -KJXUPcGTkBm/3tmvIuAMxn/MRvlEPW1nwQG81QXltObHRF5123Tl1px30Y8B00/V -VBqeKw7sMLF0b4PmnegPz77UhsGikffPJwLt6VUO0j52RW/XvpletgqcxWMHqVLK -z0V73UyaJT3wEm6zEjJINPfPwcw46IeOXcnEekon3JbDxtvm8Q706YOziPStGcel -hh+5myPwDwMgc/mH+jDBK8vyaYGb+xViHK9Fa70jkcSX/AOmYYRKfKZbaR8ba/Ee -xosT4eW/v04AyK9nfcPNbhg= ------END CERTIFICATE-----` - -const oddSize1784 = ` ------BEGIN CERTIFICATE----- -MIIC8DCCAfoCCQC/HRHmKuAz7jANBgkqhkiG9w0BAQsFADBeMQswCQYDVQQGEwJV -UzELMAkGA1UECAwCTlkxFjAUBgNVBAcMDU5ldyBZb3JrIENpdHkxEDAOBgNVBAoM -B0Zha2VPcmcxGDAWBgNVBAMMD29kZC5rZXlzaXplLmNvbTAeFw0yMjA4MjEwMjMw -NTlaFw0yMzA4MjEwMjMwNTlaMF4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTEW -MBQGA1UEBwwNTmV3IFlvcmsgQ2l0eTEQMA4GA1UECgwHRmFrZU9yZzEYMBYGA1UE -AwwPb2RkLmtleXNpemUuY29tMIH9MA0GCSqGSIb3DQEBAQUAA4HrADCB5wKB3zfP -RDCqxw7BH7cXm1vMEmH4EAJuVpWYJGTGr8krhJUyRiW0RwYyA7uBVMQi5+zCl+/P -oCBwPNwe8LmDhjmT6O0OLhOTeEGITlggSZOskppfpxEOM450S9i9RhYHnGxim8uO -tUoHJfaUy8IeKYHA/RGS2OaE704F3HsQzvTplmYiXiD0cc9O55y/CMno/KPU0Fw0 -/CdWsU2vbxQlmvnB4gS/7bou/XvFhsaa7fQZrh2RewA3FnJkJhKsIMOeOu7Svj6Z -w6zDW+CV2us16I+kmim2Qe4b+VXlTp1qSvVmd2UCAwEAATANBgkqhkiG9w0BAQsF -AAOB4AAUW8wOhOQRJGghghlM5e+zWMno15D2keoUBVvOyapJjgnrJkFpWLBt+Pr6 -R9Oo1BFMpmSp4VNQcKQ84ddVNoSpgzKk6M4C4g8IEyzh8OWu7YEg81J2n90g8xMA -6iYZenPB9FDgylPGOfE4pz/G06aMW24BT64cX+p9+Skiw1lqkD3Copj3tSlNmkKS -rNJFWh3NMI1M1r+WiG4AhGPSwWyL6Z2eni9ggpLNeAF0hHtcK6lBF6NLxt3NjHi/ -UrUNQTca4I10vp16gVCdU3AsMmaYXLn7vqoiexdgzQmQqsJW ------END CERTIFICATE-----` - -const good2048 = ` ------BEGIN CERTIFICATE----- -MIIDVDCCAjwCAQEwDQYJKoZIhvcNAQELBQAwbTEZMBcGA1UEAwwQU2VsZi1TaWdu -ZWQgUm9vdDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlVUMQ0wCwYDVQQHDARMZWhp -MRkwFwYDVQQKDBBTZWxmLVNpZ25lZCBJbmMuMQwwCgYDVQQLDANTUkUwHhcNMjAw -MTEzMDU1MTE4WhcNMjAwMTIwMDU1MTE4WjBzMQswCQYDVQQGEwJVUzELMAkGA1UE -CAwCVVQxDTALBgNVBAcMBExlaGkxGTAXBgNVBAoMEFNlbGYtU2lnbmVkIEluYy4x -DDAKBgNVBAsMA1NSRTEfMB0GA1UEAwwWdmFsaWQuZGlnaWNlcnR0ZXN0LmNvbTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/1VPx0IrEnQcPQZ6sElobO -CsO8C7oTqZFpJttujRMCWpvKAizlx+tEocZTBnUcTupC0NA2FCg7CakS+JZ9phHC -mnnC5Fq2Eeehgg4vhVtU/hi2BRUW+QUV8SnIBoid6KZckaHg1qBCxT0KPH5iGpAG -S3dRpmNAgQ9x3lrHrRj+qmQxTUQ8eeUzQZhifBm+y/NmZVZq+r4MXh8imK44d1SD -BoQTSS7+jLU3SnFKu+R/y4IUU53JakJBrD1Vst6+FMgDIXD1pj+cDVlCHlNw9ccB -XSJG3VpBXdNvlxodOO7gBVIbW8zzg2WLcNJRzSKHFR9WdFhKMJzpe3dXTf9b7MkC -AwEAATANBgkqhkiG9w0BAQsFAAOCAQEABdU8Bb8EqrRlErbeKbjQen2kN7UpFHL5 -TpyxQL3tlvW4W2ougiTWy6j5FX4EMCjTPg4/tBcR97Kqe4uU01JSRoCBd6zKAmrD -VZaUBWc1ly8iskOO+vZjuFa1wxhX/ugjqWHnfREqm3QHX4nuQEwjvHgZLQmuq21/ -Zx3gd64aD7xL5pXt+/DILPNUD3OYKBpQ8DGW3CxKXEQZvN/D4opk7FFMnSWPCiFo -6wAwChXZBzX+v8hoFHt83QsnpFOrVD1H/RBuNBp0VBuySRlVwPYVDcutTrqMEz3z -d7+0ksvbDUmlMMZRIppmHit25taSAGPxmprKhIs2U/39qbfsrXwp4Q== ------END CERTIFICATE-----` - -func TestWeakKeyBad1024(t *testing.T) { - var r DebianWeakKey - - block, _ := pem.Decode([]byte(weak1024)) - if block == nil { - panic("failed to parse certificate PEM") - } - crt, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Errorf("error parsing certificate for test: %v", err) - } - - pk := crt.PublicKey.(*rsa.PublicKey) - ks := pk.Size() * 8 - mod := fmt.Sprintf("%x", pk.N) - r.Check(ks, mod) - - if r.Vulnerable != vulnerable { - t.Errorf("Did not detect weak key, got: %v, want: %v.", r.Vulnerable, vulnerable) - } - -} - -func TestWeakKeyBad2048(t *testing.T) { - var r DebianWeakKey - - block, _ := pem.Decode([]byte(weak2048)) - if block == nil { - panic("failed to parse certificate PEM") - } - crt, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Errorf("error parsing certificate for test: %v", err) - } - - pk := crt.PublicKey.(*rsa.PublicKey) - ks := pk.Size() * 8 - mod := fmt.Sprintf("%x", pk.N) - r.Check(ks, mod) - - if r.Vulnerable != vulnerable { - t.Errorf("Did not detect weak key, got: %v, want: %v.", r.Vulnerable, vulnerable) - } - -} - -func TestWeakKeyGood2048(t *testing.T) { - var r DebianWeakKey - - block, _ := pem.Decode([]byte(good2048)) - if block == nil { - panic("failed to parse certificate PEM") - } - crt, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Errorf("error parsing certificate for test: %v", err) - } - - pk := crt.PublicKey.(*rsa.PublicKey) - ks := pk.Size() * 8 - mod := fmt.Sprintf("%x", pk.N) - r.Check(ks, mod) - - if r.Vulnerable == vulnerable { - t.Errorf("Did not detect weak key, got: %v, want: %v.", r.Vulnerable, notVulnerable) - } - -} - -func TestWeakKeyUncommonKeySize(t *testing.T) { - var r DebianWeakKey - - block, _ := pem.Decode([]byte(oddSize1784)) - if block == nil { - panic("failed to parse certificate PEM") - } - crt, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Errorf("error parsing certificate for test: %v", err) - } - - pk := crt.PublicKey.(*rsa.PublicKey) - ks := pk.Size() * 8 - mod := fmt.Sprintf("%x", pk.N) - r.Check(ks, mod) - - if r.Vulnerable != uncommonKey { - t.Errorf("wrong return, got: %v, want: %v.", r.Vulnerable, uncommonKey) - } -} - -func TestWeakKeyMissingKeyFile(t *testing.T) { - commonKeySizes = []int{512, 1024, 1784, 2048, 4096} - var r DebianWeakKey - - block, _ := pem.Decode([]byte(oddSize1784)) - if block == nil { - panic("failed to parse certificate PEM") - } - crt, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Errorf("error parsing certificate for test: %v", err) - } - - pk := crt.PublicKey.(*rsa.PublicKey) - ks := pk.Size() * 8 - mod := fmt.Sprintf("%x", pk.N) - err = r.Check(ks, mod) - - if err == nil { - t.Errorf("Expected error for odd key size") - } - -} From a6437dcfcc806908f0f692b745af5a7f883d32a8 Mon Sep 17 00:00:00 2001 From: jsandas Date: Sun, 19 Jul 2026 14:40:52 -0600 Subject: [PATCH 4/4] remove nmap from Dockerfile and folder structure --- Dockerfile | 4 - resources/nmap/ssl-ccs-injection.nse | 328 --------------------------- 2 files changed, 332 deletions(-) delete mode 100644 resources/nmap/ssl-ccs-injection.nse diff --git a/Dockerfile b/Dockerfile index 9856943..be3e8d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,6 @@ WORKDIR /go/src/tlstools RUN go mod download -RUN apt-get update && apt install -y nmap - RUN CGO_ENABLED=0 go build ./cmd/tlstools RUN CGO_ENABLED=0 go build ./cmd/tlstools-cli @@ -17,7 +15,6 @@ RUN CGO_ENABLED=0 go build ./cmd/tlstools-cli FROM debian:bookworm AS base RUN apt-get update && apt upgrade -y \ - && apt-get install -y nmap \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -25,7 +22,6 @@ RUN useradd -r appuser COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=ghcr.io/jsandas/debian-weakkeys /usr/share/openssl-blacklist/* /opt/tlstools/resources/weakkeys/ -COPY resources/nmap /opt/tlstools/resources/nmap USER appuser diff --git a/resources/nmap/ssl-ccs-injection.nse b/resources/nmap/ssl-ccs-injection.nse deleted file mode 100644 index fd34bb1..0000000 --- a/resources/nmap/ssl-ccs-injection.nse +++ /dev/null @@ -1,328 +0,0 @@ -local nmap = require('nmap') -local shortport = require('shortport') -local sslcert = require('sslcert') -local stdnse = require('stdnse') -local vulns = require('vulns') -local tls = require 'tls' -local tableaux = require "tableaux" - -description = [[ -Detects whether a server is vulnerable to the SSL/TLS "CCS Injection" -vulnerability (CVE-2014-0224), first discovered by Masashi Kikuchi. -The script is based on the ccsinjection.c code authored by Ramon de C Valle -(https://gist.github.com/rcvalle/71f4b027d61a78c42607) - -In order to exploit the vulnerablity, a MITM attacker would effectively -do the following: - - o Wait for a new TLS connection, followed by the ClientHello - ServerHello handshake messages. - - o Issue a CCS packet in both the directions, which causes the OpenSSL - code to use a zero length pre master secret key. The packet is sent - to both ends of the connection. Session Keys are derived using a - zero length pre master secret key, and future session keys also - share this weakness. - - o Renegotiate the handshake parameters. - - o The attacker is now able to decrypt or even modify the packets - in transit. - -The script works by sending a 'ChangeCipherSpec' message out of order and -checking whether the server returns an 'UNEXPECTED_MESSAGE' alert record -or not. Since a non-patched server would simply accept this message, the -CCS packet is sent twice, in order to force an alert from the server. If -the alert type is different than 'UNEXPECTED_MESSAGE', we can conclude -the server is vulnerable. -]] - ---- --- @usage --- nmap -p 443 --script ssl-ccs-injection --- --- @output --- PORT STATE SERVICE --- 443/tcp open https --- | ssl-ccs-injection: --- | VULNERABLE: --- | SSL/TLS MITM vulnerability (CCS Injection) --- | State: VULNERABLE --- | Risk factor: High --- | Description: --- | OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before --- | 1.0.1h does not properly restrict processing of ChangeCipherSpec --- | messages, which allows man-in-the-middle attackers to trigger use --- | of a zero-length master key in certain OpenSSL-to-OpenSSL --- | communications, and consequently hijack sessions or obtain --- | sensitive information, via a crafted TLS handshake, aka the --- | "CCS Injection" vulnerability. --- | --- | References: --- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0224 --- | http://www.cvedetails.com/cve/2014-0224 --- |_ http://www.openssl.org/news/secadv_20140605.txt - -author = "Claudiu Perta " -license = "Same as Nmap--See https://nmap.org/book/man-legal.html" -categories = { "vuln", "safe" } -dependencies = {"https-redirect"} - -portrule = function(host, port) - return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port) -end - -local Error = { - NOT_VULNERABLE = 0, - CONNECT = 1, - PROTOCOL_MISMATCH = 2, - SSL_HANDSHAKE = 3, - TIMEOUT = 4 -} - ---- --- Reads an SSL/TLS record and returns true if it's any fatal --- alert and false otherwise. -local function fatal_alert(s) - local status, buffer = tls.record_buffer(s) - if not status then - return false - end - - local position, record = tls.record_read(buffer, 1) - if record == nil then - return false - end - - if record.type ~= "alert" then - return false - end - - for _, body in ipairs(record.body) do - if body.level == "fatal" then - return true - end - end - - return false -end - ---- --- Reads an SSL/TLS record and returns true if it's a fatal, --- 'unexpected_message' alert and false otherwise. -local function alert_unexpected_message(s) - local status, buffer - status, buffer = tls.record_buffer(s, buffer, 1) - if not status then - return false - end - - local position, record = tls.record_read(buffer, 1) - if record == nil then - return false - end - - if record.type ~= "alert" then - -- Mark this as VULNERABLE, we expect an alert record - return true,true - end - - for _, body in ipairs(record.body) do - if body.level == "fatal" and body.description == "unexpected_message" then - return true,false - end - end - - return true,true -end - -local function test_ccs_injection(host, port, version) - local hello = tls.client_hello({ - ["protocol"] = version, - -- Only negotiate SSLv3 on its own; - -- TLS implementations may refuse to answer if SSLv3 is mentioned. - ["record_protocol"] = (version == "SSLv3") and "SSLv3" or "TLSv1.0", - -- Claim to support every cipher - -- Doesn't work with IIS, but IIS isn't vulnerable - ["ciphers"] = tableaux.keys(tls.CIPHERS), - ["compressors"] = {"NULL"}, - ["extensions"] = { - -- Claim to support common elliptic curves - ["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"]( - tls.DEFAULT_ELLIPTIC_CURVES), - }, - }) - - local status, err - local s - local specialized = sslcert.getPrepareTLSWithoutReconnect(port) - if specialized then - status, s = specialized(host, port) - if not status then - stdnse.debug3("Connection to server failed: %s", s) - return false, Error.CONNECT - end - else - s = nmap.new_socket() - status, err = s:connect(host, port) - if not status then - stdnse.debug3("Connection to server failed: %s", err) - return false, Error.CONNECT - end - end - - -- Set a sufficiently large timeout - s:set_timeout(10000) - - -- Send Client Hello to the target server - status, err = s:send(hello) - if not status then - stdnse.debug1("Couldn't send Client Hello: %s", err) - s:close() - return false, Error.CONNECT - end - - -- Read response - local done = false - local i = 1 - local response - repeat - status, response, err = tls.record_buffer(s, response, i) - if err == "TIMEOUT" or not status then - stdnse.verbose1("No response from server: %s", err) - s:close() - return false, Error.TIMEOUT - end - - local record - i, record = tls.record_read(response, i) - if record == nil then - stdnse.debug1("Unknown response from server") - s:close() - return false, Error.NOT_VULNERABLE - elseif record.protocol ~= version then - stdnse.debug1("Protocol version mismatch (%s)", version) - s:close() - return false, Error.PROTOCOL_MISMATCH - elseif record.type == "alert" then - for _, body in ipairs(record.body) do - if body.level == "fatal" then - stdnse.debug1("Fatal alert: %s", body.description) - -- Could be something else, but this lets us retry - return false, Error.PROTOCOL_MISMATCH - end - end - end - - if record.type == "handshake" then - for _, body in ipairs(record.body) do - if body.type == "server_hello_done" then - stdnse.debug1("Handshake completed (%s)", version) - done = true - end - end - end - until done - - -- Send the change_cipher_spec message twice to - -- force an alert in the case the server is not - -- patched. - - -- change_cipher_spec message - local ccs = tls.record_write( - "change_cipher_spec", version, "\x01") - - -- Send the first ccs message - status, err = s:send(ccs) - if not status then - stdnse.debug1("Couldn't send first ccs message: %s", err) - s:close() - return false, Error.SSL_HANDSHAKE - end - - -- Optimistically read the first alert message - -- Shorter timeout: we expect most servers will bail at this point. - s:set_timeout(stdnse.get_timeout(host)) - -- If we got an alert right away, we can stop right away: it's not vulnerable. - if fatal_alert(s) then - s:close() - return false, Error.NOT_VULNERABLE - end - -- Restore our slow timeout - s:set_timeout(10000) - - -- Send the second ccs message - status, err = s:send(ccs) - if not status then - stdnse.debug1("Couldn't send second ccs message: %s", err) - s:close() - return false, Error.SSL_HANDSHAKE - end - - -- Read the alert message - local vulnerable - status,vulnerable = alert_unexpected_message(s) - - -- Leave the target not vulnerable in case of an error. This could occur - -- when running against a different TLS/SSL implementations (e.g., GnuTLS) - if not status then - stdnse.debug1("Couldn't get reply from the server (probably not OpenSSL)") - s:close() - return false, Error.SSL_HANDSHAKE - end - - if not vulnerable then - stdnse.debug1("Server returned UNEXPECTED_MESSAGE alert, not vulnerable") - s:close() - return false, Error.NOT_VULNERABLE - else - stdnse.debug1("Vulnerable - alert is not UNEXPECTED_MESSAGE") - s:close() - return true - end -end - -action = function(host, port) - local vuln_table = { - title = "SSL/TLS MITM vulnerability (CCS Injection)", - state = vulns.STATE.NOT_VULN, - risk_factor = "High", - description = [[ -OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h -does not properly restrict processing of ChangeCipherSpec messages, -which allows man-in-the-middle attackers to trigger use of a zero -length master key in certain OpenSSL-to-OpenSSL communications, and -consequently hijack sessions or obtain sensitive information, via -a crafted TLS handshake, aka the "CCS Injection" vulnerability. - ]], - references = { - 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0224', - 'http://www.cvedetails.com/cve/2014-0224', - 'http://www.openssl.org/news/secadv_20140605.txt' - } - } - - local report = vulns.Report:new(SCRIPT_NAME, host, port) - - -- client hello will support multiple versions of TLS. We only retry to fall - -- back to SSLv3, which some implementations won't allow in combination with - -- newer versions. - for _, tls_version in ipairs({"TLSv1.2", "SSLv3"}) do - local vulnerable, err = test_ccs_injection(host, port, tls_version) - - -- Return an explicit message in case of a TIMEOUT, - -- to avoid considering this as not vulnerable. - if err == Error.TIMEOUT then - return "No reply from server (TIMEOUT)" - end - - if err ~= Error.PROTOCOL_MISMATCH then - if vulnerable then - vuln_table.state = vulns.STATE.VULN - end - break - end - end - - return report:make_output(vuln_table) -end