PHP--安全特性
强类型与弱类型
强类型
所谓强类型(Strongly typed),顾名思义就是强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型。J
java、.NET、C++等都是强类型语言,在变量使用之前必须声明变量的类型和名称;且不经强制转换不允许两种不同类型的变量互相操作。
弱类型
- 对数据的类型要求并不严格,可以让数据类型互相转换。
PHP中“==”和“===”
“==”和“===”都是用来比较两个数值是否相等的操作符。
当比较的两个值都是相同类型时候“==”和“===”是相等的。
1==1(两个int)1.0==1.0(两个float)‘H1TerHub’==’H1TerHub‘(两个字符串)
ATTENTION:在前后两个值的类型不一样的时候,‘==’会自动转换类型
一般看到“==”就可以关注是否存在弱类型比较
“==”类型转换的规则
1、字符串和数字比较,字符串会被转换成数字。
“admin”==0(true)//admin被转换成数字,由于admin是字符串,转换失败,int(admin)=0,所以比较结果是true。
2、混合字符串转换成数字,看字符串的第一个。
“1admin”==1 “2admin”==2
3、字符串开头以xex开头,x代表数字。会被转换成科学计数法。
x*10^x的形式。
1 | ‘2e2’=2*10^2=200 |
bool类型的true跟任意字符串可以弱类型相等
1 |
|
强|弱比较
强比较
===
:先比较类型是否相同;再比较值弱类型比较
==
:会将字符类型转换为相同类型,在比较值ps:若比较数字和字符串 | 涉及数字内容的字符串;则字符串会转换为数值并按数值进行比较
eg:
当一个字符串欸当作一个数值来取值,其结果和类型如下:如果该字符串没有包含’.’,’e’,’E’并且其数值值在整形的范围之内 该字符串被当作int来取值,其他所有情况下都被作为float来取值,该字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为0。
1
2
3
4
5
6 var_dump("admin"==0); //true
# admin为字符串,转换即为0var_dump("1admin"==1); //true
# 字符串中的数值的开始部分决定了其值var_dump("admin1"==1) //false
var_dump("admin1"==0) //true
var_dump("0e123456"=="0e4456789"); //true
# 将0e|0E识别为科学计数法;而0的n次方始终为0,故相等Copy
一些php函数
md5()
1 | md5(string,raw) |
- String: 必需,为要计算的字符串
- Raw:
- true: 原始16字符二进制格式
- false:32字符十六进制数(默认)
利用md5($pass,true)构造万能密码sql注入
后端查询语句:
1 | select * from 'admin' where password=md5($pass,true) |
若MD5值经hex转换为字符串后为’or’+balabala这样的字符串;那么拼接的查询语句为:
1 | select * from `admin` where password=''or'balabala' |
当’or’后的值为true时,即可构成万能密码;在此利用到一个mysql特性: 在mysql里面,在用作布尔型判断时,以1开头的字符串会被当做整型数
(测试时发现只要是数字都可以) (ps:这种情况必须有单引号括起来 如password='xxx' or '1xxxxxxxxx'
就相当于password='xxx' or 1
;故返回值为true)
常用payload:ffifdyop
md5强碰撞脚本
弱比较bypass
md5弱比较形式:if($a != $b && md5($a) == md5($b))
这里有两种方法
- 0e绕过
- 数组绕过
0e绕过:是md5加密后是0exxxxx的形式,在==弱比较时,会被当做科学技术法,众所周知,0的任何次方都是0,自然判断为true
大佬整理的md5加密后0e开头
数组绕过:a[]=a&b[]=b,传入参数为数组则MD5返回NULL,null=null,判断为true,成功绕过
1 | 一些md5编码后得到0exxx(此处xxx为十进制字符)的字符串 |
补一些脚本
1 | # 生成md5值为0exxx,还有一些套娃关卡的第一关也是要求验证码,改一下就能用了 |
加密后弱相等
形式如下:if ($md5==md5($md5))
可以找0e开头并且md5后仍然0e开头的字符串,这样0==0,就可以绕过了。
这里可以用0e215962017。
1 | # 0e215962017 |
强比较bypass
md5强比较形式:if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2']))
0e绕过不能用了,因为强比较时,0exxx不再被当做科学计数法,而是被当做字符串。
数组绕过仍然可以。
数组绕过
1
eg:(md5($id) === md5($gg) && $id !== $gg)直接数组绕过:?id[]=1&gg[]=2
还有最近碰到的:
md5强碰撞:md5强碰撞
1
2
3
4(string)$_POST['a1']!==(string)$_POST['a2']&&md5($_POST['a1'])===md5($_POST['a2'])}
# 最后转换为字符串比较,因此使用数组就不可行了
(md5(implode('',$_GET['username']))===md5(implode('',$_GET['password']))
# implode()会先把数组元素拼接成字符串再进行md5加密,使用数组就不可行了只能使用两组MD5值相同的不同字符串了,这里可以用脚本跑, 下面是url编码过后的值:
1
2
3
4
5# 1
a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2
# 2
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
substr()、sha1()、base64_decode
substr()
、sha1()
、base64_decode()
只能处理传入的字符串数据 当传入数组后会报出Warning错误,但仍会正常运行并返回值,当==左右两边都错误时,并且正常运行返回相同的值,就可以是判定条件成立
bypass:对substr()
、sha1()
、base64_decode()
传入数组则返回null
1 | $a=[];var_dump(substr($a, 123)); //NULL |
extract()变量覆盖
代码
1 |
|
extract() 函数
从数组中将变量导入到当前的符号表。该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。该函数返回成功设置的变量数目。为了拿到flag,需要auth值为1,此处我们利用extract()变量覆盖的缺陷,将auth覆盖为1。
intval()
获取变量的整数值
1 | intval ( mixed $value , int $base = 10 ) : int |
value:要转换的数量值,base:转换所用进制
三个特性:
成功:返回var的整数值; 失败 or 空数组:返回0; 非空数组:返回1
如果
base
是 0,通过检测value
的格式来决定使用的进制:- 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
- 如果字符串以 “0” 开始,使用 8 进制(octal);否则,
- 将使用 10 进制 (decimal)。
base为0,变量在遇上数字或正负符号才做转换,遇到非数字或字符串结束时以(\0)结束转换,ps:前提是进行弱类型比较
默认遇到
非数字字符
就会停止识别 如:intval($_GET[1])
传入1=666aa;intval得到结果为666Intval在处理字符串型的科学计数法时只输出e前的数字,而+1后又作为数字处理
echo intval(1e10); ->10000000000
echo intval(“1e10”); ->1
echo intval(“1e10”+1); ->10000000001
is_numeric()
is_numeric() :判断变量是否为数字或数字字符串,不仅检查10进制,16进制也可以。
is_numeric函数对于空字符%00,无论是%00放在前后都可以判断为非数值,而%20空格字符只能放在数值后。所以,查看函数发现该函数对对于第一个空格字符会跳过空格字符判断,接着后面的判断因此输入%20password在解析变量名就会变成password
bypass:
1 | passwd=1234567%20passwd=1234567%00 |
此外:在某些cms中,会利用如下代码检测用户输入
1 | # 该片段判断参数s是否为数字,是则带入数据库查询,不是则返回0 |
但可以将sql语句转换为16进制传给参数
in_array()
1 | bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) |
ps:strict相当于是否开启强比较
- 不提供strict参数 (即默认为
false
)时,会进行松散比较
,判断needle是否在数组haystack中 - strict=
true
;还会比较needle和haystack中元素类型是否相同
1 | $array=[0,1,2,'3'];var_dump(in_array('abc', $array)); //true |
array_search()和in_array()类似
1 |
|
json()
1 |
|
输入一个json类型的字符串,json_decode函数解密成一个数组,判断数组中key的值是否等于$key
的值,但是$key
的值我们不知道
这时我们构造一个和任意字符串返回为真的数组{“key”:true}。即可绕过
payload=message={“key”,true}
ereg()
正则表达式匹配,在php7.0.0版本后被去除
存在NULL截断漏洞,可以使用%00截断来绕过正则匹配~
像ctfshow web108
1 if(ereg("^[a-zA-Z]+$", $_GET['c'])===FALSE))就可以用
a%00
来绕过,在%00后就可以任意传入了
preg_match()
- preg_match只能处理字符串,当传入数组时会返回false
- PHP利用PCRE回溯次数限制绕过某些安全限制
.
不会匹配换行符; eg:preg_match('/^.*(flag).*$/', $a)
可令a="\nflag"
而非多行模式下,$
会忽略末尾的%0a
即空字符; eg:preg_match('/^flag$/', $_GET['a']) && $_GET['a'] !== 'flag'
可输入a=flag%0a
eval()
PHP: eval - Manual eval — 把字符串作为PHP代码执行:
1 eval(string `$code`): [mixed]code为要执行的字符串,传入的代码不能包含打开/关闭PHP tags;且要以分号结尾
(实际可以把eval($code)的效果看成将$code这部分直接插入到php代码里~)
eval是语言构造器而不是一个函数,不能被可变函数调用
可变函数即变量名加括号,PHP系统会尝试解析成函数,如果有当前变量中的值为命名的函数,就会调用。如果没有就报错。 · 可变函数不能用于例如 echo,print,unset(),isset(),empty(),include,require eval() 以及类似的语言结构。需要使用自己的包装函数来将这些结构用作可变函数
assert()
assert把整个字符串当作php代码执行,而eval是把合法的php代码执行
在PHP7.1版本以后, assert()默认不再可以执行代码 (assert在更新后无法将使用字符串作为参数,而GET或POST传入的数据默认就是字符串类型)
preg_replace()
preg_replace() /e模式下可以执行代码:深入研究preg_replace与代码执行
preg_replace — 执行一个正则表达式的搜索和替换(PHP 4, PHP 5, PHP 7)
搜索
subject
中匹配pattern
的部分, 以replacement
进行替换。第一个参数
$pattern
:搜索的模式,可以是一个字符串或者字符串数组,可以加\e
修正符。第二个参数
$replacement
:要替换的字符。第三个参数
$subject
:需要被处理的字符串。问题出在第一个参数的
\e
修正符上。当加上了\e
修正符号时,$replacement
会被当做php代码片段执行。这个环境需要在php5.4
下。php7.0
完全放弃了该函数,php5.5的后续版本
会爆出提示,要求preg_replace_callback()
来代替该函数。
creat_function()
代码注入 解析create_function()(seebug.org)
创建匿名函数:
1 create_function('$name','echo $name."a"')就类似于
1 function name($name) { echo $name."a";}那么传入
a=;}phpinfo();/*
就会得到:
1 function name($name) { echo $name;}phpinfo();/*;};}将前面的语句和函数闭合,/*把后面的;}注释掉,phpinfo();就成功执行了
curl()
php curl实现发送get和post请求 - 简书 (jianshu.com)
网鼎杯-Fakebook-反序列化和SSRF和file协议读取文件 (shuzhiduo.com)
strcmp()
strcmp(str1,str2)
:比较两个字符串str1和str2
- str1<str2 返回<0
- str1>str2返回>0
- str1=str2 返回0
ps:数据类型不匹配(即传入非字符串类型),也会返回0 (仅php<5.3)
bypass:同样的,给strcmp的参数为数组也会返回null
1 | # 传入 passwd[]=xxx |
open_basedir()绕过
chdir()、ini_set()函数组合
利用ini_set()设置php.ini的值,在函数执行时生效,脚本结束后恢复原状。
1 | ini_set ( string $varname , string $newvalue ) : string |
varname是需要设置的值;newvalue是设置成为新的值 成功时返回旧的值,失败时返回 FALSE
1 | payload: |
glob:/
glob://协议是php5.3.0以后一种查找匹配的文件路径模式,而单纯传参glob://是没办法列目录的,需要结合其他函数方法
scandir()+glob://
只能列出根目录以及open_basedir()允许目录下的文件
1 |
|
DirectoryIterator+glob://
DirectoryIterator是php5中增加的一个类,为用户提供一个简单的查看目录的接口,利用此方法可以绕过open_basedir限制。(但是似乎只能用于Linux下)
1 | payloadL: |
opendir()+readdir()+glob://
同样只能列出根目录已经open_basedir()允许的目录
1 |
|
一些特性
PHP处理上传文件
php在处理上传文件时,会将上传文件放在临时文件夹
命名格式为:/tmp/php??????
(windows下则有[.tmp
]后缀) php[0-9A-Za-z]{3,4,5,6}
默认为php+4/6位随机数字和大小写字母
php短标签
<?=
是 <?php echo
的简写形式
做题时可能遇到php被ban,就可以用短标签来绕过
以下取自php官方文档
当解析一个文件时,PHP 会寻找起始和结束标记,也就是
<?php
和?>
,这告诉 PHP 开始和停止解析二者之间的代码。此种解析方式使得 PHP 可以被嵌入到各种不同的文档中去,而任何起始和结束标记之外的部分都会被 PHP 解析器忽略。PHP 有一个 echo 标记简写
<?=
, 它是更完整的<?php echo
的简写形式ps:短标记 (第三个例子) 是被默认开启的,但是也可以通过 short_open_tag php.ini 来直接禁用。如果 PHP 在被安装时使用了 —disable-short-tags 的配置,该功能则是被默认禁用的。
分号; 被过滤
之前做题遇到,分号;
被ban掉,那么咱们传入的语法就不正确,无法正常运行
绕过方法是利用?>
来结尾 要注意的是:?>
后的php代码就不会被正常解析,而是当成html输出到页面上
00截断
条件:
- PHP版本小于5.3.4
- php.ini中的magic_quotes_gpc设置为Off
00截断的原理:ascii中的0作为特殊字符保留,表示字符串结束
像十六进制的0x00、url编码中的%00,具体使用情况视环境而定