Компания
476,70
рейтинг
29 апреля 2013 в 15:50

Разработка → Валидация запросов ASP.NET: от <?i[a-z!/\?]|&# до XSS Type-1 WAF

«Возможность валидации запросов в ASP.NET спроектирована для выполнения базового контроля входных данных. Она не предназначена для принятия решений, касающихся защищенности разрабатываемых веб-приложений. Только сами разработчики могут определить, какой контент должен обрабатывать их код. Microsoft рекомендует выполнять проверку всех входных данных, получаемых из любых источников. Мы стремимся способствовать разработке защищенных приложений нашими клиентами, и функционал валидации запросов был спроектирован и внедрен для того, чтобы помочь разработчикам в этом направлении. Для получения дополнительной информации о наших рекомендациях по разработке, читайте статью MSDN: msdn.microsoft.com/en-us/library/ff649487.aspx#pagguidelines0001_inputdatavalidation».

Официальный статус ASP.NET Request Validation по версии Microsoft Security Response Center

Несмотря на столь внезапный ответ MSRC на недавний отчет исследовательского центра Quotium об обнаружении очередного способа обхода валидации запросов в ASP.NET, стоит заметить, что она все же предназначена именно для принятия решений, касающихся защищенности веб-приложения. В пользу этого говорит и название класса, реализующего основной набор проверок (System.Web.CrossSiteScriptingValidation) и сама его суть, заключающаяся в предотвращении некоторого подмножества атак класса XSS Type-1 (отраженное выполнение межсайтовых сценариев), и оригинальная статья от разработчиков веб-стека. Другой вопрос, насколько эффективно мог бы быть реализован этот функционал и как из имеющегося примитивного регулярного фильтра получить полноценный web application firewall, защищающий от любых векторов XSS Type-1?

Чтобы ответить на этот вопрос, необходимо разобраться в деталях реализации валидации запросов в различных версиях платформы .NET Framework, ее ограничениях, известных способах обхода и возможностях расширения ее функционала.

1. Эволюция валидации запросов ASP.NET


Во всех версиях ASP.NET (совпадающих с версиями платформы .NET Framework), начиная с v1.1 и заканчивая v4.5, валидация запросов сводится к поиску в различных элементах HTTP-запроса вхождений цепочек регулярного множества, которое описывает черный список опасных значений. С точки зрения кодирования, ее осуществляет распознающий автомат, реализованный из соображений производительности вручную, без использования стандартных регулярных выражений. Множество опасных значений содержит элементы языка HTML, которые могут нарушить целостность выходного документа, если будут использованы в нем без достаточной предварительной обработки.

