TALOS-2025-2223 || Cisco Talos情报组 - 全面威胁情报
Talos漏洞报告
TALOS-2025-2223
OpenPLC OpenPLC_v3 ModbusTCP服务器拒绝服务漏洞
2025年10月7日
CVE编号
CVE-2025-53476
概述
OpenPLC _v3 a931181e8b81e36fadf7b74d5cba99b73c3f6d58版本的ModbusTCP服务器功能中存在拒绝服务漏洞。通过特制的一系列网络连接,可导致服务器无法处理后续Modbus请求。攻击者可通过建立一系列TCP连接来触发此漏洞。
确认受影响的版本
以下版本经过Talos测试或验证,或由供应商确认为受影响版本:
OpenPLC _v3 a931181e8b81e36fadf7b74d5cba99b73c3f6d58
产品URL
OpenPLC_v3 - https://github.com/thiagoralves/OpenPLC_v3
CVSSv3评分
5.3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
CWE
CWE-775 - 有效生命周期后未释放文件描述符或句柄
详细分析
OpenPLC是一种开源可编程逻辑控制器(PLC),旨在为自动化提供低成本解决方案。该平台由两部分组成:运行时和编辑器。运行时可以部署在多种平台上,包括Windows、Linux和各种微控制器。OpenPLC的常见用途包括家庭自动化和工业安全研究。OpenPLC支持多种协议的通信,包括Modbus和EtherNet/IP。运行时还提供对通过EtherNet/IP传输的PCCC的有限支持。
OpenPLC处理Modbus连接时存在资源耗尽拒绝服务状况。通过创建并放弃许多并发会话,然后无限期保持一个最终连接打开,可能耗尽服务器进程可用的所有文件描述符并中断新连接。
OpenPLC处理Modbus消息的过程始于server.cpp中的startServer函数,其中无限循环在定义的端口(默认为TCP/502)上等待新连接。当客户端尝试连接时,waitForClient函数通过accept建立连接,从而为连接分配套接字文件描述符([1])。随后该连接被设置为阻塞模式([2])。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int waitForClient(int socket_fd, int protocol_type)
{
char log_msg[1000];
int client_fd;
struct sockaddr_in client_addr;
...
while (*run_server)
{
client_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &client_len); // [1]
if (client_fd > 0)
{
SetSocketBlockingEnabled(client_fd, true); // [2]
break;
}
sleepms(100);
}
return client_fd;
}
|
当连接成功建立后,将创建调用handleConnections函数的新线程来处理新流([3]),然后主线程的执行循环返回等待下一个连接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
void startServer(uint16_t port, int protocol_type)
{
...
while(*run_server)
{
client_fd = waitForClient(socket_fd, protocol_type);
...
else
{
int arguments[2];
pthread_t thread;
int ret = -1;
sprintf(log_msg, "Server: Client accepted! Creating thread for the new client ID: %d...\n", client_fd);
log(log_msg);
arguments[0] = client_fd;
arguments[1] = protocol_type;
ret = pthread_create(&thread, NULL, handleConnections, (void*)arguments); // [3]
if (ret==0)
{
pthread_detach(thread);
}
...
|
在handleConnections内部,Modbus消息在循环中处理,在等待客户端消息([4])、处理该消息以及相应响应或退出([5])之间循环。
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
|
void *handleConnections(void *arguments)
{
...
while(*run_server)
{
//unsigned char buffer[NET_BUFFER_SIZE];
//int messageSize;
if (protocol_type == MODBUS_PROTOCOL)
{
messageSize = readModbusMessage(client_fd, buffer, sizeof(buffer) / sizeof(buffer[0])); // [4]
}
...
if (messageSize <= 0 || messageSize > NET_BUFFER_SIZE)
{
// 出现问题或客户端已关闭连接
if (messageSize == 0)
{
sprintf(log_msg, "Modbus Server: client ID: %d has closed the connection\n", client_fd);
log(log_msg);
}
else
{
sprintf(log_msg, "Modbus Server: Something is wrong with the client ID: %d message Size : %i\n", client_fd, messageSize);
log(log_msg);
}
break;
}
processMessage(buffer, messageSize, client_fd, protocol_type); // [5]
}
...
|
readModbusMessage使用read调用([6]和[7])从当前流中摄取消息数据并返回读取的总字节数。如果客户端在此read调用执行后但在接收到任何数据之前关闭连接,由于先前的配置([2]),线程将无限期阻塞。
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
|
int readModbusMessage(int fd, unsigned char *buffer, size_t bufferSize)
{
int messageSize = 0;
// 读取modbus TCP/IP ADU帧头直到长度字段。
#define MODBUS_HEADER_SIZE 6
if (bufferSize < MODBUS_HEADER_SIZE)
{
return -1;
}
do
{
int bytesRead = read(fd, buffer + messageSize, MODBUS_HEADER_SIZE - messageSize); // [6]
if (bytesRead <= 0)
{
return bytesRead;
}
messageSize += bytesRead;
} while (messageSize < MODBUS_HEADER_SIZE);
// 读取长度(字节5和6)。
uint16_t length = ((uint16_t)buffer[4] << 8) | buffer[5];
size_t totalMessageSize = MODBUS_HEADER_SIZE + length;
if (totalMessageSize > bufferSize)
{
return -1;
}
// 读取消息的其余部分。
while (messageSize < totalMessageSize)
{
int bytesRead = read(fd, buffer + messageSize, totalMessageSize - messageSize); // [7]
if (bytesRead <= 0)
{
return bytesRead;
}
messageSize += bytesRead;
}
return messageSize;
}
|
了解这一点后,客户端可以通过打开大量连接但从不发送初始请求来占用OpenPLC进程可用的所有套接字文件描述符。当此操作成功时,主机系统将显示大量处于CLOSE_WAIT状态的套接字(如下所示),表明连接已由客户端关闭但仍由服务器保持打开。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
user@machine:~/src/OpenPLC_v3$ sudo netstat -tpn | grep ":502" | wc -l
1003
user@machine:~/src/OpenPLC_v3$ sudo netstat -tpn | grep ":502" | head
tcp 1 0 10.211.55.20:502 10.211.55.40:50778 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:43992 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:46760 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:50816 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:46516 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:42970 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:43936 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:37884 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:47258 CLOSE_WAIT 491897/./core/openp
tcp 1 0 10.211.55.20:502 10.211.55.40:56744 CLOSE_WAIT 491897/./core/openp
user@machine:~/src/OpenPLC_v3$
|
此状态还反映在OpenPLC日志中(如下所示),表明分配的文件描述符已增加到接近给定进程允许的最大打开文件数(在Ubuntu上默认为1024,可通过ulimit -n验证)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
...
Server: Client accepted! Creating thread for the new client ID: 1023...
Server: waiting for new client...
Server: Thread created for client ID: 1023
Modbus Server: client ID: 1023 has closed the connection
Terminating Modbus connections thread
Server: Client accepted! Creating thread for the new client ID: 1023...
Server: waiting for new client...
Server: Thread created for client ID: 1023
Modbus Server: client ID: 1023 has closed the connection
Terminating Modbus connections thread
Server: Client accepted! Creating thread for the new client ID: 1023...
Server: waiting for new client...
Server: Thread created for client ID: 1023
...
|
此时,服务器基本上已被限制为一次处理一个连接,而不是同时处理多个连接。通过向服务器打开一个最终连接(这次也从客户端端处于阻塞模式)并立即进行recv调用而不是发送Modbus请求,OpenPLC将在readModbusMessage中阻塞,等待永远不会到来的请求。由于进程的最大打开文件数将已达到,其他线程上的任何新连接将无法可靠处理。
时间线
- 2025年7月16日 - 供应商披露
- 2025年7月16日 - 供应商响应
- 2025年9月23日 - 状态更新请求
- 2025年9月23日 - 供应商响应
- 2025年9月24日 - 响应确认
- 2025年10月7日 - 公开发布
致谢
由Cisco Talos成员发现。