The Rabbit Sends in a Little Contract
- Category: Blockchain
- 300 Points
- Solved by the JCTF Team
Description
0x046ca747d8472d2f8c070655aed06b841215d4b3
Solution
This is the contract we got and it looks like we need to pass the flag as bytes and pass the verifier conditions.
/**
*Submitted for verification at Etherscan.io on 2022-12-11
*/
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes calldata flag) external returns(bool);
}
contract CheckFlag {
IVerifier _verifier;
constructor(address verifier) {
_verifier = IVerifier(verifier);
}
function check(bytes calldata flag) payable external returns(bool){
require(msg.value > 13333333333333333337 ether, "Please pay rabbit hole entrance fee");
require(flag.length == 18);
require(uint256(keccak256(abi.encodePacked(flag[:7], flag[17]))) == 49459084011290387902369587151867275004690538990200813105748590866129266398873);
return _verifier.verify(flag[7:17]);
}
}
I'm using https://emn178.github.io/online-tools/keccak_256.html to calculate keccak256
The flag is 18 bytes long and the first contract trim the first 7 seven bytes and the last one so we can guess it is INTENT{
and }
abi.encodePacked(flag[:7], flag[17]) = "INTENT{}"
keccak256(abi.encodePacked(flag[:7], flag[17])) = "6d58d97a386ee471a3357f7561e3659cdd5e5cd80d0b698a0afa88f7a58bd699"
uint256(keccak256(abi.encodePacked(flag[:7], flag[17]))) = 49459084011290387902369587151867275004690538990200813105748590866129266398873
The verifier passed to the constructor:
000000000000000000000000726afed38eaa7aab9540c0b4b4adc66c1fb14a41
-----Decoded View---------------
Arg [0] : verifier (address): 0x726aFED38Eaa7aaB9540c0b4b4ADc66c1Fb14a41
-----Encoded View---------------
1 Constructor Arguments found :
Arg [0] : 000000000000000000000000726afed38eaa7aab9540c0b4b4adc66c1fb14a41
The flag [7:17] bytes are passed to the verifier.
Verifier 1 0x726aFED38Eaa7aaB9540c0b4b4ADc66c1Fb14a41
/**
*Submitted for verification at Etherscan.io on 2022-12-11
*/
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier1 {
IVerifier _verifier;
uint value = 0x72;
constructor(address verifier) {
_verifier = IVerifier(verifier);
}
function verify(bytes memory flag) external returns(bool){
require(uint(uint8(flag[0])) == value);
return _verifier.verify(flag);
}
}
flag[0] = 0x72 = 'r'
Trimmed Flag = r[X][X][X][X][X][X][X][X][X]
Verifier 2 0x207197Bd280527E0ef590E46C6BBEBb5AeF66094
/**
*Submitted for verification at Etherscan.io on 2022-12-11
*/
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier2 {
address public owner;
IVerifier _verifier;
uint _value;
constructor(address verifier) {
owner = msg.sender;
_verifier = IVerifier(verifier);
}
function verify(bytes memory flag) external returns(bool){
require(uint(uint8(flag[1])) == _value);
return _verifier.verify(flag);
}
function setValue (uint value) external{
require(msg.sender == owner);
_value = value;
}
}
The flag stored in value of the contract so we can see the transaction 0xe0f9a294f62146eb1170e9a6426d70d4e6e522f36533db98d067a3c269e3cdfd
Function: setValue(uint256 value)
MethodID: 0x55241077
[0]: 0000000000000000000000000000000000000000000000000000000000000034
flag[1] = 0x34 = '4'
Trimmed Flag = r4[X][X][X][X][X][X][X][X]
Verifier 3 0xAaD84ecaD496f819f20F06DD6EC34b8d2dE270cc
/**
*Submitted for verification at Etherscan.io on 2022-12-11
*/
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier3 {
address public alice;
IVerifier _verifier;
address _target;
uint value = 0x66;
constructor(address verifier, address target) {
_verifier = IVerifier(verifier);
_target = target;
}
function verify(bytes memory flag) external returns(bool){
uint size = getSize(_target);
require(uint(uint8(flag[2])) == size-265);
return _verifier.verify(flag);
}
function getSize(address _addr) internal returns (uint) {
uint32 size;
assembly {
size := extcodesize(_addr)
}
return size;
}
}
The address was set in the constructor to 0x9e69f292F83ED4145ed87EA21f55BefF37342781
We got the extcode size using python:
infura_url='https://mainnet.infura.io/v3/TOKEN'
w3 = Web3(Web3.HTTPProvider(infura_url))
#Check Connection
print(w3.isConnected())
print(len(w3.eth.get_code("0x9e69f292F83ED4145ed87EA21f55BefF37342781")))
Reault: 363
size = extcodesize(_addr) = 363
flag[2] = size - 265 = 363 - 265 = 0x62 = 'b'
Trimmed Flag = r4b[X][X][X][X][X][X][X]
Verifier 4 0x10a29fCA0D0661f8bCe563EB8beC5e0BCf529101
We need to decompile the code and understand it so we will brute-force it later :)
The same address created all the contracts so we can see Verifier 5 in 0x9ddf5b332cd24d62807b3e2e7362792897fc387c
Verifier 5 0xcfddc063dd95ffaf4e2f32e6fbc9e49a02dec0ae
/**
*Submitted for verification at Etherscan.io on 2022-12-11
*/
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier5 {
address public owner;
address _verifier;
address _impl;
uint[79] public values = [126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48]; //FLAG
uint index;
constructor(address verifier, address implementation) {
owner = msg.sender;
_verifier = verifier;
_impl = implementation;
}
function verify(bytes memory flag) external returns(bool){
(bool success, bytes memory returnData) = _impl.delegatecall(
abi.encodeWithSignature("verify(bytes)", flag)
);
return abi.decode(returnData, (bool));
}
function setIndex (uint i) external{
require(msg.sender == owner);
index = i;
}
}
It is just calling the verifier implementation that passed to the constuctor 0x2B5F0F6a2492Fd99691e6513D96826E263989E21
When contract A executes delegatecall to contract B, B's code is executed with contract A's storage, msg.sender and msg.value.
/**
*Submitted for verification at Etherscan.io on 2022-12-11
*/
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract ImplVerifier5 {
address public owner;
IVerifier _verifier;
address magic = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
uint[79] public values = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126];
uint index = 0;
constructor(address verifier) {
owner = msg.sender;
_verifier = IVerifier(verifier);
}
function verify(bytes memory flag) external returns(bool){
require(uint(uint8(flag[4])) == values[index+10]);
return _verifier.verify(flag);
}
function setIndex (uint i) external{
require(msg.sender == owner);
index = i;
}
}
The index of Verifier 5 was set in this transaction to 0 0xc221f790e5947955d7b2c94b142639d362821ff2fffe20cb599a386266096320
flag[4] = values[index+10] = values[10] = 116 = "t"
Trimmed Flag = r4b[X]t[X][X][X][X][X]
Verifier 6 0x0C24243F50d67f3E96ac30EB74eCe6Be8feA8EE1
/**
*Submitted for verification at Etherscan.io on 2022-12-11
*/
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier6 {
address public alice;
IVerifier _verifier;
uint8 value = 0x89 + 0x66;
constructor(address verifier) {
_verifier = IVerifier(verifier);
}
function verify(bytes memory flag) external returns(bool){
uint8 mod = 0x74;
value += mod;
require(uint(uint8(flag[5])) == value);
return _verifier.verify(flag);
}
}
flag[5] = (0x89 + 0x66 + 0x74) % 256 = 99 = "c"
Trimmed Flag = r4b[X]tc[X][X][X][X]
Verifier 7 0xc3b47aA7c213279DECd826243F98D25eaf8Ffacb
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier7 {
address public alice;
IVerifier _verifier;
IERC20 _rhol;
uint value = 0xFF;
constructor(address verifier, address token) {
_verifier = IVerifier(verifier);
_rhol = IERC20(token);
}
function verify(bytes memory flag) external returns(bool){
value = _rhol.totalSupply() / 10**18;
value += _rhol.balanceOf(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
value = value - 1331;
require(uint(uint8(flag[6])) == value);
return _verifier.verify(flag);
}
}
We need to get the totalSupply and balance of 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
in token 0x0f117cE0915a7e4aFff87f180c87491ce3bf4E49
infura_url='https://mainnet.infura.io/v3/TOKEN'
w3 = Web3(Web3.HTTPProvider(infura_url))
#Check Connection
print(w3.isConnected())
abi=json.loads('[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]')
address="0x0f117cE0915a7e4aFff87f180c87491ce3bf4E49"
address=Web3.toChecksumAddress(address)
contract=w3.eth.contract(address=address, abi=abi)
totalSupply=contract.functions.totalSupply().call()
print(f"totalSupply={totalSupply}")
balanceOf=contract.functions.balanceOf("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").call()
print(f"balanceOf={balanceOf}")
Result:
totalSupply=1337000000000000000042
balanceOf=42
flag[6] = (1337000000000000000042 / 10**18) + 42 - 1331 = "0"
Trimmed Flag = r4b[X]tc0[X][X][X]
Verifier 8 0x639D7b5a3cbc0338660f445e08D961e4bcfBd9c1
/**
*Submitted for verification at Etherscan.io on 2022-12-11
*/
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier8 {
address public alice;
IVerifier _verifier;
constructor(address verifier) {
_verifier = IVerifier(verifier);
}
uint value = 0x1F;
function verify(bytes memory flag) external returns(bool){
require(uint(uint8(flag[7])) == value ^ uint(uint8(msg.data[1])));
return _verifier.verify(flag);
}
}
msg.data[1] is the second byte of the method id https://ethereum.stackexchange.com/a/50616
The method id is 0x8e760afe as we can see in writeContract
msg.data[1] = 0x76
flag[7] = 0x1F ^ msg.data[1] = 0x1F ^ 0x76 = 0x69 = "i"
Trimmed Flag = r4b[X]tc0i[X][X]
Verifier 9 0xCE43F918E590B5A4f47B30574069a94F2473693b
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier9 {
address public alice;
address _verifier;
uint mod = 0x49;
mapping(string => uint256[]) tea;
constructor(address verifier) {
_verifier = verifier;
tea["green"] = [0x41,0x42,0x43,0x44,0x45];
tea["black"] = [0x46,0x47,0x48,0x49,0x4A];
tea["oolong"] = [0x4B,0x4C,0x4D,0x4E,0x4F];
tea["masala"] = [0x50,0x51,0x52,0x53,0x54];
tea["earlgrey"] = [0x55,0x56,0x57,0x58,0x59];
tea["white"] = [0x5A,0x5B,0x5C,0x5D,0x5E];
tea["ginger"] = [0x5F,0x60,0x61,0x62,0x63];
tea["mint"] = [0x64,0x65,0x66,0x67,0x68];
tea["lemon"] = [0x69,0x6A,0x6B,0x6C,0x6D];
tea["chamomile"] = [0x6E,0x6F,0x70,0x71,0x72];
tea["hibiscus"] = [0x73,0x74,0x75,0x76,0x77];
tea["rooibos"] = [0x78,0x79,0x7A,0x7B,0x7C];
}
function verify(bytes memory flag) external returns(bool){
uint value;
uint slot;
slot = uint256(keccak256(abi.encode(3326828573661424032217781112562256063166426064292145799638376177296211491616)));
assembly {
value := sload(slot)
}
require(uint(uint8(flag[8])) == value);
_verifier.call(
abi.encodeWithSignature("verify(bytes )", flag)
);
return true;
}
}
We will brute-force it later :)
Verifier 10 0x44aC5B57C74FC129c10DCbd1d0F6c30e179Ced9a
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
interface IVerifier {
function verify(bytes memory flag) external returns(bool);
}
contract Verifier10 {
address public alice;
IVerifier _verifier;
uint value = 0x55;
constructor(address verifier) {
_verifier = IVerifier(verifier);
}
function verify(bytes memory flag) external returns(bool){
require(uint(uint8(flag[9])) == value);
return _verifier.verify(flag);
}
function verify1(bytes memory flag) external returns(bool){
require(uint(uint8(flag[9])) == 0x55);
return _verifier.verify(flag);
}
function verify2(bytes memory flag) external returns(bool){
require(uint(uint8(flag[9])) == 0x44);
return _verifier.verify(flag);
}
function verify3(bytes memory flag) external returns(bool){
require(uint(uint8(flag[9])) == 0x33);
return _verifier.verify(flag);
}
function verify4(bytes memory flag) external returns(bool){
require(uint(uint8(flag[9])) == 0x61);
return _verifier.verify(flag);
}
function verify5(bytes memory flag) external returns(bool){
require(uint(uint8(flag[9])) == 0x38);
return _verifier.verify(flag);
}
function verify6(bytes memory flag) external returns(bool){
require(uint(uint8(flag[9])) == 0x70);
return _verifier.verify(flag);
}
fallback() external {
bytes memory flag = abi.decode(msg.data[4:], (bytes));
require(uint(uint8(flag[9])) == 0x35);
}
}
In order to get the last char we need to see how Verifier 9 called this verifier
_verifier.call(
abi.encodeWithSignature("verify(bytes )", flag)
);
There is no matching function in verifier 10 because of the whitespace after the bytes, so it will get to the fallback()
flag[9] = 0x35 = "5"
Trimmed Flag = r4b[X]tc0i[X]5
Let brute-force the 2 missing chars
we will brute-force it from the last unsolved verifier in order to get valid verifier on success
Verifier 9 (brute-force)
abi=json.loads('[{"inputs":[{"internalType":"address","name":"verifier","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"alice","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"flag","type":"bytes"}],"name":"verify","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]')
address="0xCE43F918E590B5A4f47B30574069a94F2473693b"
address=Web3.toChecksumAddress(address)
contract=w3.eth.contract(address=address, abi=abi)
found = False
for f9 in [0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77, 0x78,0x79,0x7A,0x7B,0x7C]:
try:
res=contract.functions.verify(b"r4b" + b"\xFF" + b"tc0i" + f9.to_bytes(1, byteorder="little") + b"5").call()
print(res)
found = True
except:
pass
if found:
exit()
Result: 110 = "n"
Trimmed Flag = r4b[X]tc0in5
Verifier 4 (brute-force)
We will do it using the Verifier 1 because we know it's abi and we got the rest of the flag
abi=json.loads('[{"inputs":[{"internalType":"address","name":"verifier","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes","name":"flag","type":"bytes"}],"name":"verify","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]')
address="0x726aFED38Eaa7aaB9540c0b4b4ADc66c1Fb14a41"
address=Web3.toChecksumAddress(address)
contract=w3.eth.contract(address=address, abi=abi)
found = False
for f4 in range(0x30, 0x7F):
try:
res=contract.functions.verify(b"r4b" + f4.to_bytes(1, byteorder="little") + b"tc0in5").call()
print(f4)
found = True
except:
pass
if found:
exit()
Result: 49 = "1"
Trimmed Flag = r4b1tc0in5
The full flag is INTENT{r4b1tc0in5}