Впервые, механизм валидации запросов был реализован в ASP.NET v1.1 и использовал достаточно широкий черный список. Блокировка обработки запроса осуществлялась в том случае, если какие-либо параметры строки запроса или значения полей форм соответствовали любому из регулярных выражений:

  • <?i[a-z!/]
  • (?i:script)\s?\:
  • (?i:on[a-z])*\s*=
  • (?i:ex)pression\(

Нет ничего удивительного в том, что разработчики предпочитали полностью отключать эту возможность из-за большого числа ложных срабатываний. Поэтому в ASP.NET v2.0 множество опасных значений было сильно сокращено и дошло до v4.5 в неизмененном виде:

  • #&
  • <?i[a-z!/\?]

На этапе подготовки ASP.NET v4.0 разработчики также заявляли о том, что множество (?i:script)\s?: будет возвращено в список, однако этого не произошло ни в v4.0, ни в v4.5.

От версии к версии изменялось не только множество опасных значений, но также область валидации и доступные разработчикам возможности по контролю этого процесса. Так, в v2.0 появилась возможность отключать валидацию запросов для отдельных страниц, а в v4.0 был введен новый режим т.н. отложенной гранулярной валидации, в котором проверка каждого из параметров осуществляется при обращении к нему из кода веб-приложения, а не на этапе предварительной обработки запроса. Начиная с v4.0 в область валидации, помимо параметров строки запроса и значений полей форм, входят также:

  • значения всех элементов из Request.Cookies;
  • имена загруженных файлов из Request.Files;
  • значения Request.RawUrl, Request.Path и Request.PathInfo

2. Валидация запросов в ASP.NET v4.x


В последних версиях ASP.NET, в рамках валидации запроса, осуществляется также ряд дополнительных проверок, которые выполняются на самых ранних этапах его жизненного цикла. Их полный список приведен в таблице:
ПРОВЕРКА НАСТРОЙКИ И ЗНАЧЕНИЯ
Проверка длины Request.Path Атрибут maxUrlLength в секции <httpRuntime>. Может определяться как глобально для всего приложения, так и для отдельных виртуальных путей или страниц.

Блокирует обработку HTTP-запроса, содержащего путь длиннее 260 символов. Это значение может быть увеличено до пределов, определенных в IIS или http.sys.
Проверка длины фрагмента Request.RawUrl, содержащего строку запроса Атрибут maxQueryStringLength в секции <httpRuntime>. Может определяться как глобально для всего приложения, так и для отдельных виртуальных путей или страниц.

Блокирует обработку HTTP-запроса, содержащего строку запроса длиннее 2048 символов. Это значение может быть увеличено до пределов, IIS или http.sys.
Сканирование Request.Path на предмет наличия в нем символов, определенных в ASP.NET как потенциально опасные Атрибут requestPathInvalidCharacters в секции <httpRuntime>. Может определяться как глобально для всего приложения, так и для отдельных виртуальных путей или страниц.

Блокирует обработку HTTP-запроса, если путь в нем содержит любой из символов:
  • < (атаки XSS)
  • > (атаки XSS)
  • * (атаки на канонизацию файловых имен)
  • % (атаки на URL-декодер)
  • : (атаки на альтернативные потоки данных NTFS)
  • & (атаки на парсер строки запроса)
  • \ (атаки на канонизацию файловых путей)
  • ? (атаки на парсер строки запроса)

В атрибуте requestPathInvalidCharacters запрещенные символы обрамляются двойными кавычками и разделяются запятыми.

Последовательность символов прохода пути "\.." не включена в данный список в связи с тем, что IIS v6+ автоматически осуществляет канонизацию URI, корректно обрабатывая такие последовательности. На практике, ошибок связанных с появлением в пути символов прямого слеша также не возникает, т.к. в процессе канонизации они заменяются обратными.
Поиск подходящей управляемой конфигурации для каждого значения Request.Path Атрибут relaxedUrlToFileSystemMapping в секции <httpRuntime>. Может определяться только глобально для всего приложения.

По умолчанию этот атрибут установлен в false, что предписывает ASP.NET рассматривать составляющую пути в URL как корректный файловый путь, соответствующий правилам NTFS. Это ограничение можно отключить установкой значения атрибута в true.
Проверка Request.QueryString, Request.Form, Request.Files, Request.Cookies, Request.Path, Request.PathInfo, Request.RawUrl на потенциально опасные значения Атрибут requestValidationMode в секции <httpRuntime>. Может определяться как глобально для всего приложения, так и для отдельных виртуальных путей или страниц.

Устанавливает режим, в котором будет осуществляться валидация запросов к веб-приложению. Значение 4.0 (по умолчанию) включает отложенную гранулярную валидацию, которая осуществляется при непосредственном доступе кода веб-приложения к элементам из области валидации. Установка этого атрибута в значение 2.0 возвращает режим валидации, используемый в предыдущих версиях ASP.NET.

Атрибут requestValidationType в секции <httpRuntime>. Может определяться только глобально для всего приложения.

Устанавливает тип наследника класса RequestValidator, реализующего функционал валидации запросов. По умолчанию используется класс System.Web.Util.RequestValidator.
Последняя проверка как раз и является той видимой частью айсберга, называемой валидацией запросов, и доступной разработчикам веб-приложений для расширения ее функционала.

3. Внутреннее устройство валидации запросов


Исходный код метода IsValidRequestString класса System.Web.Util.RequestValidator, используемого по умолчанию для валидации запросов в ASP.NET v2.0+, выглядит примерно так:

protected internal virtual bool IsValidRequestString(
    HttpContext context,
    string value,
    RequestValidationSource requestValidationSource,
    string collectionKey,
    out int validationFailureIndex)
{
    if (requestValidationSource == RequestValidationSource.Headers)
    {
        validationFailureIndex = 0;
        return true;
    }
    return !CrossSiteScriptingValidation.IsDangerousString(value, out validationFailureIndex);
}

Следует отметить тот факт, что еще до вызова метода IsValidRequestString из строки, передаваемой в параметре value, вырезаются все вхождения нулевого байта. Это поведение реализовано в методе ValidateString класса HttpRequest и не может быть переопределено разработчиком.

Как видно из исходного кода, основной функционал валидации запросов реализован в методе IsDangerousString класса CrossSiteScriptingValidation:

internal static bool IsDangerousString(string s, out int matchIndex)
{
    matchIndex = 0;
    int startIndex = 0;
    while (true)
    {
        int num2 = s.IndexOfAny(startingChars, startIndex);
        if (num2 < 0)
        {
            return false;
        }
        if (num2 == (s.Length - 1))
        {
            return false;
        }
        matchIndex = num2;
        char ch = s[num2];
        if (ch != '&')
        {
            if ((ch == '<') && ((IsAtoZ(s[num2 + 1]) || (s[num2 + 1] == '!')) || ((s[num2 + 1] == '/') || (s[num2 + 1] == '?'))))
            {
                return true;
            }
        }
        else if (s[num2 + 1] == '#')
        {
            return true;
        }
        startIndex = num2 + 1;
    }
}

Очевидно, что данный фильтр представляет собой автомат, распознающий в переданной ему строке вхождения цепочек регулярного множества <?i[a-z!/\?]|&#. Кроме того, в классе CrossSiteScriptingValidation определены также два вспомогательных метода, недоступных для расширения или изменения их функционала:


internal static bool IsDangerousUrl(string s)
{
    if (string.IsNullOrEmpty(s))
    {
        return false;
    }
    s = s.Trim();
    int length = s.Length;
    if (((((length > 4) && ((s[0] == 'h') || (s[0] == 'H'))) && ((s[1] == 't') || (s[1] == 'T'))) && (((s[2] == 't') || (s[2] == 'T')) && ((s[3] == 'p') || (s[3] == 'P')))) && ((s[4] == ':') || (((length > 5) && ((s[4] == 's') || (s[4] == 'S'))) && (s[5] == ':'))))
    {
        return false;
    }
    if (s.IndexOf(':') == -1)
    {
        return false;
    }
    return true;
}

internal static bool IsValidJavascriptId(string id)
{
    if (!string.IsNullOrEmpty(id))
    {
        return CodeGenerator.IsValidLanguageIndependentIdentifier(id);
    }
    return true;
}

Первый осуществляет проверку URL и считает опасными все значения, не удовлетворяющие множеству ^(?i:https?:)|[^:]. Второй проверяет значение аргумента на соответствие правилу грамматики для идентификаторов языка: ^(?i:[a-z_][a-z0-9_])$. Оба метода вызывались из IsDangerousString в рамках валидации запросов в ASP.NET v1.1. Во всех остальных версиях они используются лишь в некоторых элементах управления ASP.NET WebForms в качестве методов функциональной проверки и не приводят к возникновению исключения RequestValidationException.

4. Недостатки стандартной реализации и способы ее обхода


Очевидно, что стандартная реализация валидации запросов обладает рядом недостатков, делающих ее действительно непригодной для принятия решений, касающихся защищенности веб-приложения.

Во-первых, рассмотренные проверки способны защитить лишь от ограниченного подмножества атак класса XSS Type-1, требующих открытия тега для их проведения. В том случае, если проведение атаки отраженного XSS возможно в результате внедрения значения параметров внутрь тега, атрибута или кода клиентского сценария, стандартная валидация запросов уже не сможет ее предотвратить.

Во-вторых, контроль по «черному списку», сам по себе, не является достаточной мерой обеспечения защищенности. Именно этим обусловлено наличие нескольких известных способов обхода стандартной валидации запросов:

  • Ограничение на <?i[a-z!/\?] можно обойти, используя знак процента между открывающей угловой скобкой и именем тега (<%img src=# onerror=alert(1)/>). В этом случае, HTML парсер IE v9- будет считать это корректным определением тега. В ряде случаев, если веб-приложение реализует Unicode-канонизацию параметров запроса, возможен также обход с использованием Unicode-wide значений (%uff1c img%20src%3D%23%20onerror%3Dalert%281%29%2f%uff1e).
  • Ограничение на (?i:script)\s?\: и (?i:ex)pression\( обходится с использованием пробельных символов внутри script и между expression и открывающей скобкой (java%09script:alert(1) и expression%09(alert(1))).
  • Ограничение на #& не учитывает существования именованных ссылок на сущности HTML, которые также можно использовать в ряде векторов (javascript%26Tab;:alert(1)). Здесь необходимо также отметить, что стандартная реализация HTML-декодера ASP.NET (HttpUtility.HtmlDecode) «знает» лишь о существовании 253 именованных ссылок на сущности HTML, в то время как в стандарте HTML их определено существенно больше. Это позволяет пробрасывать в выходной документ множество HTML-сущностей, даже если веб-приложение осуществляет HTML-декодирование значений параметров на этапе предварительной обработки входных данных.

Но самым главным недостатком стандартной реализации является этап обработки запроса, на котором выполняется его валидация. Даже при включенном отложенном режиме, не имея информации о содержимом уже сформированного ответного документа, нельзя сделать корректные предположения об опасности того или иного параметра для конкретного класса атак. Например, если параметр, содержащий элементы разметки HTML не попадает в ответ сервера, довольно странно утверждать о его потенциальной опасности с точки зрения XSS Type-1. Это примерно то же самое, что утверждать об опасности значения «SELECT» не владея информацией о том, попадает ли оно в конечном итоге в SQL-запрос. Следуя подобной логике, разработчикам ASP.NET стоило бы также включить в валидацию запроса поиск в его параметрах элементов синтаксиса SQL, прохода путей, выражений XPath и прочих последовательностей символов, характерных для инъекций в какие-либо языки, а не ограничивать себя лишь небольшим подмножеством атак XSS конкретного типа. Разумеется, подобный подход генерирует массу ложноположительных срабатываний, что приводит, как к полному отключению валидации для всего приложения, так и к появлению инструментов, позволяющих сделать это без особых усилий (например, nuget.org/packages/DisableRequestValidation).

Тем не менее, все эти недостатки можно устранить, воспользовавшись возможностью, рассмотренной в следующем разделе.

5. Расширение функционала валидации запросов


Начиная с ASP.NET v4.0, у разработчиков появилась возможность расширять функционал валидации запросов, в т.ч. полностью переопределяя стандартную реализацию. Для того, чтобы это осуществить, достаточно создать наследника класса System.Web.Util.RequestValidator, переопределив в нем метод IsValidRequestString. Этот метод вызывается при необходимости проверки очередного параметра запроса и принимает следующие аргументы:

  • HttpContext context – контекст HTTP-запроса, в рамках которого осуществляется проверка;
  • string value – значение, подлежащее проверке;
  • RequestValidationSource requestValidationSource – источник, которому принадлежит проверяемое значение;
  • string collectionKey – имя проверяемого значения в источнике;
  • out int validationFailureIndex – выходной параметр, содержащий смещение внутри value, начиная с которого был обнаружен опасный символ или -1 в противном случае;

Например, чтобы устранить возможность обхода валидации при помощи сочетания символов <%, можно реализовать следующее расширение:


using System;
using System.Web;
using System.Web.Util;

namespace RequestValidationExtension
{
    public class BypassRequestValidator : RequestValidator
    {
        public BypassRequestValidator() { }

        protected override bool IsValidRequestString(
            HttpContext context, 
            string value, 
            RequestValidationSource requestValidationSource, 
            string collectionKey, 
            out int validationFailureIndex)
        {
            validationFailureIndex = -1;

            for (var i = 0; i < value.Length; i++)
            {
                if (value[i] == '<' && (i < value.Length - 1) && value[i + 1] == '%')
                {
                    validationFailureIndex = i;
                    return false;
                }
            }
            
            return base.IsValidRequestString(
                context, 
                value, 
                requestValidationSource, 
                collectionKey, 
                out validationFailureIndex);
        }
    }
}

После чего, установив в секции httpRuntime конфигурации веб-приложения значение атрибута requestValidationType = «RequestValidationExtension.BypassRequestValidator», получить веб-приложение, защищенное от данного способа обхода валидации.

6. Улучшенная валидация запросов


Используя возможность расширения функционала валидации запросов, вполне реально устранить все существующие проблемы текущей реализации и получить полноценный XSS Type-1 WAF. Для этого проверку значений параметров запроса необходимо осуществлять непосредственно перед отправкой ответа, когда он уже сформирован и его содержимое известно. Это необходимое условие для достоверной оценки влияния параметров запроса на целостность ответа. Однако, архитектура валидации запросов ASP.NET не предоставляет возможности выполнить его в рамках своего фукционала, что приводит к необходимости разбить всю процедуру на три этапа.

Функционал первого этапа реализован непосредственно в расширении стандартной валидации запросов. На этом этапе, каждый проверяемый параметр сопоставляется с множеством ^?i[A-Za-z0-9_]+$ и, в том случае, если сопоставление не прошло, параметр помечается как подлежащий проверке позднее. Таким образом, собирается информация обо всех потенциально опасных параметрах запроса, для которых была запрошена проверка (то есть параметрах, реально использованных в веб-приложении при обработке запроса). Это обеспечивает полную интеграцию с существующей архитектурой валидации запросов, а также избавляет от необходимости подвергать дополнительным проверкам заведомо неопасные параметры.

Второй этап представляет собой выходной фильтр ASP.NET и реализует наблюдение за потоком, в который осуществляется запись всех фрагментов ответа, формируемых на различных стадиях жизненного цикла запроса. Содержимое всего ответа также сохраняется для обработки на третьем этапе.

Третий этап, реализованный в виде HTTP-модуля, обрабатывающего событие EndRequest, осуществляет оценку влияния всех параметров, собранных на первом этапе, на целостность ответа, полученного на втором. В случае выявления нарушения целостности ответа проверка считается проваленной. Оценка влияния параметров запроса на целостность основана на нечетком поиске в ответе так называемых точек вставки — мест, в которых фрагмент ответа приблизительно совпадает со значением любого из параметров. Множество точек вставки образует карту вставок, с использованием которой можно провести куда более обоснованную проверку наличия запрещенных символов в значениях параметров, а также выявить характер их влияния на целостность ответа.

Последняя задача решается с использованием парсеров всех языков, которые могут встретиться в ответе (HTML, JavaScript, CSS). Сопоставление деревьев разбора, полученных в результате парсинга различных фрагментов ответа, с данными из карты вставок дает полную информацию о том, какие из узлов того или иного дерева были внедрены в ответ значением того или иного параметра.

Детальное описание алгоритма проверки
  1. Если в выходном документе содержаться нулевые байты, то проверка провалена.
  2. Для каждого элемента списка параметров P осуществляется нечеткий поиск с порогом 0.75 всех вхождений его значения в текст ответа R. Границы каждого найденного вхождения определяют область вставки. Множество всех областей вставки составляет карту вставок M.
  3. Если M пустое, то проверка считается пройденной.
  4. Для каждого элемента M осуществляется проверка на соответствие его значения регулярному множеству <?i[a-z!/\?%]. Если соответствие выявлено, то проверка провалена.
  5. Выходной текст R разбирается HTML-парсером в дерево R’.
  6. Если в результате парсинга возникли какие-либо ошибки и места их возникновения имеют пересечения с элементами M, то проверка провалена.
  7. Все последующие шаги повторяются для каждого узла N дерева R’, описывающего HTML-тег или комментарий.
  8. Если начальная позиция N в R имеет пересечения с элементами M, то проверка считается проваленной.
  9. Если N описывает HTML-тег, то для каждого его атрибута, положение которого в R имеет пересечение с элементами M, осуществляется проверка по алгоритму, описанному ниже.
  10. Если N описывает тег , то для его значения innerText (коду сценария) осуществляется проверка по алгоритму, описанному ниже.
  11. Если N описывает тег , то для его значения innerText (коду определения стилей) осуществляется проверка по алгоритму, описанному ниже.
  12. Проверка считается пройденной, если она не была провалена на предыдущих шагах.

Алгоритм проверки атрибутов элементов HTML (принимает на вход значение атрибута A, карту вставок M и текст ответа R):

  1. Если начальная позиция A в R имеет пересечение с элементами M, то проверка считается проваленной.
  2. Если положение значения A в R не имеет пересечения с элементами M, то проверка считается пройденной.
  3. Если название A содержится в списке атрибутов-обработчиков событий, то его значение проверяется по алгоритму, описанному ниже.
  4. Если название A совпадает с элементом списка атрибутов ссылочного типа, то выполняются следующие шаги:
    1. Если значение A содержит подстроку "&#" или именованную ссылку на HTML-сущность, то проверка считается проваленной.
    2. Если значение A не содержит ":", то проверка считается пройденной.
    3. Значение A разбирается URI-парсером в объект U.
    4. Если при разборе возникли ошибки, то проверка считается проваленной.
    5. Если U не описывает абсолютный путь, то проверка считается проваленной.
    6. Если U описывает путь со схемой, присутствующей в списке опасных, то проверка считается проваленной.
  5. Если имя A = «style», то его значение проверяется по алгоритму, описанному ниже.

Алгоритм проверки кода клиентских сценариев и значений атрибутов-обработчиков событий (принимает на вход значение Vs, содержащее код проверяемого сценария, и значение Vm элемента множества M, с которым было выявлено пересечение):

  1. Если наибольшая общая подстрока L от Vs и Vm меньше 7, то проверка считается пройденной.
  2. Значение Vs разбирается парсером JavaScript в дерево Vs’
  3. Если при разборе возникли ошибки, то проверка считается проваленной.
  4. Если количество токенов в Vs’ меньше 5 или количество узлов в Vs’ меньше 2, то проверка считается пройденной.
  5. Если значение L целиком является значением одного токена Vt дерева Vs’, то проверка считается пройденной.
  6. JavaScript-декодированное значение Vt подвергается рекурсивной проверке, как если бы оно было текстом ответа, полностью сформированного из параметра Vm.

Алгоритм проверки кода определения стилей абсолютно аналогичен предыдущему, за исключением использования парсера CSS и иных пороговых значений для элементов дерева разбора и наибольшей общей подстроки.

Proof-of-Concept реализация описанного алгоритма доступна на GitHub. На момент подготовки статьи нет известных способов обхода этого фильтра. Проведенные тесты показали отсутствие ощутимого влияния на производительность веб-приложений в случаях, когда запрос не содержит опасных значений и 7-15% замедление формирования ответа в противном случае. С учетом того, что Proof-of-Concept версия использует сторонние парсеры, решающие гораздо более общую задачу, чем это требуется в рамках алгоритма валидации ответа, оптимальная реализация этих компонент позволит достичь производительности, достаточной для уверенного применения решения в продуктивных средах.

7. Выводы


Реализация функционала валидации запросов в текущих версиях ASP.NET малоэффективна и не решает проблему защиты от атак класса XSS Type-1. Тем не менее, ее нынешняя архитектура и возможности расширения позволяют самостоятельно решить данную проблему, используя метод валидации ответа, описанный в данной статье.

Однако не следует забывать о том, что наиболее эффективной защитой от подобных атак являются не сторонние навесные решения, а корректная реализация обработки входных и выходных данных самими разработчиками. И использование Irv или более комплексных продуктов (таких как mod-security for IIS или .NetIDS) не избавляет разработчика от необходимости следовать базовым правилам разработки защищенного кода (например, www.troyhunt.com/2011/12/free-ebook-owasp-top-10-for-net.html или wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines).
Автор: @VladimirKochetkov
Positive Technologies
рейтинг 476,70

Комментарии (3)

  • 0
    Спасибо, очень полезная информация.
  • 0
    А не смотрели встроенный в .Net 4.5 System.Web.Security.AntiXss.AntiXssEncoder?
    • 0
      Да, конечно смотрел и даже использовал) Это перекочевавшая в ASP.NET часть функционала заброшенного WPL. Жаль, что ее рантайм-часть (сломанная в последнем релизе, к слову сказать) пока не перекочевала во фреймворк =/ Средства энкодинга были в ASP.NET и раньше, только не настолько развитые (System.Web.HttpUtility). Кроме того, есть достаточно популярный проект AntiSamy.NET, реализующий несколько иной, но весьма эффективный подход к фильтрации пользовательского ввода/вывода.

      Однако, все эти средства рассчитаны на то, что разработчики будут использовать их в своих проектах. Иными словами, они представляют собой встраиваемые решения. Статья же описывает навесные (внешние) средства защиты потенциально-уязвимых ASP.NET приложений. Разумеется, они менее предпочтительны, чем встраиваемые и являются дополнительной мерой обеспечения защищенности (см. последний абзац).

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка