HackTheBox Business CTF: Vault Of Hope—NotADemocraticElection CTF Writeup

Welcome back to another writeup! In this post, I’ll be walking you through my step-by-step solution to NotADemocraticElection, a blockchain-based challenge from HackTheBox. This challenge dives into the world of smart contracts and decentralized applications, testing not only your technical skills but also your ability to spot logic flaws hidden within Solidity code. If you’ve ever been curious about how vulnerabilities in blockchain smart contracts can be exploited, or how attackers can manipulate elections on-chain, this writeup will break it all down in a clear and approachable way. I’ll explain the thought process, the weaknesses I identified, and how I crafted the exploit to achieve the final flag.

Challenge Description

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?

For this challenge we're given a two solidity files: NotADemocraticElection.sol and Setup.sol .

Setup.sol

Setup contract is responsible for initializing everything for us. Inside the constructor, it creates a fresh instance of the NotADemocraticElection contract, which represents the election system we’re trying to exploit.

At deployment, two political parties are automatically registered:

  • ALF → Automata Liberation Front

  • CIM → Cyborgs Independence Movement

Additionally, the constructor simulates a participant named Satoshi Nakamoto who immediately places a 100 ETH collateral to cast the very first vote. This makes it look like Satoshi is actively backing one of the parties from the start, giving that side an advantage.

Our ultimate objective in this challenge is clearly defined in the isSolved() function. It checks whether the declared winner of the election is the CIM party. In other words, no matter what’s happening under the hood, we need to manipulate the contract in such a way that the Cyborgs Independence Movement emerges victorious.

NotADemocraticElection.sol

Since the contract is already initialized with two political parties, we can ignore the constructor and focus only on the mechanics that drive the voting process.

Core Logic of the Election Contract

  • Collateral deposit → Before voting, a participant must call depositVoteCollateral(). This function binds a name/surname pair to an address and locks in some ETH as collateral. Each identity can only register once.

  • Voting → Once registered, the voter can use vote() to support one of the two parties. Their vote isn’t simply “one account = one vote.” Instead, the weight of the vote is tied to the collateral they provided.

  • Winner check → Every time a vote is cast, checkWinner() verifies if a party has reached the TARGET_VOTES threshold. If so, it sets that party as the official winner.

  • Signature system → To identify voters, the contract generates a signature through getVoterSig(), which combines the name and surname using abi.encodePacked.

Why Brute-Force Voting Doesn’t Work

At first sight, it might look like you could spam vote() calls with minimal collateral to push your chosen party over the finish line. But the challenge designers were a step ahead. Simply limiting voters to one account is not sufficient, because someone could create thousands of wallets. That’s why this election is “not democratic” — it’s collateral-based voting, where ETH acts as the barrier against Sybil attacks and flash loan tricks.

In theory, you could try depositing 1 ETH and looping votes thousands of times, or splitting your deposit into smaller chunks like 0.5 ETH and doubling the number of transactions. But practically, the cost of gas makes this strategy impossible — your ETH balance would run dry long before you could hit the target. This eliminates the “brute-force” approach.

The Real Weakness: Signature Collisions

The interesting part lies in the getVoterSig() function. Instead of hashing the inputs properly, it uses abi.encodePacked to concatenate the voter’s name and surname. This approach is known to cause collisions, meaning two different (name, surname) pairs can end up producing the exact same signature.

And that’s the loophole. By exploiting signature collisions, it becomes possible to forge voter identities or trick the system into reusing collateral. Instead of playing by the intended rules of depositing ETH and casting votes, we can abuse this design flaw to bypass restrictions and push the CIM party to victory.

The vulnerability lies in how the contract identifies voters. It strictly checks the exact spelling of the registered name and surname to ensure that only the account that deposited collateral can vote:

This check prevents direct impersonation, but it doesn’t stop us from registering slightly altered versions of a legitimate voter. For example, we could create new entries like "Satosh iNakamoto" or "Sato shiNakamoto"—these are treated as completely separate voters during registration.

However, when determining voting weight, the contract calls:

Because getVoterSig generates a signature from the concatenated name and surname using abi.encodePacked, small variations in the name can produce a signature that collides with the original "Satoshi Nakamoto" signature. As a result, our spoofed voter entries inherit the vote weight already assigned to the original voter.

By creating multiple such variations—like "Satos hiNakamoto" or "Sato shiNakamoto"—we can repeatedly cast votes using the same underlying weight. Each new vote effectively multiplies the influence of the original voter, allowing us to quickly reach the vote target and ensure that our chosen party, CIM, wins the election.

Exploitation

Note: for this exploitation to work, we will need the ABI version of the NotADemocraticElection.sol .

Here's the Python script that I've made to compile the NotADemocraticElection.sol to ABI:

Run and you should have the ABI version stored in NotADemocraticElection_abi.json .

Now that we have the ABI, it's time to manipulate the system to our advantage and make CIM the winner for this election.

exploit.py

It directly exploits the signature collision weakness in the NotADemocraticElection contract by registering a slightly modified voter name ("SatoshiN akamoto") that collides with the original "Satoshi Nakamoto" signature. It first deposits a minimal vote collateral for this new “fake” voter, which passes the contract’s uniqueness check. Then, it automates multiple vote() calls using the colliding signature, effectively reusing the original voter’s weight for each vote. By repeating this process, the script multiplies the original voter’s influence, allowing the attacker to rapidly accumulate enough votes to make the CIM party win, bypassing the intended protections against multiple voting.

Here's the output:

At this stage, we can reconnect to the interface via netcat and retrieve the flag!

Now CIM is officially winning the celebration right now ^^

In this challenge, we explored a cleverly disguised vulnerability in the NotADemocraticElection smart contract. The contract attempted to secure the election by requiring voters to deposit ETH collateral, ostensibly preventing multiple votes from a single account. However, the system relied on getVoterSig using abi.encodePacked to calculate voter signatures, which is vulnerable to collisions. This allowed us to create slightly altered voter names that technically passed uniqueness checks but shared the same underlying signature, effectively inheriting the original voter’s weight.

By leveraging this weakness, we automated the process of registering fake voters and casting multiple votes using the colliding signatures. This exploit enabled us to bypass the intended safeguards, quickly accumulate enough votes, and ensure that the CIM party reached the target threshold to win.

This challenge highlights a critical lesson in smart contract security: even mechanisms that appear robust, like vote collateral or uniqueness checks, can be subverted if underlying assumptions—such as the uniqueness of abi.encodePacked outputs—are flawed. It serves as a reminder to carefully consider encoding methods, edge cases, and potential attack vectors when designing blockchain applications.

Last updated