初学者大都有疑问,如何入门安全?事实上,知识是一点一滴积累起来的,肯下功夫就一定会有收获。例如进行代码审计,有经验了自然能更快定位到问题函数,再如 CTF 比赛,Get 的知识点多了处理起来就会更得心应手。本次议题中绿盟科技工业物联网安全实验室的邓永凯将以 PHP 语言为例,为我们揭示开发手册背后存在的安全隐忧,可以看到所有这些示例都是演讲者经年累积下来的并融入了自身的体会,从中透露出的是严谨的治学以及“细心+多动手”,而通过字符串操作、文件操作、过滤函数等方面的讲解,又使得我们对开发过程中可能引入的问题有了更直观认识。当然,编程语言只是作为和计算机交流的媒介,此中讨论放之其它语言也是皆准的,该文章不仅适合安全人员,开发人员更要值得借鉴,安全与开发更多时候是相辅相成的。

——看雪『Pwn』版主BDomne

邓永凯(ID:xfkxfk),绿盟科技工业物联网安全实验室研究员,从事安全产品开发、漏洞挖掘研究、安全应急响应等工作7年,目前从事工业物联网安全研究。

知名安全电子期刊《安全参考》、《书安》创始人兼负责人,逢魔安全实验室创始人。

擅长Web安全,物联网安全,代码审计,漏洞挖掘,安全开发等,在国内多个漏洞提交平台及SRC为互联网厂商、通用应用程序厂商、IoT及安防设备厂商提交大量漏洞并获官方致谢。

以下内容为邓永凯在看雪2018安全开发者峰会上的演讲实录:

我给大家带来的议题是“潜伏在PHP Manual背后的特性及漏洞”,这是一个很广泛的问题,不只是PHP Manual,各种各样开发手册中会潜伏各种各样的安全隐患。为什么说是安全隐患?因为这些东西的函数、方法、说明在普通开发者或者初学者眼里没有任何问题,是很安全的,直接拿过来用就行了,比如代码可以抄过来就用。但如果不深入了解方法里面隐藏的功能,或者这些功能在开发者是看不到的,你得阅读它的语言源代码,才能够找到特殊的东西。

我是来自绿盟科技工业物联网安全实验室的,上午我们的同事讲了工业物联网里的硬件,下午我来给大家讲应用安全的问题。

现在有各种各样的开发语言,前端、后端、移动端的各种各样开发语言百花齐放。

入门者、初学者该学习哪种开发语言?有的人是哪种开发语言最赚钱,就学哪种开发语言。可是应该怎么选择一门或者多门开发语言?该如何学习它?最简单的是自学开发手册,看各种图书,基础教程、核心课程,有的报公开课和培训班。但公开课、培训班和书籍都是暂时的,对你影响最大的、你用得最多的应该就是官方开发手册了,不仅是在学习中,在你工作编码时还得翻过来看这些开发手册,因为语言里面有很多种方法,你不可能把所有方法都记住,所以用得时候得翻开发手册看方法是怎么用的。

很多人认为开发手册里面的标准方法是没有问题的,但通过我们的研究或者大量实际案例场景的分析,但是它的源码,就会发现里面存在非常多的安全隐患。如果你直接拿来用,或者没有深入了解的话,就会给你的项目代码里面、应用里面埋下一颗不安全的种子,只是缺少一个触发的导火线。所以开发手册里也存在很多问题,需要你去发现、去挖掘。

今天我们以PHP这个语言开发手段切入来讲,基础操作一类型与转换里面存在哪些安全隐患点?PHP里面这些类型很常见,我们今天主要挑出三个点-Boolean型、整型、浮点型。Boolean型的特点是你在运算时,需要一个,不管是不是Boolean型,自动转换成Boolean型。所以在比较的时候,0、0.0、字符串0都是Boolean型。还有整型和浮点型,整型和浮点型的大小、长度是和你的操作系统有关系,在32位操作系统它最大的整型是21亿,64位的话应该是很大了。但如果一个整型数字超出了它的大小范围,就会自动变成一个浮点型,这会带来什么问题?举个例子,8-6.4的确是等于1.6,但在计算机计数器里它不是真正的等于1.6,它是等于非常接近1.6的数。0.1+0.7再乘以10,它等于7而不是等于8,为什么?因为积数的原因,它是一个非常接近8的数,其实它不是8,等于7。

