Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save FiloSottile/8e0fe1a3523126e8c581 to your computer and use it in GitHub Desktop.
Save FiloSottile/8e0fe1a3523126e8c581 to your computer and use it in GitHub Desktop.
crypto/tls: support SSLv2 compatibility handshakes - based on https://github.com/golang/go/issues/3930#issuecomment-69873720
From 6ec6e3f7b176547783b2c464d54bc1a1f7d884f7 Mon Sep 17 00:00:00 2001
From: Filippo Valsorda <filippo@cloudflare.com>
Date: Mon, 7 Dec 2015 15:44:34 +0000
Subject: [PATCH] crypto/tls: support SSLv2 compatibility handshakes
---
src/crypto/tls/conn.go | 103 ++++++++++++++++++++++++++++++++++++-
src/crypto/tls/handshake_server.go | 7 ++-
2 files changed, 107 insertions(+), 3 deletions(-)
diff --git a/src/crypto/tls/conn.go b/src/crypto/tls/conn.go
index e3dcf15..b723ae7 100644
--- a/src/crypto/tls/conn.go
+++ b/src/crypto/tls/conn.go
@@ -46,6 +46,9 @@ type Conn struct {
// firstFinished contains the first Finished hash sent during the
// handshake. This is the "tls-unique" channel binding value.
firstFinished [12]byte
+ // v2ClientHello contains the CH as sent by the client if it's been
+ // unwrapped from a SSLv2 compatibility handshake
+ v2ClientHello []byte
clientProtocol string
clientProtocolFallback bool
@@ -507,6 +510,103 @@ func (hc *halfConn) splitBlock(b *block, n int) (*block, *block) {
return b, bb
}
+func unwrapSSLv2Handshake(c *Conn, b *block) error {
+ // The high bit must be 1 for a SSLv2 compatible ClientHello
+ if b.data[0]&128 != 128 {
+ c.sendAlert(alertUnexpectedMessage)
+ return c.in.setErrorLocked(errors.New("tls: v2 handshake unwrapping error"))
+ }
+
+ recordLength := uint16(b.data[0]&0x7f)<<8 | uint16(b.data[1])
+ mType := uint8(b.data[2])
+ majVer := uint8(b.data[3])
+ minVer := uint8(b.data[4])
+
+ if mType != typeClientHello {
+ c.sendAlert(alertUnexpectedMessage)
+ return c.in.setErrorLocked(errors.New("tls: v2 handshake unwrapping error"))
+ }
+
+ // We can only unwrap SSLv2 ClientHello if TLS/SSLv3 is supported
+ if majVer < 3 {
+ c.sendAlert(alertProtocolVersion)
+ return c.in.setErrorLocked(errors.New("tls: unsupported version in v2 handshake"))
+ }
+
+ if err := b.readFromUntil(c.conn, int(2+recordLength)); err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ if e, ok := err.(net.Error); !ok || !e.Temporary() {
+ c.in.setErrorLocked(err)
+ }
+ return err
+ }
+
+ cipherSpecLen := uint16(b.data[5])<<8 | uint16(b.data[6])
+ if (cipherSpecLen % 3) != 0 {
+ c.sendAlert(alertHandshakeFailure)
+ return c.in.setErrorLocked(errors.New("tls: v2 handshake unwrapping error"))
+ }
+
+ // Session ID length MUST be zero for a SSLv2 ClientHello
+ sessionIdLen := uint16(b.data[7])<<8 | uint16(b.data[8])
+ if sessionIdLen != 0 {
+ c.sendAlert(alertHandshakeFailure)
+ return c.in.setErrorLocked(errors.New("tls: v2 handshake unwrapping error"))
+ }
+
+ challengeLen := uint16(b.data[9])<<8 | uint16(b.data[10])
+ // The spec is contradictory here -- it says challengeLen must be 32, but
+ // also specifies how to handle challenge lengths greater or less than that
+ // Anyway according to rfc2246 must have at least 16 bytes of challenge data
+ // See also "Netscape SSLv2 challenge length bug" in
+ // http://www.dcs.ed.ac.uk/home/crypto/SSLeay/vendor-bugs.html
+ if challengeLen < 16 {
+ c.sendAlert(alertHandshakeFailure)
+ return c.in.setErrorLocked(errors.New("tls: v2 handshake unwrapping error"))
+ }
+
+ if recordLength != 9+cipherSpecLen+challengeLen {
+ c.sendAlert(alertHandshakeFailure)
+ return c.in.setErrorLocked(errors.New("tls: v2 handshake unwrapping error"))
+ }
+
+ cipherSpecs := b.data[11 : 11+cipherSpecLen]
+ challengeData := b.data[11+cipherSpecLen : 11+cipherSpecLen+challengeLen]
+
+ b, c.rawInput = c.in.splitBlock(b, int(2+recordLength))
+
+ // Create a normal ClientHello message and write it to the handshake buffer
+ helloMsg := clientHelloMsg{}
+ helloMsg.vers = uint16(majVer)<<8 | uint16(minVer)
+ helloMsg.sessionId = []byte{0} // Session ID must be zero
+ helloMsg.compressionMethods = []uint8{compressionNone}
+ // Only use the last 32 bytes of the challenge data, padded to the right
+ if len(challengeData) < 32 {
+ helloMsg.random = make([]byte, 32-len(challengeData))
+ helloMsg.random = append(helloMsg.random, challengeData...)
+ } else {
+ helloMsg.random = challengeData[len(challengeData)-32:]
+ }
+
+ for i := 0; i < len(cipherSpecs); i += 3 {
+ if cipherSpecs[i] != 0 {
+ continue
+ }
+ cipher := uint16(cipherSpecs[i+1])<<8 | uint16(cipherSpecs[i+2])
+ helloMsg.cipherSuites = append(helloMsg.cipherSuites, cipher)
+ }
+
+ c.hand.Write(helloMsg.marshal())
+
+ c.v2ClientHello = make([]byte, recordLength)
+ copy(c.v2ClientHello, b.data[2:])
+ c.in.freeBlock(b)
+
+ return nil
+}
+
// readRecord reads the next TLS record from the connection
// and updates the record layer state.
// c.in.Mutex <= L; c.input == nil.
@@ -556,8 +656,7 @@ Again:
// is always < 256 bytes long. Therefore typ == 0x80 strongly suggests
// an SSLv2 client.
if want == recordTypeHandshake && typ == 0x80 {
- c.sendAlert(alertProtocolVersion)
- return c.in.setErrorLocked(errors.New("tls: unsupported SSLv2 handshake received"))
+ return unwrapSSLv2Handshake(c, b)
}
vers := uint16(b.data[1])<<8 | uint16(b.data[2])
diff --git a/src/crypto/tls/handshake_server.go b/src/crypto/tls/handshake_server.go
index e16cddc..9df9b68 100644
--- a/src/crypto/tls/handshake_server.go
+++ b/src/crypto/tls/handshake_server.go
@@ -352,7 +352,12 @@ func (hs *serverHandshakeState) doFullHandshake() error {
// certificates won't be used.
hs.finishedHash.discardHandshakeBuffer()
}
- hs.finishedHash.Write(hs.clientHello.marshal())
+ if c.v2ClientHello != nil {
+ hs.finishedHash.Write(c.v2ClientHello)
+ c.v2ClientHello = nil
+ } else {
+ hs.finishedHash.Write(hs.clientHello.marshal())
+ }
hs.finishedHash.Write(hs.hello.marshal())
c.writeRecord(recordTypeHandshake, hs.hello.marshal())
--
2.7.0
@RalphCorderoy
Copy link

The setting of helloMsg.random has a bug; challengeData[32-len(challengeData):] only works when len() is 32, but the code is trying to work with it being longer too. Also, many of the errors.New() are the same constant string giving no clue as to the error site, and it would be nice to have the errant value in the error too, e.g. if sessionIdLen isn't zero, what is it.

@jehiah
Copy link

jehiah commented Jan 6, 2016

challengeLen is a uint16 but line 79 of this diff has if len(challengeLen) < 16 { should that just be if challengeLen < 16 ?

As that seems to be a compile error @FiloSottile i'm curious how much mileage you've gotten out of this patch.

@FiloSottile
Copy link
Author

@jehiah ah, that's a merge mistake, thanks. The patch I was using actually had that check commented out. You will also notice it fails to return an error correctly (c.sendAlert returns nil on success).

I'll fix this and look into the other two comments, but again, THIS IS NOT SAFE TO RELY ON.

@FiloSottile
Copy link
Author

@RalphCorderoy thanks, that was clearly meant to be len(challengeData)-32, and it would have caused a crash, fixed in the new version.

Now I want to see if go-fuzz would have found it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment