Oct 30, 2009

SQL Injection в DocsVision 3.6


Внедрение SQL-кода (англ. SQL injection) — один из распространённых способов взлома сайтов и программ, работающих с базами данных, основанный на внедрении в запрос произвольного SQL-кода.
Внедрение SQL, в зависимости от типа используемой СУБД и условий внедрения, может дать возможность атакующему выполнить произвольный запрос к базе данных (например, прочитать содержимое любых таблиц, удалить, изменить или добавить данные), получить возможность чтения и/или записи локальных файлов и выполнения произвольных команд на атакуемом сервере.

Существует миф, что SQL Injection возможна только в Web-приложениях, но, к несчастью для ВСЕХ разработчиков, внедрение произвольного кода возможно в любом приложении где используется СУБД (так же любая).
В этой статье я хочу показать, что от внедрения SQL-кода не застрахован никто, в тои числе и профессиональные разработчики, пишушие крупные промышленные системы, которые успешно используются на предприятиях. В качестве “жертвы” выступит крупная система документооборота DocsVision 3.6. Опишу суть SQL Injection в DV: допустим у нас официально куплено N-лицензий этой системы документооборота, но мы решили увеличить кол-во этих лицензий путём внедрения своего произвольного кода. Изначально может показаться, что задача практически нерешаемая, так как это комерческий продукт и наверняка разработчики уделили достаточно много времени для его защиты, но…

Запускаем SQL Server Profiler и анализируем все запросы, которые генерит приложение DocsVision, особое внимание уделяем коду, который отсылает приложение при каждом новом подключении пользователя.
И вот, что удалось “поймать”:
1.SELECT COUNT(*) FROM
2.(SELECT DISTINCT [UserID], [ComputerName] FROM [dbo].[dvsys_sessions]) t0
Где [UserID] – идентификатор пользователя, а [ComputerName] – рабочая станция, с которой идёт подключение. Не трудно сделать вывод, что приложение считает кол-во уникальных подключений [UserID]+[ComputerName]. Теперь мы знаем какие значения отслеживает приложение, но, не имея исходного кода, мы не можем поменять текст запроса, который зашит внутри кода, НО мы можем изменить объект к которому идёт обращение, а именно таблицу, которая хранит информацию об этих подключениях: [dbo].[dvsys_sessions].

Смотрим DDL-скрипт этой таблицы:
01.CREATE TABLE [dbo].[dvsys_sessions](
02.    [SessionID] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
03.    [UserID] [uniqueidentifier] NULL,
04.    [LocaleID] [int] NOT NULL,
05.    [LoginTime] [datetime] NOT NULL,
06.    [LastAccessTime] [datetime] NOT NULL,
07.    [ComputerName] [varchar](32) NULL,
08. CONSTRAINT [PK_dvsys_sessions] PRIMARY KEY CLUSTERED
09.(
10.    [SessionID] ASC
11.)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
12.) ON [PRIMARY]
13. 
14.GO

Ну и сразу же возникает идея: обнулять (NULL) поля [UserID] и [ComputerName], но значения всё-таки нужны и просто удалить мы их не можем, для того, чтобы обмануть приложение, создадим в этой таблице 2 наших поля [UserID2] и [ComputerName2], в которые будем дублировать значения из полей [UserID] и [ComputerName], а их сами очищать с помощью DML-триггера. Тогда DDL-скрипт таблицы будет выглядить так:

01.CREATE TABLE [dbo].[dvsys_sessions](
02.    [SessionID] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
03.    [UserID] [uniqueidentifier] NULL,
04.    [LocaleID] [int] NOT NULL,
05.    [LoginTime] [datetime] NOT NULL,
06.    [LastAccessTime] [datetime] NOT NULL,
07.    [ComputerName] [varchar](32) NULL,
08.    [UserID2] [uniqueidentifier] NULL,
09.    [ComputerName2] [varchar](32) NULL,
10. CONSTRAINT [PK_dvsys_sessions] PRIMARY KEY CLUSTERED
11.(
12.    [SessionID] ASC
13.)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
14.) ON [PRIMARY]
15. 
16.GO

А код триггера, который и будет выполнять всю “грязную” работу:

01.CREATE TRIGGER [dbo].[FuckOffConnections] ON [dbo].[dvsys_sessions]
02.FOR INSERT
03.AS
04.Update t1
05.set
06.UserID=null,
07.ComputerName=null,
08.UserID2=t2.UserID,
09.ComputerName2=t2.ComputerName
10.From dvsys_sessions t1 inner join inserted t2 on t1.SessionID=t2.SessionID

