00×1-数组绕过md5函数加密

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
$flag = 'flag{test}';
if (isset($_GET['username']) and isset($_GET['password'])) {
if ($_GET['username'] == $_GET['password']){
print 'Your password can not be your username.';
}else if (md5($_GET['username']) === md5($_GET['password'])){
die('Flag: '.$flag);
}else{
print 'Invalid password';
}
}
?>

绕过方法:

我们要知道md5函数加密在低版本中是无法处理数组的(但是md5处理数组时会返回空值)。

那么突破口就来了,但是:

两个返回的都是null,自然是相同的,但是代码中又要求我们不相同。

这样值就不一样了,flag到手

00×2-数组绕过sha()函数比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$flag = "flag";
if (isset($_GET['name']) and isset($_GET['password'])){
var_dump($_GET['name']);
echo " ";
var_dump($_GET['password']);
var_dump(sha1($_GET['name']));
var_dump(sha1($_GET['password']));
if ($_GET['name'] == $_GET['password']){
echo 'Your password can not be your name!';
}else if (sha1($_GET['name']) === sha1($_GET['password'])){
die('Flag: '.$flag);
}else{
echo 'Invalid password.';
}
}else{
echo 'Login first!';
}
?>

绕过方法:

这sha1函数加密,绕过方式和MD5一样,这里就不详讲了。

sha1()函数无法处理数组类型,将报错并返回false。

拿到flag

00×3-数组返回NULL绕过

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$flag = "flag";
if (isset ($_GET['password'])) {
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE){
echo 'You password must be alphanumeric';
}else if (strpos ($_GET['password'], '--') !== FALSE){
die('Flag: ' . $flag);
}else{
echo 'Invalid password';
}
}
?>

主要代码块:

1
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)

代码解释:

ereg函数会对你传入的password从a-z,A-Z,0-9 进行匹配,将你的密码限制在这三种字符中。

ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字母的字符是大小写敏感的。

ereg()限制password的格式,只能是数字或者字母。但ereg()函数存在NULL截断漏洞,可以使用%00绕过验证。

这里ereg有两个漏洞:

1
2
%00截断及遇到%00则默认为字符串的结束
当ntf为数组时它的返回值不是FALSE

strpos — 查找字符串首次出现的位置

作用:主要是用来查找字符在字符串中首次出现的位置。

strpos()如果传入数组,会返回NULL(和MD5,sha1类似无法处理数组,返回值为NULL)

payload:

flag到手。

00×4-弱类型整数大小比较绕过之is_numeric函数

示例代码:

1
2
3
4
$temp = $_GET['password'];
is_numeric($temp)?die("no numeric"):NULL;
if($temp>1336){
echo $flag;

函数解释:

is_numeric() 函数用于检测变量是否为数字或数字字符串。

我们传入的值会被is_numeric函数进行检测,如果为数字就直接输出no numeric,所以我们要后者使其返回为NULL,并且大于1366.

绕过方法:

使用is_numeric函数传入的整数需要与字符串进行匹配的绕过原理

演示源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include_once "flag.php";

if(isset($_GET['key'])) {
$key = $_GET['key'];
if(!is_numeric($key)) {
exit("Just num!");
}
$key = intval($key);
$str = "123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3";
if($key == $str) {
echo $flag;
}
}else {
echo "Try to find out source file!";
}
?>

00×5-弱类型整数大小比较绕过之=

php中有如下两种比较符号:两个等号和三个等号(这一点和Javascript)有些类似

1
2
$a==$b
$a===$b

我们来一下php官方手册的说法

1
 $a == $b 等于 TRUE,如果类型转换后 $a 等于 $b。$a === $b 全等 TRUE,如果 $a 等于 $b,并且它们的类型也相同。

解释:

明确的看到,两个等于号的等于会在比较的时候进行类型转换的比较。

如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行。此规则也适用于 switch 语句。当用 === 或 !== 进行比较时则不进行类型转换,因为此时类型和数值都要比对.

明确的写出了 如果一个数值和一个字符串比较,那么会将字符串转换为数值(而不是相反,将数值转化为字符串)

然而,php是如何将一个字符串转化为数值的呢,我们继续查看php手册

当一个字符串被当作一个数值来取值,其结果和类型如下:如果该字符串没有包含 ‘.’,’e’ 或 ‘E’ 并且其数字值在整型的范围之内(由 PHP_INT_MAX 所定义),该字符串将被当成 integer 来取值。其它所有情况下都被作为 float 来取值。该字符串的开始部分决定了它的值。如果该字符串以合法的数值开始,则使用该数值。否则其值为 0(零)。合法数值由可选的正负号,后面跟着一个或多个数字(可能有小数点),再跟着可选的指数部分。指数部分由 ‘e’ 或 ‘E’ 后面跟着一个或多个数字构成。

这是官方手册上面的几个例子:

1
2
3
4
5
6
7
8
9
10
11
<?php
$foo = 1 + "10.5"; // $foo is float (11.5)
$foo = 1 + "-1.3e3"; // $foo is float (-1299)
$foo = 1 + "bob-1.3e3"; // $foo is integer (1)
$foo = 1 + "bob3"; // $foo is integer (1)
$foo = 1 + "10 Small Pigs"; // $foo is integer (11)
$foo = 4 + "10.2 Little Piggies"; // $foo is float (14.2)
$foo = "10.0 pigs " + 1; // $foo is float (11)
$foo = "10.0 pigs " + 1.0; // $foo is float (11)
?>

我们大概可以总结出如下的规则:当一个字符串被转换为数值时

  • 如果一个字符串为 “合法数字+e+合法数字”类型,将会解释为科学计数法的浮点数

  • 如果一个字符串为 “合法数字+ 不可解释为合法数字的字符串”类型,将会被转换为该合法数字的值,后面的字符串将会被丢弃

  • 如果一个字符串为“不可解释为合法数字的字符串+任意”类型,则被转换为0! 为0…为0

1
2
3
4
5
6
<?php
'a'==0 // true
'12a'==12 //true
'1'==1 //true
'1aaaa55sss66'==1 //true
?>

当然,上面的那些等式对于=都是false的,原本一些应该用=的地方误用了==,导致了可以注入的地方。

弱类型比较绕过==

示例代码 1:

1
2
3
4
5
6
7
8
if (isset($_POST['password'])) {
$password = $_POST['password'];
if (is_numeric($password)) {
echo "password can't be number</br>";
}elseif ($password == 404) {
echo "Password Right!</br>";
}
}

解释:

传入的参数需要是数字型的但是if函数判断的时候有不能是数字

发现比较函数的时候使用的是弱类型比较==

1
2
3
elseif ($password == 404) {
echo "Password Right!</br>";
}

因此可以传入

1
2
3
4
5
404num
进行绕过
原理同
'1aaaa55sss66'==1 //true

示例代码 2:

利用转为数字后相等的漏洞#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
if (isset($_GET['v1']) && isset($_GET['v2'])) {
$logined = true;
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if (!ctype_alpha($v1)) {$logined = false;}
if (!is_numeric($v2) ) {$logined = false;}
if (md5($v1) != md5($v2)) {$logined = false;}

if ($logined){

// continuue to do other things
} else {
echo "login failed"
}
}
?

解释:

这是一个ctf的题目,非常有趣,可以看到,要求给出两字符串,一个是纯数字型,一个只能出现字符,使两个的md5哈希值相等,然而这种强碰撞在密码学上都是无法做到的。

但是我们看到,最终比较两者的哈希的时候,使用的是等于 而不是 全等于 ,因此可以利用一下这个漏洞

再回头看一 md5() 函数

1
string md5 ( string $str [, bool $raw_output = false ] )

str原始字符串。raw_output如果可选的 raw_output 被设置为 TRUE,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。

可以知道,第二个参数为true的时候,显示16位的结果,而为false和没有第二个参数时,为32位的16进制码(16位的结果是把32位的作为ASCII码进行解析)

16进制的数据中是含有e的,可以构建使得两个数字比较的,这里有一个现成的例子:

1
2
3
4
5
md5('240610708') 
//0e462097431906509019562988736854.
md5('QNKCDZO')
//0e830400451993494058024219903391

可以看到,这两个字符串一个只包含数字,一个只包含字母,虽然两个的哈希不一样,但是都是一个形式:0e 纯数字这种格式的字符串在判断相等的时候会被认为是科学计数法的数字,先做字符串到数字的转换。

转换后都成为了0的好多好多次方,都是0,相等。(大家可以自己尝试一下)因此

1
2
3
md5('240610708')==md5('QNKCDZO'); //True
md5('240610708')===md5('QNKCDZO'); //False

用===可以避免这一漏洞。

示例代码3:

利用 类’a’==0的漏洞#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

if (isset($_POST['json'])) {

$json = json_decode($_POST['json']);
$key ="**********************";
if ($json->key == $key) {
//login success ,continue
} else {
//login failed ,return
}

?>

解释:

这次这个例子是传入一个JSON的数据,JSON在RESTful的网站中是很常用的一种数据传输的格式。这个表单会把一个name为key的input的数据作为json传到服务端

1
2
{"key":"your input"}

我们该如何破解?想”a”==0这个漏洞,之用我们使$json->key是一个数字类型的变量就可以,怎么做到呢?

php的json_decode()函数会根据json数据中的数据类型来将其转换为php中的相应类型的数据,也就是说,如果我们在json中传一个string类型,那么该变量就是string,如果传入的是number,则该变量为number。因此,我们如果传入一个数字,就可以使之相等。网页中的表单可能限制了所有的输入都是string,即使输入数字,传入的东西也是

1
2
{"key":"0"}

这是一个字符串0,我们需要让他为数字类型,用burp拦截,把两个双引号去掉,变成这样:

1
2
{"key":0}

即可。

延申:

值得讨论的一点是,在这种方法的漏洞利用中,很难在直接表单类型的POST的数据中使用,这是为什么呢,这个和HTTP协议有关。首先,我们看一下,在POST给服务器的数据中,有几种类型,也就是HTTP header中的Content-Type:

1
2
3
4
5
application/x-www-form-urlencoded
multipart/form-data
application/json
application/xml

第一个application/x-www-form-urlencoded,是一般表单形式提交的content-type第二个,是包含文件的表单。第三,四个,分别是json和xml,一般是js当中上传的.

但是因为在直接的POST的payload当中是无法区分字符串和数字的,因为在其中并没有引号出现,举一个抓包的例子

1
2
3
4
5
6
7
8
9
10
11
12
POST /login HTTP/1.1
Host: xxx.com
Content-Length: 41
Accept: application/json, text/javascript,application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Connection: close

username=admin&password=admin

可以看到,payload是放在http包的最后面的,而且都是以没有引号的形式传递的,并没有办法区分到底是字符串还是数字。因此,PHP将POST的数据全部保存为字符串形式,也就没有办法注入数字类型的数据了而JSON则不一样,JSON本身是一个完整的字符串,经过解析之后可能有字符串,数字,布尔等多种类型。

强类型比较绕过===

代码:

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
    echo $flag;
}

主要代码分析:

===的比较

1
2
3
if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
    echo $flag;
}

