CardanoPlace

Vacuumlabs CTF

In the past few weeks I started to participate in the Vacuumlabs CTF in which one has to break Cardano Smart Contracts that are written using Aiken. I mainly participated in this CTF to learn about known smart contract exploits, because I have to write a few contracts myself for this page. Each of the following chapters will contain a spoiler block that explains my solution in more detail such as,

this spoiler block.

If you don’t want to know the solution, don’t click on these blocks. Everything outside these is general enough that I think it won’t spoil the solution. The general blocks contain what the main takeaways were for me and how I approached the problem itself.

Content

00 Hello World

This part of the CTF was just to ensure that everything was set up correctly. It also explained the general way to setup the following challenges, such as copying the scripts/player_template.ts to scrips/player.ts and only working in this file to solve the challenges. Solving this challenge only required examining the contract and setting the redeemer to the correct value.

This part was also ment to teach the participants about Lucid. Lucid is a framework that allows us to build transactions in a relatively simple way and convert plutus datums between CBOR and a more readable object representation. It also allows us to query the blockchain by calling the Blockfrost API behind the scenes.

Common lucid functions that I used throughout the CTF:

I think its also good to note here that CBOR.me is very useful to understand the underlying representation of plutus data.

01 Sell NFT

This is the first real challenge that required reading through the contracts source code and thinking of ways to attack vulnerabilities in it. A smart contract that secures an UTxO in aiken has the following form:

validator {
    fn can_spend(datum: MyDatum, redeemer: MyRedeemer, context: ScriptContext) -> Bool {
        ...
    }
}

datum is the datum of a transaction input at the address of the script used in the transaction. The redeemer has to be provided in the transaction for each input at a script address and similar to datum each script input has its own redeemer. context is the script context that contains the plutus visible details of the whole transaction (see stdlib.ScriptContext).

In order to solve this problem you need to make sure you understand that the validator is run for each input. Each input validates if it can be spent by just checking if the price contained in the datum was sent to the address of the seller:

(output.address == datum.seller)? && (lovelace_of(output.value) >= datum.price)?

But what happens if someone is selling more than one NFT, and we buy them in a single transaction? The seller expected to reaceive the sell price of each NFT to be added together and transfered to him, but the validator only checks if enough lovelace were transfered to buy the NFT at the current input. We can use this to our advantage and just transfer the larger of the two sell prices to the sellers address and buy both NFTs for just that amount. This vulnerability is called Double Satisfaction and has been explained in detail in the two posts by the Vacuumlabs Team (Post1, Post2)

02 Vesting

This problem required us to unlock the funds, locked at a vesting contract before the deadline was reached. To exploit the contract one has to read up on how time is represented in smart contracts. The Cardano Docs contain everything needed to solve this problem.

The time of evaluation of an transaction with an Finite upper bound can be anywhere from now until that time. The check in time_elapsed is therefore not checking if the time has elapsed, just that an validity range with an upper bound behind the deadline has been set. We can therefore just create a transaction without waiting and set the upper validity range to a time behind the deadline. This will unlock the funds at the vesting contract without waiting for the vesting deadline to be reached.

Note: For this contract to work as intended the lower_bound has to be used. This forces the transaction to only be valid after the deadline has been reached.

03 Multisig Treasury V1

This problem contained two contracts that interact with one another. The contracts were both quite lengthy but finding the exploit was pretty straight forward:

A big thing every cardano smart contract developer has to know is that the validator is only run for each Input. But anyone is able to create a Transaction that creates UTxOs at the scripts address. To break the contract we just need to create an UTxO at the address of the multisig validator that already contains both required signatories in the attached datum.

04 TipJar

This was the first problem that required us to stop others from using the contract, compared to the previous contracts where we had to explicitly steal/unlock funds we were not intended to unlock. This is not something that a lot of people think of, because they don’t understand why somebody would just want to stop others from using the DApp, but there is a variaty of reasons someone would try to do this. Here are some that come to mind:

When trying to stop others from using the contract, one has to know about all the possible ways a trasaction can fail. Cardano uses a two-phase validation scheme. The first phace checks if the transaction was constructed correctly. This includes checking it agains the network limit parameters and if fee payment is possible. The second check is done by executing the validator scripts for each input and checking if spending is possible for all of the script inputs.

Side-Note: Normally transactions are checked locally before they are sent to the nodes, so the user does not pay a fee or collateral for an impossible transaction.

I approached this problem by trying to create a datum at the script output, that is so big, that adding anything to it would make the transaction too big to be submitted. The protocol parameter controlling this is maxTxSize which is set to 16384 bytes. With some trial and error in the emulator I created a datum that contains a 14323 byte long message. When sombody else is trying to extend this datum any more the transaction size limit will be reached and tipping becomes impossible. This can be prevented by setting a specific message length limit in the contract.

05 Purchase Offer

Solving this problem turned out to be a bit more tricky than the previous ones. On the surface the script looks quite simple, because it only checks three distinct conditions:

  1. Was something sold that I was interested in
  2. Did I receive the asset
  3. Don’t allow more than one purchase offer script input

Only if all of these are fulfilled we can retrieve the locked funds. Can we find a vulnerability in any of these?

When going over the checks I noticed that the third check is not enough as it is implemented. The check is performed by comparing the address of the currently evaluated inpput againts all the inputs of the transaction and checking if there is only one. But there is a big flaw in this check. A cardano address is composed of two parts! The first part is a required payment credential and the second part is an optional staking credential. So we could potentially not find an input for the same script if it has a different staking credential. There are multiple utxos at the location of the script which we are given via gameData.scriptUtxos. In order to find such a pair of utxos that we can exploit with double satisfaction, I grouped the utxos by the owner as mentioned in the datum. And luckily for us we got a match where the stake credential differs and one of the purchase offers wants any token of the desired policyID!

One possible solution to mitigate this attack is to check for other scripts by only checking against the payment credentials of the script address.

06 TipJar V2

Similar to the first TipJar our goal in this challange was to stop others from tipping. After a small chat with Michal from Vacuumlabs I found out that my solution was not the one they intended participants to find. It’s important to note here that I am only writing about the first exploit I found for each task. This does not mean that it is the only exploit.

The contract was modified in such a way that my attack did not work anymore. But I found a way to DoS the app again by just modifying the datum.

I researched quite a bit around and stumbled across the following:

If any definite-length text string inside an indefinite-length text string is invalid, the indefinite-length text string is invalid. Note that this implies that the UTF-8 bytes of a single Unicode code point (scalar value) cannot be spread between chunks: a new chunk of a text string can only be started at a code point boundary. CBOR

This let me to try just a broken UTF-8 sequences. This broke the frontends code which tried to extend the list of messages by first converting the datum to a JS Object which was not possible and the tests pass. Depending of your definition of DoSing the smart contract this might not be a valid solution for this problem, and I would partially agree. But even if only the frontend code of the DApp is broken, most users are no longer able to interact with the contract.

More to come

Back to Table Of Content