Всё, что делает этот триггер-это дублирует записи в наши внедрённые поля, очищая первоисточник. Таким образом, не зависимо от кол-ва подключений к DocsVision, на запрос зашитий в системе всегда будет возвращать одну запись, т.е. система ВСЕГДА будет думать, что используется только одна лицензия.
Но на этом наше SQL-внедрение не закончено, ведь на эту таблицу могут ссылаться другие объекты базы данных, что может привести к сбою в работе. Для поиска всех объектов, которые ссылаются на таблицу [dbo].[dvsys_sessions], выполним запрос:

1.select distinct OBJECT_NAME(id) obj
2.from sys.sysdepends
3.where depid=OBJECT_ID('dbo.dvsys_sessions')
В результате мы получили 4 объекта:
1.dvsys_log_write_message
2.dvsys_session_get_info
3.dvsys_session_list
4.FuckOffConnections

т.к. FuckOffConnections-это наш триггер, то нам достаточно поправить всего 3 объекта (dvsys_log_write_message, dvsys_session_get_info, dvsys_session_list).
На деле изменения затронули всего 3 объекта, причём всего пару строк:

dvsys_log_write_message
001.ALTER PROCEDURE [dbo].[dvsys_log_write_message] (
002.    @UserID AS uniqueidentifier,
003.    @SessionID AS uniqueidentifier,
004.    @Type AS int,
005.    @Operation As int,
006.    @Code AS int,
007.    @TypeID AS uniqueidentifier = NULL,
008.    @ResourceID AS uniqueidentifier = NULL,
009.    @ParentID AS uniqueidentifier = NULL,
010.    @NewResourceID AS uniqueidentifier = NULL,
011.    @ResourceName As nvarchar(512) = NULL,
012.    @Data AS ntext = NULL
013.    )
014.AS
015.BEGIN
016.    SET NOCOUNT ON
017.    DECLARE @ComputerName AS varchar(32)
018.    DECLARE @InternalData AS varchar(128)
019.    SELECT @InternalData = NULL
020. 
021.    -- Retrieve ComputerName
022.    --Оригинальный код
023.    --SELECT @ComputerName = [ComputerName]
024.    --FROM [dbo].[dvsys_sessions] WITH(NOLOCK)
025.    --WHERE [SessionID] = @SessionID
026. 
027.    --Код, который мы внедрили:
028.    SELECT @ComputerName = [ComputerName2]
029.    FROM [dbo].[dvsys_sessions] WITH(NOLOCK)
030.    WHERE [SessionID] = @SessionID
031. 
032.    -- Retrieve LoginTime for Logout operation
033.    IF @Operation = 2
034.    BEGIN
035.        SELECT @InternalData = CONVERT(varchar(128), [LoginTime], 20)
036.        FROM [dbo].[dvsys_sessions] WITH(NOLOCK)
037.        WHERE [SessionID] = @SessionID
038.    END ELSE
039.    -- Get Card description and Card type ID
040.    IF (@Operation >= 3 AND @Operation <= 7) OR @Operation = 20
041.    BEGIN
042.        SELECT @ResourceName = [Description], @TypeID = CardTypeID
043.        FROM [dbo].[dvview_instances] WITH(NOLOCK)
044.        WHERE InstanceID = @ResourceID
045.    END ELSE
046.    -- Get Card description, Card type ID and topic name
047.    IF @Operation = 27
048.    BEGIN
049.        SELECT @ResourceName = [Description], @TypeID = CardTypeID, @InternalData = [Topic]
050.        FROM [dbo].[dvview_instances] WITH(NOLOCK)
051.        WHERE InstanceID = @ResourceID
052.    END ELSE
053.    -- Get InstanceID and description of Card that has row with specified ResourceID in section with specified TypeID
054.    IF (@Operation >= 8 AND @Operation <= 12) OR @Operation = 13 OR @Operation = 19
055.    BEGIN
056.        IF @ParentID IS NULL
057.        BEGIN
058.            DECLARE @GetRowParentID AS nvarchar(256)
059.            SELECT @GetRowParentID = N'SELECT @ParentID = [InstanceID] FROM [dvtable_{' + CONVERT(nvarchar(36), @TypeID) +
060.                N'}] WITH(NOLOCK) WHERE [RowID] = ''{' + CONVERT(nvarchar(36), @ResourceID) + N'}'''
061. 
062.            EXECUTE [dbo].[sp_executesql] @GetRowParentID, N'@ParentID uniqueidentifier OUTPUT', @ParentID OUTPUT
063.        END
064. 
065.        SELECT @ResourceName = [Description]
066.        FROM [dbo].[dvview_instances] WITH(NOLOCK)
067.        WHERE InstanceID = @ParentID
068.    END ELSE
069.    -- Get File name
070.    IF ((@Operation >= 14) AND (@Operation <= 18)) OR (@Operation = 21) OR (@Operation = 24) OR (@Operation = 25)
071.    BEGIN
072.        SELECT @ResourceName = [Name]
073.        FROM [dbo].[dvview_files] WITH(NOLOCK)
074.        WHERE FileID = @ResourceID
075. 
076.        -- Get Destination File name
077.        IF (@Operation = 25)
078.        BEGIN
079.            SELECT @InternalData = [Name]
080.            FROM [dbo].[dvview_files] WITH(NOLOCK)
081.            WHERE FileID = @ParentID
082.        END
083.    END ELSE
084.    -- Get Card description
085.    IF @Operation = 19
086.    BEGIN
087.        SELECT @ResourceName = [Description]
088.        FROM [dbo].[dvview_instances] WITH(NOLOCK)
089.        WHERE InstanceID = @ParentID
090.    END ELSE
091.    -- Get Report alias
092.    IF @Operation = 23
093.    BEGIN
094.        SELECT @ResourceName = [Alias]
095.        FROM [dbo].[dvsys_reports] WITH(NOLOCK)
096.        WHERE ID = @ResourceID
097.    END ELSE
098.    -- Get Card Type alias
099.    IF @Operation = 26
100.    BEGIN
101.        SELECT @ResourceName = [Alias]
102.        FROM [dbo].[dvsys_carddefs] WITH(NOLOCK)
103.        WHERE CardTypeID = @ResourceID
104.    END
105. 
106.    INSERT [dbo].[dvsys_log] WITH(ROWLOCK) (UserID, ComputerName, [Date], Type, Operation, Code, TypeID, ResourceID, ParentID, ResourceName, NewResourceID, Data)
107.    VALUES(@UserID, @ComputerName, GETDATE(), @Type, @Operation, @Code, @TypeID, @ResourceID, @ParentID, @ResourceName, @NewResourceID, ISNULL(@Data, @InternalData))
108.END