payload1 数组绕过

1
2
param1[]=111&param2[]=222

payload2 MD5值碰撞

1
2
param1=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%A2&param2=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

00x6-强MD5值碰撞

实例代码:

1
2
3
if((string)$_POST['param1']!==(string)$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
    echo $flag;
}

真实md5碰撞,因为此时不能输入数组了,只能输入字符串

给两个md5碰撞的链接:

https://www.jianshu.com/p/c9089fd5b1ba

https://crypto.stackexchange.com/questions/1434/are-there-two-known-strings-which-have-the-same-md5-hash-value

直接先给出两个md5碰撞的值(都是经过url编码的)

1
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

1
2
&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

md5碰撞的详解、及实现方法:

image-20220327160826932

这两串比较像的hex形式的bin文件,其md5是相同的

给出将这两串hex字符串转化为bin文件的代码,其实就是将hex字符串转化为ascii字符串,并写入文件

image-20220327160905888

碰撞脚本

hex2bin.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!coding:utf-8
hexString1 = '4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa200a8284bf36e8e4b55b35f427593d849676da0d1555d8360fb5f07fea2'
hexString2 = '4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa202a8284bf36e8e4b55b35f427593d849676da0d1d55d8360fb5f07fea2'

hexList1 = []
intList1 = []
asciiString1 =''

while True:
intString1 = hexString1[0:2]
hexString1 = hexString1[2:]
hexList1.append(intString1)
if (hexString1 == ''):
break

for i in hexList1:
intList1.append(int(i,16))
for j in intList1:
asciiString1 += chr(int(j))

f = open('1.bin','w')
f.write(asciiString1)
f.close()

hexList2 = []
intList2 = []
asciiString2 =''

while True:
intString2 = hexString2[0:2]
hexString2 = hexString2[2:]
hexList2.append(intString2)
if (hexString2 == ''):
break

for i in hexList2:
intList2.append(int(i,16))
for j in intList2:
asciiString2 += chr(int(j))

f = open('2.bin','w')
f.write(asciiString2)
f.close()

考虑到要将一些不可见字符传到服务器,这里可以使用url编码

image-20220327161137871

url编码脚本

urlencode.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!coding:utf-8
import urllib

urlString1=''
urlString2 = ''

for line in open('1.bin'):
urlString1 += urllib.quote(line)

for line in open('2.bin'):
urlString2 += urllib.quote(line)

print urlString1
print urlString2

payload:

1
2
param1=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%A2&param2=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

image-20220327161300645

00x7-intval函数绕过

实例:

可以看到一共有三层,第一层是intval函数的关卡

