개요
이전 글에서 톰캣에 존재했던 HTTP Request Smuggling 취약점(CVE-2022-42252
)의 취약한 소스 코드를 분석했다. 이번에는 동적 디버깅을 통해 취약점의 존재를 확인해본다. 패치 전의 소스코드로 구동되는 상태에서 스캔 툴을 돌려서 취약점의 존재를 확인하고, 패치 후의 소스코드로 구동해서 스캔 툴을 돌렸을 때 취약점이 없어졌는지 확인하는 것이 목표다.
간단히 말하면 아래와 같은 것을 해본다.
출처: https://insbug.medium.com/apache-tomcat-request-smuggling-vulnerability-cve-2022-42252-836cb4bcb3d
테스트는 두 가지 종류로 나눠서 진행한다. 하나는 JUnit을 통한 유닛 테스트이고, 다른 하나는 서버를 구동한 후 Burp Suite를 통해 요청을 보내고 그 것이 어떻게 처리되는지를 동적디버깅을 통해 확인해보는 테스트이다.
JUnit 테스트
먼저 간단한 JUnit 테스트를 실시한다.
JUnit 테스트 실행방법
TestHttp11InputBuffer.java
는 유닛테스트 코드가 적혀있다. Eclipse 에서는 실행버튼을 누르면 알아서 Junit을 실행해준다.
유닛테스트 실행 결과는 다음과 같다.
취약점의 존재를 확인하는 JUnit 테스트코드
취약점이 수정된 이후의 커밋에는 취약점이 수정된 것을 테스트하는 코드가 test/org/apache/coyote/http11/TestHttp11InputBuffer.java
에 존재한다. 다음과 같다.
private void doTestInvalidContentLength(boolean rejectIllegalHeader) {
getTomcatInstance().getConnector().setProperty("rejectIllegalHeader", Boolean.toString(rejectIllegalHeader));
String[] request = new String[1];
request[0] =
"POST /test HTTP/1.1" + CRLF +
"Host: localhost:8080" + CRLF +
"Content-Length: 12\u000734" + CRLF +
"Connection: close" + CRLF +
CRLF;
InvalidClient client = new InvalidClient(request);
client.doRequest();
Assert.assertTrue(client.getResponseLine(), client.isResponse400());
Assert.assertTrue(client.isResponseBodyOK());
}
이 코드를 취약한 코드에서 실행하면 테스트가 성공하지 못할 것이다.
몇 번 시행착오 끝에 다음과 같은 테스트 코드를 작성했다. 실행해보면 테스트가 실패한다. (400응답이 회신되지 않았다. 따라서 코드에 취약점이 있다고 판단할 수 있다.)
package org.apache.coyote.http11;
import org.apache.catalina.startup.SimpleHttpClient;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.junit.Assert;
import org.junit.Test;
public class TestCVE_2022_42252 extends TomcatBaseTest {
private static final String CR = "\r";
private static final String LF = "\n";
private static final String CRLF = CR + LF;
@Test
public void testInvalidContentLength01() {
doTestInvalidContentLength(false);
}
// CVE-2022-42252 취약점이 수정된 것을 확인하는 코드
// 페이로드에 포함된 \u0007은 유니코드 캐릭터 번호를 의미한다. https://www.compart.com/en/unicode/U+0007 에 의하면 Alert을 의미한다.
private void doTestInvalidContentLength(boolean rejectIllegalHeader) {
getTomcatInstance().getConnector().setProperty("rejectIllegalHeader", Boolean.toString(rejectIllegalHeader));
String[] request = new String[1];
request[0] =
"POST / HTTP/1.1" + CRLF +
"Host: localhost:8080" + CRLF +
"Content-Length: 12\u000734" + CRLF +
"Connection: close" + CRLF +
CRLF;
System.out.println(request[0]);
InvalidClient client = new InvalidClient(request);
client.doRequest();
System.out.println(client.getResponseLine()); // HTTP/1.1 404
Assert.assertTrue(client.getResponseLine(), client.isResponse400()); // 취약점이 있는 코드에서는 400을 돌려주지 않으므로 실패하는 것이 맞다.
// Assert.assertTrue(client.isResponseBodyOK());
}
/**
* Invalid request test client.
*/
private class InvalidClient extends SimpleHttpClient {
private final String[] request;
public InvalidClient(String[] request) {
this.request = request;
}
private Exception doRequest() {
Tomcat tomcat = getTomcatInstance();
tomcat.getConnector().setProperty("rejectIllegalHeader", "false");
tomcat.addContext("", TEMP_DIR);
try {
tomcat.start();
setPort(tomcat.getConnector().getLocalPort());
// Open connection
connect();
setRequest(request);
processRequest(); // blocks until response has been read
// Close the connection
disconnect();
} catch (Exception e) {
return e;
}
return null;
}
@Override
public boolean isResponseBodyOK() {
if (getResponseBody() == null) {
return false;
}
return true;
}
}
}
실패했다. 취약점 수정후에 성공하는 코드이므로 실패하는 것이 취약점이 있는 코드에서는 실패하는게 맞다.
CL헤더가 삭제되는 조건을 이해하기 위한 추가 테스트
톰캣에서 CL 헤더를 삭제하는 부분은 Http11InputBuffer 클래스의 994라인~998라인의 다음 코드다. 문자가 Constants.HT과 같지 않고, HttpParser.isControl 의 리턴값이 true인 경우에 해당 헤더는 삭제된다.
※ 여기서 Constants.HT는 탭(\t)을 의미한다.
} else if (chr != Constants.HT && HttpParser.isControl(chr)) {
// Invalid value
// Delete the header (it will be the most recent one)
headers.removeHeader(headers.size() - 1);
return skipLine();
HttpParser.isControl의 동작은 다음 테스트 코드로 테스트할 수 있다.
package org.apache.coyote.http11;
import org.apache.tomcat.util.http.parser.HttpParser;
public class TestEncode {
public static void main(String[] args) {
char[] ch = {'\u03C3', '\u0007', '\uac00'};
for(int i=0; i < ch.length; i++) {
System.out.println(ch[i]);
System.out.println(HttpParser.isControl(ch[i]));
}
}
}
실행 결과는 다음과 같다. 특정 유니코드(‘\u007’)일 때만 HttpParser.isControl의 체크 결과가 true였다. 즉, CL헤더가 없는 것처럼 동작시키기 위한 특정 페이로드가 존재했던 것이다.
σ
false
true
가
false
동적디버깅으로 톰캣 내부의 값을 확인하기
STEP 1. 톰캣을 구동하고 타겟 엔드포인트를 선정하고 Burp Suite에서 HTTP요청 전송 준비
타겟 엔드포인트는 /examples/servlets/servlet/RequestParamExample
를 선정했다. 기본적으로 톰캣 샘플에 존재하는 경로다.
다음과 같이 UI에서 입력한 파라메터를 HTML 응답 페이지에 보여주는 페이지다.
요청을 Burp Suite로 캡쳐했다.
POST /examples/servlets/servlet/RequestParamExample HTTP/1.1
Content-Length: 32
Host: localhost:8080
firstname=moon&lastname=jaewoong
STEP 2. 적절한 소스코드 라인에 브레이크 포인트 찍기
- 브레이크 포인트를 찍을 곳은
Http11InputBuffer.java
의 998라인이다. - 여기에 헤더에 이상한 값이 있을 경우는 헤더를 삭제하는 코드가 있다.
STEP 3. 요청을 보내서 HTTP 요청이 어떻게 처리되는지 확인
시도 1. Content-Length 헤더에 한글 유니코드 문자 삽입
Burp Suite로 CL헤더의 값에 본래는 들어갈 수 없는 값 (여기에서 유니코드로 “한”이라는 한글을 인코딩한 0xed 0x95 0x9c
)을 넣었다.
브레이크 포인트를 찍은 곳에서 확인해보면 제대로 유니코드 바이트가 들어온 것처럼 보인다.
기대되는 동작은 톰캣이 이 헤더를 지워서 없었던 것 처럼 동작하는 것이다. 즉, 200 정상 처리 응답이 돌아올 것을 기대한다.
그런데 돌아오는 응답을 보면 400응답이다.
여기 코드가 동작하는지도 모른다. 그러나 브레이크 포인트를 찍어봐도 여기가 잡히지 않는다. 다른 곳에서 400응답이 처리된 것 같다.
parseHeaders에 찍어보니 return true; 에서 결렸다. 헤더를 파싱한 결과가 true (정상처리)인 것이다.
시도 2. POC에 사용된 유니코드로 시도
원인을 알아냈다! Burp Suite에 지정한 유니코드가 잘못되었던 것이 원인이었다.
일단 다음과 같이 유니코드비슷하게 Content-Length 숫자 뒷 부분에 0x00 0x00 0x07 을 넣었다.
이 요청을 보내보니 톰캣서버에서 SkipLine 함수가 실행되어 그 결과, 다음과 같이 Content-Length가 사라진 것을 확인했다!
하지만 여전히 400응답은 돌아온다. 서버측에서 Exception을 출력한 결과다.
java.lang.IllegalArgumentException: HTTP メソッド名 [firstname=moon&lastname=jaewoong...] に無効な文字が含まれています。HTTP メソッド名は決められたトークンでなければなりません
시도 3. HTTP 요청의 Content-Length 값을 Body의 크기에 맞춰서 변경
원인을 드디어 알아냈다. 위와같은 에러메세지가 되돌아온 것은 Content-Length 에 지정한 숫자가 10으로 실제 페이로드 길이보다도 짧았기 때문이다. 정확한 크기 32를 지정해주니 다음처럼 200응답이 돌아왔다!! CL.0중에서 백엔드 서버가 0가 되는 동작을 확인한 것이다!
참고로 HTTP요청상에서 유니코드 문자는 유니코드 번호와 동일한 바이트를 전송하면 되는 듯 하다. 예를들어 U+0007 같은 경우 2바이트로 0x00 0x07 를 보내면 된다.
취약점 수정 후 소스 코드로 테스트
취약점 수정후 소스 코드로 톰캣을 구동하고, 공격용 페이로드를 보내서 HTTP 요청이 어떻게 처리되는지 확인해본다.
STEP 1. 소스코드 되돌리기
수정된 커밋ID인 a1c07906d8dcaf7957e5cc97f5cdbac7d18a205a
의 버전으로 소스코드를 변경한다.
git reset --hard a1c07906d8dcaf7957e5cc97f5cdbac7d18a205a
STEP 2. JUnit 테스트
다음과 같이 추가된 테스트 testInvalidContentLength01 과 testInvalidContentLength02 이 성공하는 것을 확인했다.
STEP 3. 동적 디버깅으로 값 확인
-
/conf/server.xml
을 보면<Server port="8005" shutdown="SHUTDOWN">
코드로 돌아가 있다. 여기를 8006으로 변경한다. -
소스 코드를 재빌드한다.
ant
-
start-tomcat 설정도 원래대로 돌아가 있다. 디버그 모드로 구동하기 위해서 이클립스에서 Run > Run Configuration … 메뉴로 들어가서 start-tomcat 설정을 선택한다. Arguments 탭에서
-Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n
를 추가해준다. -
톰캣을 구동한다.
-
이어서 디버그 프로그램을 구동한다.
-
브레이크 포인트를 건다.
Http11InputBuffer.java
의 994 라인에 건다.
- Burp Suite로 요청을 보내본다. 그러면 브레이크포인트 시점에 헤더의 값이 다음과 같은 것을 볼 수 있었다. 수정전에는 content-length 헤더가 사라져있었다.
또한 톰캣서버 콘솔에서는 다음과 같은 에러가 출력되었다.
java.lang.IllegalArgumentException: HTTP ヘッダー行 [content-length:3220x000x074]は RFC 7230 に適合しないため無視します。
at org.apache.coyote.http11.Http11InputBuffer.skipLine(Http11InputBuffer.java:1093)
at org.apache.coyote.http11.Http11InputBuffer.parseHeader(Http11InputBuffer.java:994)
at org.apache.coyote.http11.Http11InputBuffer.parseHeaders(Http11InputBuffer.java:619)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:535)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:885)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1693)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:834)
되돌아온 응답은 400응답이었다. (취약점 수정전에는 200응답이었다.) 요청이 정상처리 되지 않으므로 HTTP 요청 스머글링은 불가능할 것이다. 이로서 취약점이 수정된 것을 확인했다.
참고
- 취약점 정보: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-42252
- 취약점 POC: https://insbug.medium.com/apache-tomcat-request-smuggling-vulnerability-cve-2022-42252-836cb4bcb3d
- 톰캣 설정: https://tomcat.apache.org/tomcat-8.5-doc/config/http.html
- 취약점 수정 코드: https://github.com/apache/tomcat/commit/a1c07906d8dcaf7957e5cc97f5cdbac7d18a205a
- 로깅레벨: https://docs.oracle.com/javase/jp/6/api/java/util/logging/Level.html