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-кода, мы увеличили кол-во лицензий до бесконечности (при этом совсем не затронув само приложение и не утратив его полную работоспособность).

No comments:

Post a Comment