timezone |
---|
Asia/Taipei |
- ่ชๆไป็ดน
Software Engineer @google. CTF since 2017. Blog here.
- ไฝ ่ช็บไฝ ๆๅฎๆๆฌๆฌกๆฎ้ ทๅญธ็ฟๅ๏ผ
Can't say yes, but I hope this could get me motivated.
- ่จญๅฎ Foundry ็ฐๅขใhttps://book.getfoundry.sh/getting-started/installation
- ๆ Damn Vulnerable DeFiใEthTaipei CTF 2023 ่ท MetaTrust CTF 2023 ็้ก็ฎๅ
ๆๅ
ไธไพๅ็จใ
- ๆ EthTaipei CTF 2023 ็็ญๆกๅชๆใ
- MetaTrust CTF 2023 repo ่ฆ่ฝๆๆ Foundry ็ๆจกๅผ๏ผๅฐ่งฃ้ก็ๆๅๅๆๅฎๅ่ฝๆ้ๅปใ
็ฎๆจ๏ผๅฎๆ Damn Vulnerable DeFi + EthTaipei CTF 2023 ๅ MetaTrust CTF 2023ใ
Note
2024.09.07๏ผๆ่ฆบๅพ่ฝไธ็้ก่งฃๅๅฎ Damn Vulnerable DeFi ๅทฒ็ถๅพ่ถณๅค ไบ...
Progress
- Damn Vulnerable DeFi (2/18)
- EthTaipei CTF 2023 (0/5)
- MetaTrust CTF 2023 (0/22)
Reference: https://eips.ethereum.org/EIPS/eip-3156
A flash loan is a smart contract transaction in which a lender smart contract lends assets to a borrower smart contract with the condition that the assets are returned, plus an optional fee, before the end of the transaction.
pragma solidity =0.8.25;
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156.sol";
contract ExploitContract is IERC3156FlashBorrower {
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external override returns (bytes32) {
// Take the money from receiver
return keccak256("IERC3156FlashBorrower.onFlashLoan");
}
}
# We can use -vvvv to make the log super-verbose
forge test --match-test test_unstoppable -vvvv
This is what it looks when vault.flashLoan(exploit, address(token), 1e18, bytes("00"));
is called, when my exploit contract is given above.
Time used: ~1h 55m
The goal of the challenge is to pause the vault. One way to trigger is to make the vault.flashLoan
raise an exception -- thus the vault will be stopped when isSolved
is called.
try vault.flashLoan(this, asset, amount, bytes("")) { /* omitted */ } catch { /* pauses the vault here! */ }
// called in src/unstoppable/UnstoppableMonitor.sol:checkFlashLoan(100e18) (line 41)
// called in test/unstoppable/Unstoppable.t.sol:_isSolved() (line 106)
To make this happen, we can make convertToShares(totalSupply) != balanceBefore
. In that's the case, the transaction will be reverted -- and thus raising an exception. We can simply transfer 1 DVT to the vault to halt the contract.
Time used: ~5h 35m
The goal of the challenge is to drain the WETH from the NaiveReceiverPool
and the FlashLoanReceiver
contracts (which initially had 1000 WETH and 10 WETH).
Ideas:
- We can call
pool.flashLoan(receiver, address(weth), 0, bytes(""));
and it will take 1 WETH away fromFlashLoanReceiver
each time. - We can use
BasicForwarder
to make_msgSender()
to be theNaiveReceiverPool
. However, it will always appendrequest.from
(you need its private key). We can bypass by callingMulticall.multicall
fromBasicForwarder.execute
. In that way, the appendedrequest.from
will not be used.
Progress
- Damn Vulnerable DeFi (4/18)
- EthTaipei CTF 2023 (0/5)
- MetaTrust CTF 2023 (0/22)
Time used: ~1h 40m
The goal of the challenge is to drain the token from TrusterLenderPool
. Additionally flashLoan
is protected by the re-entrancy guard.
Function definition for Address.functionCall
: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v5.1/contracts/utils/Address.sol#L62-L64
Since token
is a ERC20 token, why not use the approve
method? This allows our attacking contract to spend money on behalf of the pool, thus we can transfer funds out of the pool afterwards.
References:
- https://medium.com/mr-efacani-teatime/%E9%96%92%E8%81%8A%E5%8A%A0%E5%AF%86%E8%B2%A8%E5%B9%A3%E6%9C%80%E6%99%AE%E9%81%8D%E7%9A%84%E6%94%BB%E6%93%8A%E6%89%8B%E6%B3%95-re-entrancy-attack-ea63e90da7a7
- https://solidity-by-example.org/hacks/re-entrancy/
- https://github.com/pcaversaccio/reentrancy-attacks?tab=readme-ov-file (โญ Collection of re-entrancy attacks)
Time used: ~45m
We create an ExploitContract
to drain the funds from SideEntranceLenderPool
. To start with, we call flashLoan
to get 1000 ETH. We then deposit the money to the pool. Since the pool's balance is unchanged, it is considered repayed. The only difference is, we have 1000 ETH deposited to the pool.
After that, we can simply withdraw the amount to the recovery wallet.
Progress
- Damn Vulnerable DeFi (5/18)
- EthTaipei CTF 2023 (0/5)
- MetaTrust CTF 2023 (0/22)
Time used: ~1h 15m
Important: We are also rewarded. 0x44E97aF4418b7a17AABD8090bEA0A471a366305C
appeared on line 755 in both files. We are the 188th entry.
To (almost) drain the reward distributor, we can repeatedly send the same claim in the same transaction. This is because _setClaimed
will be called once.
Preparation for Damn Vulnerable DeFi: Selfie... Maybe?
- https://www.rareskills.io/post/erc20-votes-erc5805-and-erc6372
- https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#ERC20Votes
Progress
- Damn Vulnerable DeFi (6/18)
- EthTaipei CTF 2023 (0/5)
- MetaTrust CTF 2023 (0/22)
Time used: ~45m
The two lines in README.md corresponds to the first two private keys of the trusted accounts:
0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744
0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159
Therefore, we can control the price by updating the median. Since we have 2 out of 3 trusted accounts compromised, it is easily achievable.
We can follow the procedures to drain the pool:
- Update the price of the NFT to 0.1 ETH and buy it.
- Update the price of the NFT to 999.1 ETH and sell it. We will gain 999 ETH here.
- Update the price of the NFT to 999 ETH, pretending that nothing has happened.
Progress
- Damn Vulnerable DeFi (6/18)
- EthTaipei CTF 2023 (1/5)
- MetaTrust CTF 2023 (0/22)
Time used: ~35m
If we call arcade.changePlayer(address(0x0));
, we can see that the event Transfer
is called before PlayerChanged
. Thus we are able to steal other's account by transferring the current player to the victim.
From Solidity Underhanded Contest 2022, a submission mentioned that the parameters had a bizarre evaluation order:
- the indexed parameters will first be evaluated right-to-left;
- the non-indexed parameters will then be evaluated left-to-right.
In our case, newPlayer
will be evaluated earlier than oldPlayer
in event PlayerChanged(address indexed oldPlayer, address indexed newPlayer);
. Hence, the actual behaviour of the changePlayer
function being:
- sets the current player to
newPlayer
- redeems the current player's (
newPlayer
's) score to the sender (us).
Therefore we are able to steal 190 PRIZE. If we call .earn
and .redeem
before we exploit, we will be able to loot for another 10 PRIZE.
Progress
- Damn Vulnerable DeFi (6/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
Time used: ~1h 15m
Re-entrance attack: During withdraw
, the NFT is first transferred from the pool to our address then decreases the _balances[msg.sender]
. Also onERC721Received
on the receipient's contract will be called.
We can make onERC721Received
to transfer (not deposit) the NFT, then withdraw that immediately. This heuristic should be called only once.
In that case, _balances[msg.sender] -= 1 ether
will be executed twice. For Solidity < 0.8, SafeMath is required to prevent integer overflows -- and it isn't used. Therefore, we eventually have _balances[msg.sender] == uint256(-1 ether)
.
Time used: 5h 30m and ongoing...
https://hackmd.io/@KryptoCampDev/Web3-Proxy-Contract?utm_source=preview-mode&utm_medium=rec#ERC1967
Progress
- Damn Vulnerable DeFi (7/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
Time used: ~6h 20m
The vulnerability comes from ClimberTimelock.execute
. The "ready for execution" check comes after the user-provided functions are called.
The goal is to find a way to make getOperationState(id)
to be OperationState.ReadyForExecution
. Otherwise, the entire call will be reverted and the exploit will be useless.
I thought of something like making keccak256(abi.encode(...))returning the same values of two items, but that would require either (1) hash collision, or (2) ambiguity from
abi.encode`. Of course that wouldn't be (1), and (2) is not doable according to https://ethereum.stackexchange.com/questions/113188/can-abi-encode-receive-different-values-and-return-the-same-result.
Eventually, I created a contract (took me so long to figure out) that enrolls the proper parameters to ClimberTimelock.schedule
. Also, we would need to make the exploit contract an admin/proposer; and to update the delay to zero for immediate action.
With the exploit contract promoted to an admin, we can upgrade ClimberVault
and inject a function to drain the tokens in the vault.
Progress
- Damn Vulnerable DeFi (8/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
- OnlyPwner.xyz (7/16)
Time used: ~45m
A vulnerability here being, when one fills a offer, the price is rounded down. On the other hand, when an offer is cancelled, the price is rounded up.
Additionally, it is possible to fill the first offer with 133 shards for free. Cancelling the offer, one could steal 9.975e-6 DVT. We can buy more shards and generate more benefits afterwards.
Warning
No public writeups allowed, but I finished Freebie (~1h 40m), Tutorial (~5m), Reverse Rugpull (~15m), Under the Flow (~35m) and Please Sign Here (~15m), All or Nothing (~1h 05m), Multisig (~2h 15m).
Progress
- Damn Vulnerable DeFi (8/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
- OnlyPwner.xyz (9/16)
Warning
Finished Proof of Work (~1h 10m), Payday (~1h 10m).
Time used: 2h 20m and ongoing...
Time used: 55m and ongoing...
- https://solidity-by-example.org/hacks/re-entrancy/ (and the other pages)
Time used: 1h25m and ongoing...
Progress
- Damn Vulnerable DeFi (8/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
- OnlyPwner.xyz (10/16)
forge debug --debug src/bridge-takeover/Bridge.sol --sig "voteForNewRoot(bytes)" "0x"
Warning
Finished Bridge Takeover (~2h 15m)... Finally a solve after few days.
Progress
- Damn Vulnerable DeFi (8/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
- OnlyPwner.xyz (11/16)
Warning
Finished 13th Airdrop (~2h 15m).
Time used: 4h 30m and ongoing...
Progress
- Damn Vulnerable DeFi (8/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
- OnlyPwner.xyz (12/16)
Warning
Finished Wrapped Ether (~2h 20m).
I was stuck for 2 hours because of a weird Solidity behaviour on public
vs external
. I still couldn't figure out why...
Time used: 6h 30m and ongoing...
Progress
- Damn Vulnerable DeFi (9/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
- OnlyPwner.xyz (12/16)
- https://medium.com/@jamesowen.dev/what-is-uniswap-v1-v2-v3-v4-563440e0885f
- https://jeiwan.net/posts/programming-defi-uniswap-1/
- https://docs.uniswap.org/contracts/v1/reference/exchange
Time used: ~1h 10m
The goal is to drain the 100,000 DVTs in the lending pool. The deposit to borrow depends on the balance of ETH & DVT in the Uniswap exchange (namely, $\text{ETH}{ex}$ and $\text{DVT}{ex}$). The below expression is the price (in ETH) to borrow 1 DVT:
$$2 \times \frac{\text{ETH}{ex}}{\text{DVT}{ex}}.$$
Additionally, we can call uniswapV1Exchange.tokenToEthSwapOutput
to buy some ETH. Since
In that case, we will make the price (in ETH) to borrow 1 DVT to be
Progress
- Damn Vulnerable DeFi (11/18)
- EthTaipei CTF 2023 (2/5)
- MetaTrust CTF 2023 (0/22)
- OnlyPwner.xyz (12/16)
Time used: ~35m
Puppet v2 is similar to the last challenge, except that
- it is using Uniswap v2, and
- the numbers (e.g. the player balance, the price, etc) are different.
This time, even if we swap all of our 10000 DVTs to ETH, we are unable to "borrow" all of the DVTs using the swapped ETH.
We can instead do the following:
- swap 10000 DVT to ETHs,
- borrow 100000 DVTs,
- swap 100000 DVTs to ETHs,
- borrow 900000 DVTs,
- swap 100000 DVTs from ETHs.
Time used: ~1h 10m
There is an vulnerability that we can buy the NFT even without the token. This is because that in _buyOne
, only msg.value >= priceToPay
is required. Thus we are able to pay 15 ETH to buyMany
to buy all of the NFTs.
However, we only have 0.1 ETH. We can use the "flash loan" provided by Uniswap v2. There is a (WETH, DVT) pair that has sufficient liquidity.
Solved Doju, 8Inch and Tonyallet in BlazCTF 2024.
Solved TonyLend and Cyber Cartel in BlazCTF 2024.