Skip to content

RSync: Heap Buffer Overflow, Info Leak, Server Leaks, Path Traversal and Safe links Bypass

High
rcorrea35 published GHSA-p5pg-x43v-mvqj Feb 19, 2025

Package

Rsync (Debian)

Affected versions

3.2.7

Patched versions

3.4.0

Description

Summary

In this report, we describe multiple vulnerabilities we discovered in Rsync.

The first pair of vulnerabilities are a Heap Buffer Overflow and an Info Leak. When combined, they allow a client to execute arbitrary code on the machine a Rsync server is running on. The client only requires anonymous read-access to the server.

We developed a reliable Proof-of-Concept exploit for the following Binary release:

Distro: Debian 12
Rsync version: 3.2.7
Binary MD5: 003765c378f66a4ac14a4f7ee43f7132
Invocation:Rsync --daemon

The two vulnerabilities reside in code that is reachable in any configuration and should thus be exploitable in any binary that includes the vulnerable code.

Additionally, we disclose 3 further vulnerabilities:

A Path Traversal issue in the client that allows a malicious server to exfiltrate the contents of any file on the client’s machine
A bypass for the --safe-links CLI flag that can allow a malicious server to place unsafe symbolic links in a clients directory
A Path Traversal issue in the client that allows a malicious server to overwrite arbitrary files on the client's machine when either the -l and the -l isn't formatted ( but the subsequent -a and --archive are).

Severity

High - Multiple vulnerabilities impacting Rsync ranging from moderate (6.5) to Critical (9.8) CVSS scores which could allow a client to execute arbitrary code.

Proof of Concept

CVE-2024-12084 - Heap Buffer Overflow in Checksum Parsing

When the checksums are read by the daemon, two different checksums read:

  1. A 32-bit Adler-CRC32 Checksum
  2. A digest of the file chunk. The digest algorithm is determined at the beginning of the protocol negotiation.
    The corresponding code can be seen below:
    sender.c
        s->sums = new_array(struct sum_buf, s->count);

        for (i = 0; i < s->count; i++) {
                s->sums[i].sum1 = read_int(f);
                read_buf(f, s->sums[i].sum2, s->s2length);

Most importantly, note that sum2 field is filled with s->s2length bytes. sum2 always has a size of 16:
Rsync.h

#define SUM_LENGTH 16
// . . .
struct sum_buf {
        OFF_T offset;           /**< offset in file of this chunk */
        int32 len;              /**< length of chunk of file */
        uint32 sum1;            /**< simple checksum */
        int32 chain;            /**< next hash-table collision */
        short flags;            /**< flag bits */
        char sum2[SUM_LENGTH];  /**< checksum  */
};

s2length is an attacker-controlled value and can have a value up to MAX_DIGEST_LEN bytes, as the next snipper shows:

io.c

        sum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f);
        if (sum->s2length < 0 || sum->s2length > MAX_DIGEST_LEN) {
                rprintf(FERROR, "Invalid checksum length %d [%s]\n",
                        sum->s2length, who_am_i());
                exit_cleanup(RERR_PROTOCOL);
        }

The problem here is that MAX_DIGEST_LEN can be larger than 16 bytes, depending on the digest support the binary was compiled with:

md-defines.h

#define MD4_DIGEST_LEN 16
#define MD5_DIGEST_LEN 16
#if defined SHA512_DIGEST_LENGTH
#define MAX_DIGEST_LEN SHA512_DIGEST_LENGTH
#elif defined SHA256_DIGEST_LENGTH
#define MAX_DIGEST_LEN SHA256_DIGEST_LENGTH
#elif defined SHA_DIGEST_LENGTH
#define MAX_DIGEST_LEN SHA_DIGEST_LENGTH
#else
#define MAX_DIGEST_LEN MD5_DIGEST_LEN
#endif

SHA256 support is common and sets the MAX_DIGEST_LENGTH value to 64. As a result, an attacker can write up to 48 bytes past the sum2 buffer limit.
This was the case for the following Ubuntu and Debian latest releases:
The Heap Buffer overflow was introduced with commit ae16850.

CVE-2024-12085 - Info Leak via uninitialized Stack contents defeats ASLR

The daemon matches checksums of chunks the client sent to the server against the local file contents in hash_search(). Part of the function prologue is to allocate a buffer on the stack of MAX_DIGEST_LEN bytes:
match.c

static void hash_search(int f,struct sum_struct *s,
                        struct map_struct *buf, OFF_T len)
{
        OFF_T offset, aligned_offset, end;
        int32 k, want_i, aligned_i, backup;
        char sum2[MAX_DIGEST_LEN];

The daemon then iterates over the checksums the client sent and generates a digest for each of the chunks and compares them to the remote digest:

                        if (!done_csum2) {
                                map = (schar *)map_ptr(buf,offset,l);
                                get_checksum2((char *)map,l,sum2);
                                done_csum2 = 1;
                        }

