semtax의 개발 일지

톰캣 에서는 어떻게 JSESSIONID 를 만드는 것일까? 본문

개발/Java

톰캣 에서는 어떻게 JSESSIONID 를 만드는 것일까?

semtax 2020. 5. 28. 23:49
반응형

개요

이번 포스팅에서는 톰캣 에서 JSESSIONID를 어떤 방식으로 만드는지에 대해서 포스팅 해보겠습니다.

왜 하필 톰캣인가?

사실 자바 서블릿은 완전한 구현체가 아닌 일종의 표준 내지 가이드 라인입니다.

즉, 껍데기만 존재하고 실제로는 서블릿 컨테이너를 구현하는 각 컨테이너 구현체 마다 실제적인 구현 방법은 전부 다릅니다.

따라서, JSESSIONID 를 만드는 방식도 각 서블릿 컨테이너 구현체 마다 다릅니다.

그리고, 다른 서블릿 컨테이너 구현체도 많지만.. 그래도 가장 유명(?) 하고 가장 오래된 서블릿 컨테이너 구현체가 톰캣이라, 톰캣을 고르게 되었습니다.

그럼 JSESSIONID는 뭐하는데 쓰는 것일까?

먼저, JSESSIONID가 무엇인지 알기 전에 아래와 같은 질문을 해봅시다.

세션은 도대체 무엇인가?

사실 세션은 진짜로 앞뒤 다 떼고 이야기 하면 Key-Value 저장소 즉, 자바의 Map 입니다.

실제로도 세션 구현체 내부에서도 자바의 ConcurrentMap 을 사용해서 세션을 구현하고 있습니다.

그리고 나면 다음 질문도 해볼 수 있습니다.

그렇다면 사용자가 여러명이면 세션도 여러개일텐데, 어떻게 구현 하는가?

사실 이 질문도 간단합니다. 대략적으로 아래와 같은 구조를 가지는 자료구조를 만들면 됩니다.

Map<SessionStorageKey, SessionStorage>

저 위에 있는 SessionStorageKey 의 역할을 하는것이 바로 JSESSIONID 입니다.

결국, JSESSIONID는 서블릿 컨테이너가 있는 웹서버에 접속한 여러 사용자 각각의 세션 공간을 관리하기 위해 만들어진 일종의 키값이라고 보면 됩니다.

코드로 생각해보면 대략적으로 아래 구조라고 생각하시면 됩니다.

Map<JSESSIONID, Map<AttrubiteKey, Value>>

이제, 톰캣에서 JSESSIONID를 어떻게 만드는지 알아보도록 합시다.

톰캣에서는 어떻게 JSESSIONID 를 만들까?

그럼 이제 톰캣(Tomcat) 에서 JSESSIONID 를 어떻게 만드는지 알아보도록 하겠습니다.

일단, HttpServletRequest 부터 계속 파고들면서 JSESSIONID 를 찾는것도 좋기는 하지만, 귀찮으므로(?) 저 찾는 과정을 생략하고, 어디에 있는지부터 바로 말하도록 하겠습니다.

실제로 JSESSIONID 를 만드는 부분은 org.apache.catalina.session 패키지의 ManagerBase 클래스의 createSession 메소드 에서 JSESSIONID 를 아래와 같이 생성 하게 됩니다.

package org.apache.catalina.session;

....


public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {


    ......


        public Session createSession(String sessionId) {

        if ((maxActiveSessions >= 0) &&
                (getActiveSessions() >= maxActiveSessions)) {
            rejectedSessions++;
            throw new TooManyActiveSessionsException(
                    sm.getString("managerBase.createSession.ise"),
                    maxActiveSessions);
        }

        // Recycle or create a Session instance
        Session session = createEmptySession();

        // Initialize the properties of the new session and return it
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
        String id = sessionId;
        if (id == null) {
            id = generateSessionId();
        }
        session.setId(id);
        sessionCounter++;

        SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
        synchronized (sessionCreationTiming) {
            sessionCreationTiming.add(timing);
            sessionCreationTiming.poll();
        }
        return session;
    }


    ......


}

그리고 위의 코드 처럼 createSession 메소드는 내부적으로 org.apache.catalina.util 패키지의 StandardSessionIdGenerator 클래스를 사용하여 JSESSIONID 를 생성 하게 됩니다.

실제 코드는 아래와 같습니다.

package org.apache.catalina.util;

public class StandardSessionIdGenerator extends SessionIdGeneratorBase {

    @Override
    public String generateSessionId(String route) {

        byte random[] = new byte[16];
        int sessionIdLength = getSessionIdLength();

        // Render the result as a String of hexadecimal digits
        // Start with enough space for sessionIdLength and medium route size
        StringBuilder buffer = new StringBuilder(2 * sessionIdLength + 20);

        int resultLenBytes = 0;

        while (resultLenBytes < sessionIdLength) {
            getRandomBytes(random);
            for (int j = 0;
            j < random.length && resultLenBytes < sessionIdLength;
            j++) {
                byte b1 = (byte) ((random[j] & 0xf0) >> 4);
                byte b2 = (byte) (random[j] & 0x0f);
                if (b1 < 10)
                    buffer.append((char) ('0' + b1));
                else
                    buffer.append((char) ('A' + (b1 - 10)));
                if (b2 < 10)
                    buffer.append((char) ('0' + b2));
                else
                    buffer.append((char) ('A' + (b2 - 10)));
                resultLenBytes++;
            }
        }

        if (route != null && route.length() > 0) {
            buffer.append('.').append(route);
        } else {
            String jvmRoute = getJvmRoute();
            if (jvmRoute != null && jvmRoute.length() > 0) {
                buffer.append('.').append(jvmRoute);
            }
        }

        return buffer.toString();
    }
}

