代码审计目的
代码审计指的是对源代码进行检查,寻找代码中的bug以及安全缺陷(漏洞)。代码审计这是一个需要多方面技能的技术,也是需要一定的知识储备。我们需要掌握多种编程语言,安全工具的使用、漏洞原理、漏洞的修复方式、函数的缺陷等等,如果再高级一些,我们需要学习不同的设计模式,编程思想、MVC框架以及常见的框架,开发项目的思维。那么对于像我这样的小白应该是需要一个路线,一个流程。
代码审计基础
代码审计入门基础:html/js基础语法、PHP基础语法 ,面向对象思想,PHP小项目开发(Blog、注册登录、表单、文件上传、留言板等),Web漏洞挖掘及利用,Web安全工具基本使用(burpsuite、sqlmap等),代码审计工具(seay审计系统、zend studio+xdebug等)
代码审计两种基本方式:
通读所有源码:通读所有代码作为一种最麻烦的方法也是最全面的审计方法。特别是针对大型程序,源码成千上万行。当然了解整个Web应用的业务逻辑,才能挖掘到更多更有价值的漏洞。
功能点审计:根据漏洞对应发生函数进行功能行审计,常会用到逆向溯源数据流方法进行审计。
代码审计两种基本方法:正向追踪数据流:跟踪用户输入参数 -> 来到代码逻辑 -> 最后审计代码逻辑缺陷 -> 尝试构造payload
逆向溯源数据流:字符串搜索指定操作函数 -> 跟踪函数可控参数 -> 审计代码逻辑缺陷 -> 尝试构造payload
现cms可分大体两类:
单入口cms:不管访问哪个模块都使用同一个入口文件,常见的MVC框架采用这种模式。
多入口cms:每个模块都有一个入口文件(可以前端设置一个入口文件 index.php,后端创建一个入口文件admin.php,前后端的入口文件是独立的)。
代码审计思路
从个人角度出发,如果环境允许的话,可以先选择做一个”程序员“再来做代码审计。
因为从开发者的位置去思考问题,可以快速定位问题。学习面向对象编程以及面向过程编程,编写一些项目提升对代码的理解能力,再是对各种漏洞可以独立挖掘利用并能理解漏洞的危害,这里我们主要针对PHP源码做审计。
接下来我们从三个层次开始我们的源码审计思路
1.确定要审计的源码是什么语言
2.确定该源码是单入口还是多入口
3.确定该语言的各种漏洞诞生的函数
PHP核心配置
一个漏洞在不同环境造成的结果也是不一样的。由于关于php.ini配置的内容过于多,这里推荐浏览官方文档https://www.php.net/manual/zh/ini.php,我们在这里主要列下php.ini 主要使用的安全配置。
safe_mode=off
PHP的safe_mode 功能是一个过时的安全特性,在早期的 PHP 版本中(比如 PHP 4 和早期的 PHP 5)默认启用。它旨在限制脚本能访问的文件路径、系统资源等,以防恶意攻击。然而,从 PHP 5.3 开始,safe_mode 已经不再推荐使用,并在 PHP 7 中完全移除。所以,safe_mode 主要影响的是 PHP 4.x 和早期的 PHP 5.x 版本。safe_mode是提供一个基本安全的共享环境。在一个多用户共享的phpweb服务器上,当这台服务器开启了safe_mode模式,有以下函数将会受到影响。首先,以下尝试访问文件系统的函数将会被限制,运行服务器的用户id,如果想要尝试操作某个文件,必须要用户对该文件的读取或者写入的访问权限。因此,在safe_mode打开的情况下,下列函数将会收到限制:
ckdir,move_uploaded_file,chgrp,parse_ini_file,chown,rmdir,copy,rename,fopen,require,highligh
safe_mode_allowed_env_vars=string
限制环境变量的存取,指定php程序可以改变的环境变量的前缀,当这个选项的值为空时,那么php可以改变任何环境变量
safe mode_exec dir ="xxx"
外部程序执行目录
当安全模式被激活,safe_mode_exec_dir参数限制通过exec()函数执行的可执行文件到指定的目录。举例来说,如果你想限制在/usr/local/bin目录执行功能,你可以使用这个指令:safe_mode_exec_dir = "/usr/local/bin"一般情况下,如果不需要执行什么程序,建议不要指定执行系统程序的目录。可以指定一个目录,然后把需要执行的程序拷贝到这个目录即可,例如:safe_mode_exec_dir = /temp/cmd
但是,更推荐不要执行任何程序。这种情况下,只需要将执行目录指向网页目录即可
例如 safe_mode_exec_dir= /usr/www
注意 : 执行目录的路径要以实际操作系统目录路径为准
disable_functions
这里是禁用函数的地方,一个黑名单,如果拿到shell却执行不了命令,多半就是这里出了问题。为了更安全的运行PHP,可以用此指令来禁止一些敏感函数的使用,当你想用本指令禁止一些危险函数时,切记把dl()函数也加到禁止列表,攻击者可以利用dl()函数加载自定义的php扩展突破disable_functions.配置禁止函数时可以使用逗号分隔函数名。注意同时还需要禁用LD_PRELOAD和putenv()这两个函数。
COM组件com.allow_dcom= false
PHP设置在安全模式下(safe_mode),仍允许攻击者使用COM()函数来创建系统组件来执行任意命令,推荐关闭这个函数。 使用COM()函数需要在PHP.ini中配置extension=php_com_dotnet.dll,如果PHPversion<5.4.5则不需要
register_globals=off
register_globals 全局变量注册开关
php.ini的register_globals选项的默认值为OFF,在4.2版本之前是默认开启的,当设定为On时,程序可以接收来自服务器的各种环境变量,包括表单提交的变量,这是对服务器分厂不安全的, register_globals = off时,服务器端获取数据的时候用$_GET['name']来获取数据。 register_globals = on时,服务端使用POST或GET提交的变量,都将自动使用全局变量的值来接受。
magic_quotes_gpc=on
magic_quotes_gpc = on 魔术引号自动过滤
PHP5.4.0被移除 magic_quotes_gpc = off 在php.ini中默认是关闭的,如果打开它,将自动把用户提交对sql的查询的语句进行转换,如果设置成ON,php会把所有的单引号,双引号,和反斜杠和空字符(NULL)加上反斜杠()进行转义 它会影响HTTP请求的数据(GET,POST.COOKIE),开启它会提高网站的安全性。
allow_url_include=off
该配置为ON的情况下,同时allow_url_open = on时,可以直接包含远程文件,若包含的变量为可控的情况下,可以直接控制变量来执行PHP代码。即该选项设置:是否允许include()和require()函数包含URL(HTTP,HTTPS)作为文件处理。该设置对本地文件包含不会造成影响。
allow_url_open=on
允许本地PHP文件通过调用url重写来打开或者关闭写权限,默认的封装协议提供的ftp和http协议来访问文件。该设置对本地文件包含不会造成影响。
expose_php=off
在 PHP 中,expose_php 是一个 PHP 配置选项,用于控制是否在 HTTP 响应头中显示 PHP 版本信息。当expose_php 的值为 On 时,PHP 版本信息会显示在响应头中,否则不会显示。expose_php 主要是为了提供给攻击者更多的信息,以便他们利用已知的 PHP 漏洞。因此,建议在生产环境中关闭这个选项,以增加服务器的安全性。
底层原理是在 PHP 的源码中,有一个默认的 HTTP 响应头模板,当 expose_php 值为 On 时,PHP 会将当前 PHP 的版本信息填充到响应头模板中,然后发送给客户端。当 expose_php 值为 Off 时,PHP 不会填充版本信息到响应头模板中。
当expose_php = On时,会出现php版本信息,当expose_php = Off时,便不会出现php的版本信息
upload_tmp_dir
上传文件临时保存的目录,如果不设置的话,则采用系统的临时目录。
open_basedir
用户可访问目录 open_basedir = D:\WWW
能够控制PHP脚本只能访问指定的目录,这样能够避免PHP脚本访问不应该访问的文件,一定程度上限制了webshell的危害
display_errors
内部错误选项,表明是否显示PHP脚本的内部错误,网站发布后建议关闭PHP的错误回显
session_use_trans_sid
session_use_trans_sid=off
如果启用 session.use_trans_sid,会导致 PHP 通过 URL传递会话 ID,这样一来,攻击者就更容易劫持当前会话,或者欺骗用户使用已被攻击者控制的现有会话。
手动调试代码
在进行手动代码调试时,可以让程序输出一些中间变量或者是计算结果的值,这样方便做进一步的判断
echo
exit();
print_r
var_dump();
debug_zval_dump();
debug_print_backtrace();
echo "<script>alert($estr);</script>"";
die("<script>alert($estr);</script>");
PHP的弱类型
比较符号==与===
比较符号 == 与 ===
== 两个等号是先把等号两边的变量转化成相同的类型,如果转换类型后的结果是相等的,就认为相等。如果整型跟字符型比较字符或从左往右提取整型直到遇到字符结束,再比较
=== 三个等号是先判断两边变量的数据类型,如果数据类型相同,再去判断两边的值,如果值相等,那么为真
具体执行过程
1.判断全等于操作符两边的数据类型是否相同 如果不相同,则返回false
2.判断全等于操作符两边的值是否相等,如果不相等,则返回false
<?php
var_dump(0=="admin"); //true
echo "<br>";
var_dump(123==="123"); //false
echo "<br>";
var_dump(123=="123"); //true
echo "<br>";
var_dump(123=="123c"); //true
echo "<br>";
var_dump(123<"124c"); //true
echo "<br>";
var_dump(0=="hahaha"); //true
echo "<br>";
var_dump(1=="1admin"); //true
echo '<br>';
var_dump(1=="admin1"); //false
echo '<br>';
var_dump(0=="admin1"); //true
echo '<br>';
//数字和数组
$arr = array();
var_dump(0==$arr); //false
echo '<br>';
//字符串和数组
$arr = array();
var_dump("0"==$arr); //false
echo '<br>';
//"合法数字+e+合法数字"类型的字符串
var_dump("0e123456"=="0e4456789"); //true
echo '<br>';
var_dump("1e1"=="10"); //true
echo '<br>';
var_dump("1e2"=="10"); //false
?>
array_search与is_array in_array
is_array:判断传入的是不是一个数组。
array_search(x,$数组):在数组中寻找与指定值(x)相等的值,array_search函数 类似于"==",会进行类型的转换。
<?php
if(!is_array($_GET['test'])){
exit();
}
$test = $_GET['test'];
echo count($test);
for($i = 0;$i<count($test) ;$i++ ){
if($test[$i] === "admin"){
echo "error";
exit();
}
$test[$i] = intval($test[$i]);
}
if(array_search("admin",$test) === 0){
echo "flag";
}else{
echo "false";
}
我们可以传入test[]=0来进行绕过,首先test是一个数组,符合is_array的判断,然后test=0;在array_search中0==admin为true,绕过了array_search,得到flag
in_array
in_array函数与array_search也是一样的问题,利用类似于"=="进行判断,会进行类型的转换。作用是判断说查询数据是否在数组中。
<?php
$array = [0,1,2,'3'];
var_dump(in_array('abc',$array)); //true
var_dump(in_array('2bc',$array)); //true
var_dump(in_array('345',$array)); //false
is_numeric
检测变量是否为数字或者数字字符串,如果变量是数字和数字字符串则返回true,否则返回false
<?php
$a = 1233;
$b = '123';
$c = "1234";
$d = "123c";
var_dump(is_numeric($a)); //true
var_dump(is_numeric($b));//true
var_dump(is_numeric($c));//true
var_dump(is_numeric($d));//false
strcmp
比较函数如果两者相等返回0,string1>string2返回值大于0 反之小于0。在5.3及以后的php版本中,当strcmp()括号内是一个数组与字符串比较时,也会返回0。该函数本质是字符的ASCII码的比较
<?php
$a = "123";
$b = 123;
var_dump(strcmp($a,$b)); //0
$a1 = "123c";
var_dump(strcmp($a1,$b)); //1
var_dump(strcmp($a1,$a)); //1
$b1 = 1234;
var_dump(strcmp($b,$b1)); //-1
$str = 'ABCD';
var_dump(strcmp($b1,$str)); //-1
switch
如果switch是数字类型的case的判断时,switch会将参数转换为int类型
<?php
$choice = "2xx";
switch($choice){
case 1:
echo "1";
break;
case 2:
echo "2";
break;
case 0:
echo "0";
break;
}
//输出结果为 2
md5
科学计数法 0e绕过
md5() 遇到「公式」,会先「运算」,再对运算结果「计算」MD5。由于0和任何数相乘都等于0,所以0e开头的任何数,其MD5都是相同的。一些md5值为0e开头的字符
QNKCDZO => 0e830400451993494058024219903391
240610708 => 0e462097431906509019562988736854md5() 不能处理数组,数组都返回null。同时会报一个Warning,不影响执行,不用管。
sha1
sha1函数和md5函数一样不能判断数组的值。
$array1=[1,2,3];
$array2=[4,5,6];
var_dump(sha1($array1)===sha1($array2)); //true
empty与isset
变量为:0,"0",null,'',false,array()时,使用empty函数,返回的都是true
变量未定义或者为null时,isset函数返回的为false,其他都为true
$a = null;
$b = 0;
$c = "";
$d = "0";
$e = false;
$f = array();
var_dump(empty($a));//true
var_dump(empty($b));//true
var_dump(empty($c));//true
var_dump(empty($d));//true
var_dump(empty($e));//true
var_dump(empty($f));//true
var_dump(isset($a));//false
var_dump(isset($b));//true
var_dump(isset($c));//true
var_dump(isset($d));//true
var_dump(isset($e));//true
var_dump(isset($f));//true
学习"漏洞"函数
全局变量和超全局变量
全局变量/超全局变量
全局变量:定义在函数外部的就是全局变量,它的作用域从定义处一直到文件结尾。
函数内定义的变量就是局部变量,它的作用域为函数定义范围内,函数之间存在作用域,局部变量互不影响。函数内访问全局变量需要 global关键字或者使用 $GLOBALS[index]数组。
超全局变量:
超全局变量 在 PHP 4.1.0 中引入,是在全部作用域中始终可用的内置变量。PHP 中的许多预定义变量都是“超全局的”,这意味着它们在一个脚本的全部作用域中都可用。在函数或方法中无需执行 global $variable; 就可以访问它们。
参考 https://www.runoob.com/php/php-superglobals.html
常用的超全局变量
$GLOBALS
$_SERVER
$_REQUEST
$_POST
$_GET
$_FILES
$_ENV
$_COOKIE
$_SESSION
SQL注入
select update insert into delete
代码执行
eval()
usort()
uasort()
assert()
array_map()
preg_replace()
array_filter()
call_user_func()
create_function()
call_user_func_array()
文件操作函数:
fputs(fopen('shell.php','w'),'<?php eval($_POST[cmd])?>');
动态函数:$_GET['a']($_GET['b'])
命令执行
system()
exec()
passthru()
shell_exec()
XSS跨站脚本
print_r
echo
printf
die
var_dump
var_export
文件上传漏洞
move_uploaded_file()
文件包含
include()
include_once() 只包含一次
require()
require_once() 只包含一次
伪协议
详细参考 https://segmentfault.com/a/1190000018991087
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
任意文件下载
fopen() readfile() file_get_contents()
任意文件删除
unlink()
任意文件读取
file()
fgets()
fgetss()
fopen()
readfile()
fpassthru()
parse_ini_file()
file_get_contents()
变量覆盖
$
$a='hello';
$b='world';
$$a=$b;
echo($hello); echo '<br>';
echo($a);
输出
world
hello
这里先是定义了a,b两个变量,分别赋值hello和world。接着,$$a表示将变量a的值变成一个变量即$hello,然后将变量b的值赋给它,最后输出world。
所以在源码中看到$$可以想到变量覆盖漏洞。
反序列化漏洞
unserialize()
魔法方法
__construct()//每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作__destruct()//某个对象的所有引用都被删除或者当对象被显式销毁时执行
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__sleep() //使用serialize时触发
__wakeup() //使用unserialize时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__set_state()//当调用 var_export() 导出类时,此静态方法会被自动调用。
__clone()//当使用 clone 复制一个对象时自动调用
__debuginfo()//使用 var_dump() 打印对象信息时自动调用