                        if (memcmp(sum2,s->sums[i].sum2,s->s2length) != 0) {
                                false_alarms++;
                                continue;
                        }

Notably, the number of bytes that are compared again are s2length bytes. In this case, the comparison does not go out of bounds since s2length can be a maximum of MAX_DIGEST_LEN.
However, the local sum2 buffer, not to be confused with the attacker-controlled s->sums[i].sum2, is a buffer on the stack that is not cleared and thus contains uninitialized stack contents.
A malicious client can send a (known) xxhash64 checksum for a given chunk of a file, which leads to the daemon writing 8 bytes to the stack buffer sum2. The attacker can then set s2length to 9 bytes. The result of such a setup would be that the first 8 bytes match and an attacker-controlled 9th byte is compared with an unknown value of uninitialized stack data.
An attacker can divide a file into 255 chunks and as a result leak one byte per file download. An attacker can incrementally repeat the process, either in the same connection or by resetting the connection.
As a result, they can leak MAX_DIGEST_LEN - 8 bytes of uninitialized stack data, which can contain pointers to Heap objects, Stack cookies, local variables and pointers to global variables and return pointers. With those pointers they can defeat ASLR.

CVE-2024-12086 - Server leaks arbitrary client files

When a client connects to a malicious server the server is able to leak the contents of an arbitrary file on the client’s machine.
In read_ndx_and_attrs the client will read fnamecmp type as well as the xname from the server if the server sets the appropriate flags. The flag sanitize_paths will not be set for the client.

        if (iflags & ITEM_BASIS_TYPE_FOLLOWS)
                fnamecmp_type = read_byte(f_in);
        *type_ptr = fnamecmp_type;

        if (iflags & ITEM_XNAME_FOLLOWS) {
                if ((len = read_vstring(f_in, buf, MAXPATHLEN)) < 0)
                        exit_cleanup(RERR_PROTOCOL);

                if (sanitize_paths) {
                        sanitize_path(buf, buf, "", 0, SP_DEFAULT);
                        len = strlen(buf);
                }
        } else {
                *buf = '\0';
                len = -1;
        }
        *len_ptr = len;

The caller (recv_files) then uses the server provided values to determine a file to compare the incoming data with.

    case FNAMECMP_FUZZY:
    if (file->dirname) {
        pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname);
        fnamecmp = fnamecmpbuf;
    } else
        fnamecmp = xname;
        break;
…
fd1 = do_open(fnamecmp, O_RDONLY, 0);

In receive_data the contents of the file specified by xname are copied into the destination file. This can be achieved by the server sending a negative token.

    while ((i = recv_token(f_in, &data)) != 0) {
..snip..
            if (i > 0) {
..snip..
            }
..snip..
            if (fd != -1 && map && write_file(fd, 0, offset, map, len) != (int)len)

The server sends a checksum to compare. If they don't match, a 0 is returned.

        if (fd != -1 && memcmp(file_sum1, sender_file_sum, xfer_sum_len) != 0)
                return 0;

When the return value is 0 the receiver will then send a MSG_REDO to the generator. The generator will then write a message to the server.
The server can use this as a signal to determine if the checksum they sent was correct. By starting off with a blength of 1 a malicious server is able to determine the contents of the target file byte by byte.

CVE-2024-12087 - Server can make client write files outside of destination directory using symbolic links

When the syncing of symbolic links is enabled, either through the -l or -a (--archive) flags, a malicious server can make the client write arbitrary files outside of the destination directory.
A malicious server can send the client a file list such as:

symlink ->/arbitrary/directory
symlink/poc.txt

Symbolic links, by default, can be absolute or contain characters such as ../../.
In practice, the client validates the file list and when it sees the symlink/poc.txt entry, it will look for a directory called symlink, otherwise it will error out. If the server sends symlink as a directory and a symlink, it will only keep the directory entry, thus the attack requires some more details to work.
In inc_recurse mode, which the server can enable for the client, the server sends the client multiple file lists. The deduplication of the entries happens on a per-file-list basis. As a result, a malicious server can send a client multiple file lists, where:

# file list 1:
.
./symlink (directory)
./symlink/poc.txt (regular file)

# file list 2:
./symlink -> /arbitrary/path (symlink)

As a result, the symlink directory is created first and symlink/poc.txt is considered a valid entry in the file list. Then, the attacker changes the type of symlink to a symbolic link.
When the server then instructs the client to create the symlink/poc.txt file, it will follow the symbolic link and thus files can be created outside of the destination directory.

CVE-2024-12088 --safe-links Bypass

The --safe-links CLI flag makes the client validate any symbolic links it receives from the server. The desired behavior is that symbolic links target can only be 1) relative to the destination directory and 2) never point outside of the destination directory.
The unsafe_symlink() function is responsible for validating these symbolic links. The function calculates the traversal depth of a symbolic link target, relative to its position within the destination directory.
As an example, the following symbolic link is considered unsafe:
{DESTINATION}/foo -> ../../
As it points outside the destination directory. On the other hand, the following symbolic link is considered safe as it still points within the destination directory:
{DESTINATION}/foo -> a/b/c/d/e/f/../../
This function can be bypassed as it does not consider if the destination of a symbolic link contains other symbolic links in the path. For example, take the following two symbolic links:
{DESTINATION}/a -> .
{DESTINATION}/foo -> a/a/a/a/a/a/../../
In this case, foo would actually point outside the destination directory. However, the unsafe_symlink function assumes that a/ is a directory and that the symbolic link is safe.

Further Analysis

Rsync patches for these vulnerabilities can be found here: https://download.samba.org/pub/rsync/NEWS#3.4.0.

Timeline

Date reported: 10/29/2024
Date fixed: 01/14/2024
Date disclosed: 02/19/2025

Severity

High

CVE ID

CVE-2024-12084

Weaknesses

No CWEs

Credits