-
Notifications
You must be signed in to change notification settings - Fork 0
/
tlsscan.go
251 lines (203 loc) · 8.08 KB
/
tlsscan.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package main
import (
"crypto/rand"
"crypto/tls"
"encoding/binary"
"encoding/json"
"flag"
"fmt"
"log"
"net"
"os"
"strings"
"github.com/aws/aws-lambda-go/lambda"
)
// Look up TLS version names
var tlsNumtoName = map[uint16]string{
0x300: "SSLv3",
0x301: "TLSv1_0",
0x302: "TLSv1_1",
0x303: "TLSv1_2",
0x304: "TLSv1_3",
}
type jsonOutput struct {
CipherSuites []string `json:"ciphersuites"`
TLSVersions []string `json:"tlsversion"`
}
type lamdaRequest struct {
ConnectString string `json:"connnectString"`
}
func cipherSuiteTest(cipherSuites []uint16, tlsMin uint16, tlsMax uint16, host string) (uint16, bool) {
// Work out how large our (almost) static packet is going to be. Only the ciphersuites and the hostname
// for SNI will determine the size. 58 bytes is the static portion of the packet
var selectedCipher uint16
hostname := strings.Split(host, ":")[0]
packetSize := (len(cipherSuites) * 2) + len(hostname) + 59
packet := make([]byte, packetSize)
offset := copy(packet, []byte{0x16}) // TLS Handshake
binary.BigEndian.PutUint16(packet[offset:], uint16(tlsMin)) // Minimum TLS Version
offset += 2
binary.BigEndian.PutUint16(packet[offset:], uint16(packetSize-5)) // Length
offset += 2
offset += copy(packet[offset:], []byte{0x01}) // HandShake Type (client hello)
offset += copy(packet[offset:], []byte{0x00}) // Padding masquerading as length ;)
binary.BigEndian.PutUint16(packet[offset:], uint16(packetSize-9)) // The actual length
offset += 2
binary.BigEndian.PutUint16(packet[offset:], uint16(tlsMax)) // Max TLS Version
offset += 2
rand.Read(packet[offset:32]) // 32 bytes of random
offset += 32
offset += copy(packet[offset:], []byte{0x00}) // Session ID length
binary.BigEndian.PutUint16(packet[offset:], uint16(len(cipherSuites)*2)) // Length of ciphersuites field
offset += 2
for _, suite := range cipherSuites {
binary.BigEndian.PutUint16(packet[offset:], suite) // Supported ciphersuites list
offset += 2
}
offset += copy(packet[offset:], []byte{0x01}) // Compression Methods Length
offset += copy(packet[offset:], []byte{0x00}) // Compression method of null
binary.BigEndian.PutUint16(packet[offset:], uint16(len(hostname)+9)) // Real Length
offset += 2
offset += copy(packet[offset:], []byte{0x00, 0x00}) // SNI Extension
binary.BigEndian.PutUint16(packet[offset:], uint16(len(hostname)+5)) // Extensions Length
offset += 2
binary.BigEndian.PutUint16(packet[offset:], uint16(len(hostname)+3)) // Hostname Section Length
offset += 2
offset += copy(packet[offset:], []byte{0x00}) // SNI Type (DNS)
binary.BigEndian.PutUint16(packet[offset:], uint16(len(hostname))) // Hostname Length
offset += 2
offset += copy(packet[offset:], string(hostname))
// And make a connection.....
conn, err := net.Dial("tcp", host)
// Quick error check
if err != nil {
// Could not connect, burn it all down!!!
if conn != nil {
defer conn.Close()
}
log.Printf("Connection failed: %v", err)
return 0x000, false
}
// Send a packet...
_, err = conn.Write(packet)
if err != nil {
// Could not connect, burn it all down!!!
defer conn.Close()
log.Printf("Connection failed: %v", err)
return 0x000, false
}
// ... and get a reply?
buffer := make([]byte, 65535)
_, err = conn.Read(buffer)
// Super quick check that this is a good response
// The buffer is bigger than the packet, but pre-filled with 0's. If we go off the end, we'll
// just end up returning 00's
// 0 = content type (0x16 is TLS handshake)
// 1 = First byte (of two) of the TLS version.... They all start with 0x03 :)
// 5 = TLS Handshake type (0x02 is server hello)
if buffer[0] == 0x16 && buffer[1] == 0x03 && buffer[5] == 0x02 {
// buffer[43] is the location of the offset to the selected ciphersuite.
// so we return whatever is at this offset further than it is, clear? Great!
selectedCipher = binary.BigEndian.Uint16(buffer[44+buffer[43]:])
return selectedCipher, true
}
// Otherwise something went wrong
return 0x0000, false
}
// makeTLSConnection will establish a TLS connection and return the uint16 representing the established ciphersuite and
// a bool representing if the handshake was completed successfully
func makeTLSConnection(cipherSuites []uint16, tlsMin uint16, tlsMax uint16, host string) (uint16, bool) {
// TLS configuration used by the client. It is deliberately terrible ;)
var myTLSConfig = tls.Config{
MinVersion: tlsMin,
MaxVersion: tlsMax,
InsecureSkipVerify: false, // Yeah, checking certs would be nice
//VerifyPeerCertificate // <- can do cert pinning
SessionTicketsDisabled: false,
CipherSuites: cipherSuites,
PreferServerCipherSuites: true, // This lets us enumerate the servers preferred order
}
// use tls.Dial to establish a connection to host using tlsConfig as configuration
conn, err := tls.Dial("tcp", host, &myTLSConfig)
if err != nil {
//fmt.Printf("There seems to be a problem :(\n")
return 0x0000, false
}
// Close the connection when we're done
defer conn.Close()
// Get details of connection state
state := conn.ConnectionState()
// Return the ciphersuite from the state
return state.CipherSuite, true
}
func testHost(host *string) (string, []byte) {
// Mmmm variables
var preferredSuites []uint16
var preferredSuitesHuman []string
var cipherSuites []uint16
var selectedSuite uint16
var tlsSupport []string
var tlsMax uint16 = 0x304 // TLS 1.3... we need to support, I just need a sane upper bound
var tlsMin uint16 = 0x300 // SSLv3 ... not even attempting SSLv2, it's super rare and horrible
var handshakeSuccess = true
// List of ciphersuites to test. As suites are accepted by the server they will be removed from the list and
// the connection retried in order to enumerate the next ciphersuite which the server permits. This adds
// all the suites
for i := range cipherSuiteList {
cipherSuites = append(cipherSuites, i)
}
// Supported CipherSuite Test (using full spectrum TLS versions)
for handshakeSuccess == true {
// Make a TLS connection and add the negotiated protocol to the preferredSuites
selectedSuite, handshakeSuccess = cipherSuiteTest(cipherSuites, tlsMin, tlsMax, *host)
if handshakeSuccess == true {
preferredSuites = append(preferredSuites, selectedSuite)
preferredSuitesHuman = append(preferredSuitesHuman, cipherSuiteList[selectedSuite])
}
// Change the selectedSuite to NULL in the ciphersuites list. This is a cheap and lazy way of
// effectively removing the element. If something *does* negotiate NULL then there are *serious*
// problems.
for i := 0; i < len(cipherSuites) && handshakeSuccess == true; i++ {
if cipherSuites[i] == selectedSuite {
cipherSuites[i] = 0xFFFF // non-existant ciphersuite
}
}
}
// Try all the TLS versions
for tlsMin < tlsMax {
// Let's be nice and use the preferredSuites that we already know about...
// Deliberately use tlsMax for both values to force this version of TLS
_, handshakeSuccess = makeTLSConnection(preferredSuites, tlsMax, tlsMax, *host)
// Lower the max until we find all supported (and unsupported) TLS versions
if handshakeSuccess == true {
tlsSupport = append(tlsSupport, tlsNumtoName[tlsMax])
}
tlsMax--
}
// Make some output
var outputStruct jsonOutput
outputStruct.CipherSuites = preferredSuitesHuman
outputStruct.TLSVersions = tlsSupport
jsonOutput, _ := json.Marshal(outputStruct)
textOutput := string(jsonOutput)
return textOutput, jsonOutput
}
// lambdaSetup is used to perform the lambda only steps so that the same tool can be ran
// either on the commandline or in a lambda function in AWS
func lambdaSetup(event lamdaRequest) ([]byte, error) {
myArg := event.ConnectString
_, output := testHost(&myArg)
return output, nil
}
func main() {
if len(os.Getenv("_LAMBDA_SERVER_PORT")) > 0 {
// We're running in a lambda, let's handle accordingly
lambda.Start(lambdaSetup)
}
// Implied that this is *not* a lambda and so we can parse commandline args
var host = flag.String("host", "127.0.0.1:443", "host to test, format: hostname:port")
flag.Parse() // Parse commandline options
// Run test on host
textOutput, _ := testHost(host)
fmt.Printf("%s\n", textOutput)
}