**security-research** Public
# RSync: Heap Buffer Overflow, Info Leak, Server Leaks, Path Traversal and Safe links Bypass
## Package
## Affected versions
## Patched versions
## 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 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; /**s2length = protocol_version s2length 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:
“`
#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)) 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