Translation
This post is about vulnerabilities I found in Japanese OSS softwares.
I've written the same content in other languages so the translator is not required for this content.
Please check my company's blog for more detailed information.
- Japanese: https://flattsecurity.hatenablog.com/entry/2020/10/26/150047
- English: https://flattsecurity.hatenablog.com/entry/2020/11/02/124807
Flatt Security Inc.Flatt Security Inc. provides security assessment services. We are willing to have offers from overseas.
If you have any question, please contact us by https://flatt.tech/en/. Thank you in advance for reading this article.
Note: ์ค์ํ์ง ์์ ๋ด์ฉ์ ์ ๋ถ ์ง์ ์ผ๋ ์ํด๋ฐ๋๋๋ค.
์๋ ํ์ธ์. ์ฃผ์ํ์ฌ Flatt Security์ stypr (@stereotype32) ์ ๋๋ค.
์์ ๊ณต๊ฐํ๋ ๊ธฐ์ฌ (https://flattsecurity.hatenablog.com/entry/2020/08/04/120000)์์ ์ธ๊ธํ ๋๋ก, ์ ๋ ํ์ฌ Flatt Security์์ 0day hunting๊ณผ ๊ธฐ์ ์ฐ๊ตฌ๋ฅผ ์ค์ ์ผ๋ก ์งํํ๊ณ ์์ต๋๋ค.
๋ณธ ๊ธฐ์ฌ์์๋ ์ ๊ฐ 5์์ ์ ์ฌ ํ ์ง๊ธ๊น์ง ๋ฐ๊ฒฌํ 0day๋ค์ ๋ํ ๊ธฐ์ ์ ์ธ ์ค๋ช ์ ํ๊ณ ์ ํฉ๋๋ค. ์ฌํ๊น์ง ์ฐพ์ ์ทจ์ฝ์ ์ด ๊ฝค ๋ง๊ธฐ ๋๋ฌธ์ ๋ณธ ๊ธ์ด ๋งค์ฐ ๊ธธ์ด์ง ์ ์์ผ๋ฏ๋ก, ํ์ฌ๊น์ง ํด๊ฒฐ๋ ์ทจ์ฝ์ ๋ค ์ค ๊ฐ์ธ์ ์ผ๋ก ํฅ๋ฏธ๋กญ๊ฒ ์๊ฐํ๋ ์ทจ์ฝ์ ๋ค์ ์์ฃผ๋ก ๊ธฐ์ ์ ์ธ ๋ถ๋ถ์ ์ค์ ์ผ๋ก ์ค๋ช ํ๋ ค ํฉ๋๋ค.
0day hunting ์ธ์๋ ๋ค๋ฅธ ์ฌ๋ฏธ๋ ์ ๋ฌด๋ค๋ ๋ณํํ๊ณ ์๊ธฐ ๋๋ฌธ์, 0day hunting์ ์ค์ ๋ก ์๋นํ ์๊ฐ์ ํ ์ ํ๋น ์ต์ 1์ผ ~ ์ต๋ 1 ์ฃผ ๋ด์ ๊ธฐ๊ฐ์ผ๋ก ์์ ํ๊ณ ์งํํ์์ต๋๋ค. ์ ์ฌ ํ ์ง๊ธ๊น์ง ์ด๋ฏธ ๋ง์ ์ทจ์ฝ์ ์ ์ฐพ์๊ณ , ์์ง ํจ์น๋์ง ์์ ์ทจ์ฝ์ ๋ ์๊ณ , ๋ชจ๋ ์ทจ์ฝ์ ์ ํ๋์ฉ ์๊ฐํ๋ ๊ฒ์ ์กฐ๊ธ ์ด๋ ต๊ธฐ ๋๋ฌธ์ ์ดํ์ ์ถ๊ฐ์ ์ธ ๊ธ์ ์์ฑํ ์์ ์ ๋๋ค.
์ฐ์ ์ต์ด ์ฐ๊ตฌ ํ๊ฒ์ ์ผ๋ณธ ๋ด์์ ์์ฃผ ์ฌ์ฉํ๊ณ ์๋ ์คํ์์ค ์ ํ๋ค์ ์์ฃผ๋ก ์งํํ์์ต๋๋ค. ์ด๋ ์ผ๋ณธ์์ ๊ฐ๋ฐ๋ ์คํ์์ค ์ ํ์ด ๊ต์ฅํ ๋ง๋ค๋ ์ ๊ณผ ์ ๊ฐ ์ฌ๋ฌ ํ๊ฒฝ์์ ํ ์คํธํ ์ ์๋ค๊ณ ์ค์ค๋ก ํ๋จํ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
๋ณธ ๊ธ์์๋ ํ์ฌ๊น์ง ๋ณด๊ณ ํ ์ทจ์ฝ์ ์ ์ผ๋ถ๋ฅผ ์๊ฐํ๊ณ ์์ต๋๋ค. ๋ณด๊ณ ์ ์ค์ ๋ก ์ ์ฉ์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ ์ฆ๋ช ํ๊ธฐ ์ํด ์ฆ๋ช ์ฝ๋(์ดํ PoC)๋ฅผ ์์ฑํ์์ผ๋, ๊ฐ ์ทจ์ฝ์ ์ ์ํ์ฑ์ ๊ณ ๋ คํ์ฌ PoC ์ฝ๋๋ ๋ฐ๋ก ๊ฐ์ ํ์ง ์๊ณ ์ทจ์ฝ์ ๋ค์ ๋ํด์๋ PoC ์ฝ๋๊ฐ ์คํ๋์์ ๋๋ฅผ ๋ นํํ ์์์ผ๋ก ๋์ฒดํ์์ต๋๋ค. ์ด ๋ถ๋ถ์ ๋ํด์๋ ๋๊ทธ๋ฝ๊ฒ ์ดํดํด ์ฃผ์ จ์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค.
์ทจ์ฝ์ ๋ค์ ์ ๋ณดํ๋๋ฐ ํ๋ ฅํด์ฃผ๊ณ ์๋ง๊ฒ ๋์ํด์ฃผ์ ๊ฐ ์ ํ์ ๊ฐ๋ฐ์๋ถ๋ค๊ณผ JPCERT/CC ๋ด๋น์ ๋ถ๋ค, ๊ทธ๋ฆฌ๊ณ ์ ๋ก๋ฐ์ด๋ฅผ ๋ณด๊ณ ํ๋ ๊ณผ์ ์์ ๋ง์ ๋์์ ์ฃผ์ ์ฌ๋ด ์ฌ๋ฌ๋ถ๊ป ๊ฐ์ฌ์ ๋ง์์ ๋๋ฆฝ๋๋ค.
BaserCMS๋ ์์ ๋กญ๊ฒ ์น์ฌ์ดํธ๋ฅผ ์์ฑํ ์ ์๋ ์ผ๋ณธ ์คํ์์ค CMS ํ๋ซํผ์ ๋๋ค. CakePHP์์ ๊ตฌํ๋ CMS์ด๋ฉฐ, ์๋ ๊ฐ ๊พธ์คํ ๊ฐ๋ฐ์๋ค์ด ์ปจํธ๋ฆฌ๋ทฐํธ๋ฅผ ํ๊ณ ์์ด ๋ง์ ์ฌ์ดํธ์์ BaserCMS๋ฅผ ์ฌ์ฉ ์ค์ ์์ต๋๋ค.
ํด๋น ์ทจ์ฝ์ ์ ๋ค์๊ณผ ๊ฐ์ ์ ์ ์กฐ๊ฑด์ด ํ์ํฉ๋๋ค.
์ฐ์ Cross-Site Scripting(XSS)์ ๋ํด ๋จผ์ ์ค๋ช ํ๋๋ก ํ๊ฒ ์ต๋๋ค. ์ผ๋ฐ์ ์ธ XSS ์ทจ์ฝ์ ์ ์ฐพ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๊ฒ ์ง๋ง, ์ผ๋ฐ์ ์ธ ๊ฒฝ์ฐ ์ฌ์ฉ์์ ์ ๋ ฅ์ ์ ๋๋ก sanitize๋ฅผ ํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ฐ์ํ๋ sanitize๋ฅผ ์ ๋๋ก ์ํ๋์ง๋ฅผ ํ์ธํ๋ ํํ๋ก ์ ๊ทผํ๋ฉด ์ข์ต๋๋ค.
์๋ฅผ ๋ค์ด, app/webroot/theme/admin-third/ThemeFiles/admin/index.php:48 ๋ฅผ ๋ณด์๋ฉด ๋ค์๊ณผ ๊ฐ์ด currentPath์ ๋ํด echo๋ฅผ ํ๊ณ ์์ต๋๋ค.
<div class="em-box bca-current-box"><?php echo __d('baser', '็พๅจใฎไฝ็ฝฎ') ?>๏ผ<?php echo $currentPath ?>...
currentPath๋ฅผ ํธ์ถํ๋ ๋ถ๋ถ์ ์ถ์ ํด๋ณด๋ฉด lib/Baser/Controller/ThemeFilesController.php:171 ์์ ๊ฐ์ง๊ณ ์ค๋๋ฐ, ์ ๋ ฅ๋ฐ์ ๊ฒฝ๋ก์ ๋ํด ํน๋ณํ sanitize๋ฅผ ํ๊ณ ์์ง ์๊ธฐ ๋๋ฌธ์ XSS๊ฐ ๋ฐ์ํฉ๋๋ค.
public function admin_index() {
...
$args = $this->_parseArgs(func_get_args());
extract($args);
...
$currentPath = str_replace(ROOT, '', $fullpath);
$this->subMenuElements = ['theme_files'];
$this->set('themeFiles', $themeFiles);
$this->set('currentPath', $currentPath);
$this->set('fullpath', $fullpath);
...
$this->help = 'theme_files_index';
...
}
...
๊ณ์ํด์ Remote Code Execution ์ทจ์ฝ์ ์ ๋ํด ์ค๋ช ํ๊ฒ ์ต๋๋ค. ์ผ๋ฐ์ ์ธ PHP ์ ํ์ ๊ฒฝ์ฐ, PHPํ์ผ์ ์์์ ์ผ๋ก ์ ๋ก๋๋ฅผ ํ๊ณ ์ ๋ก๋๋ PHPํ์ผ์ ๊ฒฝ๋ก๋ฅผ ํ์ ํ๊ณ ์์ผ๋ฉด, ๊ณต๊ฒฉ์๊ฐ ์ํ๋ ์์์ ์ฝ๋๋ฅผ ์คํํ ์ ์๊ธฐ ๋๋ฌธ์ PHP ํ์ผ์ ์์์ ์ผ๋ก ์ ๋ก๋ํ ์ ์์ต๋๋ค.
lib/Baser/Plugin/Uploader/Controller/UploaderFilesController.php:291 ๋ฅผ ๋ณด์๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๊ฐ ์์ต๋๋ค.
public function admin_ajax_upload() {
...
$this->layout = 'ajax';
Configure::write('debug',0);
...
$user = $this->BcAuth->user();
if(!empty($user['id'])) {
$this->request->data['UploaderFile']['user_id'] = $user['id'];
}
$this->request->data['UploaderFile']['file']['name'] = str_replace(['/', '&', '?', '=', '#', ':'], '', h($this->request->data['UploaderFile']['file']['name']));
$this->request->data['UploaderFile']['name'] = $this->request->data['UploaderFile']['file'];
$this->request->data['UploaderFile']['alt'] = $this->request->data['UploaderFile']['name']['name'];
$this->UploaderFile->create($this->request->data);
...
if($this->UploaderFile->save()) {
echo true;
}
...
...
}
์ฌ๊ธฐ์ ํ์ผ ๋ช ์ ๋ํ ๊ฒ์ฆ์ ํ๊ณ ์์ง๋ง, ํ์ฅ์์ ๋ํ ๊ฒ์ฆ์ ํ๊ณ ์์ง ์์ต๋๋ค.
๋์ฑ ๋ ์ธ๋ถ์ ์ธ ํ์ธ์ ํ๊ธฐ ์ํด $this->UploaderFile ์ ํ์ธํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ ๋ณผ ์ ์์ต๋๋ค.
class UploaderFile extends AppModel {
...
public function __construct($id = false, $table = null, $ds = null) {
...
if(!BcUtil::isAdminUser()) {
$this->validate['name'] = [
'fileExt' => [
'rule' => ['fileExt', Configure::read('Uploader.allowedExt')],
'message' => __d('baser', '่จฑๅฏใใใฆใใชใใใกใคใซๅฝขๅผใงใใ')
]
];
}
parent::__construct($id, $table, $ds);
...
}
}
ํ์คํ ํ์ฅ์ ๊ฒ์ฆ์ ํ๊ณ ์์ง๋ง, Admin ๊ณ์ ์ด ์๋๋๋ง ์๋ํ๊ณ ์์ต๋๋ค. ์ฆ, ๊ด๋ฆฌ์์๊ฒ XSS๋ฅผ ์ฑ๊ณตํ๋ ๊ฒฝ์ฐ ํ์ผ ๊ฒ์ฆ์ ๋ฌด์ํ์ฑ ์ ๋ก๋๋ฉ๋๋ค.
์๋ฐ์คํฌ๋ฆฝํธ์์ ํ์ผ ์ ๋ก๋๊ฐ ํ์ํ ๊ฒฝ์ฐ FormData ํด๋์ค์์ Blob Object ๋ฅผ ์ด์ฉํ๋ฉด ํ์ผ ์ ๋ก๋๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
์ด๋ฅผ ํตํด XSS๋ฅผ ํตํด ์์์ ์ธ ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋ ์ ์๋ ์ํฉ์์ ์์์ ์ธ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ์ฌ ์ ๋ก๋๋ฅผ ํ ์ ์์ต๋๋ค.
...
filename = Math.floor(Math.random() * Math.floor(13371337)) + 'exploit.php';
...
var blob = new Blob(["[email protected]<pre><?php $_GET[cmd]($_GET[arg]); ?></pre>"]);
var fd = new FormData();
fd.append('data[_Token][key]', token);
fd.append('data[UploaderFile][file]', blob, filename);
// Upload XHR Request
...
ํด๋น ๊ณต๊ฒฉ์ด ์๋ํ๋์ง์ ๋ํ PoC๋ ๋ค์ GIF๋ก ๋์ฒดํฉ๋๋ค.
https://i.imgur.com/J3ZnpZg.gif
๊ฐ๋ฐ์๋ถ๊ป์ ๋น ๋ฅด๊ฒ ๋์ํ์ จ๊ณ , CVE ํ๋ก์ธ์ค๋ ๋ง์น ์ํ์ ๋๋ค.
https://basercms.net/security/20200827
EC-Cube๋ ์ผ๋ณธ์์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๊ณ ์๋ค๋ EC ์ฌ์ดํธ ์คํ์์ค์ ๋๋ค.
Symfony๋ก ๊ตฌํ๋ ์ ํ์ด๋ฉฐ , ์ด ์ ํ ๋ํ ๊พธ์คํ ๊ฐ๋ฐ๋๊ณ ์์ต๋๋ค. ํ๊ฒ์ ์ ํ๋ ๋์ค udon์จ๊ฐ ๋ค์ ํ๊ฒ์ผ๋ก ์ถ์ฒํด์ฃผ์ ์ ์งง์ ๊ธฐ๊ฐ์ด์ง๋ง ๊ฒํ ํด๋ณด๊ฒ ๋์์ต๋๋ค.
ํด๋น ์ทจ์ฝ์ ์ ๋ค์๊ณผ ๊ฐ์ ์ ์ ์กฐ๊ฑด์ด ํ์ํฉ๋๋ค.
Unauthenticated ํ๊ฒฝ์ด APP_DEBUG=1 ์ด์ด์ผ ํฉ๋๋ค. * ์ผ๋ฐ์ ์ธ Docker๋ก ์ค์น๋ฅผ ํตํด ์ฌํ์ด ๊ฐ๋ฅํฉ๋๋ค.
Authenticated์ ๊ฒฝ์ฐ ๊ด๋ฆฌ์ ๊ถํ์ ๊ฐ์ง๊ณ ์์ด์ผ ํฉ๋๋ค.* ์ด๋ XSS์ ๊ฐ์ ์ทจ์ฝ์ ์ ํตํด์ ์ถ๊ฐ์ ์ผ๋ก ๊ณต๊ฒฉํ ์ ์์ต๋๋ค.
Unauthenticated์ ๊ฒฝ์ฐ๋ฅผ ๋จผ์ ์ค๋ช ํ๊ฒ ์ต๋๋ค. ๋จผ์ APP_DEBUG=1์ด๋ผ๋ ์ ์ ์กฐ๊ฑด์ด ํ์ํฉ๋๋ค๋ง, ๊ธฐ๋ณธ์ ์ผ๋ก EC-Cube์ ๊ณต์ ์ค์น ๊ฐ์ด๋๋ฅผ ํตํด Docker ํํ๋ก ์ค์นํ๊ฒ ๋๋ฉด DEBUG ๋ชจ๋๋ ์๋์ผ๋ก ํ์ฑํ ๋ฉ๋๋ค.
$ # https://doc4.ec-cube.net/quickstart_install#3dockerใไฝฟ็จใใฆใคใณในใใผใซใใ
$ git clone https://github.com/ec-cube/ec-cube
...
$ cd ec-cube; git checkout c1dbe4267e1a3f353522835a22793aa278f42ef3 # ์ทจ์ฝ์ ์ ๋ณด ์์
Note: switching to 'c1dbe4267e1a3f353522835a22793aa278f42ef3'.
...
$ docker build -t eccube4-php-apache .
Sending build context to Docker daemon 23.16MB
Step 1/21 : FROM php:7.3-apache-stretch
...
$ docker run --name ec-cube -p "8080:80" -p "4430:443" eccube4-php-apache
...
^C
$ docker start ec-cube
ec-cube
$ docker exec -it ec-cube bash
[email protected]:/var/www/html# cat .env | grep DEBUG
APP_DEBUG=1
์ฐ์ ์์ ๊ฐ์ด ์๋ ๋๋ ๊ฒ์ ํ์ธํ์ผ๋ฉด, http://[๋ณธ์ธ_IP]:8080/ ๋ก ์ ์ํฉ๋๋ค.
์ ์คํฌ๋ฆฐ์ท์ ์ ๋ณด์๋ฉด ์ฐ์ธก ํ๋จ์ sf ๋ผ๋ ๋ก๊ณ ๊ฐ ๋ณด์
๋๋ค. ๋๋ฌ์ sf๊ฐ ์๊ธด ๋งํฌ๋ฅผ ํ๋ฒ ๋ ๋๋ฅด๋ฉด ๋ค์๊ณผ ๊ฐ์ ํ์ด์ง๊ฐ ๋์ต๋๋ค.. ๊ฐ๋ sf ์ด๋ฏธ์ง๊ฐ ์๋์ค๋ ๊ฒฝ์ฐ๋ ์์ผ๋ ๋ฐ๋ก /_profiler/
๋ก ์ ์ํฉ๋๋ค.
์๋ Symfony Profiler๋ผ๊ณ ํ๋ ๊ธฐ๋ฅ์ธ๋ฐ, ๊ด๋ จ ์๋ฃ๊ฐ ์ธํฐ๋ท์์ ์ ์๋์ค๋ ํธ์ ๋๋ค. ์ด ๊ธฐ๋ฅ์ ๋ํด ๋จ์ํ ์์ฝํด์ ์ค๋ช ํ์๋ฉด Symfony๋ก ๊ฐ๋ฐํ๋ ์ ํ์์ ๋ฌธ์ ๊ฐ ์๊ฒผ์๋ ๋๋ฒ๊น ์ ํธ๋ฆฌํ๊ฒ ํด์ฃผ๋ ๋๊ตฌ์ ๋๋ค. ๋ฌผ๋ก ์ด ๊ธฐ๋ฅ์ ๋๋ฒ๊ทธ ๋ชจ๋๊ฐ ํ์ฑํ ๋์์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
Symfony ์์ฒด๋ ๊ฝค ์์ ํ ํ๋ ์์ํฌ์ด์ง๋ง, ์ด ๊ธฐ๋ฅ์ด ํ์ฑํ ๋ ์ ๋ณด์์์ผ๋ก ์ ๋ง ์ทจ์ฝํด์ง๋๋ค. ์๋ฅผ ๋ค์ด, Profiler๋ฅผ ์๋์ ๊ฐ์ด Profile Search๋ผ๋ ๊ธฐ๋ฅ์ด ์์ต๋๋ค.
์์์ ๋ณผ ์ ์๋ฏ, ์ ๊ฐ ์ก์ ํ ์์ฒญ์ ์ ๋ถ ์ด๋ํ ์ ์์ต๋๋ค. Token์ ๋๋ฌ๋ณด๋ฉด ๋ค์ ์คํฌ๋ฆฐ์ท๊ณผ ๊ฐ์ ํ์ด์ง๊ฐ ๋์ต๋๋ค. ์ฌ๊ธฐ๋ฅผ ์์ธํ ๋ณด๋ฉด POST ํ๋ผ๋ฏธํฐ์ ๋ํ ๊ฐ์ ์ฝ์ ์ ์์ต๋๋ค. ์ด ๊ธฐ๋ฅ์ ํตํด ๊ด๋ฆฌ์๋ฐ ์ฌ์ฉ์์ ๊ณ์ ์ ๋ณด๋ฅผ ํ์ทจํ ์ ์์ต๋๋ค.
์ด์ ์ ์ทจ์ฝ์ ์ ํตํด ๊ด๋ฆฌ์ ๊ณ์ ์ผ๋ก ์ ์์ ์ผ๋ก ๋ก๊ทธ์ธ ํ ์ ์๋ค๊ณ ๊ฐ์ ํ๊ณ Remote Code Execution์ ๋ํด ์ค๋ช ํ๊ฒ ์ต๋๋ค. ์ด์ ๊ด๋ฆฌ์๋ก ๋ก๊ทธ์ธ์ ํด์ ์ฌ์ดํธ๋ฅผ ๋๋ฌ๋ค๋ณด๋ฉด, ํ์ผ ๊ด๋ฆฌ๋ผ๋ ๊ธฐ๋ฅ์ด ์์ต๋๋ค.
์์ BaserCMS์์ ์ค๋ช ํ ๊ฒฝ์ฐ์ ๋ง์ฐฌ๊ฐ์ง์ ๋๋ค. PHP ๊ธฐ๋ฐ ์ ํ์์๋ PHP ํ์ฅ์์ ๊ฐ์ PHPํ์ผ ์ ๋ก๋๋ฅผ ์ฑ๊ณตํ ๋ค์, PHP ํ์ผ ๊ฒฝ๋ก๋ฅผ ์์๋ด์ด ์ ์ํ๋ฉด ์์์ ์ฝ๋๋ฅผ ์คํํ ์ ์์์ต๋๋ค.
src/Eccube/Controller/Admin/Content/FileController.php:52๋ฅผ ํ์ธํด๋ณด๋ฉด ์ด๋์ ๋ ๋ณด์ ์กฐ์น๋ ํ๊ณ ์์ง๋ง, ($jaiNowDir ๋ฑ์ ํ์ธํ๋ฉด ์ ์ ์์ต๋๋ค.) ์ค์ ๋ก ํ์ฅ์์ ๋ํ ํ์ธ์ ์์ด ๊ทธ๋๋ก ํ์ผ์ ์ด๋ํด๋ฒ๋ฆฝ๋๋ค.
public function index(Request $request)
{
$form = $this->formFactory->createBuilder(FormType::class)
->add('file', FileType::class, [
'multiple' => true,
'attr' => [
'multiple' => 'multiple'
],
])
->add('create_file', TextType::class)
->getForm();
// user_data_dir
$userDataDir = $this->getUserDataDir();
$topDir = $this->normalizePath($userDataDir);
...
$htmlDir = $this->normalizePath($this->getUserDataDir().'/../');
...
$nowDir = $this->checkDir($this->getUserDataDir($request->get('tree_select_file')), $this->getUserDataDir())
? $this->normalizePath($this->getUserDataDir($request->get('tree_select_file')))
: $topDir;
...
$nowDirList = json_encode(explode('/', trim(str_replace($htmlDir, '', $nowDir), '/')));
$jailNowDir = $this->getJailDir($nowDir);
$isTopDir = ($topDir === $jailNowDir);
$parentDir = substr($nowDir, 0, strrpos($nowDir, '/'));
...
if ('POST' === $request->getMethod()) {
switch ($request->get('mode')) {
...
case 'upload':
$this->upload($request);
break;
...
}
}
...
}
...
public function upload(Request $request)
{
$form = $this->formFactory->createBuilder(FormType::class)
->add('file', FileType::class, [
'multiple' => true,
'constraints' => [
new Assert\NotBlank([
'message' => 'admin.common.file_select_empty',
]),
],
])
->add('create_file', TextType::class)
->getForm();
$form->handleRequest($request);
...
if (!$form->isValid()) {
foreach ($form->getErrors(true) as $error) {
$this->errors[] = ['message' => $error->getMessage()];
}
return;
}
...
$data = $form->getData();
$topDir = $this->getUserDataDir();
$nowDir = $this->getUserDataDir($request->get('now_dir'));
...
foreach ($data['file'] as $file) {
$filename = $this->convertStrToServer($file->getClientOriginalName());
try {
$file->move($nowDir, $filename);
$successCount ++;
...
private function getUserDataDir($nowDir = null)
{
return rtrim($this->getParameter('kernel.project_dir').'/html/user_data'.$nowDir, '/');
}
์ ๊ธฐํ๊ฒ๋ ์ด๋์ ๋ ๋ณด์์ด๋ผ๋ ๊ฒ์ ํ๊ธฐ ์ํด .htaccess ํ์ผ๋ ์์ฑ๋์ด ์์ง๋ง composer, docker, ์ค์ ํ์ผ๋ค๋ง ํ์ธํ๋ ๊ฒ์ผ๋ก ๊ทธ์น๊ณ ์์ต๋๋ค.
<FilesMatch "^composer|^COPYING|^\.env|^\.maintenance|^Procfile|^app\.json|^gulpfile\.js|^package\.json|^package-lock\.json|web\.config|^Dockerfile|\.(ini|lock|dist|git|sh|bak|swp|env|twig|yml|yaml|dockerignore)$">
order allow,deny
deny from all
</FilesMatch>
์ด๋ฅผ ํตํด ๊ด๋ฆฌ์ ๊ถํ์์ PHP ํ์ผ์ ์ ๋ก๋ํ์ฌ http://[host]/user_data/[ํ์ผ๋ช ].php๋ก ํ์ผ์ ์ ๋ก๋ํ ์ ์์ต๋๋ค.
์๋์ ๊ฐ์ ๋ฃจํด์ผ๋ก ๊ตฌํํ๋ฉด PoC๊ฐ ์์ฑ๋ฉ๋๋ค.
Symfony Profiler๋ฅผ ํตํด admin ํด๋์ directory, ๊ด๋ฆฌ์์ ID, PW๋ฅผ ๊ฐ๋ก์ฑ๋๋ค.
๊ฐ๋ก์ฑ ๊ณ์ ์ ๋ณด๋ฅผ ํตํด admin์ผ๋ก ๋ก๊ทธ์ธ ํ ๋ค์, PHP ํ์ผ์ ์ ๋ก๋ ํฉ๋๋ค.
html/user_data/์ ์ ๋ก๋๋ ํ์ผ์ ์ง์ URL๋ก ๋ถ๋ฌ์ค๋ฉด PHP ์ฝ๋๊ฐ ์คํ๋ฉ๋๋ค.
ํด๋น ๊ณต๊ฒฉ์ด ์๋ํ๋์ง์ ๋ํ PoC๋ ๋ค์ ์์์ผ๋ก ๋์ฒดํฉ๋๋ค.
https://www.youtube.com/watch?v=w6otERfcAww
์ค์ ๋ก ํด๋น ์ทจ์ฝ์ ์ ๋ํด ๊ฐ๋ฐ์์ ์ค๋๊ธฐ๊ฐ ์๊ฒฌ์ ๋๋์๊ณ , ๊ฐ๋ฐ์๊ฐ ๊ฐ ์ฒ์ ๋ฌธ์ํ๊ณ ํ์๋ฅผ ํ ๊ฒฐ๊ณผ ๊ฐ๋ฐ์๋ ์ทจ์ฝ์ ์ผ๋ก ์ธ์ ํ์ง ์๊ธฐ๋ก ํ๋จํ์์ต๋๋ค. ์ด์ ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
PHP ํ์ผ์ด ๊ด๋ฆฌ์ ๊ถํ์์ ์ ๋ก๋ ๋๋ ์ ์ EC-Cube์ ์์ฒด ์ฌ์
์ด๊ธฐ์ค์ ์ด ๋๋ฒ๊ทธ ๋ชจ๋๊ฐ ๋๋ ์ ์ ์ ํํ์์๋ ๋ฐ์ํ์ง ์๊ธฐ ๋๋ฌธ์ ์ทจ์ฝ์ ์ด ์๋๋ผ๋ ํ๋จ
์ด๋ฏธ ๋์ ์กฐ์น๋ฅผ ์ํด ํ๊ฒฝ์ค์ ์ ๊ดํ ๊ฐ์ด๋ ๊ณต์ , ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ, ๋ณด์ ๊ด๋ จ ํ๋ฌ๊ทธ์ธ ๊ณต์ ๋ฐ ๊พธ์คํ ์ฃผ์ ํ๊ธฐ๋ฅผ ํ๊ณ ์์https://www.ec-cube.net/products/detail.php?product_id=2040https://doc4.ec-cube.net/environmental_setting
์ค์ ๋ก ์ธํฐ๋ท ์์ ํด๋น ์ทจ์ฝ์ ์ ํตํด ํผํด๋ฐ์ ์ ์๋ ์ฌ์ดํธ๊ฐ ๋ค์ ์กด์ฌํ๊ณ ์๊ณ (์ด๋ฏธ ํผํด๋ฅผ ๋ฐ์ ์ฌ์ดํธ๋ ์๊ฒ ์ง๋ง), ์์ ์๊ฐํ BaserCMS์ ์ทจ์ฝ์ ์ ๊ฒฝ์ฐ ์์ ๊ฐ์ ์ํฉ์์ PHP ํ์ผ ์ ๋ก๋๊ฐ ์๋๋๋ก ์์ ํ๋ ์์ ๋์์ ์ทจํ๊ธฐ ๋๋ฌธ์, ๊ต์ฅํ ๋นํฉ์ค๋ฌ์ ๊ณ ๋ฉ๋ํ๊ธฐ ์ด๋ ค์ ์ต๋๋ค. ๊ฐ๋ฐ์๋ค๋ ๋ณด์ ์กฐ์น๋ฅผ ์ํด .htaccess ์์ฑ๋ฑ ๊ฐ๋ฐ์๊ฐ ๋ง์ ๋ ธ๋ ฅ์ ํ์์๋ ๋ง์ ๋๋ค.
๊ฒฐ๊ตญ EC-Cube ๊ฐ๋ฐ์ฌ๊ฐ ์ฃผ๋ณํ๊ธฐ ๋ฐ ์๋ง์ ๋์์ ๊พธ์คํ ํด๋๊ฐ๊ธฐ๋ก ํ์๊ณ , ์ ๋ณดํ ์ทจ์ฝ์ ์ PoC๋ฅผ ์ ์ธํ ์ด๋ ์ ๋์ ์ ๋ณด๋ฅผ ๊ณต๊ฐํ ์ ์๋ค๋ ์กฐ๊ฑด ํ์ Close ํ์์ต๋๋ค.
ํ์ฌ๋ก์จ๋ ํด๋น ์ทจ์ฝ์ ์ ๋์ํ๊ธฐ ์ํด localhost๋ฅผ ์ ์ธํ ์ด๋ ํ๊ฒฝ์์๋ debug๊ฐ ํ์ฑํ๋์ง ์๋๋ก ์ฌ๋ฌ๋ฒ ํ์ธํ๋ ๋ฐฉ๋ฒ ๋ฟ์ ๋๋ค.
์ ๋ณด์์ ์ค๊ฐ ๋ด์ฉ์ ๋ํ ์์ธํ ์ ๋ณด๋ ์ ๊ฐ์ธ ๋ธ๋ก๊ทธ์์ ์์ฑํ์ผ๋ ๊ถ๊ธํ์ ๋ถ๋ค์ ์ด์ชฝ์์ ํ์ธํด์ฃผ์ธ์.
https://blog.harold.kim/2020/09/unauthenticated-authenticated-remote-code-execution-in-ec-cube
SOY CMS๋ ๋ธ๋ก๊ทธ์ ์ธํฐ๋ท ์ผํ๋ชฐ์ ๊ตฌ์ถ ํ ์์๋ ์์ ๋๊ฐ ๋์ CMS (์ฝํ ์ธ ๊ด๋ฆฌ ์์คํ )์ ๋๋ค. ์คํ ์์ค ์ํํธ์จ์ด๋ก ๊ณต๊ฐํ๊ณ ์๊ธฐ ๋๋ฌธ์ ๋ฌด๋ฃ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ธํฐ๋ท์ ๊ฒ์ํ๋ ๋์ค ์ฐ์ฐํ ๋ฐ๊ฒฌํ ์ ํ์ด์ง๋ง, ๋ญ๊ฐ ์๋ช ์ผ์ค๋ ๊ทธ๋ ๊ณ ์์ง๊น์ง ๊พธ์คํ ๊ฐ๋ฐ๋๊ณ ์๋ ์ ํ์ด๋ผ ํ๋จํ์ฌ ๋์ ํด๋ณด๊ธฐ๋ก ํ์์ต๋๋ค.
์ ์ ์กฐ๊ฑด์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ฐ์ Cross-Site Scripting์ ๋ํด ์ค๋ช ํ๊ฒ ์ต๋๋ค.
์ฐ์ ์ฝ๋๋ฅผ ๋ถ์ํ๋ค ๋ณด๋ฉด cms/app/webapp/inquiry/admin.php์์ ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ๋ณด๊ฒ ๋ฉ๋๋ค.
class SOYInquiryApplication{
...
function init(){
$level = CMSApplication::getAppAuthLevel();
...
CMSApplication::main(array($this,"main"));
...
}
...
function main(){
...
$arguments = CMSApplication::getArguments();
...
foreach($arguments as $key => $value){
if(is_numeric($value)){
$flag = true;
}
if($flag){
$args[] = $value;
}else{
$classPath[] = $value;
}
}
$path = implode(".",$classPath);
$classPath = $path;
...
if(preg_match('/^Help/',$classPath)){
CMSApplication::setActiveTab(4);
}
if(!SOY2HTMLFactory::pageExists($classPath)){
return $classPath;
}
...
}
$app = new SOYInquiryApplication();
$app->init();
page๊ฐ ์กด์ฌํ์ง ์์ผ๋ฉด $classPath๋ฅผ ๋ฐ๋ก ๋ฆฌํดํ๋ ๊ฒ์ด ๋ณด์ฌ ๋ฌด์ธ๊ฐ์ ์์ํจ์ ๋๋ผ๊ณ ์ด๊ฒ์ ๊ฒ ์๋ํด๋ณธ ๊ฒฐ๊ณผ XSS๊ฐ ๋ฐ๊ฒฌ๋์์ต๋๋ค. Root Cause๋ฅผ ์ฐพ์๋ณด๊ธฐ ์ํด ์ด ํจ์๋ฅผ ํธ์ถ์์ ์ถ๋ ฅํ๊ธฐ ๊น์ง์ ๋ฃจํด์ ์์๋๋ก trace ํด๋ณด์์ต๋๋ค.
ํ๋ก๊ทธ๋จ์ด ์คํ๋ ๋ ์ฒ์์๋ app/index.php ์์ CMSApplication::run() ์ผ๋ก ์์ํฉ๋๋ค.
<?php
define("CMS_APPLICATION_ROOT_DIR", dirname(__FILE__) . "/");
define("CMS_COMMON", dirname(dirname(__FILE__)) . "/common/");
include_once(dirname(__FILE__)."/webapp/base/config.php");
try{
//ใขใใชใฑใผใทใงใณใฎๅฎ่ก
CMSApplication::run();
//่กจ็คบ
CMSApplication::display();
}catch(Exception $e){
$exception = $e;
include_once(CMS_COMMON . "error/admin.php");
}
์ดํ์, CMSApplication ๋ฅผ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ๋ณด๊ฒ ๋ฉ๋๋ค.
class CMSApplication {
...
public static function run(){
$self = CMSApplication::getInstance();
$self->root = SOY2PageController::createRelativeLink("./");
....
//pathinfoใใใขใใชใฑใผใทใงใณIDใๅๅพ
$pathinfo = (isset($_SERVER["PATH_INFO"])) ? $_SERVER["PATH_INFO"] : "";
...
$paths = array_values(array_diff(explode("/",$pathinfo),array("")));
if(count($paths)<1){
SOY2PageController::redirect("../admin/");
exit;
}
$self->applicationId = $paths[0];
$self->arguments = array_slice($paths,1);
...
$cacheDir = dirname(dirname(dirname(__FILE__)))."/cache/".$self->applicationId."/";
...
//ใขใใชใฑใผใทใงใณใฎ่ชญใฟ่พผใฟ
include_once($base . $self->applicationId . "/admin.php");
...
$self->application = call_user_func($self->appMain);
}
public static function display(){
$self = CMSApplication::getInstance();
include_once(dirname(__FILE__) . "/" . $self->mode . ".php");
}
public static function main($func){
$obj = CMSApplication::getInstance();
$obj->appMain = $func;
}
...
public static function display(){
$self = CMSApplication::getInstance();
include_once(dirname(__FILE__) . "/" . $self->mode . ".php");
}
...
}
$classPath
๊ฐ ๋๋ฌํ๋ ์ง์ ๊น์ง์ ์ฝ๋๊ฐ ์คํ๋๋ ๊ณผ์ ์ ์์๋๋ก ๋์ดํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ต์ด ํ๋ก๊ทธ๋จ์ด ์คํ๋ ๋๋ app/index.php์์ ์์ํ๋ฉฐ CMSApplication::run(); ์ด ์คํ๋ฉ๋๋ค. run() ํจ์๊ฐ ์์ํ๋ ์์ ์์๋ถํฐ ์ค์ํ ๋ถ๋ถ๋ง ๋์ดํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
$pathinfo = $_SERVER["PATH_INFO"]
๋ ํ์ฌ ์คํํ๋ ํ์ผ์ ๊ฒฝ๋ก์
๋๋ค.์๋ฅผ ๋ค์ด http://hoge.com/index.php/inquiry/blah
๋ฉด $pathinfo
๋ /1/2 ๊ฐ ๋ฉ๋๋ค.
$pathinfo ๋ฅผ ํตํด $self->arguments , $self->applicationId ๋ฑ์ ๊ฐ์ด ์ถ๊ฐ๋ฉ๋๋ค.
์ ๋ ฅ๋ฐ์ $self->applicationId๋ฅผ ํตํด $base . "inquiry/admin.php" ๊ฐ include ๋ฉ๋๋ค.
SOYInquiryApplication์ ์ฝ๋์ ์ตํ๋จ์ ๋ณด๋ฉด $app = new SOYInquiryApplication(); $app->init(); ์ด ์คํ๋ฉ๋๋ค.
$app->init() ์ด ์คํ๋๋ ๊ณผ์ ์ ๋ณด๋ค๋ณด๋ฉด CMSApplication::main(array($this,"main")); ์ ํธ์ถํฉ๋๋ค.
์ดํ ๋ง์ง๋ง์ $self->application = call_user_func($self->appMain); ๋ฅผ ํตํดSoyInquiryApplication->main()์ ํธ์ถํ๊ฒ ๋ฉ๋๋ค.
class๋ $arguments = CMSApplication::getArguments(); ๋ฅผ ๊ฐ์ง๊ณ $classPath๋ฅผ ์์ฑํฉ๋๋ค.
ํ์ง๋ง ์๊น ์ธ๊ธํ๋๋ก ์กด์ฌํ์ง ์๋ page์ธ ๊ฒฝ์ฐ return $classPath๊ฐ ์์ผ๋ฏ๋ก ๋ฐ๋ก ๋ฆฌํดํฉ๋๋ค.
๊ฒฐ๊ตญ ์ต์ข ์ ์ผ๋ก๋ $self->application = $classPath ๊ฐ ์ ๋ ฅ๋ฉ๋๋ค.
์ฌ๊ธฐ์์ ๋ฌธ์ ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์์์ ์งํํ๋ ์ผ๋ จ์ ๊ณผ์ ๋ค ์ค์ $pathinfo์ ๋ํ ๋ฌธ์์ด ๊ฒ์ฆ์ด ํ๊ฐ๋ ์์์ต๋๋ค.
$pathinfo๋ก ์์ฑ๋ $classPath ๊ฐ ์๋ชป๋ ๊ฒฝ์ฐ ๊ทธ๋๋ก ํด๋น ๊ฐ์ ๋ฆฌํดํฉ๋๋ค.
๊ฒฐ๊ตญ $self->application = $classPath ์์ $classPath๋ $pathinfo ์ ์ ๋ ฅํ๋ ๊ฐ ๊ทธ๋๋ก assign๋ฉ๋๋ค.
์ด์ CMSApplication::run() ๋ค์์ผ๋ก ์คํ๋๋ CMSApplication::display(); ์ฝ๋๊ฐ ์คํ๋๋ฉด include_once(dirname(FILE) . "/" . $self->mode . ".php"); ๊ฐ ์คํ๋ฉ๋๋ค.
์ฌ๊ธฐ์ $self->mode ๋ ๊ธฐ๋ณธ ํ ํ๋ฆฟ์ ๋๋ค. ์ด ํ ํ๋ฆฟ์ ์์ค์ฝ๋๋ฅผ ์ ๊น ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
...
<div id="tabs" class="content-wrapper">
<?php CMSApplication::printTabs(); ?>
</div>
<div id="content" class="content-wrapper last"><?php CMSApplication::printApplication(); ?></div>
...
์ฌ๊ธฐ์ CMSApplication::printApplication() ์ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ๋ณผ ์ ์์ต๋๋ค.
public static function printApplication(){
$self = CMSApplication::getInstance();
echo $self->application;
}
๊ทธ๋ ์ต๋๋ค. ์๊น ์ ์ ์ธ๊ธํ $self->application = $classPath ์ด๋ฏ๋ก, ์ ์์ ์ธ ๊ฒฝ๋ก๋ฅผ ์ ๋ ฅํ๊ฒ ๋๋ฉด ๊ทธ๋๋ก HTML ํ๊ทธ๊ฐ ์ ๋ ฅ๋๊ฒ ๋๊ณ , ์ด๋ฅผ ํตํด Javascript๋ฅผ ์คํํ ์ ์์ต๋๋ค.
๊ณ์ํด์ Remote Code Execution์ ๋๋ค. ํด๋น ์ทจ์ฝ์ ์โleading to RCEโ ๋ผ๊ณ ์ธ๊ธํ์๋๋ฐ, ์ด๋ ์์ง ํจ์น๋์ง ์์ ๊ธฐ์กด ์ด์๋ฅผ ์ด์ฉํ์ฌ ๊ณต๊ฒฉํ์์ต๋๋ค.
Issue #5: getshell https://github.com/inunosinsi/soycms/issues/5
๊ด๋ฆฌ์ ๊ถํ์์ ์์๋ก index.php ์์ค์ฝ๋๋ฅผ ์์ ํ ์ ์๋ ๋ฌธ์ ์ ์ด ๋ฐ๊ฒฌ๋์๋๋ฐ, ์ด ๋ฌธ์ ์ ์ด ์์ง ํจ์น๋์ง ์์๋ฏ ํ์ฌ ์ด ์ทจ์ฝ์ ์ ์ด์ฉํ์ฌ RCE๋ฅผ ์ฑ๊ณต์ํฌ ์ ์์์ต๋๋ค.
๋จ๋ ์ผ๋ก ๊ฐ๋ฐ์ ๋ด๋นํ๊ณ ๊ณ์ ๊ฒ ๊ฐ์ ์ง์ PR์ ์์ฒญํ์ฌ ์ฝ๋๋ฅผ ์์ ํ์์ต๋๋ค.
RCE์ ๊ฒฝ์ฐ Referer ๊ฒ์ฌ์ CSRF ํ ํฐ๋ค์ ํ์ฉํ์ฌ ์์ ํ์์ต๋๋ค.
์ ์ ์กฐ๊ฑด์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๊ด๋ฆฌ์ ๊ถํ์ธ ์ํ์์ ์๋
๊ด๋ฆฌ์๊ฐ ์๋ ๊ฒฝ์ฐ, XSS๋ฅผ ์ด์ฉํ ๊ณต๊ฒฉ. ์์ ์๊ฐํ CVE-2020-15183๋ฅผ ํ์ฉํ ์ ์์ต๋๋ค.
์์ ์ธ๊ธํ์๋ XSS๋ฅผ ํตํด ์ฌ์ฉํ ์ ์๋ ๋ ๋ค๋ฅธ RCE ๊ฐ์ ฏ์ ๋๋ค.
๋ฌด์ธ๊ฐ ๋๋จํ ๊ฒ์ ์๋์ง๋ง, elFinder์์ ๋ฐ์ํ ์ ์๋ ๋ฌธ์ ์ ์ด ์ค์ ๋ก ๋ฐ๊ฒฌ๋์๋ค๋ ์ ์ด ํฅ๋ฏธ๋ก์ ์ต๋๋ค.
SoyCMS์ ์๋soycms/js/elfinder/php/connector.php:143-159๋ฅผ ํ์ธํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ต์ ์ด ์์ต๋๋ค
$opts = array(
// 'debug' => true,
'roots' => array(
// Items volume
array(
'driver' => 'LocalFileSystem', // driver for accessing file system (REQUIRED)
'path' => $path, // path to files (REQUIRED)
'URL' => $url, // URL to files (REQUIRED)
//'trashHash' => 't1_Lw', // elFinder's hash of trash folder
'winHashFix' => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too
'uploadDeny' => array('all'), // All Mimetypes not allowed to upload
'uploadAllow' => array('image', 'text/plain', 'text/css', 'application/zip', 'application/epub+zip','application/pdf'),// Mimetype `image` and `text/plain` allowed to upload
'uploadOrder' => array('deny', 'allow'), // allowed Mimetype `image` and `text/plain` only
'accessControl' => 'access' // disable and hide dot starting files (OPTIONAL)
),
)
);
์์ ๊ฐ์ด mimetype ์ ๋ํ ํ์ธ๋ง ํ๊ณ ์ค์ ๋ก ํ์ฅ์๋ฅผ ํ์ธํ์ง๋ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ณต๊ฒฉ์๋ ์์ ํ์ผ์ ์ ๋ก๋ ํ ์ ์์ต๋๋ค.
์ด ๋ฌธ์ ์ ๋ํด elFinder์์ ์ด์๊ฐ ์ฌ๋ฌ๋ฒ ์ ๊ธฐ๋์์๋๋ฐ, ๊ฒฐ๊ตญ ์ ์ผํ ํด๊ฒฐ์ฑ ์ $opts์ attributes๋ฅผ ์ถ๊ฐํ์ฌ ์์ ๊ฐ์ ํ์์ ์์ ๋ ๊ฒ์ ๋๋ค.
'uploadOrder' => array('deny', 'allow'), // allowed Mimetype `image` and `text/plain` only
'accessControl' => 'access', // disable and hide dot starting files (OPTIONAL)
'attributes' => array(
//ใใญใณใใณใณใใญใผใฉใผ
array(
'pattern' => '/\\.php(\\.old(\\.[0-9][0-9])?)?$/',
'read' => false,
'write' => false,
'locked' => true,
'hidden' => true,
),
)
),
์ ๊ณต๊ฒฉ์ ์ํด BaserCMS์์ ์ด์ฉํ์๋ Blob Object๋ฅผ ํ์ฉํ์ฌ ๊ณต๊ฒฉํ์์ต๋๋ค.
ํด๋น ๊ณต๊ฒฉ์ด ์๋ํ๋์ง์ ๋ํ PoC๋ ๋ค์ ์์์ผ๋ก ๋์ฒดํฉ๋๋ค.
https://www.youtube.com/watch?v=FWIDFNXmr9g
๋จ๋ ์ผ๋ก ๊ฐ๋ฐ์ ๋ด๋นํ๊ณ ๊ณ์ ๊ฒ ๊ฐ์ ์ง์ PR์ ์์ฒญํ์ฌ ์ฝ๋๋ฅผ ์์ ํ์์ต๋๋ค.
์ ์ ์กฐ๊ฑด์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ฐ์ ์ฝ๋๋ฅผ ์กฐ๊ธ ์ฐพ์๋ณด๋ค ๋ณด๋ฉดcms/app/webapp/inquiry/pages/Template/EditPage.class.php ์ ๊ฐ์ ์ฝ๋๋ฅผ ๋ณผ ์ ์์ต๋๋ค.
function doPost(){
$target = $this->target;
$dir = SOY2::RootDir() . "template/";
if(!file_exists($dir . $target) || !is_writable($dir.$target)){
CMSApplication::jump("Template");
exit;
}
$path = $dir . $target;
//bk
$content = file_get_contents($path);
file_put_contents($path . "_" . date("YmdHis"),$content);
$content = $_POST["content"];
file_put_contents($path,$content);
CMSApplication::jump("Template");
exit;
}
function __construct() {
$target = @$_GET["target"];
$this->target = $target;
$dir = SOY2::RootDir() . "template/";
if(!file_exists($dir . $target) || !is_writable($dir.$target)){
CMSApplication::jump("Template");
exit;
}
parent::__construct();
$path = $dir . $target;
$content = file_get_contents($path);
$this->createAdd("target","HTMLLabel",array(
"text" => $target
));
$this->createAdd("content","HTMLTextArea",array(
"name" => "content",
"value" => $content
));
}
์ ์ฝ๋์ doPast ๋ฉ์๋์์๋ CSRF ํ ํฐ์ ๊ฒ์ฆํ์ง๋ ์๊ณ ๊ทธ๋๋ก ํ์ผ์ ์ ๋ ฅํด๋ฒ๋ฆฝ๋๋ค. ์ด๋ฅผ ํตํด ์ธ๋ถ URL์์ ๋ณธ์ธ ์ฌ์ดํธ๋ก ์ ๊ทผํ๋ฉด CSRF๋ฅผ ํตํด ์๋ฒ์ ์ ์์ ์ธ ํ์ผ์ ๊ฐ์ ๋ก ์ ์ฅํ ์ ์๊ฒ ๋ฉ๋๋ค.
๊ทธ ์ธ์ ํด๋น ํ์ด์ง์์ ์์ ๋๋ ํ ๋ฆฌ์ ํ์ผ๋ค์ ๋ฆฌ์คํ ํ๊ณ includeํ๋ ์ทจ์ฝ์ ์ด __construct์ ์์์ผ๋, ์ด๋ ๋ฐ๋ก CVE๋ฅผ ๋ฑ๋กํ์ง ์๊ณ ๊ทธ๋ฅ ํจ์นํ์์ต๋๋ค.
ํด๋น ๊ณต๊ฒฉ์ด ์๋ํ๋์ง์ ๋ํ PoC๋ ๋ค์ ์์์ผ๋ก ๋์ฒดํฉ๋๋ค.
https://www.youtube.com/watch?v=ffvKH3gwyRE
๋จ๋ ์ผ๋ก ๊ฐ๋ฐ์ ๋ด๋นํ๊ณ ๊ณ์ ๊ฒ ๊ฐ์ ์ง์ PR์ ์์ฒญํ์ฌ ์ฝ๋๋ฅผ ์์ ํ์์ต๋๋ค.
์ ์ ์กฐ๊ฑด์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๋๋ถ๋ถ์ Soy CMS๋ฅผ ์ฌ์ฉ์ค์ธ ์ฌ์ดํธ์์ Soy Inquiry๋ฅผ ์ฌ์ฉํ๊ณ ์์ผ๋ฉฐ, ์ค์ ๋ก ์ด ์ทจ์ฝ์ ์ ํตํด ๊ณต๊ฒฉ์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ PoC๋ฅผ ํตํด ์ฆ๋ช ํ์๊ธฐ ๋๋ฌธ์ ์ค์ ์ํฅ๋ ฅ์ ๋ณด๊ณ ํ 4๊ฐ์ ์ทจ์ฝ์ ์ค ๊ฐ์ฅ ๋์ต๋๋ค.
์ฐ์ ๊ธฐ๋ณธ ํ๋ฌ๊ทธ์ธ Soy Inquiry๊ฐ ์ ์์ ์ผ๋ก ์๋ํ๋ค๊ณ ๊ฐ์ ํ์ฌ, cms/app/webapp/inquiry/page.php๋ฅผ ๋ถ์ํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ด ๋์ต๋๋ค.
if(isset($_POST["form_value"]) && isset($_POST["form_hash"])){
$value = base64_decode($_POST["form_value"]);
//ไธๆญฃใชๆธใๆใใงใชใๅ ดๅใฎใฟ
if(md5($value) == $_POST["form_hash"]){
$_POST["data"] = unserialize($value);
}
}
์ ์ฝ๋์์๋ ๋๊ฐ์ง์ ๋ฌธ์ ์ ์ด ์์ต๋๋ค.
md5($value) == $_POST["form_hash"]
์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ไธๆญฃใชๆธใๆใใงใชใๅ ดๅใฎใฟ ์ ์ํด md5($value) == $_POST["form_hash"] ๋ฅผ ์์ฑํ๊ณ ์์ผ๋, ์ค์ ๋ก๋ md5๋ ์ํธํ๊ฐ ์๋ ๊ทธ์ ๋จ๋ฐฉํฅ ํด์ฌ์ด๋ฉฐ $_POST["form_value"]์ $_POST["form_hash"]๋ ์ฌ์ฉ์๊ฐ ์ปจํธ๋กคํ ์ ์๊ธฐ ๋๋ฌธ์ ์๋ฌด๋ฐ ๋ณดํธ๊ฐ ๋์ด์์ง ์์ต๋๋ค.
$_POST["data"] = unserialize($value);
๊ฐ์ฅ ์ํํ ์ฝ๋์ ๋๋ค. PHP์ ๊ณต์ ๋ฌธ์ ๋ฅผ ์ฐธ๊ณ ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ด ์์ต๋๋ค.
Do not pass untrusted user input to unserialize() regardless of the options value of allowed_classes. Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this. Use a safe, standard data interchange format such as JSON (via json_decode() and json_encode()) if you need to pass serialized data to the user. If you need to unserialize externally-stored serialized data, consider using hash_hmac() for data validation. Make sure data is not modified by anyone but you.
๋ง ๊ทธ๋๋ก, unserialize() ๋ ์ ๋ ์ค์ ์ฌ์ฉํ๋ ์ฝ๋์ ์์ด์๋ ์๋๋ ์ํํ ์ฝ๋๋ค ์ค ํ๋์ ๋๋ค. ์ธํฐ๋ท์ ๊ฒ์ํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด unserialize() ์ ๋ํ ์ํ์ฑ ๋ฐ ๋์ฑ ์ ์๊ฐํ๊ณ ์์ต๋๋ค.
์ค์ ๋ก unserialize()๊ฐ ํธ์ถ๋๊ธฐ ์ ์ Soy Inquiry๋ ํ ๋ฌธ์๋ฅผ ํธ์ถํ๊ธฐ ์ํด ์ ๋ง ๋ง์ class๋ค์ ํธ์ถํ๊ณ ์๊ณ , ๊ทธ ์ค ์ํํด๋ณด์ด๋ class๋ฅผ ์ ํ์ฉํ์ฌ ์์์ ์ฝ๋๋ฅผ ์คํํ ์ ์์์ต๋๋ค.
์ ์ ๊ฒฝ์ฐ PoC๋ฅผ ์์ฑํ๊ธฐ ์ํด ์๋์ ์ ์ฌํ๊ฒ ์ฝ๋๋ฅผ ์์ฑํ์๊ณ , ๋ค์๊ณผ ๊ฐ์ ํํ์ PHP์ฝ๋๋ฅผ ํตํด ์์์ PHP ์ฝ๋๋ฅผ ์คํํ ์ ์์์ต๋๋ค. ์ด๋ณด๋ค ๋์ฑ ์งง์ ํ์ด๋ก๋๋ฅผ ์์ฑํ ์ ์๋ค๊ณ ์๊ฐํ๊ธฐ ๋๋ฌธ์ ๋์ ํด๋ณด๊ณ ์ถ์ผ์ ๋ถ์ ํ๋ฒ ๋์ ํด๋ณด์ธ์.
<?php
class ...ClassHoge2 {
...
}
class ...ClassHoge1 {
...
}
$exploit = serialize(Array(
"column_1" => ...ClassHoge1,
"column_2" => "[email protected]",
"column_3" => "c",
"column_4" => "e",
"column_5" => "e",
"column_6" => "f",
"hash" => "1337",
"captcha" => "1337",
));
$form_hash = md5($exploit);
$form_value = base64_encode($exploit);
echo "form_hash = '$form_hash'\n";
echo "form_value = '$form_value'\n";
?>
unserialize() exploit์ ๊ดํ ํ์ ๋๋ฆฌ์๋ฉด, unserialize๋ฅผ ๋น ๋ฅด๊ณ ํจ์จ์ ์ด๊ฒ exploitํ๋ ๋ฐฉ๋ฒ์ผ๋ก๋ ์ค์ ๋ก unserialize() ๋ช ๋ น์ด๊ฐ ์คํ๋๊ธฐ ๋ฐ๋ก ์ ์ var_dump(get_declard_classes());์ ๊ฐ์ ์ฝ๋๋ฅผ ์ถ๊ฐํ๋ฉด ๋์ฑ ๋ ๋น ๋ฅด๊ณ ํจ์จ์ ์ด๊ฒ ์ฐพ์ ์ ์์ต๋๋ค.
ํด๋น ๊ณต๊ฒฉ์ด ์๋ํ๋์ง์ ๋ํ PoC๋ ๋ค์ ์์์ผ๋ก ๋์ฒดํฉ๋๋ค.
https://www.youtube.com/watch?v=zAE4Swjc-GU
๋จ๋ ์ผ๋ก ๊ฐ๋ฐ์ ๋ด๋นํ๊ณ ๊ณ์ ๊ฒ ๊ฐ์ ์ง์ PR์ ์์ฒญํ์ฌ ์ฝ๋๋ฅผ ์์ ํ์์ต๋๋ค.
๋๋ฌด ๊ธ์ด ๊ธธ์ด์ก๋ค์.
์ด๋ฒ ๊ธ์์๋ 4๊ฐ์ ์ ํ์์ ์ด 6๊ฐ์ง์ ์ทจ์ฝ์ ์ ์๊ฐํ์ง๋ง, ์์ง ์ทจ์ฝ์ ์ด ํจ์น๋์ง ์์๊ฑฐ๋ ์๋ฒฝํ ํจ์น๊ฐ ์งํ๋์ง ์์ ์ทจ์ฝ์ ์ด 25๊ฑด ์ด์ ๋ ์๊ธฐ ๋๋ฌธ์, ๊ด๋ จ ๋ด์ฉ์ ์ถํ ํจ์น๊ฐ ์ด๋์ ๋ ๋๋๊ฒ ๋๋ฉด ๋์ค์ ๋ธ๋ก๊ทธ์ ๊ฒ์ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ์ ๋ง ๊ฐ์ฌํฉ๋๋ค.