Авторские статьи Новая альтернатива Benchmark'y или эффективный blind SQL-injection

Discussion in 'Статьи' started by Elekt, 11 Mar 2007.

  1. Elekt

    Elekt Banned

    Joined:
    5 Dec 2005
    Messages:
    946
    Likes Received:
    427
    Reputations:
    508

    Новая альтернатива Benchmark'y или эффективный blind SQL-injection


    В статье Вам будет показан новый и универсальный способ обходиться без использования benchmark в blind sql-inj
    в так называемых в народе "неюзабельных" бажных запросах как UPDATE, DELETE, REPLACE, UPDATE и прочих.

    Найденный Альтернативный способ позволяет именно ПОЛУЧАТЬ требуемую информацию из базы данных, а не банально модифицировать записи.

    Будут описаны все особенности известных способов атак SQL-inj, применимых к перечисленным выше операторам.

    А также мы подробно рассмотрим аспекты практического использования бенчмарка при написании эксплойтов.

    Примечание: в статье приведены примеры для mysql, однако опиcанное сработает практичеcки на всех субд(с поправкой на синтаксис запросов, разумеется).



    [1] INTRO

    Программисты умнеют, багов в селект-запросах становится всё меньше, а проблема инъекций в
    INSERT,UPDATE,REPLACE,DELETE и прочих остаётся актуальной.
    Часто вижу вопросы на форумах: "..подскажите, что мне делать, если - 'mysql error INSERT INTO table_name VALUES(...'"
    Видя такую ошибку, новичок в хаке закроет браузер, любитель постит сотый вопрос на форумах...
    В ответ как правило тишина, либо советы которыми он не в силах воспользоваться,
    поскольку раскрутка blind sql, ввиду объёма запросов, требует применения автоматизации, то есть умения писать эксплоит.

    Способы давно известны и подробно расписаны 1dt.w0lf ещё за 14 ноября 2004, не читал её только ленивый:
    http://www.securitylab.ru/contest/212099.php
    Данную статью можно считать её продолжением.



    [2] INSERT и другие
    описаны известные атаки на insert\update и тд, кто знает - пропускаем

    Что можно из этого выжать?

    1) (случай общий)Модификация данных без возможности просмотра результата
    Идеальный случай - sql-inj в имени таблицы. Здеcь возможно всё:
    Code:
    INSERT INTO [COLOR=Red][SQL][/COLOR] VALUES ('stat','stat2');
    Добавление нового пользователя:
    Code:
    INSERT INTO [COLOR=Red]mysql.user (user, host, password) VALUES ('newadmin', 'localhost', PASSWORD('passwd'))/*[/COLOR] VALUES ('stat');
    2) DoS-атака
    Но подобные sql-inj в 99% присутствуют в имени столбца, что не позволяют изменить модифицируемую таблицу на полезную нам.
    Code:
    INSERT INTO table VALUES ('stat','[COLOR=Red][SQL][/COLOR]');
    А раз так, то кроме порчи\засорения\dosa бд мы модификацией данных ничего не добьемся.
    SQL:
    Code:
    INSERT INTO table VALUES ('stat','[COLOR=Red]bla'),('test', 'demo[/COLOR]');
    DoS:
    Code:
    INSERT INTO table VALUES ('stat','[COLOR=Red]bla' and BENCHMARK(10000000,BENCHMARK(10000000,md5(now()))) ) /*[/COLOR]');
    3) Разделение запросов в MSSQL, PostgreSql, Oracle
    Что же объединяет эти три популярные субд?
    Такая замечательная вещь, как поддержка разделения запросов, используя точку с запятой, перевод каретки и прочее.
    Таким образом мы можем добавить абсолютно ЛЮБОЙ sql-запрос, отделив его ";" от основного.
    Что можно полезного вставить - ищем в гугле под конкретную бд.

    4) Модификация данных с возможностью просмотра результата
    Здесь уже не важно, где инъекция - в имени таблицы или столбца.
    Мы можем увидеть результат выполнения запроса, пускай и косвенно.
    А значит провести атаку не проблема - чисто технические особенности выяснения результата.
    Пример: пусть веб-приложение ПУБЛИЧНО ведёт статистику посещений и есть уязвимость в столбце с User-Agent.
    Тогда, результат выполнения sql-inj мы можем наблюдать на паге вывода статистики посещаемости.

    5) Использование BENCHMARK
    Мы используем BENCHMARK, что позволяет по времени ответа сервера определить результат выполнения запроса.
    Этот запрос будет для if положителен и ответ от сервера придёт сразу:
    Code:
    INSERT INTO table VALUES ('stat','[COLOR=Red]1' and 1=if(ascii(lower(substring((select users from mysql.user limit 1),1,1)))>=1,1,benchmark(999999,md5(now()))) )/*[/COLOR] ,'stat2');
    А этот - отрицателен и на исполнение бенчмарка уйдёт время, потому ответ от сервера придёт сразу:
    Code:
    INSERT INTO table VALUES ('stat','[color=red]1' and 1=if(ascii(lower(substring((select users from mysql.user limit 1),1,1)))>=254,1,benchmark(999999,md5(now()))) )/*[/color] ,'stat2');
    Разумеется, вместо INSERT может стоять любой другой оператор, в том числе и SELECT, ведь с ним тоже бывают cложные случаи инъекций.



    [3] Bencmark для хакера
    особенности эксплуатации benchmark, кто знает - пропускаем

    Применение бенчмарка это своего рода переход ко второму измерению.
    От измерения разности получаемой информации к измерению разности времени на исполнение запроса.

    Если кто-то сомневается, что под бенчмарк вообще пишут эксплойты - пробегитесь по багтракам и уверьтесь в обратном - они есть, правда не много.

    В реализации сплойт на бенчмарке чуть-чуть сложнее чем для слепого посимвольного брута.

    Особенности benchmark:

    - пожалуй самая важная деталь - бенчмарк создаёт серьёзную нагрузку на процессор сервера.
    Причём эта нагрузка длиться практически постоянно во время работы эксплойта.
    Администратор может не смотреть логи обращений\ошибок, но вполне может поинтересоваться, почему сервер притормаживает и в top->P "mysqld" занимает первое место...

    - время работы эксплойта тем больше, чем большую длину записи мы хотим получить.
    На моей практике для перебора 32-х символьного хеша уходит больше часа.
    Иногда несколько часов - по обстоятельствам, описанным ниже.

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

    - параметр измерения производительности, то есть количество итераций, в нашем примере - 999999
    benchmark(999999,md5(user())
    По личному опыту, с момента написания статьи 1dt.w0lf'ом это число изменилось почти на порядок вместе с ростом производительности современных серверов.
    В эксплойте желательно делать автоподстройку этого параметра для универсальности, добиваясь приемлемого времени ответа....

    - и соответственно, подобрав число в benchmark'e нужно задать таймаут ответа - timeout
    Это будет среднее арифметическое от времени выполнения true\false запроса.

    - Замечу: при написании эксплойта учитывайте, что по статистике общее число "неверных" запросов превышает общее число верных,
    а значит нужно поставить бенчмарк-задержку так, чтобы она срабатывала при ВЕРНОМ ответе. Вот так:
    Code:
    if( ?,  true, false )
    1 = if( 1=1 , benchmark(999999,md5(user()), 1 )
    Тем самым мы сэкономим время и снизим нагрузку на сервер.

    - не забывайте, что серверу необходимо дать отдых после каждого бенчмарка, дать восстановиться так сказать.
    Иначе следующий запрос может иметь непредвиденное время выполнения и даст вам ошибочные данные.
    Об этой маленькой детали часто забывают, поскольку тестят двиг локально, либо непонятно о чём думают, релизя свой код без предварительного тестирования.
    Сам лично долго удивлялся, непонимая, почему первый символ брутится правильно, но дальше идёт мусор - сервер просто давился бенчмарком.
    Желательно выбирать время задержки в расчете на 1-1,5 больше, чем время исполнения бенчмарка.
    Да, это существенно замедлит брут, но обеспечит качество результата.

    Если обратится непосредственно к коду, то символьный брутер 1dt.w0lf'а без труда модифицируется с учетом бенчмарка:
    необходимо

    - изменить sql под бенчмарк
    Code:
    $http_query = $path."?Cat=&page=1&like=".$username."' AND 1=if(ascii(substring(CONCAT(U_LoginName,CHAR(58),U_Password),".$s_num.",1))".$ccheck.",benchmark(999999,md5(user()),1)/*";
    - timeout - таймаут.
    Но вот возвращать sub check($) теперь должна результат в зависимости от того, уложился ли сервер в таймаут, или нет.
    Code:
    $mcb_reguest = LWP::UserAgent->new(timeout=>$timeout) or die;
    - sleep()
    После каждого ложного запроса, а значит подвергнутого пытке бенчмарком, дадим серверу отдохнуть N секунд. пусть восстановится.

    вот и всё.



    [4] Альтернатива benchmark'y
    ну вот, собственно, самое сладкое

    Копаясь с бенчмарком меня как и любого из Вас одолевало желание найти замену этому геморойному, но единственному способу.
    И способ был найден!

    На самом деле разность выводимой инфы и времени - не единственные факторы.

    Давайте вернёмся к началу и вспомним, с чего начинается любая sql-inj?
    Конечно с ковычки в запросе! ...и выводе сообщения ошибки соответственно.
    Но подойдем к этому логически - мы ставим ковычку, mysql проверяет запрос, находит в нём ошибку - сообщает нам.
    Следите за мыслью: мускул сначала проверяет на корректность, затем либо проводит запрос и выводит результат, либо выводит еррор.
    А теперь зададим себе вопрос - может ли быть иначе? Ну, разумеется может, в том то всё и дело.
    Может возникнуть ситуация, когда проверку на корректность запрос пройдет, выполнится,.. но результат даст нам сообщение об ошибке.
    Тогда наша задача сводится к простейшей ПРОВАКАЦИИ такого запроса + необходимо связать параметры в запросе таким образом, чтобы
    мы могли манипулировать результатом запроса и соответственно получать полезные данные.

    Ставим задачу:
    Срыв запроса неявным условием и как следствие - намеренный вызов ошибки, которая и поможет отличить true\false query.

    Эта мысль приходит в голову и я начинаю вспоминать различные ошибки бд.

    Сначала попытался используя IF сорвать запрос путем подстановки неверных имён таблиц\столбцов\типов данных:
    Code:
    select if(1=1,null,blaaaah);
    Однако mysql, сцуко, умный и проверяет тип данных перед выполнением на корректность, что меня обломало.
    You have an error in your SQL syntax
    Попытка изменять имя не столбца, а таблицы также не увенчалась успехом.
    Code:
    select null from if(1=1,users,blaaaah);
    Кстати, видимо алгоритм парсинга запроса в мускуле на корректность предполагает имя таблицы как константы, а здесь она не задана явно.
    Он проверяет существование столбца в заведомо известной таблице и если таблица не определена, например я через if её задаю, то мускул шлёт меня подальше.
    Принцип проверки в других базах данных я не проверял, так что возможно такие простые, а главное, универсальные трюки там прокатят.

    Какие ещё ошибки могут возникнуть при выполнении запроса?
    Самой популярной после "You have an error in your SQL syntax" является пожалуй "The used SELECT statements have a different number of columns"
    Но здесь проверка так же происходит до выполнения.

    "Operand should contain 1 column(s)" - опять же нельзя по той же причине.

    "warning: mysql_fetch_array..." - можно, но php-ошибка специфическая, под конкретный двиг.


    ..а следующая по популярности в моём рейтинге идёт "Subquery returns more than 1 row".... Да, да, он самый, вездесущий лимит.

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

    Встаёт вопрос, как запрос надо задать, чтобы вероятно вызвать такую ошибку?

    Требуется спровоцировать два условия:
    когда ошибка 100% выведется и 100% не выведется.

    Предположим, что нам заведомо известны имена таблиц и столбцов в уязвимом приложении.
    Пусть пароль хешируется md5. Тогда длинна любого пароля = 32 символа.
    А длина id или логина наверняка будет меньше, не правда ли?...
    length(id)=(1-5..) и length(password)=(32)
    Тогда наш эксплоит будет выглядеть следующим образом:

    false:
    Code:
    ...123' and 1=(select null from users where length(if(ascii(substring((select password from users where uid=1),1,1))>254,password,uid))>5)/* 
    Первый символ пароля наверняка меньше чем ascii(254).
    Значит if возвратит length(id)>5, а так как столько пользователей наверняка нет(ну можно циферку и побольше брать), то селект возвратит NULL.
    В результате мы ничего не увидим, будто и нет никакой инъекции.

    true:
    Code:
    ...123' and 1=(select null from users where length(if(ascii(substring((select password from users where uid=1),1,1))>1,password,uid))>5)/*
    Поскольку первый символ пароля наверняка больше ascii(1), то условие истинно
    Значит if возвратит length(password)>5, что приведёт к выводу N результатов в селект запросе.
    Но в условие может быть сравнен только один результат 1=(*),... что спровоцирует ошибку "Subquery returns more than 1 row" !!!

    Как всё просто, не правда ли?

    Уверен, что поискав найдутся и другие подобные ошибки, проявляющиеся именно после выполнения query.


    Особенности "more than 1 row":

    1) важнейшее: стандартная скорость посимвольного брута при полной заменяемости бенчмарка, экономим время и не грузим сервер.

    2) цена этой производительности - масса ерроров в логах mysql, что может привлечь внимание администратора.

    3) единственно, требуется знать некоторые данные, как то - имена столбцов и таблиц, чтобы провоцировать ошибку "more than 1 row"



    Возможно, вы найдете более совершенный и универсальный способ, в отличии от моего. Удачи!



    [5] Links

    Ссылки. Публичные эксплойты, использующие benchmark blind SQL-inj :


    08 марта, 2007 г. PHP-Nuke <= 8.0 Final (INSERT) Blind SQL Injection Exploit (mysql)

    08 марта, 2007 г. PHP-Nuke <= 8.0 Final (HTTP Referers) Remote SQL Injection Exploit

    08 марта, 2007 г. PHP-Nuke <= 8.0 Final (INSERT) Remote SQL Injection Exploit

    21 февраля 2007 г. Blind sql injection attack in INSERT syntax on PHP-nuke <=8.0 Final

    1 декабря 2006 г. Invision Gallery 2.0.7 SQL Injection Vulnerability

    8 сентября 2006 г. PHPFusion <= 6.01.4 extract()/_SERVER[REMOTE_ADDR] sql injection exploit

    25 июля 2006 г. SQL-Injection in Shop-Script PRO & Shop-Script Premium all version

    3 мая 2006 г. sBlog SQL Injection and Path Disclosure Vulnerability

    24 марта 2004 г. MS Analysis v2.0 module for PhpNuke MS Analysis


    10 февраля 2004 г. SQL injection in Php-Nuke 7.1.0

     
    #1 Elekt, 11 Mar 2007
    Last edited by a moderator: 2 Oct 2014
  2. Elekt

    Elekt Banned

    Joined:
    5 Dec 2005
    Messages:
    946
    Likes Received:
    427
    Reputations:
    508
    Сразу скажу, я обещал пару месяцев назад статью для .uT!L!Te.
    Статья будет отдана ему для публикации где-то там.
    Как только он свой езин выпустит, либо по наступлении апреля - я спускаю её в паблик.
     
  3. podkashey

    podkashey С крышкой по жизни!

    Joined:
    18 Jun 2005
    Messages:
    783
    Likes Received:
    351
    Reputations:
    353
    Элект, давай исправляй на UNION, "чтоб не вводить других в заблуждение"(c) Elekt
    гыыг... ;)
     
  4. Elekt

    Elekt Banned

    Joined:
    5 Dec 2005
    Messages:
    946
    Likes Received:
    427
    Reputations:
    508
    езин вышел (http://www.pyccxak.com/goch_1/), а значит статью пора спускать.

    Наслаждайтесь оригиналом - форматирование текста не урезанное, читать приятнее =)
     
    #4 Elekt, 12 Mar 2007
    Last edited by a moderator: 12 Mar 2007
  5. VampiRUS

    VampiRUS Elder - Старейшина

    Joined:
    31 Dec 2005
    Messages:
    219
    Likes Received:
    105
    Reputations:
    57
    статья отличная.
    ток ещё ошибки подправить надо:
    говорилось что
    следовательно
    После каждого Верного запроса, а значит подвергнутого пытке бенчмарком, дадим серверу отдохнуть N секунд. пусть восстановится.
     
  6. Elekt

    Elekt Banned

    Joined:
    5 Dec 2005
    Messages:
    946
    Likes Received:
    427
    Reputations:
    508
    =================================

    Как совершенно верно заметил podkashey мне в асю - "CREATE, DROP" притянуты зауши, так как не держат после себя условий.
    Потому насчет этих функций я прогнал - извините. поправил.
    Ну, расстраиваться собственно неочем - крейт с дропом используются в основном только при инсталяции.

    =================================

    Как совершенно верно заметил товарищ ZaCo,

    Что же объединяет эти три популярные субд?
    Такая замечательная вещь, как поддержка разделения запросов точкой с запятой...


    - "...вообще говоря, ';' не так чтобы и разделитель двух запросов... два запроса можно разделить исполльзуя ЛЮБОЙ разделитель в том числе пробел\перевод каретки табуляция и тд..."

    поправил.

    =================================

    Всего через два часа после публикации алгоритма podkashey нашел более универсальный метод провакации основаный на union.
    select 1 union select 2
    куда это вставить, сами подумайте =)

    а плюсы можно ставить ему же =)

    =================================

    Это сказал ему я =)))
    Спасибо за редактирование и правку грамматики, для меня это было не столь важно.

    На самом деле нет ничего удивительного в том, что актуальность материала начинает доходит до всех спустя некоторое время.
    Материал специфисеский и осознать его полезность сразу может лишь тот, кто не по наслышке знаком с реализацией сабжа.
    Фактически, это маленький прорыв в sql-inj, своего рода полезнейшая фича, без которой было очень неудобно.
     
  7. Elekt

    Elekt Banned

    Joined:
    5 Dec 2005
    Messages:
    946
    Likes Received:
    427
    Reputations:
    508
    пришло на мыло дополнение

     
  8. Elekt

    Elekt Banned

    Joined:
    5 Dec 2005
    Messages:
    946
    Likes Received:
    427
    Reputations:
    508

    Insert, Update, Replace

    При попытке присвоить колонке значение NULL, которая не может быть равна NULL, вызывает ошибку:

    (Column '[column]' cannot be null)

    Пример:
    INSERT INTO table (`a`,`b`,`c`) VALUES ('1',if(1=1,NULL,'2'),'3')

     
  9. Scipio

    Scipio Members of Antichat

    Joined:
    2 Nov 2006
    Messages:
    736
    Likes Received:
    544
    Reputations:
    190
    Можно еще добавть, что с помощью таких выражений возможна инъекция после order by, т.к. в order by тоже возможно использование выражений (вот в limit помоему нельзя)
    возможен вариант:
    SELECT * FROM `table1` order by (select+if(2=1,1,(select 1 union select id)))
    в данном случае выдаст ошибку
    , т.к 2=1 - false
    если подставить выражение 1=1 ошибки не будет
    пример
    SELECT * FROM `table1` order by (select+if(substring(version(),1,1)=4,1,(select 1 union select id)))

    если версия мускула 4 ошибки не будет, если не 4 то будет ошибка. ну и класика жанра:
    SELECT * FROM `table1` order by -id*(substring(version(),1,1)=4)
    если версия мускула 4, то сортировка пойдет по полю ид в обратном порядке, т.к. -id*1 (aka true) будет -id, а -id*0 (ака false) будет 0, ну и с бенчмарком тоже самое
    из всех предложенных вариантов вариант с иф - бенчмарк самый универсальный, но самый долгий
     
    _________________________
  10. ZaCo

    ZaCo Banned

    Joined:
    20 Jun 2005
    Messages:
    757
    Likes Received:
    336
    Reputations:
    215
    статья стоящая, жаль что была проигнорирована большинством.

    добавлю к сообщению Scipio - твой способ подходит исключительно для операции select, предлагаю вариант, который может использоваться в любых логических условиях и без использования подзапросов:

    Code:
    select username from users 
    where "x" regexp concat("x{1,25", if(@@version<>5, "5}", "6}")) /*в случае else строка выражения выйдет за максимальный предел квантификатора*/
    
    естественно методов "провокации" в регекспах довольно много, но достаточно только одного.
     
  11. ShAnKaR

    ShAnKaR Пачка маргарина

    Joined:
    14 Jul 2005
    Messages:
    917
    Likes Received:
    297
    Reputations:
    553
    чето у меня не выдает ошибки ни как если вместо @@version использую колонку
    типа:
    просто пустое значение возвращает (
    что я делаю не так?
     
  12. oppa

    oppa New Member

    Joined:
    22 Jan 2009
    Messages:
    5
    Likes Received:
    1
    Reputations:
    0
    если подставить выражение 1=1 ошибки не будет
    пример
    SELECT * FROM `table1` order by (select+if(substring(version(),1,1)=4,1,(select 1 union select id)))
    если версия мускула 4, то сортировка пойдет по полю ид в обратном порядке, т.к. -id*1 (aka true) будет -id, а -id*0 (ака false) будет 0, ну и с бенчмарком тоже самое
    из всех предложенных вариантов вариант с иф - бенчмарк самый универсальный, но самый долгий
     
  13. Corwin

    Corwin Elder - Старейшина

    Joined:
    1 Aug 2008
    Messages:
    22
    Likes Received:
    15
    Reputations:
    0
    некоторые альтернативные запросы вызывающие more1row показаны в докладе(слайд 25) http://www.slideshare.net/sumsid1234/owasp-au-rev4?type=powerpoint
     
  14. Qwazar

    Qwazar Elder - Старейшина

    Joined:
    2 Jun 2005
    Messages:
    1,007
    Likes Received:
    908
    Reputations:
    587
    Неработает это дополнение.
     
  15. geezer.code

    geezer.code Elder - Старейшина

    Joined:
    22 Jan 2007
    Messages:
    567
    Likes Received:
    358
    Reputations:
    90
    ну как бы работает, или ты не об этом ?
    Code:
    mysql> select 1 from information_schema.tables where (select 1,2,3)=1;
    ERROR 1241 (21000): Operand should contain 3 column(s)
     
    1 person likes this.
  16. Qwazar

    Qwazar Elder - Старейшина

    Joined:
    2 Jun 2005
    Messages:
    1,007
    Likes Received:
    908
    Reputations:
    587
    Дык суть то не в том, чтобы ошибка нарисовалась, а в том чтобы она в if'е сработала. А эта ошибка отображается всегда, и в случае true и в случае false.
     
  17. [Raz0r]

    [Raz0r] Elder - Старейшина

    Joined:
    25 Feb 2007
    Messages:
    433
    Likes Received:
    485
    Reputations:
    295
    В продолжение темы насчет ошибки "Operand should contain n column(s)": http://websec.wordpress.com/2009/11/26/mysql-table-and-column-names-update-2/

    В посте идет речь о новом способе получения имен колонок, если недоступна БД information_schema. Сначала необходимо определить количество колонок с помощью способа, который упоминался выше:
    Code:
    ' AND (SELECT * FROM USER_TABLE) = (1)-- -
    -> Operand should contain 7 column(s)
    Теперь используя UNION и 1%0 можно извлечь имена колонок через ошибку:
    Code:
    ' AND (1,2,3,4,5,6,7) = (SELECT * FROM USER_TABLE UNION SELECT 1%0,2,3,4,5,6,7 LIMIT 1)-- -
    -> Column 'usr_u_id' cannot be null
    Последовательно перебирая колонки, можно получить имя каждой, если она имеет параметр NOT NULL. Кроме того, как заметил автор, этот баг был исправлен в MySQL 5.1.
    По запросу видно, что будет работать только с MySQL >= 4.1, хотя и это надо проверить.
     
    7 people like this.
  18. Root-access

    Root-access Elder - Старейшина

    Joined:
    18 Jun 2008
    Messages:
    198
    Likes Received:
    195
    Reputations:
    91
    Кстати, если взглянуть на старый метод с benchmark'ом, я не очень понимаю, зачем надо было использовать именно его, ведь можно использовать конструкцию с функцией sleep().
    Code:
    IF(1=2, 1, SLEEP(1))
    Так нагрузки не будет на сервер.
     
    1 person likes this.
  19. [Raz0r]

    [Raz0r] Elder - Старейшина

    Joined:
    25 Feb 2007
    Messages:
    433
    Likes Received:
    485
    Reputations:
    295
    SLEEP() появился в MySQL 5, поэтому его не всегда можно использовать
     
    1 person likes this.
Loading...