- Type: Exploit
- Network: Ethereum / Moonbean
- Total lost: ~611MM USD (some returned)
- Category:: Bruteforce
- Vulnerable contracts:
- Attack transactions:
- Attacker Addresses:
- Attack Block:: 12996659, 12996671
- Date: Aug 10, 2021
- Reproduce:
forge test --match-contract Exploit_PolyNetwork -vvv
- Find a string such that the sighash of
{string}(bytes,bytes,uint64)
matchesputCurEpochConPubKeyBytes
. - Execute a cross-chain tx using said string as a
_method
and calling the manager contract, telling it to put yourself as a guardian. - Forge cross-chain messages.
The Polynetwork Bridge has EthCrosschainManager contract with an _executeCrossChainTx
which, as the name implies, executes a transaction. This method takes an arbitrary contract as a parameter, and will call a method which has a sighash corresponding to {_method}(bytes,bytes,uint64)
, where {_method}
is also user supplied.
function verifyHeaderAndExecuteTx(
bytes memory proof,
bytes memory rawHeader,
bytes memory headerProof,
bytes memory curRawHeader,
bytes memory headerSig
) whenNotPaused public returns (bool){
...
require(
_executeCrossChainTx(
toContract,
toMerkleValue.makeTxParam.method,
toMerkleValue.makeTxParam.args,
toMerkleValue.makeTxParam.fromContract,
toMerkleValue.fromChainID
), "Execute CrossChain Tx failed!");
...
return true;
}
function _executeCrossChainTx(
address _toContract,
bytes memory _method,
bytes memory _args,
bytes memory _fromContractAddr,
uint64 _fromChainId
) internal returns (bool){
// Ensure the targeting contract gonna be invoked is indeed a contract rather than a normal account address
require(Utils.isContract(_toContract), "The passed in address is not a contract!");
bytes memory returnData;
bool success;
// The returnData will be bytes32, the last byte must be 01;
(success, returnData) = _toContract.call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_args, _fromContractAddr, _fromChainId)));
// Ensure the executation is successful
require(success == true, "EthCrossChain call business contract failed");
// Ensure the returned value is true
require(returnData.length != 0, "No return value from business contract!");
(bool res,) = ZeroCopySource.NextBool(returnData, 31);
require(res == true, "EthCrossChain call business contract return is not true");
return true;
}
This is intended to be implemented by contracts that want to receive cross-chain transactions, and the message is intended to be signed by a set of keepers
, a federation in charge of making sure a transaction has finalized in a network and is ready to be relayed to the other.
This federation is managed by the EthCrossChainData contract.
Now, the attacker exploited two facts:
- The
EthCrossmainManager
is set as theowner
of theEthCrossChainData
contract. - The sighash is only 4 bytes long, making it vulnerable to bruteforce.
The attacker targeted the putCurEpochConPubKeyBytes
method on the EthCrossChainData
. To perform the attack, they only had to find a _method
string so that keccak("{_method}(bytes,bytes,uint64)")[0:4] == keccak(putCurEpochConPubKeyBytes(bytes))
. Turns out that f1121318093
as _method
does the trick.
$ cast sig 'f1121318093(bytes,bytes,uint64)' ~
0x41973cd9
$ cast sig 'putCurEpochConPubKeyBytes(bytes)' ~
0x41973cd9
Note that only the four first bytes match! Finding a collision like this for the full keccak should be extremely hard (to the point of being impossible, unless keccak
is broken)
$ cast keccak 'putCurEpochConPubKeyBytes(bytes)' ~
0x41973cd9ca2c3f7fa28309a71815e084e9827b0551227e684c70c7d6c9e5e031
$ cast keccak 'f1121318093(bytes,bytes,uint64)' ~
0x41973cd95e41447fbb4f155da56b91d5b31daf7e54600218eb7b6c8384048c4c
Once this is done, the attacker can simply forge cross chain messages.
- Do not rely on
sighash
to be non-reversible by bruteforce. - Always implement as many restrictions as possible on calls to external contracts. In this case, a restriction should have been made so that cross-chain transactions to the manager are not possible for the public.