Linux 프로그램은 기본적으로 stdin, stdout, stderr 3개의 파일 디스크립터를 사용한다. 여기에 다가 file을 오픈하거나 Socket을 오픈하는 것도 파일 디스크립터를 하나씩 점유하게 된다. 다수의 요청을 처리하는 소켓 프로그램의 경우 너무 많은 요청이 들어 올 경우 파일 디스크립터의 수가 제한 되어 ‘too many open file’과 같은 에러가 발생하게 된다.
쉘에서 ulimit -a 명령어를 사용하면 현재 커널에 할당되어 있는 정보를 볼 수 있다.
core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited max nice (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 8064 max locked memory (kbytes, -l) 32 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 max rt priority (-r) 0 stack size (kbytes, -s) 10240 cpu time (seconds, -t) unlimited max user processes (-u) 8064 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited file locks (-x) unlimited
위 항목중 open files 항목이 하나의 프로세스에서 열 수 있는 파일 디스크립터의 수 이다. ulimit -n 명령어를 사용하여 설정 할 수도 있으나 여기에서는 C 언어에서 할당하는 방법을 알아보자
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
int getrlimit (int resource, struct rlimit *rlim);
int setrlimit (int resource, const struct rlimit *rlim);
struct rlimit
{
rlim_t rlim_cur; //soft
rlim_t rlim_max; //hard
};
사용법은 아래와 같다.
void print_rlim()
{
struct rlimit limit;
getrlimit(RLIMIT_NOFILE, &limit);
printf("cur=%lu max=%lu\n", limit.rlim_cur, limit.rlim_max);
}
void set_rlim()
{
struct rlimit limit;
limit.rlim_cur = 20480lu;
limit.rlim_max = 20480lu;
setrlimit(RLIMIT_NOFILE, &limit);
}
WebGoat는 웹 취약성을 이해하기 쉽도록 도와주는 툴이다.
Aspect Security사에서는 OWASP(The Open Web Application Security Project) Top 10이라는 웹 어플리케이션 취약성 10대 항목을 발표한다. 얼마전에 OWASP Top 10이 2007년판으로 개정 되면서 새로운 보안 취약성들을 공개했으니 관심있는 분은 다시 읽어 보자. 고맙게도 SecurityPlus라는 보안 카페에서 한글로 번역이 되어 있으니 쉽게 읽을 수 있을 것이다.
WebGoat는 OWASP Top 10을 기반으로 문제를 제시하고 사용자가 직접 웹취약성을 공격하여 취약성을 분석하고 해결하는 방법을 알려준다.
WebGoat 5.0 다운로드
톰캣을 설치 한 상황이라면 stand alone 파일을 다운 받고 tomcat/webapps에 압축을 푸는 것으로 설치가
끝난다. 설치 메뉴얼을 보고 tomcat/conf/tomcat-users.xml파일을 수정하면 설치는 끝.
접속 방법은
http://아이피주소/WebGoat-5.0/attack 으로 접속하면 아래와 같은 화면이 보이게 된다.
아 이 디: guest
비밀번호:guest
자 이제 Start WebGoat를 눌러 문제를 하나씩 풀어보면 된다.
좌측에는 문제들이 나열되어 있고 우측 상단 메뉴바에는 문제를 풀 수 있도록 도와주는 힌트들이 있다
Hints 항목은 문제들을 풀 수 있는 힌트들을 제시해주고
Show Params는 get이나 post의 파라미터들을 보여준다.
Show Cookies는 쿠키 내용을 보여주며
Show Java는 문제들의 java 소스 파일을 보여준다.
이런 항목을 보면서 웹 취약성을 하나하나 파헤쳐 나가면 된다.
문제를 하나 풀어보면…
General에 How to Exploit Thread Safety Problems 항목을 선택하면
쓰레드 안정성 문제에 관한 문제를 볼 수 있다. 웹 어플리케이션은 기본적으로 다수의 요청을 처리하는
concurrency 서버인 경우가 많다. 이런 경우에 쓰레드를 생각하지 않은 프로그램은 쓰레드 안정성 문제를
갖게 된다.
이 문제는 여기에 관한 문제이다. 이런 취약성을 가진 웹 어플리케이션에서는 사용자 2명이 동시에 트랜잭션 요청을 해버리면 다른 사람의 트랜잭션을 하이재킹하여 뺏어버릴 수 있다.
익스플로어에 파일-> 새로만들기 ->창을 선택하면 동일한 페이지가 열린다.
Enter user name: 에 각각 jeff와 dave로 입력하고 거의 동시에 submit을 해보자
위와 같이 두 사용자 모두 같은 이름의 정보를 나열해 버렸을 것이다.
이 외에도 웹 취약성에 관한 많은 문제가 나열되어 있으며 사용자가 직접 해킹함으로써 웹 취약성의
이해를 돕는 툴이다. 또한, 사용자가 문제를 스스로 제출하여 배포할 수도 있다.
여기에 나타난 많은 취약성들은 우리가 사용하고 있는 대부분의 웹에서 실제로 사용할 수 있는 취약성이며
실제로 공개된 많은 서버에서 이러한 취약성을 가지고 있다(실제로 대부분의 서버는 해킹 가능하다).
이러한 정보들을 가지고 악의적인 공격을 하려는 사람들은 이 프로그램을 제작한 사람들의 목표에 부합하지않는 다는 것을 명심하자
“OWASP에서의 기본 목표는 가장 공통적인 웹 어플리케이션 보안 취약점의 결과에 대해 개발자와, 설계자, 아키텍트와 조직을 교육시키는 것이다.“ -OWASP Top 10 문서중 발췌-
SSL을 사용한 통신에서 암호화 방식의 선택은 Client Hello와 Server Hello를 주고 받을 때 Cipher suites을 주고 받는다. 즉, 클라이언트에서는 자신이 지원하는 암호화 방식의 리스트(Cipher Suites)을 서버에 전송하고 서버에서는 클라이언트가 준 Cipher suites에서 자신이 지원하는 암호화 방식을 택하여 서버와 클라이언트간의 암호화 방식을 택하게 된다.
Client Hello
*** ClientHello, TLSv1
RandomCookie: GMT: 1226648498 bytes = { 200, 58, 111, 214, 101, 210, 35, 45, 30, 95, 248, 152, 164, 79, 190, 173, 246, 107, 75, 117, 87, 53, 57, 130, 181, 247, 4, 49 }
Session ID: {}
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA, SSL_DHE_DSS_WITH_DES_CBC_SHA, SSL_RSA_EXPORT_WITH_RC4_40_MD5, SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA]
Compression Methods: { 0 }
***
위에 처럼 Cipher Suites 가 ClientHello에 실려 날아가게된다.
Server Hello
*** ServerHello, TLSv1
RandomCookie: GMT: 1226648580 bytes = { 5, 238, 245, 170, 224, 35, 204, 164, 117, 25, 117, 146, 156, 96, 177, 125, 190, 18, 220, 170, 62, 65, 6, 108, 174, 253, 27, 67 }
Session ID: {103, 15, 0, 0, 41, 170, 156, 123, 141, 32, 89, 224, 116, 103, 216, 190, 48, 141, 19, 213, 138, 211, 210, 218, 165, 23, 132, 90, 120, 77, 134, 26}
Cipher Suite: SSL_RSA_WITH_RC4_128_MD5
Compression Method: 0
***
위와 같이 서버에서는 Client Hello에 실린 Cipher Suites중 하나를 택하여 사용하게 된다. 이로써 이후의 통신은 선택된 방식을 사용하여 암호화 하게 된다.
사 실 클라이언트 입장에서 자신이 지원하는 모든 Cipher suites를 전송하는게 맞으나.. 어느 회사에서 자신들은 3DES이상의 암호화만 안전한 것으로 판단하니 Cipher Suites를 바꿔 달라는 요청이 있었다.. ㅡㅡ;; 그럴바에 그냥 LDAP 서버에서 지원하는 암호화 리스트를 3DES 이상의 것들만 선택하는게 더 간단할텐데.. 왜 이런 삽질을 요구하는지 ㅡㅡ;;;;;
Java에서의 LDAP의 지원은 javax.naming 패키지에서 지원을 한다. LDAP 프로그래밍을 해 본 사람이면 아래의 구문은 익숙할 것이다.
LDAP 프로토콜을 사용한 통신
Hashtable<String,String> env = new Hashtable<String,String>(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://IP주소:389/"); env.put(Context.SECURITY_PRINCIPAL, "계정의 도메인"); env.put(Context.SECURITY_CREDENTIALS,"계정의 암호"); DirContext ctx = new InitialDirContext(env);
위의 예제는 LDAP 프로토콜을 이용하여 389번 포트로 평문 통신을 하여 사용하는 것의 예제이다.
LDAPS 프로토콜을 사용한 통신
System.setProperty("javax.net.ssl.trustStore","c:\\java\\trust.keystore");
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldaps://IP주소:636/");
env.put(Context.SECURITY_PRINCIPAL, "계정의 도메인");
env.put(Context.SECURITY_CREDENTIALS,"계정의 암호");
env.put(Context.SECURITY_PROTOCOL,"ssl");
DirContext ctx = new InitialDirContext(env);
위의 예제는 LDAPS 프로토콜을 사용하여 636 포트로 SSL암호화 통신을 한 예제이다. 다른게 있다면 포트가 636으로 바뀌었고 시스템 프로퍼티에 신뢰된 저장소(Trust store)를 지정한 것 그리고 암호화 프로토콜로 SSL을 지정 하였을 뿐이다.
이렇게 자바에서 LDAP 프로그래밍은 아주 간단히 큰 변화를 일으키지 않고도 많은 LDAP에서 LDAPS로의 변경이 가능하다. 여기서 문제가 되는 것은 이렇게 내부의 로직이 숨어 있어서 우리가 하고 싶은 Cipher Suites을 변경하는 작업이 어렵다는 점이다.
이런 문제점을 해결하기 위해 Jndi에서는 CustomSocketFactory라는 방식으로 지원을 한다. CustomSocketFactory는 이름에서도 알 수 있듯이 사용자 정의 소켓을 생성하도록 해주는 Factory 패턴이다. LDAP나 LDAPS 통신을 할때 기본적으로 SocketFactory나 이를 상속 받는 SSLSocketFactory를 사용하도록 되어 있다. 우리가 할일은 SSLSocketFactory를 상속받은 CustomSSLSocketFactory를 생성하고 jndi가 앞으로 소켓을 생성할때는 우리가 만든 CustomSSLSocketFactory를 사용하도록 지정하면 되는 것이다.
각설하고 소스코드는 다음과 같이 변한다.
System.setProperty("javax.net.ssl.trustStore","c:\\java\\trust.keystore");
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldaps://IP주소:636/");
env.put(Context.SECURITY_PRINCIPAL, "계정의 도메인");
env.put(Context.SECURITY_CREDENTIALS,"계정의 암호");
env.put(Context.SECURITY_PROTOCOL,"ssl");
env.put("java.naming.ldap.factory.socket", "CustomSSLSocketFactory");
DirContext ctx = new InitialDirContext(env);
기본 LDAPS 소스코드에다 단지
env.put(“java.naming.ldap.factory.socket”, “CustomSSLSocketFactory”); 을 삽입한 것 뿐이다.
이는 LDAP에서 사용할 Socket은 CustomSSLSocketFactory 라는 클래스를 사용하여 생성하라는 것을 알려주는 것이다.
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
public class CustomSSLSocketFactory extends SSLSocketFactory {
private SSLSocketFactory factory;
String suites[] = {
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
};
public CustomSSLSocketFactory() {
try {
SSLContext sslcontext = null;
if (sslcontext == null) {
sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(null, // No KeyManager required
new TrustManager[] { new CustomTrustManager() },
new java.security.SecureRandom());
}
factory = (SSLSocketFactory) sslcontext.getSocketFactory();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static SocketFactory getDefault() {
return new CustomSSLSocketFactory();
}
public Socket createSocket(Socket socket, String s, int i, boolean flag) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(socket, s, i, flag);
sock.setEnabledCipherSuites(suites);
return sock;
}
public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr1, int j) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(inaddr, i, inaddr1, j);
sock.setEnabledCipherSuites(suites);
return sock;
}
public Socket createSocket(InetAddress inaddr, int i) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(inaddr, i);
sock.setEnabledCipherSuites(suites);
return sock;
}
public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(s, i, inaddr, j);
sock.setEnabledCipherSuites(suites);
return sock;
}
public Socket createSocket(String s, int i) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(s, i);
sock.setEnabledCipherSuites(suites);
return sock;
}
public String[] getDefaultCipherSuites() {
return suites;
}
public String[] getSupportedCipherSuites() {
return suites;
}
}
이름에서도 알 수 있듯이 Factory 패턴은 새로운 객체를 생성하는 디자인 패턴이다. 소스코드를 살펴보면 SSLSocket을 생성하고 Cipher suites를 지정한것 밖에 없다.
그리고 Trust Store를 찾기 위해 X509TrustManager의 구현을 참조하기 때문에 가짜로 CustomTrustManager라는 파일을 하나 생성하자. 그렇지 않으면 NULL Exception이 발생하니 가짜로라도 만들어 놓아야한다. 앞의 메인 프로그램에서 시스템 프로퍼티에 Trust Store를 지정하였으므로 가짜라도 상관이 없다.
CustomTurstManager.java
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class CustomTrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] cert, String authType) {
return;
}
public void checkServerTrusted(X509Certificate[] cert, String authType) {
return;
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
이로써 LDAPS에서 사용할 클라이언트 Cipher suites는 위의 CustomSSLSocketFactory에서 지정한 것으로 Client Hello를 날리게 될 것이다. 제대로 된 Cipher suites가 날라가는지 알고 싶으면 실행시에 -Djavax.net.debug=ssl 옵션을 줘서 실행하자. SSL 통신 절차들이 모두 디버그 코드로 화면에 출력 될 것이다.
참조 : JNDI Tutorial: SSL and Custom Sockets





Recent Comments