当一个整数、一个浮点数的小数点后面的位数超过一定范围时,在大小比较的时候它就分不清楚了。比如上面这个,1.01是不等于1的,但是如果你再给它加一个小数点的话它就等于1了。所以在不同的操作系统里后面的小数点位数是不一样的,一旦超过某个位数,大小比较就会出现混乱。

举个例子,如图所示:

这道题代码的意思是你要输一个数字,这个数字正向反向都是相等的,而且通过转换之后也是相等的,而且不能是回文。什么叫“回文”?就是正向和反向都是一样的,比如“121”是一样大的。这个代码逻辑很简单,但让你按照这个代码逻辑去找这个数字的话,正常情况下你是找不到的,是不存在这种数字的。但是在32位操作系统或者64位操作系统里,你可以输入整数最大的范围,然后它通过各种函数的处理,就会满足这种条件,第一种方法是通过最大的数字来满足条件。第二种是通过小数点,达到某一个位数之后,它的比较就错乱了。第三种方法,有一种更简单的方法。

字符类型的转换有很多比较小的问题,但刚才那种问题是最常见的,而且在很多开发者知识库里应该没有这样的知识,所以这只是提出一个影子。大家看看字符操作里面方法和函数存在的问题,数组和字符串,如果一个字符串被当作数组来取值的话它会返回字符串的第一个字符,这导致什么问题?我们平时做代码审计时经常出现这种问题,字符串在数组在取值时会出现错乱的逻辑。字符串如果要当作个数组,就会导致比如你传一个单引号,它会转译成斜杠,但如果按数组去取的话取第一位,它把转译符取进去,单引号没有取进去,经常导致sql注入。平时做代码审计时经常发现这种情况,或者翻之前漏洞库有很多代码审计的案例里都有这种情况,比如你传入两个参数,它会取一个反引号,把单引号丢掉了,导致很明显的注入漏洞。

还有iconv这个函数是用来对字符串进行编码转换的,但这个函数在编码转换时会存在很多问题。第一个问题是存在截断的问题,比如你要给它输入一个图标路径,让它从UTF8的编码格式转换成GBK的编码格式,但是如果从PHP5.4版本之前,在转换时它不认识不可识别的乱码字符,会把字符后面的东西去掉了,这就导致截断的问题,这种问题在文件上传里经常会遇到,而且是文件上传绕过的一种方法。第二个问题它把内容从a编码类型转换成b编码类型时,会导入带入一些转译符进去,如果带入转译符的话就会使你代码里的逻辑产生错乱。比编码e98ca6这个字符是UTF8的,它转成GBK以后变成e5c,系统里认为e5c是个转译符,这时就把转译符带进去了,带入了转译符导致很多代码执行的漏洞,本来是没有问题的,转译之后就会产生问题。

这个函数是解析url的,我分析这个方法时找到4个有问题的点,第一个种情况,它在解析时,如果给它传一个不合格的url,或者传一个非法的url,它会访问一个host(音),这种逻辑也可以理解,也没什么错误。但2107年在国外的CTF上有这么一个题,它的原理是给它正常解析url的时候是没有问题的,但如果前面加很多斜杠的话,它解析不了的。第二种情况也是一样的,但它有版本限制,在5.3之后的新版本有,老版本反而没有,通过url去解析之后,它把正常情况下我们认为的parth解析成host了,这个案例也是在国外的CTF里遇到的。第三种情况是它如果遇到一个无效的字符,不像刚才那个截断了,它会自动用下划线给你替换了。第四种是在5.47版本之前,在解析的时候如果你把前面的协议格式去掉,它解析的时候也会存在问题,本来是host,它又解析成parth了,里面存在很多冲突的问题。

刚才是解释url,这个是解析字符串的。解析字符串的函数更有意思,导致的问题是它会触发很多变量覆盖的情况,比如这个函数是解析字符串的,给你传个数组是什么情况?在解析数组的时候解析不了的,导致后面的变量没有定义,它不是刚才的访问parth,它是访问的nor,没定义的话再传一个定义一下,就导致变量覆盖了。而且它自动给你解码一次,我传数据时先编码一次,编码之后就有可能绕过一些防护设备,绕过waf的判断,比如有a字符的编码之后就没有a字符了,这时候绕过waf了,但后台会自动解码。这个问题在之前系统里有一个真实的漏洞,跟这个原理是一模一样的,通过这种方法sql注入到你的系统后台,这是一个真实的漏洞。