要求GET传参num,而且num的值既要小于2020,加1后又要大于2021…

如果传入的num不满足条件,就会变成穷人

如果不传入num,就要去非洲

函数解释:

为了绕过这一点,我从某歌上找来了一张图片进行研究

关键点:

里面有提到很关键的地方:

1
2
echo intval(1e10);    // 1410065408
echo intval('1e10'); // 1

也就是说,如果intval函数参数填入科学计数法的字符串,会以e前面的数字作为返回值,这里是1

那么当对字符串’1e10’+1是不是可以将字符串类型强行转换成数字类型呢?

本地测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$num='2e4';
echo("intval('2e4') = ".intval($num));
echo('<br>');
echo "'2e4'+1 = ";
var_dump(($num+1));
echo('<br>');
echo("intval('2e4'+1) = ".intval($num+1));
echo('<br>');
if(intval($num) < 2020 && intval($num + 1) > 2021){
echo("you pass!");
}
?>

运行结果:

1
2
3
4
5
intval('2e4') = 2
'2e4'+1 = float(20001)
intval('2e4'+1) = 20001
you pass!

看来这样绕过是可以的

00x8-双MD5头自身比较绕过

一:自身比较

1
2
3
4
if (isset($_GET['md5'])){
   $md5=$_GET['md5'];
   if ($md5==md5($md5))
}

要让MD5加密后的值与本身的值相等

直接用脚本跑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
脚本一:
<?php
for($i=0;$i<10000000000000;$i++){
$str='0e'.(string)$i;
if(hash("md5",$str)==$str){
echo $str;
break;
}
}

脚本二:
import hashlibdef md5_enc(s):
m = hashlib.md5()
m.update(str(s).encode( 'utf-8'))return m.hexdigest()
for i in range(e, 9999999999):
i = "0e’+ str(i)
enc = md5_enc(i)
print(i+" md5 is "+enc)#md5值前两位为0e
if enc[ :2] =="Oe":
#md5值ee后为纯数字
if enc[2: ].isdigit():
result.append(i)print("Got Result:"+i)break



一般绕过md5的方法有两种,一个是以0e开头,后面全是数字的结果,这个会被解析为科学计数法为0;另一个是利用数组绕过。

利用0e绕过:

1
md5('0e215962017') ==> “0e291242476940776845150308577824

二:两个参数之间的双MD5比较

本地测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 

if (isset($_GET['a']) && isset($_GET['b'])) {
$a = $_GET['a'];
$b = $_GET['b'];
if ($a != $b && md5($a) == md5(md5($b)) {
echo "flag{XXXXX}";
} else {
echo "wrong!";
}

} else {
echo 'wrong!';
}
?>

解析:

双面的判断出现了md5(md5($b),有了前面的铺垫,这里我们第一感觉就是找到一个字符串其MD5值的MD5仍然是0e开头的那就好了。开始的时候我不敢相信,那几率得多小啊,但是在昨天做一道md5截断碰撞的时候我就来了灵感,何不尝试一下,结果发现原来这种字符串使真的存在,并且碰撞0e开头的时候不到一秒钟就能碰撞到。各位观众,下面请看:

1
2
3
4
5
6
MD5值:

md5("V5VDSHva7fjyJoJ33IQl") => 0e18bb6e1d5c2e19b63898aeed6b37ea

md5("0e18bb6e1************") => 0e0a710a092113dd5ec9dd47d4d7b86f

原来真的存在0e开头的MD5值其md5结果也是0e开头,所以此题答案便出来了。a=s1885207154a,b=V5VDSHva7fjyJoJ33IQl即可绕过if判断。

其实上面的这种双md5值0e开头的字符串有很多,但是网上似乎很见到,几乎没有,下面发布一些。

双md5结果仍为0e开头字符串大全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MD5大全:

CbDLytmyGm2xQyaLNhWn

md5(CbDLytmyGm2xQyaLNhWn) => 0ec20b7c66cafbcc7d8e8481f0653d18

md5(md5(CbDLytmyGm2xQyaLNhWn)) => 0e3a5f2a80db371d4610b8f940d296af

770hQgrBOjrcqftrlaZk

md5(770hQgrBOjrcqftrlaZk) => 0e689b4f703bdc753be7e27b45cb3625

md5(md5(770hQgrBOjrcqftrlaZk)) => 0e2756da68ef740fd8f5a5c26cc45064

7r4lGXCH2Ksu2JNT3BYM

md5(7r4lGXCH2Ksu2JNT3BYM) => 0e269ab12da27d79a6626d91f34ae849

md5(md5(7r4lGXCH2Ksu2JNT3BYM)) => 0e48d320b2a97ab295f5c4694759889f