dvsys_session_get_info
01.ALTER PROCEDURE [dbo].[dvsys_session_get_info] (
02.    @SessionID AS uniqueidentifier
03.    )
04.AS
05.BEGIN
06.--Оригинальный код
07.--  SELECT t0.UserID, LocaleID, AccountName, ISNULL(ComputerName, '')
08.--  FROM [dbo].[dvsys_sessions] t0 WITH(NOLOCK)
09.--  JOIN [dbo].[dvsys_users] t1 WITH(NOLOCK) ON t0.UserID = t1.UserID
10.--  WHERE (SessionID = @SessionID)
11. 
12.--Внедрённый код
13.    SELECT t0.UserID2 as UserID, LocaleID, AccountName, ISNULL(ComputerName2, '')
14.    FROM [dbo].[dvsys_sessions] t0 WITH(NOLOCK)
15.    JOIN [dbo].[dvsys_users] t1 WITH(NOLOCK) ON t0.UserID2 = t1.UserID
16.    WHERE (SessionID = @SessionID)
17.END

dvsys_session_list
01.ALTER PROCEDURE [dbo].[dvsys_session_list]
02.AS
03.BEGIN
04. 
05.--Оригинальный код
06.--  SELECT SessionID, AccountName, LoginTime, LastAccessTime, ComputerName
07.--  FROM [dbo].[dvsys_sessions] t0 WITH(NOLOCK)
08.--  JOIN [dbo].[dvsys_users] t1 WITH(NOLOCK) ON t0.UserID = t1.UserID
09.--  ORDER BY AccountName ASC, LoginTime DESC
10. 
11.--Внедрённый код
12.    SELECT SessionID, AccountName, LoginTime, LastAccessTime, ComputerName2 as ComputerName
13.    FROM [dbo].[dvsys_sessions] t0 WITH(NOLOCK)
14.    JOIN [dbo].[dvsys_users] t1 WITH(NOLOCK) ON t0.UserID2 = t1.UserID
15.    ORDER BY AccountName ASC, LoginTime DESC
16.END
Вот и всё, за какие-то 30-40 минут, путём внедрения своего SQL-кода, мы увеличили кол-во лицензий до бесконечности (при этом совсем не затронув само приложение и не утратив его полную работоспособность).

