개발 메모장

[보안] Replay Attack 정의 및 대응 본문

보안

[보안] Replay Attack 정의 및 대응

yyyyMMdd 2024. 1. 16. 13:10
728x90

#. Replay Attack 이란?

  • 공격자가 이전에 기록된 쿠키 및 세션 정보를 가로채서 악의적으로 재전송하는 네트워크 공격의 한 형태입니다.

  • 재생 공격의 목표는 유효한 데이터나 명령이 합법적이고 새로 생성된 것처럼 하여 시스템을 속이며 이러한 유형의 공격은 일부 시스템이 동일한 데이터의 반복 또는 중복 전송으로부터 적절하게 보호하지 못한다는 사실을 이용합니다.
  • 사용자의 쿠키 및 세션 인증 정보를 재사용하여 웹 서비스를 별도의 로그인 없이 이용이 가능한 것을 의미합니다.

  • 즉, 로그인한 사용자 정보를 쿠키 또는 세션 방식으로 저장하여 인증 확인 시 사용하는데 악성 사용자가 임의의 사용자의 쿠키 및 세션 정보를 가로채 로그인 인증을 우회할 수 있게 됩니다.

#. 대응 방안

 

  1. 사용자 인증 및 권한을 체크를 위해 사용하는 값(ID, Auth Code 등)을 GET/POST로 사용한 사용자 입력으로 처리하거나 쿠키에 저장하지 않아야 합니다.

  2. 입력 값이 사용자 아이디와 같은 파라미터가 들어오거나 쿠키에 사용자 아이디가 암호화되지 않은 평문으로 저장되어 있으면 조작이 가능하기 때문에 공격자가 원하는 아이디로 조작이 가능해집니다. 그렇기에 기본적으로 세션으로 인증 정보를 저장하고 세션 생성 시 클라이언트 IP를 세션 정보에 추가한 뒤 암호화하여 특정 사용자가 서로 다른 IP에서 로그인할 수 없도록 처리해야 합니다.

  3. 암호화를 한 후 암호화된 값의 변경 유무를 확인할 수 있도록 설정하는 것이 좋습니다.

  4. 세션 정보의 만료 날짜를 짧게 설정하고 영구적인 값은 설정하지 않는 것이 좋습니다. 또한, 로그아웃 후에는 세션 ID를 invalidate 처리해야 합니다.

#. 처리 방법

 

  • 위 대응 방안 중 2번을 이용하여 처리하였고 처리방법은 생각보다 간단합니다.

  • 모든 페이지를 관리하는 Interceptor에 로직을 추가하여 쿠키를 이용해 어떠한 페이지를 접속하여도 걸러질 수 있게 처리하였습니다.
import java.nio.charset.StandardCharsets;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class TestInterceptor extends HandlerInterceptorAdaptor {
	private static final String AES_ALGORITHM = "AES";
	private static final String SECRET_KEY = "testSecKey"; // 16바이트 이하

	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		HttpSession session = request.getSession();
		String ip = request.getRemoteAddr();

		if(session.getAttribute("ip") != null) {
			String decIp = decryptIpAddress(session.getAttribute("ip").toString());
			if(ip.equals(decIp)) {
			} else {
			    session.invalidate();
			    response.sendRedirect("/login");
			}
		} else {
		    String encIp = encryptIpAddress(request.getRemoteAddr());
		    session.setAttribute("ip", encIp);
		}
	}

	public static String encryptIpAddress(String ipAddress) {
		try {
			Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
			SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
			cipher.init(Cipher.ENCRYPT_MODE, secretKey);
			byte[] encryptedBytes = cipher.doFinal(ipAddress.getBytes(StandardCharsets.UTF_8));
			return Base64.getEncoder().encodeToString(encryptedBytes);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
	 
	public static String decryptIpAddress(String encryptedIpAddress) {
		try {
			Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
			SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
			cipher.init(Cipher.DECRYPT_MODE, secretKey);
			byte[] encryptedBytes = Base64.getDecoder().decode(encryptedIpAddress);
			byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
			return new String(decryptedBytes, StandardCharsets.UTF_8);
		} catch (Exception e) {
			e.printStackTrace();
			return null; 
		}
	}
}

 

  • 위의 내용을 정리하자면 처음 로그인한 사람의 세션에는 ip란 Attribute가 없습니다.

  • 첫 번째 else로 빠지게 되어 setAttribute를 이용해 암호화한 ip를 세션에 저장합니다.

  • 공격자는 암호화된 ip Attribute가 저장된 쿠키를 이용해 접속하기 때문에 getAttribute("ip") != null에 걸리게 됩니다.

  • 이후 세션에 저장된 암호화 ip를 복호화하여 실제 접속한 ip 값과 비교합니다.

  • 비교해서 다르다면 세션을 invalidate 하고 최초 페이지로 이동시킵니다.

#. 테스트 

 

  • 테스트를 위해 구글 웹스토어에서 editthiscookie를 설치하며 사용은 url입력창 우측에 퍼즐모양을 눌러 사용합니다.

  • 아이피가 다른 클라이언트를 준비합니다.

  • 아래 내용은 적용 전의 내용이며 위 방법을 적용 후 동일하게 처리해 보시면 되겠습니다.

#. 쿠키 복사하기


#. IP가 다른 클라이언트에서도 editthiscookie를 실행하여 복사한 쿠키를 붙여 넣어줍니다.


#. 이후 접속가능한 아무 url을 입력해 줍니다.


#. 이렇게 접속이 가능한 것을 볼 수 있습니다.

 

#. 수정 후 위와 같이 다시 테스트를 해보면 세션은 사라지고 리다이렉트를 통해 login 페이지로 이동하게 됩니다.

 


 

#. 로그인 시 로그인 정보를 DB에 저장하는 경우엔 DB에 ip를 저장한 뒤 이를 꺼내어 사용하는 것이 더 바람직합니다.

 

#. 또한 스프링 시큐리티의 maximumSessions를 이용해 간단히 처리가능합니다.

 

#. 위 내용을 참고하시어 처리하시면 쉽게 처리 가능하실 겁니다.

 

 

 

===========================================================
틀린 내용이 있거나 이견 있으시면 언제든 가감 없이 말씀 부탁드립니다!

===========================================================

728x90