这两个函数也是基于字符串处理的,你取了一些email的数据和username的数据,它判断你的username值是不是大于20个,因为输入不能太长了,只让你输入20个字符,如果大于20个字符,它就把你后面的字符去掉了。导致了什么问题?比如我第20个字符尝试单引号非法字符,它会转译,第20个字符变成转译符反斜杠,这个反斜杠就把单引号转译了,又导致SQL注入的问题。

Substr这个是字符串替换,绕过一些软waf,屡试不爽。它会从你的字符串替换掉,替换之后进行操作,如果把恶意字符替换成其他的字符就会导致问题。还有很多函数,像这些函数千万不要放在if条件判断里,为什么?比如intval在把你输入的内容转译成一个数字,但是它在转译的时候有一个特点,就是它直到遇到数字或者正负号才开始做转译或转换,如果遇到非数字或者字符串的时候它就会结束。比如这个案例里,后面是字符串,前面是它字符,然后前面给它放一个数字,这时候判断你传输的是一个数字,但是后面放到用户条件里等于没有转译是一样的,只是满足了条件,操作的时候并没有转译。下面的函数也是一样。这种函数存在很多有这种问题的方法。

其他的很多方法我不一一列举,比如date是获取当前日期的,但是它有一个反转译的功能,不知道它设计这个是干嘛的,好好取你的时间就得了,但还返转译,不知道它的设计初衷是什么。还有chr,输入一个数字它返回字符,但你输入321是a,输入一个365也是a。Json-decode它偏偏对单引号不处理,产生很多单引号逃逸等。还有其他的函数,我就不一一列举了。

在我看来文件操作的很多函数都是有问题的,我列一些常见的、常用的、出现问题频率高的函数举例。

比如file-put-contents这个函数的功能是写一个字符串到文件里,写字符串时你可以不提交字符串,你可以提交数组,它的函数功能非常强大,就把数组给你转成字符串。这也是一个真实的案例,正则去判断你传入的内容必须是大小写字母、数字和下划线、空格,这些字符之外的数据是不让你输入的,这样即使有漏洞的话也没办法写入数据进去。但它里面把太甜e和content进行了拼接,由于PHP弱类型的问题,它把字符串和数组进行拼接时,会把数组自动转成字符串,字符串是array,如果传输数组变量,它会把数组变量变成array字符串,然后和前面的字符串拼接,拼接之后就满足数字表达式了,然后绕过个正则函数。绕过之后它在写文件时又恢复回来,把数组拼接成字符传,你可以把PHP代码数据写到文件里。

Open-bsedir这个文件是为了限制你操作敏感文件,我放到一个目录项目,而这个目录你是操作不了的。我要想读你这个文件,肯定得绕过你这个限制。怎么绕过?第一种方法,我们利用glob的协议,这个协议跟刚才的函数功能差不多,但不是同样的东西。

它利用这个协议去封装数据流,数据流会把自带的文件目录里面的东西全部列出来,而且这个协议是不受open-bsedir限制的,比如你把敏感文件放在控制的目录里,正常情况下是读不到的,但是通过这种协议去读,不用直接给它传一个文件路径,你在文件路径前面加一个协议,它就可以读出来。比如像这个案例,我设置了一个目录test,你只能操作test这个目录,test之外目录你操作不了,你读上层目录它会报警报错,说你读不了,这是有权限的。但如果你通过给它加一个协议去读,它会把目录文件读出来。我通过golb的协议,就可以绕过很多限制去读系统本来不让你读的文件。

另外一种情况是通过symlink函数,它的功能是创建链接文件的,但链接文件的时候也是受到open-bsedir限制。你可以生成一个文件,链接到password上去可以看到,如果文件目录被限制了,这时你创建不了文件,但通过左边操作给它创建一个特殊的链接文件,这时候你再去读,就可以绕过它的限制读出来。国外有一篇论文讲这个漏洞的原理讲得很清楚,大家可以仔细研究一下它到底是怎么绕过open-bsedir限制而去读数据的。

在PHP里面很多跟文件操作相关的函数都是有缓存的,比如你第一次给这个文件复制一个属性,然后你没有更新,你再去读这个文件的属性,发现这个文件属性没有改过来,还是之前的属性。所以你在里面对某个文件的属性进行修改,记得一定要执行这个clearstatcache,让它更新缓存,你的文件属性才会改过来。否则不管怎么改,它都是最原始加载出来的属性。有缓存功能的文件通过之后,上面列出这些函数都是有缓存功能,如果利用这些函数修改文件的内容或者属性,记得运行上面这个函数清缓,否则你修改之后就不生效。