위 코드를 보면, 실제 세션 길이의 2배 하고 20 바이트 더 많은 공간을 버퍼 공간으로 먼저 잡습니다.

실제 JSESSION 값을 생성하는 과정은 아래와 같습니다.

먼저, 16바이트 단위로 랜덤 값을 가지고 와서 문자열 형태의 16진수로 변환합니다.

다음으로, 라우트 값이 있는 경우, 랜덤값 뒤에 "." 문자와 같이 라우트 값을 합쳐줍니다.

마지막으로 jvmRoute 값을 "." 문자와 같이 합쳐줍니다.

참고로 저 jvmRoute 값과, route 값의 경우 서블릿 컨테이너에 접속한 사용자를 구분해주는 값이라고 생각하면 됩니다.

그리고 나서 실제 JSESSIONID 값을 반환합니다.

다음으로, 실제로 랜덤값을 가지고 오는 부분의 코드 입니다.

package org.apache.catalina.util;

import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;

...


....

protected void getRandomBytes(byte bytes[]) {

  SecureRandom random = randoms.poll();
  if (random == null) {
    random = createSecureRandom();
  }
  random.nextBytes(bytes);
  randoms.add(random);
}

...


private SecureRandom createSecureRandom() {

  SecureRandom result = null;

  long t1 = System.currentTimeMillis();
  if (secureRandomClass != null) {
    try {
      // Construct and seed a new random number generator
      Class<?> clazz = Class.forName(secureRandomClass);
      result = (SecureRandom) clazz.getConstructor().newInstance();
    } catch (Exception e) {
      log.error(sm.getString("sessionIdGeneratorBase.random",
                             secureRandomClass), e);
    }
  }

  boolean error = false;
  if (result == null) {
    // No secureRandomClass or creation failed. Use SecureRandom.
    try {
      if (secureRandomProvider != null &&
          secureRandomProvider.length() > 0) {
        result = SecureRandom.getInstance(secureRandomAlgorithm,
                                          secureRandomProvider);
      } else if (secureRandomAlgorithm != null &&
                 secureRandomAlgorithm.length() > 0) {
        result = SecureRandom.getInstance(secureRandomAlgorithm);
      }
    } catch (NoSuchAlgorithmException e) {
      error = true;
      log.error(sm.getString("sessionIdGeneratorBase.randomAlgorithm",
                             secureRandomAlgorithm), e);
    } catch (NoSuchProviderException e) {
      error = true;
      log.error(sm.getString("sessionIdGeneratorBase.randomProvider",
                             secureRandomProvider), e);
    }
  }

  if (result == null && error) {
    // Invalid provider / algorithm
    try {
      result = SecureRandom.getInstance("SHA1PRNG");
    } catch (NoSuchAlgorithmException e) {
      log.error(sm.getString("sessionIdGeneratorBase.randomAlgorithm",
                             secureRandomAlgorithm), e);
    }
  }

  if (result == null) {
    // Nothing works - use platform default
    result = new SecureRandom();
  }

  // Force seeding to take place
  result.nextInt();

  long t2 = System.currentTimeMillis();
  if ((t2 - t1) > 100) {
    log.warn(sm.getString("sessionIdGeneratorBase.createRandom",
                          result.getAlgorithm(), Long.valueOf(t2 - t1)));
  }
  return result;
}

사실 위의 코드도, 다양한 JVM 버전에서 동작해야 되서 예외 처리 때문에 매우 복잡해 보이지만 결국 랜덤값을 SHA1PRNG 또는 각 플랫폼의 기본 난수생성기를 사용해서 가져 오게 됩니다.

(사실상, 완전히 구형 플랫폼이나 특수한 환경이 아닌 경우, 대부분 SHA1PRNG 알고리즘을 사용해서 랜덤값을 가지고 온다고 생각하면 됩니다.)

그리고 위의 코드를 보고 "어 그냥 Random 클래스에서 랜덤값 쓰면 안되요?" 라고 물을 수 있습니다.

하지만, 암호학적으로 안전한 난수생성기를 쓰지 않는 경우, 사용자나 공격자들이 무식하게 경우의 수를 전부 미리 계산하고 그 결과를 이용해서 Bruteforce 하게 세션값을 위조해버리거나 조작해버리는 경우도 생길 수 있습니다.

실제로 2012년에 PHP에서 비슷한 취약점이 발견되기도 했었습니다.

https://alphaninesec.wordpress.com/2016/10/05/brute-forcing-php-session-ids/

출처

  1. https://github.com/apache/tomcat/blob/1d1d835a6784854e26c4fff8e23aef128c105f3c/java/org/apache/catalina/util/StandardSessionIdGenerator.java#L21
  2. https://github.com/apache/tomcat/blob/1d1d835a6784854e26c4fff8e23aef128c105f3c/java/org/apache/catalina/util/SessionIdGeneratorBase.java#L214
  3. http://cris.joongbu.ac.kr/course/java/api/java/security/SecureRandom.html
  4. https://alphaninesec.wordpress.com/2016/10/05/brute-forcing-php-session-ids/
  5. https://jojoldu.tistory.com/118
반응형
Comments