快元旦了,不知道该写哪一类的文章就只有发一些自己手里有的各种坚果了。都会慢慢的发出来的,有知名cms,也会有不是很知名的
漏洞文件:/apps/public/Lib/Action/AttachAction.class.php
public function ajaxUpload() { $d['type_name'] = 11; D('feedback_type')->add($d); $attach_type = t($_REQUEST['type']); $options['uid'] = $this->mid; $options['allow_exts'] = t(jiemi($_REQUEST['exts'])); $options['allow_size'] = t(jiemi($_REQUEST['size'])); $jiamiData = jiemi(t($_REQUEST['token'])); list($options['allow_exts'], $options['need_review'], $fid) = explode('||', $jiamiData); $options['limit'] = intval(jiemi($_REQUEST['limit'])); $options['now_pageCount'] = intval($_REQUEST['now_pageCount']); $data['upload_type'] = $attach_type; $info = model('Attach')->upload($data, $options); //ÉÏ´«³É¹¦ echo json_encode($info); }
可以看见存在很多变量都被控制,在上传中type这种名称的变量一般都是用于判断是否上传图片或者上传file等。这里我们选择定义成file,因为image几乎都是有图片类型限制的,file有时候会有疏忽。Exts这个变量一看就是用于类型判断或者后缀判断,但这里可以看见他在输入后还进入到了一个jiemi(解密)命名的函数,进入看看
function jiemi($text, $key = null) { if (empty($key)) { $key = C('SECURE_CODE'); } return tsauthcode($text, 'DECODE', $key); } 当key为空的时候就从配置文件中获取key的值 'DB_PORT' => 3306, // Êý¾Ý¿â¶Ë¿Ú 'DB_PREFIX' => 'ts_',// Êý¾Ý¿â±íǰ׺£¨ÒòΪÂþÓεÄÔÒò£¬Êý¾Ý¿â±íǰ׺±ØÐëдÔÚ±¾Îļþ£© 'DB_CHARSET' => 'utf8', // Êý¾Ý¿â±àÂë 'SECURE_CODE' => '283415851f59f1f6fe', // Êý¾Ý¼ÓÃÜÃÜÔ¿ 'COOKIE_PREFIX' => 'TS4_', // # cookie );
可以看见这里的code就是他的key,然后进入tsauthcode进行解密
function tsauthcode($string, $operation = 'DECODE', $key = '') { $ckey_length = 4; $key = md5($key ? $key : SITE_URL); $keya = md5(substr($key, 0, 16)); $keyb = md5(substr($key, 16, 16)); $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : ''; $cryptkey = $keya.md5($keya.$keyc); $key_length = strlen($cryptkey); $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; $string_length = strlen($string); $result = ''; $box = range(0, 255); $rndkey = array(); for ($i = 0; $i <= 255; $i++) { $rndkey[$i] = ord($cryptkey[$i % $key_length]); } for ($j = $i = 0; $i < 256; $i++) { $j = ($j + $box[$i] + $rndkey[$i]) % 256; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; } for ($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); } if ($operation == 'DECODE') { if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) { return substr($result, 26); } else { return ''; } } else { return $keyc.str_replace('=', '', base64_encode($result)); } }
是不是看着很熟悉,没错就是Ucenter的解密函数。看到这里了,我们先假象一种场景,如果我们得到了key,自定义后缀或者类型然后进行加密控制exts变量。如果这里exts是用于后缀的验证话,那么是不是就是任意文件上传?既然提出了这种假象,我们就先记录下,继续往下面看。
public function upload($data = null, $input_options = null, $thumb = false) { //echo json_encode($data); $system_default = model('Xdata')->get('admin_Config:attach'); if (empty($system_default['attach_path_rule']) || empty($system_default['attach_max_size']) || empty($system_default['attach_allow_extension'])) { $system_default['attach_path_rule'] = 'Y/md/H/'; $system_default['attach_max_size'] = '2'; // ĬÈÏ2M $system_default['attach_allow_extension'] = 'jpg,gif,png,jpeg,bmp,zip,rar,doc,xls,ppt,docx,xlsx,pptx,pdf'; model('Xdata')->put('admin_Config:attach', $system_default); } 这里是判断我们的system_default的内容是否为空,当为空的时候就进入if中赋值 if ($data['upload_type'] === 'image') { $image_default = model('Xdata')->get('admin_Config:attachimage'); $system_default['attach_max_size'] = $image_default['attach_max_size']; $system_default['attach_allow_extension'] = $image_default['attach_allow_extension']; $system_default['auto_thumb'] = $image_default['auto_thumb']; } 这里就是我们提到的type,当type为image的时候就对$system_default进行传值 $default_options = array(); $default_options['custom_path'] = date($system_default['attach_path_rule']); // Ó¦Óö¨ÒåµÄÉÏ´«Ä¿Â¼¹æÔò£º'Y/md/H/' $default_options['max_size'] = floatval($system_default['attach_max_size']) * 1024 * 1024; // µ¥Î»: Õ× $default_options['allow_exts'] = $system_default['attach_allow_extension']; // 'jpg,gif,png,jpeg,bmp,zip,rar,doc,xls,ppt,docx,xlsx,pptx,pdf' $default_options['save_path'] = UPLOAD_PATH.'/'.$default_options['custom_path']; $default_options['save_name'] = ''; //Ö¸¶¨±£´æµÄ¸½¼þÃû.ĬÈÏϵͳ×Ô¶¯Éú³É $default_options['save_to_db'] = true;
这里在代码中有注释说明,是载入配置文件中的默认配置
if ($data['upload_type'] == 'image') { $cloud = model('CloudImage'); if ($cloud->isOpen()) { return $this->cloudImageUpload($options); } else { return $this->localUpload($options); } }
这里就是判断我们的type是否是image,当是的时候则进入,这里的isOpen是判断是否开启云上传,当开启的时候则上传到云上,没开启的时候则进入localUpload这个上传中,我们可以看的时候没开的时候
private function localUpload($options) { // ³õʼ»¯ÉÏ´«²ÎÊý $upload = new UploadFile($options['max_size'], $options['allow_exts'], $options['allow_types']); // ÉèÖÃÉÏ´«Â·¾¶ $upload->savePath = $options['save_path']; // ÆôÓÃ×ÓĿ¼ $upload->autoSub = false; // ±£´æµÄÃû×Ö $upload->saveName = $options['save_name']; // ĬÈÏÎļþÃû¹æÔò $upload->saveRule = $options['save_rule']; // ÊÇ·ñËõÂÔͼ if ($options['auto_thumb'] == 1) { $upload->thumb = true; } // ´´½¨Ä¿Â¼ mkdir($upload->save_path, 0777, true); // Ö´ÐÐÉÏ´«²Ù×÷ if (!$upload->upload()) { // ÉÏ´«Ê§°Ü£¬·µ»Ø´íÎó $return['status'] = false; $return['info'] = $upload->getErrorMsg(); return $return; } else { $upload_info = $upload->getUploadFileInfo(); // ±£´æÐÅÏ¢µ½¸½¼þ±í $data = $this->saveInfo($upload_info, $options); // Êä³öÐÅÏ¢ $return['status'] = true; $return['info'] = $data; // ÉÏ´«³É¹¦£¬·µ»ØÐÅÏ¢ return $return; } }
前面的一系列传值我们跳过,直接进入upload中看
foreach ($files as $key => $file) { //¹ýÂËÎÞЧµÄÉÏ´« if (!empty($file['name'])) { $file['key'] = $key; $file['extension'] = $this->getExt($file['name']); $file['savepath'] = $savePath; $file['savename'] = uniqid().substr(str_shuffle('0123456789abcdef'), rand(0, 9), 7).'.'.$file['extension']; //$this->getSaveName($file); if ($GLOBALS['fromMobile'] == true && empty($file['extension'])) { //Òƶ¯É豸ÉÏ´«µÄÎÞºó׺µÄͼƬ£¬Ä¬ÈÏΪjpg $file['extension'] = 'jpg'; $file['savename'] = trim($file['savename'], '.').'.jpg'; } else { // ×Ô¶¯¼ì²é¸½¼þ if ($this->autoCheck) { if (!$this->check($file)) { return false; } } } 首先是遍历FILES信息把相应的值赋给对应的变量,然后 if ($GLOBALS['fromMobile'] == true && empty($file['extension'])) { //Òƶ¯É豸ÉÏ´«µÄÎÞºó׺µÄͼƬ£¬Ä¬ÈÏΪjpg $file['extension'] = 'jpg'; $file['savename'] = trim($file['savename'], '.').'.jpg';
这里的frommobile默认是开启的true,但这里的$file[‘extension’]在上面遍历的时候有赋值所以不为空则为假不进入
else { // ×Ô¶¯¼ì²é¸½¼þ if ($this->autoCheck) { if (!$this->check($file)) { return false; } } }
重点是check检查类型等
if (!$this->checkType($file['type'])) { $this->error = 'ÉÏ´«ÎļþMIMEÀàÐͲ»ÔÊÐí£¡'; return false; } //¼ì²éÎļþÀàÐÍ if (!$this->checkExt($file['extension'])) { $this->error = 'ÉÏ´«ÎļþÀàÐͲ»ÔÊÐí'; return false; }
类型是可以绕的,checkext瞅瞅
private function checkExt($ext) { if (in_array($ext, array('php', 'php3', 'exe', 'sh', 'html', 'asp', 'aspx'))) { $this->error = '²»ÔÊÐíÉÏ´«¿ÉÖ´ÐеĽű¾Îļþ£¬È磺php¡¢exe¡¢htmlºó׺µÄÎļþ'; return false; } if (!empty($this->allowExts)) { return in_array(strtolower($ext), $this->allowExts, true); } return true; }
首先第一个in_array这里的判断一看就是黑名单,大小写什么的直接就可以绕过,而这里的allowexts则是我们最初的加密的那个内容。这里是当不为空的时候则判断,所以我们不填此参数达到绕过
$return['info'] = $upload->getErrorMsg(); return $return; } else { $upload_info = $upload->getUploadFileInfo(); // ±£´æÐÅÏ¢µ½¸½¼þ±í $data = $this->saveInfo($upload_info, $options); // Êä³öÐÅÏ¢ $return['status'] = true; $return['info'] = $data; // ÉÏ´«³É¹¦£¬·µ»ØÐÅÏ¢ return $return; }
由于上传到云是需要管理自己去配置的,所以默认是不开启,只要采用的是正常上传都会存在。本地亲测试成功复现.
本文由91ri团队原创,转载请注明出处。