Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
@yuhong

This comment has been minimized.

Copy link

commented Dec 9, 2015

BTW, I think in practice most non-OpenSSL/SSLeay clients uses a 16 byte challenge in the SSLv2 ClientHello. See "Netscape SSLv2 challenge length bug" in http://www.dcs.ed.ac.uk/home/crypto/SSLeay/vendor-bugs.html for why. They fixed this text in the TLS 1.2 spec.

@RalphCorderoy

This comment has been minimized.

Copy link

commented Dec 12, 2015

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Owner Author

commented Feb 5, 2016

@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

This comment has been minimized.

Copy link
Owner Author

commented Feb 6, 2016

@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
You can’t perform that action at this time.