github twitter linkedin email rss
EthereumJS Sha3 Encoding Bug Report
Jun 7, 2017

This was a weird and interesting bug I found and reported resulting from a single line of code in EthereumJS. The Ethereum Bug Bounty Program was a pleasure to work with and is a great resource.

Bug

The sha3 method in EthereumJS-Util returned incorrect results and exhibited undocumented behavior in certain cases when a string began with 0x. This is due to the toBuffer method attempting to convert the string into hex regardless of whether the string was intended to be hexadecimal or even valid hexadecimal.

For example, ethUtil.sha3("0xqdasdaadas") would return <Buffer c5 d2 46 01 86 f7 23 3c 92 7e 7d b2 dc c7 03 c0 e5 00 b6 53 ca 82 27 3b 7b fa d8 04 5d 85 a4 70> which is the hash of a null string.

When hashing a string prefixed with 0x, it would strip the prefix and convert it into a buffer with hexadecimal encoding using the code below.

if (exports.isHexPrefixed(v)) {
  v = Buffer.from(exports.padToEven(exports.stripHexPrefix(v)), 'hex')

However, construction of the buffer stopped at the first invalid hex character. In the case of 0xqdasdaadas, qd is not a valid hexadecimal character so toBuffer returned an empty buffer as shown below.

> Buffer.from("qdasdaadas", 'hex')
<Buffer >

This lead to the sha3 function hashing an empty buffer and returning an incorrect result.

There were two ways I found that could have lead to lost funds or been exploited.

ENS Namehash

I found the Sha3 bug when comparing the implementations of the ENS namehash function on MyEtherWallet and the spec implementation. Because MEW uses the ethereumjs-util package, domains containing 0x (ie. 0xq9mb6N.eth or 0xqzabc.hello.eth) would have been hashed incorrectly and inconsistently with the most other namehash implementations using web3.js or js-sha3. That could have lead to a loss of funds for a user who buys a name beginning with 0x (at least until they could unlock the funds).

Signature Forging

There was also a way this could have been exploited in a malicious manner. I noticed a few major websites also the sha3 function while signing and verifying messages - this means if you could get someone to sign a message starting with invalid hexadecimal (ie 0xqd benign message), the websites will validate the signature any other message starting with the same invalid hexadecimal as valid when paired with the benign signature.

For example on MyEtherWallet, the same signature was valid for two different messages. Because the benign message started with 0x, the Sha3 function treated both messages as empty strings.

{"address":"0x9cce34f7ab185c7aba1b7c8140d620b4bda941d6","msg":"0xqd benign message ","sig":"0x2624a2f67d4909e7af2aff17844f40e3d5a46f7d5e1acf2bc6c8f760c3ed833758aeb6cb43ce0a07115f53f1eb753e9857ed5ef26bac77ae2ad91a431be1905e1b"}

{"address":"0x9cce34f7ab185c7aba1b7c8140d620b4bda941d6","msg":"0xqd send all your ethereum to my illegitimate address","sig":"0x2624a2f67d4909e7af2aff17844f40e3d5a46f7d5e1acf2bc6c8f760c3ed833758aeb6cb43ce0a07115f53f1eb753e9857ed5ef26bac77ae2ad91a431be1905e1b"}

Etherchain’s signature verification page, was also vulnerable to the same attack because it used EthereumJS-Util to validate signatures.

This attack is not super likely or easy to exploit - it requires you get someone to sign the original message, and that message must start with invalid hexadecimal. If you were to get that signature you could forge signatures for any arbitrary message.

To clarify, MyEtherWallet and Etherchain were not at fault for either bug, this was a side effect of the bug in the sha3() function from EthereumJS.

Fix

The fix implemented by Ethereum (diff shown below) was to replace the call to isHexPrefixed(v) with isHexString(v), which ensured that not only was it hex prefixed, but it was also valid hexadecimal.

diff --git a/index.js b/index.js
index a7d379e..2d8fb50 100644
--- a/index.js
+++ b/index.js
@@ -143,7 +143,7 @@ exports.toBuffer = function (v) {
     if (Array.isArray(v)) {
       v = Buffer.from(v)
     } else if (typeof v === 'string') {
-      if (exports.isHexPrefixed(v)) {
+      if (exports.isHexString(v)) {
         v = Buffer.from(exports.padToEven(exports.stripHexPrefix(v)), 'hex')
       } else {
         v = Buffer.from(v)

Timeline

  • May 6, 2017: Bug reported in the sha3 function
  • May 9, 2017: Bug confirmed by Ethereum
  • May 10, 2017: Signature verification exploit added to report
  • May 16, 2017: Ethereum awarded a $2,000 bounty
  • May 31, 2017: Bug fixed

Back to posts