RSS Feed
更好更安全的互联网
  • 从 Masscan, Zmap 源码分析到开发实践

    2019-10-14

    作者:w7ay@知道创宇404实验室 
    日期:2019年10月12日

    Zmap和Masscan都是号称能够快速扫描互联网的扫描器,十一因为无聊,看了下它们的代码实现,发现它们能够快速扫描,原理其实很简单,就是实现两种程序,一个发送程序,一个抓包程序,让发送和接收分隔开从而实现了速度的提升。但是它们识别的准确率还是比较低的,所以就想了解下为什么准确率这么低以及应该如何改善。

    Masscan源码分析

    首先是看的Masscan的源码,在readme上有它的一些设计思想,它指引我们看main.c中的入口函数main(),以及发送函数和接收函数transmit_thread()receive_thread(),还有一些简单的原理解读。

    理论上的6分钟扫描全网

    在后面自己写扫描器的过程中,对Masscan的扫描速度产生怀疑,目前Masscan是号称6分钟扫描全网,以每秒1000万的发包速度。

    image-20191010142518478

    但是255^4/10000000/60 ≈ 7.047 ???

    之后了解到,默认模式下Masscan使用pcap发送和接收数据包,它在Windows和Mac上只有30万/秒的发包速度,而Linux可以达到150万/秒,如果安装了PF_RING DNA设备,它会提升到1000万/秒的发包速度(这些前提是硬件设备以及带宽跟得上)。

    注意,这只是按照扫描一个端口的计算。

    PF_RING DNA设备了解地址:http://www.ntop.org/products/pf_ring/

    那为什么Zmap要45分钟扫完呢?

    在Zmap的主页上说明了

    image-20191010151936899

    用PF_RING驱动,可以在5分钟扫描全网,而默认模式才是45分钟,Masscan的默认模式计算一下也是45分钟左右才扫描完,这就是宣传的差距吗 (-

    历史记录

    观察了readme的历史记录 https://github.githistory.xyz/robertdavidgraham/Masscan/blob/master/README.md

    之前构建时会提醒安装libpcap-dev,但是后面没有了,从releases上看,是将静态编译的libpcap改为了动态加载。

    C10K问题

    c10k也叫做client 10k,就是一个客户端在硬件性能足够条件下如何处理超过1w的连接请求。Masscan把它叫做C10M问题。

    Masscan的解决方法是不通过系统内核调用函数,而是直接调用相关驱动。

    主要通过下面三种方式:

    • 定制的网络驱动
      • Masscan可以直接使用PF_RING DNA的驱动程序,该驱动程序可以直接从用户模式向网络驱动程序发送数据包而不经过系统内核。
    • 内置tcp堆栈
      • 直接从tcp连接中读取响应连接,只要内存足够,就能轻松支持1000万并发的TCP连接。但这也意味着我们要手动来实现tcp协议。
    • 不使用互斥锁
      • 锁的概念是用户态的,需要经过CPU,降低了效率,Masscan使用rings来进行一些需要同步的操作。与之对比一下Zmap,很多地方都用到了锁。
        • 为什么要使用锁?
          • 一个网卡只用开启一个接收线程和一个发送线程,这两个线程是不需要共享变量的。但是如果有多个网卡,Masscan就会开启多个接收线程和多个发送线程,这时候的一些操作,如打印到终端,输出到文件就需要锁来防止冲突。
        • 多线程输出到文件
          • Masscan的做法是每个线程将内容输出到不同文件,最后再集合起来。在src/output.c中,

    随机化地址扫描

    在读取地址后,如果进行顺序扫描,伪代码如下

    但是考虑到有的网段可能对扫描进行检测从而封掉整个网段,顺序扫描效率是较低的,所以需要将地址进行随机的打乱,用算法描述就是设计一个打乱数组的算法,Masscan是设计了一个加密算法,伪代码如下

    随机种子就是i的值,这种加密算法能够建立一种一一对应的映射关系,即在[1...range]的区间内通过i来生成[1...range]内不重复的随机数。同时如果中断了扫描,只需要记住i的值就能重新启动,在分布式上也可以根据i来进行。

    无状态扫描的原理

    回顾一下tcp协议中三次握手的前两次

    1. 客户端在向服务器第一次握手时,会组建一个数据包,设置syn标志位,同时生成一个数字填充seq序号字段。
    2. 服务端收到数据包,检测到了标志位的syn标志,知道这是客户端发来的建立连接的请求包,服务端会回复一个数据包,同时设置syn和ack标志位,服务器随机生成一个数字填充到seq字段。并将客户端发送的seq数据包+1填充到ack确认号上。

    在收到syn和ack后,我们返回一个rst来结束这个连接,如下图所示

    image-20191003223330374
    image-20191003230816536

    Masscan和Zmap的扫描原理,就是利用了这一步,因为seq是我们可以自定义的,所以在发送数据包时填充一个特定的数字,而在返回包中可以获得相应的响应状态,即是无状态扫描的思路了。 接下来简单看下Masscan中发包以及接收的代码。

    发包

    main.c中,前面说的随机化地址扫描

    image-20191003232846484

    接着生成cookie并发送

    image-20191003233102015

    看名字我们知道,生成cookie的因子有源ip,源端口,目的ip,目的端口,和entropy(随机种子,Masscan初始时自动生成),siphash24是一种高效快速的哈希函数,常用于网络流量身份验证和针对散列dos攻击的防御。

    组装tcp协议template_set_target(),部分代码

    发包函数

    可以看到它是分三种模式发包的,PF_RING,WinPcap,LibPcap,如果没有装相关驱动的话,默认就是pcap发包。如果想使用PF_RING模式,只需要加入启动参数--pfring

    接收

    在接收线程看到一个关于cpu的代码

    image-20191004003419241

    大意是锁住这个线程运行的cpu,让发送线程运行在双数cpu上,接收线程运行在单数cpu上。但代码没怎么看懂

    接收原始数据包

    主要是使用了PFRING和PCAP的api来接收。后面便是一系列的接收后的处理了。在mian.c757行

    image-20191004004238243

    后面还会判断是否为源ip,判断方式不是相等,是判断某个范围。

    接着后面的处理

    Zmap源码分析

    Zmap官方有一篇paper,讲述了Zmap的原理以及一些实践。上文说到Zmap使用的发包技术和Masscan大同小异,高速模式下都是调用pf_ring的驱动进行,所以对这些就不再叙述了,主要说下其他与Masscan不同的地方,paper中对丢包问题以及扫描时间段有一些研究,简单整理下

    1. 发送多个探针:结果表明,发送8个SYN包后,响应主机数量明显趋于平稳
    2. 哪些时间更适合扫描
      1. 我们观察到一个±3.1%的命中率变化依赖于日间扫描的时间。最高反应率在美国东部时间上午7时左右,最低反应率在美国东部时间下午7时45分左右。
      2. 这些影响可能是由于整体网络拥塞和包丢失率的变化,或者由于只间断连接到网络的终端主机的总可用性的日变化模式。在不太正式的测试中,我们没有注意到任何明显的变化

    还有一点是Zmap只能扫描单个端口,看了一下代码,这个保存端口变量的作用也只是在最后接收数据包用来判断srcport用,不明白为什么还没有加上多端口的支持。

    宽带限制

    相比于Masscan用rate=10000作为限制参数,Zmap用-B 10M的方式来限制

    image-20191010154942162

    我觉得这点很好,因为不是每个使用者都能明白每个参数代表的原理。实现细节

    image-20191010155045099
    image-20191010155334018

    发包与解包

    Zmap不支持Windows,因为Zmap的发包默认用的是socket,在window下可能不支持tcp的组包(猜测)。相比之下Masscan使用的是pcap发包,在win/linux都有支持的程序。Zmap接收默认使用的是pcap。

    在构造tcp包时,附带的状态信息会填入到seq和srcport中

    image-20191010161356014

    在解包时,先判断返回dstport的数据

    image-20191012110543094

    再判断返回的ack中的数据

    image-20191012110655331

    用go写端口扫描器

    在了解完以上后,我就准备用go写一款类似的扫描器了,希望能解决丢包的问题,顺便学习go。

    在上面分析中知道了,Masscan和Zmap都使用了pcap,pfring这些组件来原生发包,值得高兴的是go官方也有原生支持这些的包 https://github.com/google/gopacket,而且完美符合我们的要求。

    image-20191012111724556

    接口没问题,在实现了基础的无状态扫描功能后,接下来就是如何处理丢包的问题。

    丢包问题

    按照tcp协议的原理,我们发送一个数据包给目标机器,端口开放时返回ack标记,关闭会返回rst标记。

    但是通过扫描一台外网的靶机,发现扫描几个端口是没问题的,但是扫描大批量的端口(1-65535),就可能造成丢包问题。而且不存在的端口不会返回任何数据。

    控制速率

    刚开始以为是速度太快了,所以先控制下每秒发送的频率。因为发送和接收都是启动了一个goroutine,目标的传入是通过一个channel传入的(go的知识点)。

    所以控制速率的伪代码类似这样

    本地状态表

    即使将速度控制到了最小,也存在丢包的问题,后经过一番测试,发现是防火墙的原因。例如常用的iptables,其中拒绝的端口不会返回信息。将端口放行后再次扫描,就能正常返回数据包了。

    此时遇到的问题是有防火墙策略的主机如何进行准确扫描,一种方法是扫描几个端口后就延时一段时间,但这不符合快速扫描的设想,所以我的想法是维护一个本地的状态表,状态表中能够动态修改每个扫描结果的状态,将那些没有返回包的目标进行重试。

    Ps:这是针对一个主机,多端口(1-65535)的扫描策略,如果是多个IP,Masscan的随机化地址扫描策略就能发挥作用了。

    设想的结构如下

    初始数据时status为0,当发送数据时,将status变更为1,同时记录发送时间time,接收数据时通过返回的标记,dstport,seq等查找到本地状态表相应的数据结构,变更status为2,同时启动一个监控程序,监控程序每隔一段时间对所有的状态进行检查,如果发现stauts为1并且当前时间-发送时间大于一定值的时候,可以判断这个ip+端口的探测包丢失了,准备重发,将retry+1,重新设置发送时间time后,将数据传入发送的channel中。

    概念验证程序

    因为只是概念验证程序,而且是自己组包发送,需要使用到本地和网关的mac地址等,这些还没有写自动化程序获取,需要手动填写。mac地址可以手动用wireshark抓包获得。

    如果你想使用该程序的话,需要修改全局变量中的这些值

    整个go语言源程序如下,单文件。

    运行结果如下

    image-20191012135527477

    但这个程序并没有解决上述说的防火墙阻断问题,设想很美好,但是在实践的过程中发现这样一个问题。比如扫描一台主机中的1000个端口,第一次扫描后由于有防火墙的策略只检测到了5个端口,剩下995个端口会进行第一次重试,但是重试中依然会遇到防火墙的问题,所以本质上并没有解决这个问题。

    Top端口

    这是Masscan源码中一份内置的Top端口表

    可以使用--top-ports = n来选择数量。

    这是在写完go扫描器后又在Masscan中发现的,可能想象到Masscan可能也考虑过这个问题,它的方法是维护一个top常用端口的排行来尽可能减少扫描端口的数量,这样可以覆盖到大多数的端口(猜测)。

    总结

    概念性程序实践失败了,所以再用go开发的意义也不大了,后面还有一个坑就是go的pcap不能跨平台编译,只能在Windows下编译windows版本,mac下编译mac版本。

    但是研究了Masscan和Zmap在tcp协议下的syn扫描模式,还是有很多收获,以及明白了它们为什么要这么做,同时对网络协议和一些更低层的细节有了更深的认识。

    这里个人总结了一些tips:

    • Masscan源码比Zmap读起来更清晰,注释也很多,基本上一看源码就能明白大致的结构了。
    • Masscan和Zmap最高速度模式都是使用的pfring这个驱动程序,理论上它两的速度是一致的,只是它们宣传口径不一样?
    • 网络宽带足够情况下,扫描单个端口准确率是最高的(通过自己编写go扫描器的实践得出)。
    • Masscan和Zmap都能利用多网卡,但是Zmap线程切换用了锁,可能会消耗部分时间。
    • 设置发包速率时不仅要考虑自己带宽,还要考虑目标服务器的承受情况(扫描多端口时)

    参考链接


    Paper

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1052/

    作者:吴烦恼 | Categories:安全研究 | Tags:
  • 协议层的攻击——HTTP请求走私

    2019-10-11

    作者:mengchen@知道创宇404实验室
    日期:2019年10月10日

    1. 前言

    最近在学习研究BlackHat的议题,其中有一篇议题——"HTTP Desync Attacks: Smashing into the Cell Next Door"引起了我极大地兴趣,在其中,作者讲述了HTTP走私攻击这一攻击手段,并且分享了他的一些攻击案例。我之前从未听说过这一攻击方式,决定对这一攻击方式进行一个完整的学习梳理,于是就有了这一篇文章。

    当然了,作为这一攻击方式的初学者,难免会有一些错误,还请诸位斧正。

    2. 发展时间线

    最早在2005年,由Chaim Linhart,Amit Klein,Ronen Heled和Steve Orrin共同完成了一篇关于HTTP Request Smuggling这一攻击方式的报告。通过对整个RFC文档的分析以及丰富的实例,证明了这一攻击方式的危害性。

    https://www.cgisecurity.com/lib/HTTP-Request-Smuggling.pdf

    在2016年的DEFCON 24 上,@regilero在他的议题——Hiding Wookiees in HTTP中对前面报告中的攻击方式进行了丰富和扩充。

    https://media.defcon.org/DEF%20CON%2024/DEF%20CON%2024%20presentations/DEF%20CON%2024%20-%20Regilero-Hiding-Wookiees-In-Http.pdf

    在2019年的BlackHat USA 2019上,PortSwigger的James Kettle在他的议题——HTTP Desync Attacks: Smashing into the Cell Next Door中针对当前的网络环境,展示了使用分块编码来进行攻击的攻击方式,扩展了攻击面,并且提出了完整的一套检测利用流程。

    https://www.blackhat.com/us-19/briefings/schedule/#http-desync-attacks-smashing-into-the-cell-next-door-15153

    3. 产生原因

    HTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。这样一来,对同一个HTTP请求,不同的服务器可能会产生不同的处理结果,这样就产生了了安全风险。

    在进行后续的学习研究前,我们先来认识一下如今使用最为广泛的HTTP 1.1的协议特性——Keep-Alive&Pipeline

    HTTP1.0之前的协议设计中,客户端每进行一次HTTP请求,就需要同服务器建立一个TCP链接。而现代的Web网站页面是由多种资源组成的,我们要获取一个网页的内容,不仅要请求HTML文档,还有JS、CSS、图片等各种各样的资源,这样如果按照之前的协议设计,就会导致HTTP服务器的负载开销增大。于是在HTTP1.1中,增加了Keep-AlivePipeline这两个特性。

    所谓Keep-Alive,就是在HTTP请求中增加一个特殊的请求头Connection: Keep-Alive,告诉服务器,接收完这次HTTP请求后,不要关闭TCP链接,后面对相同目标服务器的HTTP请求,重用这一个TCP链接,这样只需要进行一次TCP握手的过程,可以减少服务器的开销,节约资源,还能加快访问速度。当然,这个特性在HTTP1.1中是默认开启的。

    有了Keep-Alive之后,后续就有了Pipeline,在这里呢,客户端可以像流水线一样发送自己的HTTP请求,而不需要等待服务器的响应,服务器那边接收到请求后,需要遵循先入先出机制,将请求和响应严格对应起来,再将响应发送给客户端。

    现如今,浏览器默认是不启用Pipeline的,但是一般的服务器都提供了对Pipleline的支持。

    为了提升用户的浏览速度,提高使用体验,减轻服务器的负担,很多网站都用上了CDN加速服务,最简单的加速服务,就是在源站的前面加上一个具有缓存功能的反向代理服务器,用户在请求某些静态资源时,直接从代理服务器中就可以获取到,不用再从源站所在服务器获取。这就有了一个很典型的拓扑结构。

    Topology

    一般来说,反向代理服务器与后端的源站服务器之间,会重用TCP链接。这也很容易理解,用户的分布范围是十分广泛,建立连接的时间也是不确定的,这样TCP链接就很难重用,而代理服务器与后端的源站服务器的IP地址是相对固定,不同用户的请求通过代理服务器与源站服务器建立链接,这两者之间的TCP链接进行重用,也就顺理成章了。

    当我们向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。

    3.1 CL不为0的GET请求

    其实在这里,影响到的并不仅仅是GET请求,所有不携带请求体的HTTP请求都有可能受此影响,只因为GET比较典型,我们把它作为一个例子。

    RFC2616中,没有对GET请求像POST请求那样携带请求体做出规定,在最新的RFC7231的4.3.1节中也仅仅提了一句。

    https://tools.ietf.org/html/rfc7231#section-4.3.1

    sending a payload body on a GET request might cause some existing implementations to reject the request

    假设前端代理服务器允许GET请求携带请求体,而后端服务器不允许GET请求携带请求体,它会直接忽略掉GET请求中的Content-Length头,不进行处理。这就有可能导致请求走私。

    比如我们构造请求

    前端服务器收到该请求,通过读取Content-Length,判断这是一个完整的请求,然后转发给后端服务器,而后端服务器收到后,因为它不对Content-Length进行处理,由于Pipeline的存在,它就认为这是收到了两个请求,分别是

    这就导致了请求走私。在本文的4.3.1小节有一个类似于这一攻击方式的实例,推荐结合起来看下。

    3.2 CL-CL

    RFC7230的第3.3.3节中的第四条中,规定当服务器收到的请求中包含两个Content-Length,而且两者的值不同时,需要返回400错误。

    https://tools.ietf.org/html/rfc7230#section-3.3.3

    但是总有服务器不会严格的实现该规范,假设中间的代理服务器和后端的源站服务器在收到类似的请求时,都不会返回400错误,但是中间代理服务器按照第一个Content-Length的值对请求进行处理,而后端源站服务器按照第二个Content-Length的值进行处理。

    此时恶意攻击者可以构造一个特殊的请求

    中间代理服务器获取到的数据包的长度为8,将上述整个数据包原封不动的转发给后端的源站服务器,而后端服务器获取到的数据包长度为7。当读取完前7个字符后,后端服务器认为已经读取完毕,然后生成对应的响应,发送出去。而此时的缓冲区去还剩余一个字母a,对于后端服务器来说,这个a是下一个请求的一部分,但是还没有传输完毕。此时恰巧有一个其他的正常用户对服务器进行了请求,假设请求如图所示。

    从前面我们也知道了,代理服务器与源站服务器之间一般会重用TCP连接。

    这时候正常用户的请求就拼接到了字母a的后面,当后端服务器接收完毕后,它实际处理的请求其实是

    这时候用户就会收到一个类似于aGET request method not found的报错。这样就实现了一次HTTP走私攻击,而且还对正常用户的行为造成了影响,而且后续可以扩展成类似于CSRF的攻击方式。

    但是两个Content-Length这种请求包还是太过于理想化了,一般的服务器都不会接受这种存在两个请求头的请求包。但是在RFC2616的第4.4节中,规定:如果收到同时存在Content-Length和Transfer-Encoding这两个请求头的请求包时,在处理的时候必须忽略Content-Length,这其实也就意味着请求包中同时包含这两个请求头并不算违规,服务器也不需要返回400错误。服务器在这里的实现更容易出问题。

    https://tools.ietf.org/html/rfc2616#section-4.4

    3.3 CL-TE

    所谓CL-TE,就是当收到存在两个请求头的请求包时,前端代理服务器只处理Content-Length这一请求头,而后端服务器会遵守RFC2616的规定,忽略掉Content-Length,处理Transfer-Encoding这一请求头。

    chunk传输数据格式如下,其中size的值由16进制表示。

    Lab 地址:https://portswigger.net/web-security/request-smuggling/lab-basic-cl-te

    构造数据包

    连续发送几次请求就可以获得该响应。

    image-20191009002040605

    由于前端服务器处理Content-Length,所以这个请求对于它来说是一个完整的请求,请求体的长度为6,也就是

    当请求包经过代理服务器转发给后端服务器时,后端服务器处理Transfer-Encoding,当它读取到0\r\n\r\n时,认为已经读取到结尾了,但是剩下的字母G就被留在了缓冲区中,等待后续请求的到来。当我们重复发送请求后,发送的请求在后端服务器拼接成了类似下面这种请求。

    服务器在解析时当然会产生报错了。

    3.4 TE-CL

    所谓TE-CL,就是当收到存在两个请求头的请求包时,前端代理服务器处理Transfer-Encoding这一请求头,而后端服务器处理Content-Length请求头。

    Lab地址:https://portswigger.net/web-security/request-smuggling/lab-basic-te-cl

    构造数据包

    image-20191009095101287

    由于前端服务器处理Transfer-Encoding,当其读取到0\r\n\r\n时,认为是读取完毕了,此时这个请求对代理服务器来说是一个完整的请求,然后转发给后端服务器,后端服务器处理Content-Length请求头,当它读取完12\r\n之后,就认为这个请求已经结束了,后面的数据就认为是另一个请求了,也就是

    成功报错。

    3.5 TE-TE

    TE-TE,也很容易理解,当收到存在两个请求头的请求包时,前后端服务器都处理Transfer-Encoding请求头,这确实是实现了RFC的标准。不过前后端服务器毕竟不是同一种,这就有了一种方法,我们可以对发送的请求包中的Transfer-Encoding进行某种混淆操作,从而使其中一个服务器不处理Transfer-Encoding请求头。从某种意义上还是CL-TE或者TE-CL

    Lab地址:https://portswigger.net/web-security/request-smuggling/lab-ofuscating-te-header

    构造数据包

    image-20191009111046828

    4. HTTP走私攻击实例——CVE-2018-8004

    4.1 漏洞概述

    Apache Traffic Server(ATS)是美国阿帕奇(Apache)软件基金会的一款高效、可扩展的HTTP代理和缓存服务器。

    Apache ATS 6.0.0版本至6.2.2版本和7.0.0版本至7.1.3版本中存在安全漏洞。攻击者可利用该漏洞实施HTTP请求走私攻击或造成缓存中毒。

    在美国国家信息安全漏洞库中,我们可以找到关于该漏洞的四个补丁,接下来我们详细看一下。

    CVE-2018-8004 补丁列表

    注:虽然漏洞通告中描述该漏洞影响范围到7.1.3版本,但从github上补丁归档的版本中看,在7.1.3版本中已经修复了大部分的漏洞。

    4.2 测试环境

    4.2.1 简介

    在这里,我们以ATS 7.1.2为例,搭建一个简单的测试环境。

    环境组件介绍

    环境拓扑图

    ats-topology

    Apache Traffic Server 一般用作HTTP代理和缓存服务器,在这个测试环境中,我将其运行在了本地的Ubuntu虚拟机中,把它配置为后端服务器LAMP&LNMP的反向代理,然后修改本机HOST文件,将域名ats.mengsec.comlnmp.mengsec,com解析到这个IP,然后在ATS上配置映射,最终实现的效果就是,我们在本机访问域名ats.mengsec.com通过中间的代理服务器,获得LAMP的响应,在本机访问域名lnmp.mengsec,com,获得LNMP的响应。

    为了方便查看请求的数据包,我在LNMP和LAMP的Web目录下都放置了输出请求头的脚本。

    LNMP:

    LAMP:

    4.2.2 搭建过程

    在GIthub上下载源码编译安装ATS。

    安装依赖&常用工具。

    然后解压源码,进行编译&安装。

    安装完毕后,配置反向代理和映射。

    编辑records.config配置文件,在这里暂时把ATS的缓存功能关闭。

    编辑remap.config配置文件,在末尾添加要映射的规则表。

    配置完毕后重启一下服务器使配置生效,我们可以正常访问来测试一下。

    为了准确获得服务器的响应,我们使用管道符和nc来与服务器建立链接。

    image-20191007225109915

    可以看到我们成功的访问到了后端的LAMP服务器。

    同样的可以测试,代理服务器与后端LNMP服务器的连通性。

    image-20191007225230629

    4.3 漏洞测试

    来看下四个补丁以及它的描述

    https://github.com/apache/trafficserver/pull/3192 # 3192 如果字段名称后面和冒号前面有空格,则返回400 https://github.com/apache/trafficserver/pull/3201 # 3201 当返回400错误时,关闭链接https://github.com/apache/trafficserver/pull/3231 # 3231 验证请求中的Content-Length头https://github.com/apache/trafficserver/pull/3251 # 3251 当缓存命中时,清空请求体

    4.3.1 第一个补丁

    https://github.com/apache/trafficserver/pull/3192 # 3192 如果字段名称后面和冒号前面有空格,则返回400

    看介绍是给ATS增加了RFC72303.2.4章的实现,

    https://tools.ietf.org/html/rfc7230#section-3.2.4

    在其中,规定了HTTP的请求包中,请求头字段与后续的冒号之间不能有空白字符,如果存在空白字符的话,服务器必须返回400,从补丁中来看的话,在ATS 7.1.2中,并没有对该标准进行一个详细的实现。当ATS服务器接收到的请求中存在请求字段与:之间存在空格的字段时,并不会对其进行修改,也不会按照RFC标准所描述的那样返回400错误,而是直接将其转发给后端服务器。

    而当后端服务器也没有对该标准进行严格的实现时,就有可能导致HTTP走私攻击。比如Nginx服务器,在收到请求头字段与冒号之间存在空格的请求时,会忽略该请求头,而不是返回400错误。

    在这时,我们可以构造一个特殊的HTTP请求,进行走私。

    image-20191008113819748

    很明显,请求包中下面的数据部分在传输过程中被后端服务器解析成了请求头。

    来看下Wireshark中的数据包,ATS在与后端Nginx服务器进行数据传输的过程中,重用了TCP连接。

    image-20191008114247036

    只看一下请求,如图所示:

    image-20191008114411337

    阴影部分为第一个请求,剩下的部分为第二个请求。

    在我们发送的请求中,存在特殊构造的请求头Content-Length : 56,56就是后续数据的长度。

    在数据的末尾,不存在\r\n这个结尾。

    当我们的请求到达ATS服务器时,因为ATS服务器可以解析Content-Length : 56这个中间存在空格的请求头,它认为这个请求头是有效的。这样一来,后续的数据也被当做这个请求的一部分。总的来看,对于ATS服务器,这个请求就是完整的一个请求。

    ATS收到这个请求之后,根据Host字段的值,将这个请求包转发给对应的后端服务器。在这里是转发到了Nginx服务器上。

    而Nginx服务器在遇到类似于这种Content-Length : 56的请求头时,会认为其是无效的,然后将其忽略掉。但并不会返回400错误,对于Nginx来说,收到的请求为

    因为最后的末尾没有\r\n,这就相当于收到了一个完整的GET请求和一个不完整的GET请求。

    完整的:

    不完整的:

    在这时,Nginx就会将第一个请求包对应的响应发送给ATS服务器,然后等待后续的第二个请求传输完毕再进行响应。

    当ATS转发的下一个请求到达时,对于Nginx来说,就直接拼接到了刚刚收到的那个不完整的请求包的后面。也就相当于

    然后Nginx将这个请求包的响应发送给ATS服务器,我们收到的响应中就存在了attack: 1foo: GET / HTTP/1.1这两个键值对了。

    那这会造成什么危害呢?可以想一下,如果ATS转发的第二个请求不是我们发送的呢?让我们试一下。

    假设在Nginx服务器下存在一个admin.php,代码内容如下:

    由于HTTP协议本身是无状态的,很多网站都是使用Cookie来判断用户的身份信息。通过这个漏洞,我们可以盗用管理员的身份信息。在这个例子中,管理员的请求中会携带这个一个Cookie的键值对admin=1,当拥有管理员身份时,就能通过GET方式传入要删除的用户名称,然后删除对应的用户。

    在前面我们也知道了,通过构造特殊的请求包,可以使Nginx服务器把收到的某个请求作为上一个请求的一部分。这样一来,我们就能盗用管理员的Cookie了。

    构造数据包

    然后是管理员的正常请求

    让我们看一下效果如何。

    image-20191008123056679

    在Wireshark的数据包中看的很直观,阴影部分为管理员发送的正常请求。

    image-20191008123343584

    在Nginx服务器上拼接到了上一个请求中, 成功删除了用户mengchen。

    4.3.2 第二个补丁

    https://github.com/apache/trafficserver/pull/3201 # 3201 当返回400错误时,关闭连接

    这个补丁说明了,在ATS 7.1.2中,如果请求导致了400错误,建立的TCP链接也不会关闭。在regilero的对CVE-2018-8004的分析文章中,说明了如何利用这个漏洞进行攻击。

    一共能够获得2个响应,都是400错误。

    image-20191009161111039

    ATS在解析HTTP请求时,如果遇到NULL,会导致一个截断操作,我们发送的这一个请求,对于ATS服务器来说,算是两个请求。

    第一个

    第二个

    第一个请求在解析的时候遇到了NULL,ATS服务器响应了第一个400错误,后面的bb\r\n成了后面请求的开头,不符合HTTP请求的规范,这就响应了第二个400错误。

    再进行修改下进行测试

    image-20191009161651556

    一个400响应,一个200响应,在Wireshark中也能看到,ATS把第二个请求转发给了后端Apache服务器。

    image-20191009161916024

    那么由此就已经算是一个HTTP请求拆分攻击了,

    但是这个请求包,怎么看都是两个请求,中间的GET /1.html HTTP/1.1\r\n不符合HTTP数据包中请求头Name:Value的格式。在这里我们可以使用absoluteURI,在RFC2616中第5.1.2节中规定了它的详细格式。

    https://tools.ietf.org/html/rfc2616#section-5.1.2

    我们可以使用类似GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1的请求头进行请求。

    构造数据包

    本质上来说,这是两个HTTP请求,第一个为

    其中GET http://ats.mengsec.com/1.html HTTP/1.1为名为GET http,值为//ats.mengsec.com/1.html HTTP/1.1的请求头。

    第二个为

    当该请求发送给ATS服务器之后,我们可以获取到三个HTTP响应,第一个为400,第二个为200,第三个为404。多出来的那个响应就是ATS中间对服务器1.html的请求的响应。

    image-20191009170232529

    根据HTTP Pipepline的先入先出规则,假设攻击者向ATS服务器发送了第一个恶意请求,然后受害者向ATS服务器发送了一个正常的请求,受害者获取到的响应,就会是攻击者发送的恶意请求中的GET http://evil.mengsec.com/evil.html HTTP/1.1中的内容。这种攻击方式理论上是可以成功的,但是利用条件还是太苛刻了。

    对于该漏洞的修复方式,ATS服务器选择了,当遇到400错误时,关闭TCP链接,这样无论后续有什么请求,都不会对其他用户造成影响了。

    4.3.3 第三个补丁

    https://github.com/apache/trafficserver/pull/3231 # 3231 验证请求中的Content-Length头

    在该补丁中,bryancall 的描述是

    从这里我们可以知道,ATS 7.1.2版本中,并没有对RFC2616的标准进行完全实现,我们或许可以进行CL-TE走私攻击。

    构造请求

    多次发送后就能获得405 Not Allowed响应。

    image-20191009173844024

    我们可以认为,后续的多个请求在Nginx服务器上被组合成了类似如下所示的请求。

    对于Nginx来说,GGET这种请求方法是不存在的,当然会返回405报错了。

    接下来尝试攻击下admin.php,构造请求

    多次请求后获得了响应You are not Admin,说明服务器对admin.php进行了请求。

    image-20191009175211574

    如果此时管理员已经登录了,然后想要访问一下网站的主页。他的请求为

    效果如下

    image-20191009175454128

    我们可以看一下Wireshark的流量,其实还是很好理解的。

    image-20191009180032415

    阴影所示部分就是管理员发送的请求,在Nginx服务器中组合进入了上一个请求中,就相当于

    携带着管理员的Cookie进行了删除用户的操作。这个与前面4.3.1中的利用方式在某种意义上其实是相同的。

    4.3.3 第四个补丁

    https://github.com/apache/trafficserver/pull/3251 # 3251 当缓存命中时,清空请求体

    当时看这个补丁时,感觉是一脸懵逼,只知道应该和缓存有关,但一直想不到哪里会出问题。看代码也没找到,在9月17号的时候regilero的分析文章出来才知道问题在哪。

    当缓存命中之后,ATS服务器会忽略请求中的Content-Length请求头,此时请求体中的数据会被ATS当做另外的HTTP请求来处理,这就导致了一个非常容易利用的请求走私漏洞。

    在进行测试之前,把测试环境中ATS服务器的缓存功能打开,对默认配置进行一下修改,方便我们进行测试。

    然后重启服务器即可生效。

    为了方便测试,我在Nginx网站目录下写了一个生成随机字符串的脚本random_str.php

    构造请求包

    第一次请求

    image-20191009222245467

    第二次请求

    image-20191009222313671

    可以看到,当缓存命中时,请求体中的数据变成了下一个请求,并且成功的获得了响应。

    而且在整个请求中,所有的请求头都是符合RFC规范的,这就意味着,在ATS前方的代理服务器,哪怕严格实现了RFC标准,也无法避免该攻击行为对其他用户造成影响。

    ATS的修复措施也是简单粗暴,当缓存命中时,把整个请求体清空就好了。

    5. 其他攻击实例

    在前面,我们已经看到了不同种代理服务器组合所产生的HTTP请求走私漏洞,也成功模拟了使用HTTP请求走私这一攻击手段来进行会话劫持,但它能做的不仅仅是这些,在PortSwigger中提供了利用HTTP请求走私攻击的实验,可以说是很典型了。

    5.1 绕过前端服务器的安全控制

    在这个网络环境中,前端服务器负责实现安全控制,只有被允许的请求才能转发给后端服务器,而后端服务器无条件的相信前端服务器转发过来的全部请求,对每个请求都进行响应。因此我们可以利用HTTP请求走私,将无法访问的请求走私给后端服务器并获得响应。在这里有两个实验,分别是使用CL-TETE-CL绕过前端的访问控制。

    5.1.1 使用CL-TE绕过前端服务器安全控制

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-bypass-front-end-controls-cl-te

    实验的最终目的是获取admin权限并删除用户carlos

    我们直接访问/admin,会返回提示Path /admin is blocked,看样子是被前端服务器阻止了,根据题目的提示CL-TE,我们可以尝试构造数据包

    进行多次请求之后,我们可以获得走私过去的请求的响应。

    image-20191010000428090

    提示只有是以管理员身份访问或者在本地登录才可以访问/admin接口。

    在下方走私的请求中,添加一个Host: localhost请求头,然后重新进行请求,一次不成功多试几次。

    如图所示,我们成功访问了admin界面。也知道了如何删除一个用户,也就是对/admin/delete?username=carlos进行请求。

    image-20191010000749732

    修改下走私的请求包再发送几次即可成功删除用户carlos

    image-20191010000957520

    需要注意的一点是在这里,不需要我们对其他用户造成影响,因此走私过去的请求也必须是一个完整的请求,最后的两个\r\n不能丢弃。

    5.1.1 使用TE-CL绕过前端服务器安全控制

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-bypass-front-end-controls-te-cl

    这个实验与上一个就十分类似了,具体攻击过程就不在赘述了。

    image-20190903111613344

    5.2 获取前端服务器重写请求字段

    在有的网络环境下,前端代理服务器在收到请求后,不会直接转发给后端服务器,而是先添加一些必要的字段,然后再转发给后端服务器。这些字段是后端服务器对请求进行处理所必须的,比如:

    • 描述TLS连接所使用的协议和密码
    • 包含用户IP地址的XFF头
    • 用户的会话令牌ID

    总之,如果不能获取到代理服务器添加或者重写的字段,我们走私过去的请求就不能被后端服务器进行正确的处理。那么我们该如何获取这些值呢。PortSwigger提供了一个很简单的方法,主要是三大步骤:

    • 找一个能够将请求参数的值输出到响应中的POST请求
    • 把该POST请求中,找到的这个特殊的参数放在消息的最后面
    • 然后走私这一个请求,然后直接发送一个普通的请求,前端服务器对这个请求重写的一些字段就会显示出来。

    怎么理解呢,还是做一下实验来一起来学习下吧。

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-reveal-front-end-request-rewriting

    实验的最终目的还是删除用户 carlos

    我们首先进行第一步骤,找一个能够将请求参数的值输出到响应中的POST请求。

    在网页上方的搜索功能就符合要求

    image-20191010003510203

    构造数据包

    多次请求之后就可以获得前端服务器添加的请求头

    image-20190903114123823

    这是如何获取的呢,可以从我们构造的数据包来入手,可以看到,我们走私过去的请求为

    其中Content-Length的值为70,显然下面携带的数据的长度是不够70的,因此后端服务器在接收到这个走私的请求之后,会认为这个请求还没传输完毕,继续等待传输。

    接着我们又继续发送相同的数据包,后端服务器接收到的是前端代理服务器已经处理好的请求,当接收的数据的总长度到达70时,后端服务器认为这个请求已经传输完毕了,然后进行响应。这样一来,后来的请求的一部分被作为了走私的请求的参数的一部分,然后从响应中表示了出来,我们就能获取到了前端服务器重写的字段。

    在走私的请求上添加这个字段,然后走私一个删除用户的请求就好了。

    image-20190903114641180

    5.3 获取其他用户的请求

    在上一个实验中,我们通过走私一个不完整的请求来获取前端服务器添加的字段,而字段来自于我们后续发送的请求。换句话说,我们通过请求走私获取到了我们走私请求之后的请求。如果在我们的恶意请求之后,其他用户也进行了请求呢?我们寻找的这个POST请求会将获得的数据存储并展示出来呢?这样一来,我们可以走私一个恶意请求,将其他用户的请求的信息拼接到走私请求之后,并存储到网站中,我们再查看这些数据,就能获取用户的请求了。这可以用来偷取用户的敏感信息,比如账号密码等信息。

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-capture-other-users-requests

    实验的最终目的是获取其他用户的Cookie用来访问其他账号。

    我们首先去寻找一个能够将传入的信息存储到网站中的POST请求表单,很容易就能发现网站中有一个用户评论的地方。

    抓取POST请求并构造数据包

    这样其实就足够了,但是有可能是实验环境的问题,我无论怎么等都不会获取到其他用户的请求,反而抓了一堆我自己的请求信息。不过原理就是这样,还是比较容易理解的,最重要的一点是,走私的请求是不完整的。

    image-20191010011955268

    5.4 利用反射型XSS

    我们可以使用HTTP走私请求搭配反射型XSS进行攻击,这样不需要与受害者进行交互,还能利用漏洞点在请求头中的XSS漏洞。

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-deliver-reflected-xss

    在实验介绍中已经告诉了前端服务器不支持分块编码,目标是执行alert(1)

    首先根据UA出现的位置构造Payload

    image-20190903144329596

    然后构造数据包

    此时在浏览器中访问,就会触发弹框

    image-20190903162524009

    再重新发一下,等一会刷新,可以看到这个实验已经解决了。

    5.5 进行缓存投毒

    一般来说,前端服务器出于性能原因,会对后端服务器的一些资源进行缓存,如果存在HTTP请求走私漏洞,则有可能使用重定向来进行缓存投毒,从而影响后续访问的所有用户。

    Lab地址:https://portswigger.net/web-security/request-smuggling/exploiting/lab-perform-web-cache-poisoning

    实验环境中提供了漏洞利用的辅助服务器。

    需要添加两个请求包,一个POST,携带要走私的请求包,另一个是正常的对JS文件发起的GET请求。

    以下面这个JS文件为例

    编辑响应服务器

    image-20190903170042395

    构造POST走私数据包

    然后构造GET数据包

    POST请求和GET请求交替进行,多进行几次,然后访问js文件,响应为缓存的漏洞利用服务器上的文件。

    image-20190903172338456

    访问主页,成功弹窗,可以知道,js文件成功的被前端服务器进行了缓存。

    image-20190903172544103

    6. 如何防御

    从前面的大量案例中,我们已经知道了HTTP请求走私的危害性,那么该如何防御呢?不针对特定的服务器,通用的防御措施大概有三种。

    • 禁用代理服务器与后端服务器之间的TCP连接重用。
    • 使用HTTP/2协议。
    • 前后端使用相同的服务器。

    以上的措施有的不能从根本上解决问题,而且有着很多不足,就比如禁用代理服务器和后端服务器之间的TCP连接重用,会增大后端服务器的压力。使用HTTP/2在现在的网络条件下根本无法推广使用,哪怕支持HTTP/2协议的服务器也会兼容HTTP/1.1。从本质上来说,HTTP请求走私出现的原因并不是协议设计的问题,而是不同服务器实现的问题,个人认为最好的解决方案就是严格的实现RFC7230-7235中所规定的的标准,但这也是最难做到的。

    参考链接


    Paper

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1048/

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • PhpStudy 后门分析

    2019-09-27

    作者:Hcamael@知道创宇404实验室
    时间:2019年9月26日

    背景介绍

    2019/09/20,一则杭州警方通报打击涉网违法犯罪专项行动战果的新闻出现在我的朋友圈,其中通报了警方发现PhpStudy软件被种入后门后进行的侦查和逮捕了犯罪嫌疑人的事情。用PhpStudy的Web狗还挺多的,曾经我还是Web狗的时候也用过几天,不过因为不习惯就卸了。还记得当初会用PhpStudy的原因是在网上自学一些Web方向的课程时,那些课程中就是使用PhpStudy。在拿到样本后,我就对PhpStudy中的后门进行了一波逆向分析。

    后门分析

    最近关于讲phpstudy的文章很多,不过我只得到一个信息,后门在php_xmlrpc.dll文件中,有关键词:"eval(%s(%s))"。得知这个信息后,就降低了前期的工作难度。可以直接对该dll文件进行逆向分析。

    我拿到的是2018 phpstudy的样本: MD5 (php_xmlrpc.dll) = c339482fd2b233fb0a555b629c0ea5d5

    对字符串进行搜索,很容易的搜到了函数:sub_100031F0

    经过对该函数逆向分析,发现该后门可以分为三种形式:

    1.触发固定payload:

    unk_1000D66Cunk_1000E5C4为zlib压缩的payload,后门检查请求头,当满足要求后,会获取压缩后的payload,然后执行@eval(gzuncompress(payload)),把payload解压后再执行,经过提取,该payload为:

    2.触发固定的payload2

    当请求头里面不含有Accept-Encoding字段,并且时间戳满足一定条件后,会执行asc_1000D028unk_1000D66C经过压缩的payload,同第一种情况。

    提取后解压得到该payload:

    3.RCE远程命令执行

    当请求头满足一定条件后,会提取一个请求头字段,进行base64解码,然后zend_eval_string执行解码后的exp。

    研究了后门类型后,再来看看什么情况下会进入该函数触发该后门。查询sub_100031F0函数的引用信息发现:

    该函数存在于一个结构体中,该结构体为_zend_module_entry结构体:

    sub_100031F0函数为request_startup_func,该字段表示在请求初始化阶段回调的函数。从这里可以知道,只要php成功加载了存在后门的xmlrpc.dll,那么任何只要构造对应的后门请求头,那么就能触发后门。在Nginx服务器的情况下就算请求一个不存在的路径,也会触发该后门。

    由于该后门存在于php的ext扩展中,所以不管是nginx还是apache还是IIS介受影响。

    修复方案也很简单,把php的php_xmlrpc.dll替换成无后门的版本,或者现在直接去官网下载,官网现在的版本经检测都不存后门。

    虽然又对后门的范围进行了一波研究,发现后门只存在于php-5.4.45php-5.2.17两个版本中:

    随后又在第三方网站上(https://www.php.cn/xiazai/gongju/89)上下载了phpstudy2016,却发现不存在后门:

    之后同事又发了我一份他2018年在官网下载的phpstudy2016,发现同样存在后门,跟2018版的一样,只有两个版本的php存在后门:

    查看发现第三方网站上是于2017-02-13更新的phpstudy2016。

    ZoomEye数据

    通过ZoomEye探测phpstudy可以使用以下dork:

    1. "Apache/2.4.23 (Win32) OpenSSL/1.0.2j PHP/5.4.45" "Apache/2.4.23 (Win32) OpenSSL/1.0.2j PHP/5.2.17" +"X-Powered-By" -> 89,483
    2. +"nginx/1.11.5" +"PHP/5.2.17" -> 597 总量共计有90,080个目标现在可能会受到PhpStudy后门的影响。

    可能受影响的目标全球分布概况:

    可能受影响的目标全国分布概况:

    毕竟是国产软件,受影响最多的国家还是中国,其次是美国。对美国受影响的目标进行简单的探查发现基本都是属于IDC机房的机器,猜测都是国人在购买的vps上搭建的PhpStudy。

    知道创宇云防御数据

    知道创宇404积极防御团队检测到2019/09/24开始,互联网上有人开始对PhpStudy后门中的RCE进行利用。

    2019/09/24攻击总数13320,攻击IP数110,被攻击网站数6570,以下是攻击来源TOP 20:

    攻击来源攻击次数
    *.164.246.1492251
    *.114.106.2541829
    *.172.65.1731561
    *.186.180.2361476
    *.114.101.791355
    *.147.108.2021167
    *.140.181.28726
    *.12.203.223476
    *.12.73.12427
    *.12.183.161297
    *.75.78.226162
    *.12.184.173143
    *.190.132.114130
    *.86.46.71126
    *.174.70.14992
    *.167.156.7891
    *.97.179.16487
    *.95.235.2683
    *.140.181.12080
    *.114.105.17676

    2019/09/25攻击总数45012,攻击IP数187,被攻击网站数10898,以下是攻击来源TOP 20:

    攻击来源攻击次数
    *.114.101.796337
    *.241.157.695397
    *.186.180.2365173
    *.186.174.484062
    *.37.87.813505
    *.232.241.2372946
    *.114.102.52476
    *.162.20.542263
    *.157.96.891502
    *.40.8.291368
    *.94.10.1951325
    *.186.41.21317
    *.114.102.691317
    *.114.106.254734
    *.114.100.144413
    *.114.107.73384
    *.91.170.36326
    *.100.96.67185
    *.83.189.86165
    *.21.136.203149

    攻击源国家分布:

    国家数量
    中国34
    美国1
    韩国1
    德国1

    省份分布:

    省份数量
    云南7
    北京6
    江苏6
    广东4
    香港4
    上海2
    浙江2
    重庆1
    湖北1
    四川1

    攻击payload:


    Paper

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1044/

    作者:吴烦恼 | Categories:安全研究 | Tags:
  • Thinkphp 反序列化利用链深入分析

    2019-09-23

    作者:Ethan@知道创宇404实验室
    时间:2019年9月21日

    前言

    今年7月份,ThinkPHP 5.1.x爆出来了一个反序列化漏洞。之前没有分析过关于ThinkPHP的反序列化漏洞。今天就探讨一下ThinkPHP的反序列化问题!

    环境搭建

    • Thinkphp 5.1.35
    • php 7.0.12

    漏洞挖掘思路

    在刚接触反序列化漏洞的时候,更多遇到的是在魔术方法中,因此自动调用魔术方法而触发漏洞。但如果漏洞触发代码不在魔法函数中,而在一个类的普通方法中。并且魔法函数通过属性(对象)调用了一些函数,恰巧在其他的类中有同名的函数(pop链)。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。

    漏洞分析

    首先漏洞的起点为/thinkphp/library/think/process/pipes/Windows.php__destruct()

    __destruct()里面调用了两个函数,我们跟进removeFiles()函数。

    这里使用了$this->files,而且这里的$files是可控的。所以存在一个任意文件删除的漏洞。

    POC可以这样构造:

    这里只需要一个反序列化漏洞的触发点,便可以实现任意文件删除。

    removeFiles()中使用了file_exists$filename进行了处理。我们进入file_exists函数可以知道,$filename会被作为字符串处理。

    __toString 当一个对象被反序列化后又被当做字符串使用时会被触发,我们通过传入一个对象来触发__toString 方法。我们全局搜索__toString方法。

    我们跟进\thinkphp\library\think\model\concern\Conversion.php的Conversion类的第224行,这里调用了一个toJson()方法。

    跟进toJson()方法

    继续跟进toArray()方法

    我们需要在toArray()函数中寻找一个满足$可控变量->方法(参数可控)的点,首先,这里调用了一个getRelation方法。我们跟进getRelation(),它位于Attribute类中

    由于getRelation()下面的if语句为if (!$relation),所以这里不用理会,返回空即可。然后调用了getAttr方法,我们跟进getAttr方法

    继续跟进getData方法

    通过查看getData函数我们可以知道$relation的值为$this->data[$name],需要注意的一点是这里类的定义使用的是Trait而不是class。自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。所以,这里类的继承要使用use关键字。然后我们需要找到一个子类同时继承了Attribute类和Conversion类。

    我们可以在\thinkphp\library\think\Model.php中找到这样一个类

    我们梳理一下目前我们需要控制的变量

    1. $files位于类Windows
    2. $append位于类Conversion
    3. $data位于类Attribute

    利用链如下:

    代码执行点分析

    我们现在缺少一个进行代码执行的点,在这个类中需要没有visible方法。并且最好存在__call方法,因为__call一般会存在__call_user_func__call_user_func_array,php代码执行的终点经常选择这里。我们不止一次在Thinkphp的rce中见到这两个方法。可以在/thinkphp/library/think/Request.php,找到一个__call函数。__call 调用不可访问或不存在的方法时被调用。

    但是这里我们只能控制$args,所以这里很难反序列化成功,但是 $hook这里是可控的,所以我们可以构造一个hook数组"visable"=>"method",但是array_unshift()向数组插入新元素时会将新数组的值将被插入到数组的开头。这种情况下我们是构造不出可用的payload的。

    在Thinkphp的Request类中还有一个功能filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码。

    代码位于第1456行。

    但这里的$value不可控,所以我们需要找到可以控制$value的点。