개요

Java 에서 안전하지 않은 역직렬화 취약점 예제를 쉽게 설명한 블로그가 있었다. 요점을 정리해본다.

상세

다음과 같은 코드가 있다고 하자.

User코드

  • User 클래스는 readObject 함수를 재정의했다. 이 함수는 name 값을 실행하는 코드가 왜인지(?) 들어가 있다! 취약하다.

import java.io.IOException;
import java.io.Serializable;


public class User implements Serializable{

	private String name;
	private String password;
	private String email;
	private int age;
	
	public User(String name, String password, String email, int age){
		this.name = name;
		this.password = password;
		this.email = email;
		this.age = age;
	}
	
	public String toString(){
		return "(" + name +", " + password + ", " + email + ", " + age + ")"; 
	}
	
	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 getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	
	private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
		stream.defaultReadObject();
		Runtime.getRuntime().exec(this.name);
	}

}

User 역직렬화 코드

User를 역직렬화하는 코드다. User의 name에 calc라는 값을 저장한 후에 역직렬화를 시도하면 어떻게 될까?



import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;


public class UserDeserTest {

	private static final String path = "user.ser"; //프로젝트 루트에 저장됨. 
	
	public static void serialize(List<User> users){
		try{
			FileOutputStream fos = new FileOutputStream(path);
			BufferedOutputStream bos = new BufferedOutputStream(fos);
			ObjectOutputStream out = new ObjectOutputStream(bos);
			
			for(User user: users) {
				out.writeObject(user);
			}
			
			out.writeObject(users);
			out.close();
			System.out.println("직렬화 완료");
		}catch(Exception e){
			e.printStackTrace();
		}
	}
	
	public static void deserialize(){
		try {
			FileInputStream fis = new FileInputStream(path);
			BufferedInputStream bis = new BufferedInputStream(fis);
			ObjectInputStream in = new ObjectInputStream(bis);
			
			User u1 = (User) in.readObject();
			User u2 = (User) in.readObject();
			ArrayList list = (ArrayList) in.readObject();
			
			System.out.println(u1.toString());
			System.out.println(u2.toString());
			System.out.println("count : " + list.size());
			System.out.println(list.toString());
			System.out.println("역직렬화완료");
			in.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		User u1 = new User("calc", "1234", "ldd@naver.com", 30);
		User u2 = new User("User2", "5678", "teser@gmail.com", 25);
		ArrayList list =new ArrayList<>();
		list.add(u1);
		list.add(u2);
		
		serialize(list);
		deserialize();
	}
	
	
}

실행결과

readObject가 역직렬화과정에서 자동으로 호출되어(매직메서드) 계산기가 실행된다. 아주 단순화한 것이지만 역직렬화과정에서 자동으로 실행되는 매직메서드를 공략하면 공격이 가능하다는 것을 느낄 수 있다.

참고

경로가 다른 클래스를 역직렬화시킬 수 있을까?

내용이 동일한 클래스라도 경로가 다르면 역직렬화할 수 없다. 역직렬화하려고 하면 다음과 같은 에러가 발생한다.

java.lang.ClassCastException: class evil.User cannot be cast to class User (evil.User and User are in unnamed module of loader 'app')
	at UserDeserTest.serialize(UserDeserTest.java:23)
	at UserDeserTest.main(UserDeserTest.java:63)
java.io.EOFException
	at java.base/java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2926)
	at java.base/java.io.ObjectInputStream$BlockDataInputStream.readShort(ObjectInputStream.java:3421)
	at java.base/java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:959)
	at java.base/java.io.ObjectInputStream.<init>(ObjectInputStream.java:397)
	at UserDeserTest.deserialize(UserDeserTest.java:39)
	at UserDeserTest.main(UserDeserTest.java:64)

OAST 체크를 하고 싶은 경우

다음 코드를 삽입해서 Burp의 Collaborator서버와 통신시킬 수 있다.

		String url = "https://2svr1jmg0fvhi8ei4e0glnvcy34uskg9.oastify.com";
		var client = HttpClient.newHttpClient(); //java 11
		var request = HttpRequest.newBuilder(URI.create(url))
				.GET()
				.build(); 
		HttpResponse<String> res = client.send(request, HttpResponse.BodyHandlers.ofString());
		// System.out.println(res);
	}

참고

  • https://medium.com/@dub-flow/deserialization-what-the-heck-actually-is-a-gadget-chain-1ea35e32df69