Domain Join Account – Minimum Rights

This falls under another one of those items that I have had in my private notes for a while, but can’t remember where I found it. When setting up the account in a ConfigMgr Task Sequence to join the new computer account to the domain, you must give that account rights in order for it to work. It is essentially a service account, so it should only be given the bare minimum rights. What are those rights? You can “Delegate Control” on the OU to the account and only give it “Allow” for the following:

Permission
Apply To
Reset Password
Computer Objects
Validated write to DNS host name
Computer Objects
Validated write to service principal name
Computer Objects
Read/Write Account Restrictions
Computer Objects
Create/Delete Computer Objects
This object and all descendant objects
Hopefully this will help others…and it will make it easier for me to quickly locate the next time I need to set it!

Set SPN for SQL 2005 (SCCM Remote SQL Fix)

I have found many references to issues with a remote SQL server running under a service account around the Internet. This issue only manifests itself if the SMS provider is located on the site server and the SQL server is located remotely running as a service and is running under standard privileges. The most common symptoms are errors in the installation log related to smsrprt.mof and anonymous login; posted here is a great description (http://www.eggheadcafe.com/software/aspnet/30654425/sccm-mixedmode-setup-fai.aspx)
So, here is the problem. If you are running SQL under a standard user service account as you would in a cluster or remote SQL instance the SPN must be registered with the FQDN and it must be registered both with and without the port number. There is a great description of how to do this here: http://msdn2.microsoft.com/en-us/library/ms189585.aspx; but it is related to IIS. I will give you the short version.

Method 1: The “Right” way
  1. Install the Windows 2003 support tools somewhere on a machine in the domain
  2. Login as a Domain Admin
  3. Run  setspn -A MSSQLSvc/ Note YOU MUST USE THE FQDN
  4. Run  setspn -A MSSQLSvc/:   Note YOU MUST USE THE FQDN, and the most common port is 1443
  5. Run setspn -L validate that “servicePrincipalName:” has been set like you expect
  6. Restart the SQL server after AD replication has completed
  7. Run the following query on the SQL server; this MUST return KERBEROS:
    select auth_scheme from sys.dm_exec_connections where session_id=@@spid
Method 2: The “easy” way
In adsiedit grant the service account the ability to write the servicePrincipalName to “SELF”
Taken from: http://support.microsoft.com/kb/319723

    1. Click Start, click Run, type Adsiedit.msc, and then click OK.

    2. In the ADSI Edit snap-in, expand Domain [DomainName], expand DC= RootDomainName, expand CN=Users, right-click CN= AccountName , and then click Properties.
      • DomainName is a placeholder for the name of the domain. 
      • RootDomainName is a placeholder for the name of the root domain. 
      • AccountName is a placeholder for the account that you specify to start the SQL Server service. 
      • If you specify the Local System account to start the SQL Server service, AccountName is a placeholder for the account that you use to log on to Microsoft Windows. 
      • If you specify a domain user account to start the SQL Server service, AccountName is a placeholder for the domain user account. 

    3. In the CN= AccountName Properties dialog box, click the Security tab.
    4. On the Security tab, click Advanced. 
    5. In the Advanced Security Settings dialog box, make sure that SELF is listed under Permission entries.
      • If SELF is not listed, click Add, and then add SELF.

    6. Under Permission entries, click SELF, and then click Edit.
    7. In the Permission Entry dialog box, click the Properties tab. 
    8. On the Properties tab, click This object only in the Apply onto list, and then click to select the check boxes for the following permissions under Permissions:
      • Read servicePrincipalName 
      • Write servicePrincipalName

    9. Click OK two times.

I would love to reference all the posts and blogs and KB’s that I used to come to this but I wouldn’t know where to begin. I would also like to thank my good friend Prabhu Padhi on the SMS team for fielding my call last night and offering his assistance.
Originally posted here by me: http://www.myitforum.com/forums/m_164437/mpage_1/key_/tm.htm#164437