但是有一个特例,就是unlink这个函数,它在删除本地文件时会自动更新缓存,如果它删除一个远程文件的话就不清缓存了。Unlink可以删本地文件,也可以删远程文件,但它删远程文件时清不了缓存。比如下面这个案例,它会先判断远程的文件是不是存在,然后把它删掉,删掉之后再去判断,它还存在。如果你删除本地文件它就自动删掉了,但删除远程文件它清不掉,这上下存在矛盾的逻辑关系,如果你在写代码时没有理清楚这种逻辑,会导致错误的程序控制流程。

PHP里面操作文件的函数,很多人认为它只能操作本地的文件,其实它可以通过很多其他的协议去操作远程的文件,比如通过file协议、ftp协议、glob协议、ssh2协议等,都可以操作远程文件。导致经常被绕过SSRF、XXE、读文件漏洞等。

这个案例是通过XXE漏洞,再利用远程的协议,去读一个服务器上的文件。如果通过正常情况是读不到的,但是你可以通过远程的协议,例如用ftp去读,就可以读到远程的flash文件。

文件操作的函数还有很多,还有file-exists是判断函数存在的,这个文件的频率非常高,但这个文件对回收工作“点点杠”是不生效的。还有basename和session的处理,标官的是官方标准示例代码,它是很标准的文件上传漏洞,感兴趣的同学可以去翻一下PHP官方手册里的示例代码,里面的代码是有文件生成漏洞的,而且是很标准的文件上传漏洞,如果没有安全意识的开发者去把它的代码直接抄到系统里,那就是很厉害的一个漏洞。官方手册的东西也不能相信。

还有过滤函数,过滤函数是为了官方让你的代码更安全一点,处理一些非法的操作,它就加了这些过滤函数。有些过滤函数加了进去之后,不仅没有起到作用,而且还会助漏洞一臂之力。

Escapeshellarg和escapeshellcmd函数我讲过,官方的议题是用它防止命令执行的,但是不知道为什么,过一段时间它就会爆出一个特殊的例子,我总结了这两个函数的问题,第一个问题是相互伤害型的,两个一起用的时候就没有任何效果了,这个漏洞我在去年讲过。第二个问题,本来你过滤之后是没有问题的,非得在后面加双引号把它包裹起来,这时画蛇添足,等于你没有过滤一样,为什么?因为PHP里面双引号的变量会执行的,你在命令执行双引号换成反引号,它的命令也会执行的。第三个问题,下面这种情况是今年出的一个特别有意思的以外,正常情况下用下面这种方法防止命令执行是没有任何问题的,觉得它是没有命令执行漏洞的,但有一种特殊情况,就是你如果执行的命令有一个参数,而且参数的值恰恰也可以执行命令,这时就无解了。比如query这个,可以传一个值和对进去,那个值的地方可以执行命令,这个地方是没办法改,这个代码是没有问题的,这是get相关系统里的漏洞。

Filter-var这个函数很强大,它可以过滤很多内容,你可以过滤输入的IP地址是否合法,输入的host是否合法,还可以过滤url、email。但过滤email时有一个问题,就是在email标准里面定义的前面那部分字符串可以不用双引号,但那个字符串也可以加双引号,但一旦加上双引号之后里面的字符串就可以随便输了,没有限制了,输入任何字符串都可以。如果你输入任何字符串,这时你的系统就会产生各种各样的问题。

你还可以过滤url,过滤url的时候我去官方手册也没看出什么问题,然后我就去fuzz了一下,fuzz它的协议头,再去fuzz域名后面跟一些特殊字符。跑完之后发现给域名后面加一个分号,这时它认为这是错误的,可是把加分号的http协议去掉,换成0,它认为这是合法的url。把url域名里加一个符,它也是合法的url,但加了符之后会有影响,因为在PHP里加了它意味着这是一个变量,在代码处理时会把后面这个变量替换成其他的字符。比如你要到a网站,最后跑到b网站上去了,其实就是拜这个函数所赐。

