[분석 일기] docs 정독으로 Grav CMS에서 RCE 취약점 찾은 썰
1. Grav CMS
Grav CMS는 php로 개발된 CMS 입니다.
Grav is a Fast, Simple, and Flexible, file-based Web-platform.
2. Vulnerablity
회사에서 연구 기간을 잠깐 가졌었는데, Grav CMS를 처음 접하고 취약점을 찾고 싶어서 타겟으로 선정했습니다. 3일간 총 3개의 취약점을 찾았고, 이를 제보하였습니다.
2.1 파일 확장자 검증 우회로 XSS
파일 업로드 기능에서 코드를 분석 하다가 확장자 우회를 통한 XSS 취약점을 발견했습니다. 파일 업로드 하면 파일 이름은 `$filename` 변수에 저장됩니다. 이후 `checkFilename()` 함수에서 `$filename` 을 검사합니다. 이후 `mb_strtolower()` 함수로 확장자를 소문자로 바꾸고 설정 파일에 해당 확장자가 존재하는지 확인하는 구조 입니다.
// system/src/Grav/Common/Media/Traits/MediaUploadTrait.php#L159-L170
// https://github.com/getgrav/grav/blob/develop/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php#L159-L170
// Check if the filename is allowed.
if (!Utils::checkFilename($filename)) {
throw new RuntimeException(
sprintf($this->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'), $filepath, $this->translate('PLUGIN_ADMIN.BAD_FILENAME'))
);
}
// Check if the file extension is allowed.
$extension = mb_strtolower($extension);
if (!$extension || !$this->getConfig()->get("media.types.{$extension}")) {
// Not a supported type.
throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
}
`checkFilename()` 함수는 아래와 같습니다. `pathinfo()` 함수로 확장자를 추출하여 이것저것 검증을 하고 있습니다.
// system/src/Grav/Common/Utils.php#L980-L995
// https://github.com/getgrav/grav/blob/develop/system/src/Grav/Common/Utils.php#L980-L995
public static function checkFilename($filename)
{
$dangerous_extensions = Grav::instance()['config']->get('security.uploads_dangerous_extensions', []);
$extension = static::pathinfo($filename, PATHINFO_EXTENSION);
return !(
// Empty filenames are not allowed.
!$filename
// Filename should not contain horizontal/vertical tabs, newlines, nils or back/forward slashes.
|| strtr($filename, "\t\v\n\r\0\\/", '_______') !== $filename
// Filename should not start or end with dot or space.
|| trim($filename, '. ') !== $filename
// File extension should not be part of configured dangerous extensions
|| in_array($extension, $dangerous_extensions)
);
}
// $dangerous_extensions = php, html, htm, js, exe
첫번째 코드 블록과 두번째 코드 블록에서 차이점이 존재합니다. 첫번째 코드 블록에서는 확장자를 가져와서 `mb_strtolower()` 함수를 이용하여 소문자로 변경한 뒤 검증하고 있습니다. 하지만, 두번째 코드 블록에서는 확장자를 가져오기만 하고 이를 검증하고 있습니다.
따라서, 공격자가 `test.HTML` 로 업로드를 하면 다음과 같은 흐름으로 동작합니다.
- `$filename = "test.HTML"`
- `checkFilename()` 함수에서 확장자를 추출하는데, `HTML` 확장자는 `$dangerous_extensions` 변수에 존재하지 않아 통과
- `mb_strtolower()` 함수로 확장자를 소문자로 변경하고 설정 파일에 html 확장자가 정의 되어 있기 때문에 업로드 성공
위 취약점은 서로 다르게 확장자를 검증하고 있는 이슈 였습니다. 이러한 문제점을 악용하여 다음과 같이 HTML 파일을 업로드 하여 XSS를 트리거 했습니다.
취약점을 제보 했지만, 그쪽에서 일 처리를 이상하게 해서(?).. 음
아무튼 패치는 아래처럼 `checkFilename()` 함수에서 확장자를 소문자로 변경하도록 패치했습니다.
2.2 Frontmatter 검증 미흡으로 인한 IDOR
Grav CMS는 글 작성 시, 상세 설정을 위해 Frontmatter 기능을 제공하고 있습니다. 아래 공식 docs에서는 Frontmatter를 이용하여 작성하려는 페이지에 다양한 설정을 할 수 있습니다.
Frontmatter는 오직 admin 권한을 가진 유저만 사용할 수 있으며, 아래 사진처럼 글 쓰기에서 Expert를 선택하여 사용할 수 있습니다. 예를 들어, 페이지의 title 속성을 변경할 수 있거나 markdown 활성화 여부를 설정할 수 있습니다.
저는 여기서 의문이 들었습니다. admin만 사용할 수 있는 것인가?
확인을 위해 글 작성 권한이 있는 계정을 만들고 글 작성 했을 때의 요청 패킷을 서로 비교했습니다.
왼쪽 패킷은 admin이 Frontmatter에 설정을 작성하고 전송했을 때의 요청 패킷입니다. 오른쪽은 Frontmatter 기능을 쓸 수 없는 권한이고 작성 요청 했을 때의 패킷 입니다. 확인 결과 `data[_json][header][form]` 파라미터의 존재 유무가 있었습니다.
그래서 admin 요청 패킷에 있는 `data[_json][header][form]` 파라미터를 writer 요청 패킷에 추가하여 전달했습니다. 결과는 writer도 Frontmatter 기능을 사용할 수 있었습니다. 즉, 코드 상에서는 Frontmatter 기능의 권한 검증을 제대로 하지 않고 있는 것입니다.
위 취약점 또한 제보했지만, 글 작성 시점에도 여전히 패치 되지 않았습니다.
위 IDOR 취약점으로 뭘 할 수 있을지 공식 docs를 읽었습니다. 그러다가 Frontmatter 기능으로 Contact Form을 만들 수 있다는 것을 알게 되었습니다.
2.3 Contact Form을 악용하여 RCE
Frontmatter 기능으로 contact form을 만들 수 있습니다. 아래 공식 docs를 참고 바랍니다.
예를 들어, 아래처럼 Contact Form을 작성하면 이용자는 name을 입력할 수 있고 submit 버튼을 클릭하면 서버에는 `process` 에 정의되어 있는 것 처럼 `contact-{Ymd-His-u}.txt` 파일로 저장됩니다.
---
title: Contact Form
form:
name: contact
fields:
name:
label: Name
placeholder: Enter your name
autocomplete: on
type: text
validate:
required: true
buttons:
submit:
type: submit
value: Submit
process:
save:
fileprefix: contact-
dateformat: Ymd-His-u
extension: txt
---
# Contact form
Some sample page content
위 Frontmatter 설정 코드에서 `process` 의 `save` 속성에 관심이 갔습니다. 외부 이용자가 input 태그에 값을 적고 제출하면 `process` 의 `save` 속성에 정의 되어 있는 것처럼 서버에 파일이 저장되는 로직입니다.
해당 속성을 찾아보니 공식 docs에는 `filename` 이라는 속성이 있었습니다.
즉, 아래와 같이 작성하면 서버에 저장될 `filename` 값을 설정할 수 있는 것입니다. 과연 `filename` 속성 값을 코드 상에서는 검증을 제대로 확인하는지 확인해봤습니다.
process:
- save:
filename: feedback.txt
operation: add
아래는 위 로직의 코드 일부분을 가져온 것입니다. `filename` 파라미터에 대한 검증 없이 업로드를 처리하고 있습니다.
// https://github.com/getgrav/grav-plugin-form/blob/d84c57ba923fc557d362658b43e0c570efa0ccb4/form.php#L652-L717
case 'save':
$filename = $params['filename'] ?? '';
if (!$filename) {
if ($operation === 'add') {
throw new RuntimeException('Form save: \'operation: add\' is only supported with a static filename');
}
$filename = $prefix . $this->udate($format, $raw_format) . $postfix . $ext;
}
// Process with Twig
$filename = $twig->processString($filename, $vars);
$locator = $this->grav['locator'];
$path = $locator->findResource('user-data://', true);
$dir = $path . DS . $folder;
$fullFileName = $dir . DS . $filename;
$file = File::instance($fullFileName);
$file->lock();
$form->copyFiles();
아래 처럼 `filename` 속성 값을 `test.phar` 로 변경하고 writer 권한을 가진 이용자가 IDOR 취약점을 이용하여 Frontmatter 로 Contact Form을 작성합니다. 이후 입력 값에 php 코드를 작성하고 제출하면 아래처럼 서버에 test.phar 파일이 생성 및 공격자가 작성한 php 코드가 저장됩니다. 하지만, 꺽쇠가 필터링 되어 저장된 것을 볼 수 있습니다.
process:
- save:
filename: test.phar
operation: add
이를 해결하기 위해 공식 docs를 확인해본 결과, Contact Form은 기본적으로 XSS 방지를 위해 설정이 enable 되어 있다고 합니다. 이를 비활성화 하기 위해 `xss_check: false` 라고 작성하면 된다고 합니다.
최종 poc 코드는 아래 github repo에 작성되어 있습니다.
3. Timeline
- 2023.08.07 : Grav CMS 분석 시작
- 2023.08.09 : 다수의 취약점 발견
- 2023.08.17 : 3개의 취약점을 github security에 전달
- 2023.08.22 : 일부 취약점이 패치 되었지만, 실제로는 제대로 패치 되지 않음
지금 글 작성 시점에서 최신 버전에서는 될지 모르겠지만 (확인하기 귀찮..), 개발자는 지금까지 제대로 응답을 주지 않고 있습니다. 저는 여러번 확인을 부탁했지만, 그쪽에서는 응답을 주지 않아 이렇게 블로그에 취약점을 공개합니다.
'🔒Security' 카테고리의 다른 글
ctf 문제를 통해 CSP bypass 정리하기 (0) | 2023.03.13 |
---|---|
SSRF bypass using DNS Rebinding (0) | 2023.01.04 |
nodejs unicode (0) | 2022.10.06 |
[분석일기] - php switch case (0) | 2022.09.06 |
[분석 일기] - EJS, Server Side Template Injection to RCE (CVE-2022-29078) (6) | 2022.07.21 |
댓글
이 글 공유하기
다른 글
-
ctf 문제를 통해 CSP bypass 정리하기
ctf 문제를 통해 CSP bypass 정리하기
2023.03.13 -
SSRF bypass using DNS Rebinding
SSRF bypass using DNS Rebinding
2023.01.04 -
nodejs unicode
nodejs unicode
2022.10.06 -
[분석일기] - php switch case
[분석일기] - php switch case
2022.09.06