🗂️ INDEX

글 작성자: Universe7202

 

1. Grav CMS

Grav CMS는 php로 개발된 CMS 입니다.

Grav is a Fast, Simple, and Flexible, file-based Web-platform.
 

GitHub - getgrav/grav: Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig

Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig, and Symfony - GitHub - getgrav/grav: Modern, Crazy Fast, Ridiculously Easy and Amazingly P...

github.com

 

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()` 함수에서 확장자를 소문자로 변경하도록 패치했습니다.

https://github.com/getgrav/grav/commit/80ce87e4a936a3f055d306710cf21120671585ad

 

 

2.2  Frontmatter 검증 미흡으로 인한 IDOR

Grav CMS는 글 작성 시, 상세 설정을 위해 Frontmatter 기능을 제공하고 있습니다. 아래 공식 docs에서는 Frontmatter를 이용하여 작성하려는 페이지에 다양한 설정을 할 수 있습니다.

 

Headers / Frontmatter

Grav documentation

learn.getgrav.org

 

 

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를 참고 바랍니다.

 

Example: Contact Form

Grav documentation

learn.getgrav.org

 

예를 들어, 아래처럼 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` 이라는 속성이 있었습니다.

 

Reference: Form Actions

Grav documentation

learn.getgrav.org

 

 

즉, 아래와 같이 작성하면 서버에 저장될 `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` 라고 작성하면 된다고 합니다.

https://learn.getgrav.org/17/forms/forms/form-options#xss-checks

 

 

 

최종 poc 코드는 아래 github repo에 작성되어 있습니다.

 

GitHub - Universe1122/Grav-CMS-Remote-Code-Execution

Contribute to Universe1122/Grav-CMS-Remote-Code-Execution development by creating an account on GitHub.

github.com

 

 

 

3. Timeline

  • 2023.08.07 : Grav CMS 분석 시작
  • 2023.08.09 : 다수의 취약점 발견
  • 2023.08.17 : 3개의 취약점을 github security에 전달
  • 2023.08.22 : 일부 취약점이 패치 되었지만, 실제로는 제대로 패치 되지 않음

지금 글 작성 시점에서 최신 버전에서는 될지 모르겠지만 (확인하기 귀찮..), 개발자는 지금까지 제대로 응답을 주지 않고 있습니다. 저는 여러번 확인을 부탁했지만, 그쪽에서는 응답을 주지 않아 이렇게 블로그에 취약점을 공개합니다.