Trim函数用刚才第一个字符类型的案例来举例,这个函数一个是去掉空白符的,一个是输入数字。我们再把刚才CTF题的放出来,还有第三种解法,就是看trim和numeric这两个函数的源码,找不到解码时就看函数的源码,我们通过阅读源码发现这个函数在去除首尾字符空白符时,去除的字符有空格、制表符、换行符、回车符、空字节符、垂直制表符,它把这些去掉了。但这个函数在进行变量处理的时候,默认把这些字符跳过来了,比前面那个不一样的是多了一个“斜杠f”。第三种方法就很简单,本来输入的是一个回文,你是不满足条件的,你在前面加一个“斜杠f”,它就满足条件了。所以通过阅读源码之后,会找到更简单的方法绕过它的代码。这些函数的这些内容在官方手册上是不会说的,它只会说这个函数是用来去除首尾空白符的,但是不会跟你说替换哪些空白符,这看源码之后就知道它去掉了很多字符,而且前后有些函数去掉的字符是不一样的。

Class-exists这个函数是看你的类是否存在,但这个有什么问题?它在5.4版本之后存在任意文件读取漏洞。我发了一些资料,但没有找到任何解释。这有一个设计代码是从官方手册上抄过来的,我去搜了一下这个代码,网上也没有人说它存在什么问题,但是我在测试时,它是存在任意文件读取漏洞的。由于时间关系,这个代码我就不解释了,大家下去可以调试一下。

最后一个内容,反序列化。

反序列化主要的知识点是你去找反序列化的控制数据,找它的魔法函数,看魔法函数里是否有恶意操作。右边是找反序列化的步骤和技巧,左边是魔法函数,你可以找魔法函数的特殊方法。因为反序列化这个东西,一旦漏洞挖掘时要首先构造一个反序列化的拓扑链,如果拓扑链构造不成功,比如应用函数里没有拓扑链,就可以拿PHP里原生的内容构造一个拓扑链。

这是国外商城应用程序的案例,这个方法很简单,就是你通过原生类让它请求一个地址,它就会导致一个任意文件读取的漏洞。第二个原生类是可以利用目录,把目录下所有的文件以及目录全部列出来,这是利用原生类进行序列化的技巧。还有另外两类,是通过soapClient和pdo这两个类可以去建一些文件。第三个,通过soapClient,这种原生类可以导致很多问题,比如SSRF,SSRF结合CSRF(音译)的话就可以对内网的MySQL进行攻击。比如这有一个设计代码,你去请求它,会读到远程操作系统的文件或者导致漏洞。还有其他的很多原生类都可以在反序列化里导致很大的杀伤力。

比如这里异常的类,你输入一个数据字符串,它报错抓到异常的话会把字符串原生显示出来,如果你给它XSS代码,它直接把XSS代码返回来,导致XSS漏洞。PHP原生的列出来这所有函数都存在这个问题,都可以直接导致XSS的问题。

还有很多其他的内容,我就不再一一介绍了,因为时间关系。比如openssl加密函数使用不当的话,很容易导致反转攻击和oracle问题。还有随机数的问题在PHP里也经常出现,PHP在产生随机数时会从上到下只会播种一次,但如果我们利用工具下面的mt-rand可以去爆破它的种子,爆破它的种子之后就会猜到它后面开发的随机数,在任意代码重置和加密里会出现很多问题。还有PHPwrappers相当于一个论文,这个论文里详细讲解了PHPwrappers它的功能存在漏洞的点。

很多人问,你上面讲了很多琐碎的知识,这些知识该如何发现?或者对我们有什么用?对于其他研究者来说,这些点在代码审计或者在攻防比赛里经常会出现,而且上面的很多案例都是同代码审计的案例里拿过来的。对开发者来说,如果不了解函数隐藏的用法,你直接用,就会导致很多安全问题,而且这些问题你觉得是没有问题的,因为你去官方手册搜索资料是没有问题的,但这些代码出现在安全开发者眼里就会导致各种各样的漏洞。

如何去发现?比如在平时做漏洞挖掘或者代码审计里,可以进行各种总结,然后去看它的源代码,或者进行功能的fuzz,这时就会得到很多预期的效果。要细心、多动手,遇到问题多测试,可能会返回跟你预料中不一样的结果。

由于时间关系,很多案例没有办法一一讲代码。我收集了一个资料库,里面是所有跟函数相关的问题点、安全因素隐患、实体代码,都会整理成开源的项目给大家共享出来。而且我在整理这个的时候,有一个圈里的朋友也整理了一个类似的,上面说很多内容,如果感兴趣的话可以去上面查。

谢谢大家!

*转载请注明来自看雪社区

峰会回顾系列文章: