Tomcat org.apache.catalina.LifecycleException: 프로토콜 핸들러 시작 실패
Apache Tomcat AJP 취약점(CVE-2020-1938)에 따른 톰캣 마이너 업데이트 (Apache 2.4.42 미만)
안녕하세요. eastpost 입니다.
블로그를 해야지 해야지 하다가 이제야 시작하네요.
뻘짓거리를 며칠간해서 혼자알기 아까워서 글 씁니다.
제가 맡고 있는 유지보수 사이트 중에서, 담당자에게 보안 업데이트 권고 요청 공고가 날라와 확인해보니 톰캣 마이너 업데이트가 필요하더군요.
사실 상 업데이트를 거의 안 해서 2015년 빌드인 톰캣 7.0.61을 사용 중이여서 업데이트의 필요성은 있었습니다. 서버가 자주 무응답 (503 error)에 빠지기도 해서 계속 모니터링하면서 재부팅을 시켜주기도 했구요..
권고의 내용은 아래와 같습니다.
Apache Tomcat AJP 취약점 보안 조치 권고(2차) 2020.03.02
□ 개요
o 최근 Apache Tomcat의 원격코드실행 취약점(CVE-2020-1938)을 악용할 수 있는 개념증명코드(Proof of concept code, PoC)가 인터넷상에 공개되어 사용자의 보안 강화 필요
※ 개념증명코드 : 취약점을 증명/검증할 수 있는 프로그램 또는 소스코드
□ 설명
o Tomcat이 AJP request 메시지를 처리할 때, 메시지에 대한 처리가 미흡하여 발생하는 원격코드실행 취약점(CVE-2020-1938)
※ AJP(Apache JServ Protocol) : 웹서버와 어플리케이션 서버 간 연결 요청을 8009포트를 사용하여 전달하는 프로토콜(모니터링 기능 지원)
□ 영향을 받는 버전
o Apache Tomcat
- 9.0.0.M1 ~ 9.0.30
- 8.5.0 ~ 8.5.50
- 7.0.0 ~ 7.0.99
※ 상기 버전은 AJP 커넥터가 기본으로 활성화되어 취약점에 영향 받음
□ 해결 방안
o 각 버전에 해당되는 페이지를 참고하여 최신 버전으로 업데이트 적용
- 7.0.100 이상 버전
- 8.5.51 이상 버전
- 9.0.31 이상 버전
o 임시 조치 방안(패치 적용이 어려운 경우)
- AJP 기능이 불필요한 경우 Connector 비활성화
· conf/server.xml 설정 파일 내 AJP Connector 기능 주석처리
□ 기타 문의사항
o 한국인터넷진흥원 사이버민원센터: 국번없이 118
[참고사이트]
[1] https://lists.apache.org/thread.html/r7c6f492fbd39af34a68681dbbba0468490ff1a97a1bd79c6a53610ef%40%3Cannounce.tomcat.apache.org%3E
[2] http://tomcat.apache.org/security-7.html
[3] http://tomcat.apache.org/security-8.html
[4] http://tomcat.apache.org/security-9.html
KISA - Apache Tomcat 취약점 보안 업데이트 권고 (1차)
https://www.krcert.or.kr/data/secNoticeView.do?bulletin_writing_sequence=35279
KISA - Apache Tomcat AJP 취약점 보안 조치 권고(2차)
https://www.krcert.or.kr/data/secNoticeView.do?bulletin_writing_sequence=35292
취약점에 대한 더 자세한 정보
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1938
제가 메일로 받았을 땐 해결 방안이 없는 문서였는데, 추가되어 나왔네요.
아파치 서버와 톰캣 서버를 연동해주는 AJP(Apache JServ Protocol) 관련된 심각한 취약점이 발견되어 아파치 측에서 급하게 빌드를 내놓은 것 같습니다.
제가 관리하고 있는 유지보수 사이트도 아파치 서버가 응답을 받으면 도메인 구분하여 톰캣 서버 페이지를 노출시키는 방식이였습니다. 해당 운영 서버에는 VirtualHost를 이용해서 세 개의 톰캣 서버를 운영 중이더군요. 나머지 두 개의 톰캣 서버는 제가 맡고 있는 사이트가 아니라 아파치를 건드리기가 참 무서웠습니다. (다행히 건드릴 일은 없었지만요)
우선 톰캣 디렉터리의 bin에서 sh version.sh를 실행하여 버전을 확인해봅시다.
저의 경우 7.0.61 버전이라 7.0.100 버전으로 업데이트를 해야되는 상황이였습니다.
해당 취약점 대응 버전이 (20/3/6기준으로) 나온지가 채 한 달도 안 된 시점이라, 다들 업데이트가 필요하시겠지만...
톰캣 빌드 아카이브 링크입니다. 최신 버전을 찾아봅시다. ("2020"으로 찾으시면 금방 찾을 수 있습니다)
http://archive.apache.org/dist/tomcat/
패치 전 필요하신 분들은 톰캣 백업을 전부 해두시고..
conf/server.xml 의 수정이 필요합니다.
그 전에 아파치 서버 버전을 확인해야합니다.
httpd -v 를 실행하여 버전을 확인해봅시다.
2.4.42 버전 미만인 경우와, 이상인 경우의 설정 방법을 달리 할 수 있습니다.
저의 경우 2.2.15 버전이고 업데이트 하기가 난감한 상황이라 2.4.42 버전 미만에 해당하는 대응 처리하였습니다. 사실 그 이상의 버전이라고 해도 아마 적용될 듯한 방법이긴 합니다만.. 2.4.42 미만의 버전은 이 방법밖에 없는 것 같네요.
server.xml을 열어보시면,
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
이런 식의 태그가 있을 겁니다. (포트 번호는 운영 환경 상 다를 수 있습니다)
protocol에 AJP가 써져있다면 AJP를 사용 중인 것으로 볼 수는 있으나, 기존 버전의 경우 기본적으로 박혀있는 태그라 실제로 안 쓰고 계신 분들도 있을 수 있습니다.
사용하지 않는다면 해당 태그를 지워버리시는게 가장 확실하고.. 저 같이 사용하시는 경우 태그를 다음과 같이 작성해줍니다. (아파치 2.4.42 미만의 경우)
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="::1" secretRequired="false"/>
혹은
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="0.0.0.0" secretRequired="false"/>
address : 사용 주소를 넣어줍니다. 저처럼 ::1을 넣으셔도 되고, 0:0:0:0을 넣어주셔도 됩니다.
secretRequired : 비밀값(인증키)가 필요한지에 대한 설정입니다. false를 넣어줍니다.
address 속성때문에 삽질했다고 해도 과언이 아닌데.. localhost, 127.0.0.1, 192.168로 시작하는 사설 IP, 공인 IP, 도메인을 다 넣어봐도 문제가 발생했었습니다.
어떤 문제가 발생했었냐면..
Service Temporarily Unavailable
The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.
이 페이지가 계속 노출되었습니다. 사실 저 아래 설명은 도움되는 게 1도 없었습니다.
톰캣 서버는 정상적으로 시작되었는데 말이죠.
아파치에서 AJP 관련 로그를 뿜어내는 mod_jk라는 녀석이 있습니다.
tail -f mod_jk.log 하여 확인해보니.. (로그 위치는 보통 /etc/httpd/logs입니다)
[Thu Mar 05 17:19:16.222 2020] [18908:140515944781792] [info] jk_open_socket::jk_connect.c (817): connect to ::1:8309 failed (errno=111)
[Thu Mar 05 17:19:16.222 2020] [18908:140515944781792] [info] ajp_connect_to_endpoint::jk_ajp_common.c (1068): (projNm) Failed opening socket to (::1:8309) (errno=111)
[Thu Mar 05 17:19:16.222 2020] [18908:140515944781792] [error] ajp_send_request::jk_ajp_common.c (1728): (projNm) connecting to backend failed. Tomcat is probably not started or is listening on the wrong port (errno=111)
[Thu Mar 05 17:19:16.222 2020] [18908:140515944781792] [info] ajp_service::jk_ajp_common.c (2773): (projNm) sending request to tomcat failed (recoverable), because of error during request sending (attempt=1)
[Thu Mar 05 17:19:16.322 2020] [18908:140515944781792] [info] jk_open_socket::jk_connect.c (817): connect to ::1:8309 failed (errno=111)
[Thu Mar 05 17:19:16.322 2020] [18908:140515944781792] [info] ajp_connect_to_endpoint::jk_ajp_common.c (1068): (projNm) Failed opening socket to (::1:8309) (errno=111)
[Thu Mar 05 17:19:16.322 2020] [18908:140515944781792] [error] ajp_send_request::jk_ajp_common.c (1728): (projNm) connecting to backend failed. Tomcat is probably not started or is listening on the wrong port (errno=111)
[Thu Mar 05 17:19:16.322 2020] [18908:140515944781792] [info] ajp_service::jk_ajp_common.c (2773): (projNm) sending request to tomcat failed (recoverable), because of error during request sending (attempt=2)
[Thu Mar 05 17:19:16.322 2020] [18908:140515944781792] [error] ajp_service::jk_ajp_common.c (2794): (projNm) connecting to tomcat failed (rc=-3, errors=821, client_errors=46).
[Thu Mar 05 17:19:16.322 2020] [18908:140515944781792] [info] jk_handler::mod_jk.c (2991): Service error=-3 for worker=projNm
[Thu Mar 05 17:19:16.373 2020] [24073:140515944781792] [info] jk_open_socket::jk_connect.c (817): connect to ::1:8309 failed (errno=111)
[Thu Mar 05 17:19:16.373 2020] [24073:140515944781792] [info] ajp_connect_to_endpoint::jk_ajp_common.c (1068): (projNm) Failed opening socket to (::1:8309) (errno=111)
[Thu Mar 05 17:19:16.373 2020] [24073:140515944781792] [error] ajp_send_request::jk_ajp_common.c (1728): (projNm) connecting to backend failed. Tomcat is probably not started or is listening on the wrong port (errno=111)
[Thu Mar 05 17:19:16.373 2020] [24073:140515944781792] [info] ajp_service::jk_ajp_common.c (2773): (projNm) sending request to tomcat failed (recoverable), because of error during request sending (attempt=1)
[Thu Mar 05 17:19:16.474 2020] [24073:140515944781792] [info] jk_open_socket::jk_connect.c (817): connect to ::1:8309 failed (errno=111)
[Thu Mar 05 17:19:16.474 2020] [24073:140515944781792] [info] ajp_connect_to_endpoint::jk_ajp_common.c (1068): (projNm) Failed opening socket to (::1:8309) (errno=111)
[Thu Mar 05 17:19:16.474 2020] [24073:140515944781792] [error] ajp_send_request::jk_ajp_common.c (1728): (projNm) connecting to backend failed. Tomcat is probably not started or is listening on the wrong port (errno=111)
[Thu Mar 05 17:19:16.474 2020] [24073:140515944781792] [info] ajp_service::jk_ajp_common.c (2773): (projNm) sending request to tomcat failed (recoverable), because of error during request sending (attempt=2)
[Thu Mar 05 17:19:16.474 2020] [24073:140515944781792] [error] ajp_service::jk_ajp_common.c (2794): (projNm) connecting to tomcat failed (rc=-3, errors=822, client_errors=46).
[Thu Mar 05 17:19:16.474 2020] [24073:140515944781792] [info] jk_handler::mod_jk.c (2991): Service error=-3 for worker=projNm
[Thu Mar 05 17:19:21.277 2020] [20326:140515944781792] [info] jk_open_socket::jk_connect.c (817): connect to ::1:8309 failed (errno=111)
[Thu Mar 05 17:19:21.277 2020] [20326:140515944781792] [info] ajp_connect_to_endpoint::jk_ajp_common.c (1068): (projNm) Failed opening socket to (::1:8309) (errno=111)
[Thu Mar 05 17:19:21.278 2020] [20326:140515944781792] [error] ajp_send_request::jk_ajp_common.c (1728): (projNm) connecting to backend failed. Tomcat is probably not started or is listening on the wrong port (errno=111)
[Thu Mar 05 17:19:21.278 2020] [20326:140515944781792] [info] ajp_service::jk_ajp_common.c (2773): (projNm) sending request to tomcat failed (recoverable), because of error during request sending (attempt=1)
[Thu Mar 05 17:19:21.378 2020] [20326:140515944781792] [info] jk_open_socket::jk_connect.c (817): connect to ::1:8309 failed (errno=111)
[Thu Mar 05 17:19:21.378 2020] [20326:140515944781792] [info] ajp_connect_to_endpoint::jk_ajp_common.c (1068): (projNm) Failed opening socket to (::1:8309) (errno=111)
[Thu Mar 05 17:19:21.378 2020] [20326:140515944781792] [error] ajp_send_request::jk_ajp_common.c (1728): (projNm) connecting to backend failed. Tomcat is probably not started or is listening on the wrong port (errno=111)
[Thu Mar 05 17:19:21.378 2020] [20326:140515944781792] [info] ajp_service::jk_ajp_common.c (2773): (projNm) sending request to tomcat failed (recoverable), because of error during request sending (attempt=2)
[Thu Mar 05 17:19:21.378 2020] [20326:140515944781792] [error] ajp_service::jk_ajp_common.c (2794): (projNm) connecting to tomcat failed (rc=-3, errors=823, client_errors=46).
[Thu Mar 05 17:19:21.378 2020] [20326:140515944781792] [info] jk_handler::mod_jk.c (2991): Service error=-3 for worker=projNm
(저의 경우 포트를 8009가 아니라 8309로 되어있습니다)
to ::1:8309가 보이시나요? IP는 생각지도 못하게 IPv6 로컬호스트 IP였습니다. ㅠㅠ
address에 ::1를 넣어주니 마법같이 잘 동작하더군요.
혹시라도 저같이 ::1이 아니라, 다르게 나오신다고 하면 해당 부분을 address에 기입해보시면 될것 같습니다.
::1 설명 - https://ko.wikipedia.org/wiki/Localhost
0:0:0:0 설명 - https://en.wikipedia.org/wiki/0.0.0.0
0:0:0:0은 와일드카드 개념의 IP라고 보시면 될 것 같습니다. 로컬의 모든 IPv4 주소를 의미한다고 합니다.
protocol="AJP"인 커넥터의 속성 중, 이번 보안 이슈와 긴밀한 속성들은 address, secret, secretRequired 세 속성이 있습니다.
톰캣 9 문서에도,
Use of the AJP protocol requires additional security considerations because it allows greater direct manipulation of Tomcat's internal data structures than the HTTP connectors. Particular attention should be paid to the values used for the address, secret, secretRequired and allowedRequestAttributesPattern attributes.
AJP 프로토콜을 사용하려면 HTTP 커넥터보다 Tomcat의 내부 데이터 구조를보다 직접적으로 조작 할 수 있으므로, 추가적인 보안 고려 사항이 필요합니다. address, secret, secretRequired 및 allowedRequestAttributesPattern 속성에 쓰이는 값에 특히 주의해야합니다.
라고 기재되어있습니다.
(추가로, secretRequired의 기능을 하던 기존의 requiredSecret 속성은 secretRequired 속성으로 대체되었습니다)
secret |
Only requests from workers with this secret keyword will be accepted. The default value is null. This attrbute must be specified with a non-null, non-zero length value unless secretRequired is explicitly configured to be false. |
secretRequired |
If this attribute is true, the AJP Connector will only start if the secret attribute is configured with a non-null, non-zero length value. The default value is true. |
secret |
이 비밀 키워드를 가진 작업자의 요청만 수락됩니다. 기본값은 null입니다. secretRequired 가 명시적으로 false로 구성되지 않은 경우, 속성은 널이 아닌 길이가 아닌 값으로 지정해야합니다. |
secretRequired |
이 속성이 true인 경우 secret 속성이 널이 아닌 길이가 아닌 길이 값으로 구성된 경우에만 AJP 커넥터가 시작됩니다 . 기본값은 true입니다. |
톰캣 7 - https://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html
톰캣 8 - https://tomcat.apache.org/tomcat-8.0-doc/config/ajp.html
톰캣 9 - https://tomcat.apache.org/tomcat-9.0-doc/config/ajp.html
(톰캣 8의 문서의 경우 아직 갱신되지 않은 듯 합니다. 3/12 기준)
secret, secretRequired 속성을 기재하지 않은 채로 업데이트를 시도하여 톰캣 서버를 기동하면 아래와 같은 에러 메시지가 나올 것입니다.
The AJP Connector is configured with secretRequired="true" but the secret attribute is either null or "". This combination is not valid.
AJP 커넥터는 secretRequired = "true"로 구성되었지만 secret 속성은 null 또는 "" 입니다. 이 조합은 유효하지 않습니다.
신규 버전으로 올리고, secret 속성을 설정하지 않은 채(기본값 null), secretRequired 속성이 기본적으로 true로 작동하면서 빚어진 결과이지요.
그리고 아까 제가 말씀드린 것 중, Apache 2.4.42 이상과 그 미만에 대한 대응 처리가 다르다고 하였죠? Apache 2.4.42 미만의 버전의 경우 secret 속성이 정의되어있지 않아, 톰캣 설정에서 secret를 사용한다 한들, 아파치에서 처리할 수 없는 문제가 발생합니다.
AJP secret 설정 문의글 - https://serverfault.com/questions/1004541/setting-up-ajp-secret-between-apache-and-tomcat
Apache 2.4 Apache Module mod_proxy_ajp - https://httpd.apache.org/docs/2.4/mod/mod_proxy_ajp.html
위 링크를 따라 mod_proxy_ajp 문서를 보시면 Attributes 항목 중,
?secret | 0x0C | String | Supported since 2.4.42 |
을 보실 수가 있을 겁니다.
그래서 그 이상의 버전인 경우
secret="비밀값" secretRequired="true"
이런 식으로 구성할 수 있고,
그 이상과 미만 버전인 경우
secretRequired="false"
이런 식으로 구성할 수 있습니다. 미만 버전의 경우 이 방법밖에 없겠지요.
secret 속성을 사용하여 테스트해보았는데, 접근 권한이 없다고 뜨게 됩니다.
(환경(아파치 버전)의 문제일 확률이 크겠지만, 해당 속성 관련하여 아파치쪽에 설정해야될 부분이 있지 않을까 싶습니다만 제가 찾은 문서들 중에는 알려주는 것이 없더군요.)
자, server.xml 에 대한 수정이 끝났으면, 톰캣 업데이트 작업을 합시다.
1. 톰캣 디렉터리 백업.
2. sh bin/shutdown.sh
3. bin, lib 업데이트본으로 교체
4. sh bin/startup.sh
만약 2,4번 과정에서 사용자 권한 관련 문제 발생 시,
chmod 777 /bin -R
을 실행하고 다시 시도해봅니다.
최종적으로, sh version.sh 으로 버전 확인을 해봅시다.
아.. 덧붙여서 제가 문제 해결한 직후에 찾다가 제가 원하는 걸 쓰신 분이 계셨습니다. ㅠㅠ
삽질하기 전에 이분 포스팅을 먼저 봤다면 참 좋았을텐데요..
제가 쓴 글보다 간결하게 쓰셨으니 참고하셔도 좋을 것 같습니다.
https://nirsa.tistory.com/131
기타 참고 링크 >
https://www.tenable.com/blog/cve-2020-1938-ghostcat-apache-tomcat-ajp-file-readinclusion-vulnerability-cnvd-2020-10487
https://www.cnvd.org.cn/webinfo/show/5415
https://www.toast.com/kr/support/notice/detail/1501
https://blog.alyac.co.kr/2772
https://nirsa.tistory.com/150?category=876464
취약점 점검 제공 >
https://www.chaitin.cn/en/ghostcat#online_test
이런 문제 해결 시, 제일 중요한 건 로그 보기인 듯 합니다.
(mod_jk 로그보고 바로 해결이 되었거든요)
운영에서 업데이트 시도를 계속 할 수가 없어서 로컬 환경 구축한다고 헛짓거리만 엄청했네요.
또한 케이스마다 원인이 다를 수 있으니 로그 먼저 봅시다!
출처