RSS Feed
更好更安全的互联网
  • WhatsApp UAF 漏洞分析(CVE-2019-11932)

    2019-10-28

    作者:SungLin@知道创宇404实验室
    时间:2019年10月23日

    0x00

    新加坡安全研究员Awakened在他的博客中发布了这篇[0]对whatsapp的分析与利用的文章,其工具地址是[1],并且演示了rce的过程[2],只要结合浏览器或者其他应用的信息泄露漏洞就可以直接在现实中远程利用,并且Awakened在博客中也提到了:

    1、攻击者通过任何渠道将GIF文件发送给用户其中之一可以是通过WhatsApp作为文档(例如,按“Gallery”按钮并选择“Document”以发送损坏的GIF)

    如果攻击者在用户(即朋友)的联系人列表中,则损坏的GIF会自动下载,而无需任何用户交互。

    2、用户想将媒体文件发送给他/她的任何WhatsApp朋友。因此,用户按下“Gallery”按钮并打开WhatsApp Gallery以选择要发送给他的朋友的媒体文件。请注意,用户不必发送任何内容,因为仅打开WhatsApp Gallery就会触发该错误。按下WhatsApp Gallery后无需额外触摸。

    3、由于WhatsApp会显示每个媒体(包括收到的GIF文件)的预览,因此将触发double-free错误和我们的RCE利用。

    此漏洞将会影响WhatsApp版本2.19.244之前的版本,并且是Android 8.1和9.0的版本。

    我们来具体分析调试下这个漏洞。

    0x01

    首先呢,当WhatsApp用户在WhatsApp中打开“Gallery”视图以发送媒体文件时,WhatsApp会使用一个本机库解析该库,libpl_droidsonroids_gif.so以生成GIF文件的预览。libpl_droidsonroids_gif.so是一个开放源代码库,其源代码位于[3],新版本的已经修改了decoding函数,为了防止二次释放,在检测到传入gif帧大小为0的情况下就释放info->rasterBits指针,并且返回了:

    而有漏洞的版本是如何释放两次的,并且还能利用,下面来调试跟踪下。

    0x02

    Whatsapp在解析gif图像时会调用Java_pl_droidsonroids_gif_GifInfoHandle_openFile进行第一次初始化,将会打开gif文件,并创建大小为0xa8的GifInfo结构体,然后进行初始化。

    之后将会调用Java_pl_droidsonroids_gif_GifInfoHandle_renderFrame对gif图像进行解析。

    关键的地方是调用了函数DDGifSlurp(GifInfo *info, bool decode, bool exitAfterFrame)并且传入decode的值为true,在未打补丁的情况下,我们可以如Awakened所说的,构造三个帧,连续两个帧的gifFilePtr->Image.Width或者gifFilePtr->Image.Height为0,可以导致reallocarray调用reallo调用free释放所指向的地址,造成double-free:

    然后android中free两次大小为0xa8内存后,下一次申请同样大小为0xa8内存时将会分配到同一个地址,然而在whatsapp中,点击gallery后,将会对一个gif显示两个Layout布局,将会对一张gif打开并解析两次,如下所示:

    所以当第二次解析的时候,构造的帧大小为0xa8与GifInfo结构体大小是一致的,在解析时候将会覆盖GifInfo结构体所在的内存。

    0x03

    大概是这样,和博客那个流程大概一致:

    第一次解析:

    申请0xa8大小内存存储数据

    第一次free

    第二次free

    ..

    .. 第二次解析:

    申请0xa8大小内存存储info

    申请0xa8大小内存存储gif数据->覆盖info

    Free

    Free

    ..

    ..

    最后跳转info->rewindFunction(info)

    X8寄存器滑到滑块指令

    滑块执行我们的代码

    0x04

    制作的gif头部如下:

    解析的时候首先调用Java_pl_droidsonroids_gif_GifInfoHandle_openFile创建一个GifInfo结构体,如下所示:

    我们使用提供的工具生成所需要的gif,所以说newRasterSize = gifFilePtr->Image.Width * gifFilePtr->Image.Height==0xa8,第一帧将会分配0xa8大小数据

    第一帧头部如下:

    接下来解析到free所需要的帧如下,gifFilePtr->Image.Width为0,gifFilePtr->Image.Height为0xf1c,所以newRasterSize的大小将会为0,reallocarray(info->rasterBits, newRasterSize, sizeof(GifPixelType))的调用将会free指向的info->rasterBits

    连续两次的free掉大小为x0寄存器指向的0x6FDE75C580地址,大小为0xa8,而x19寄存器指向的0x6FDE75C4C0,x19寄存器指向的就是Info结构体指针

    第一次解析完后info结构体数据如下,info->rasterBits指针指向了0x6FDE75C580,而这里就是我们第一帧数据所在,大小为0xa8:

    经过reallocarray后将会调用DGifGetLine解码LZW编码并拷贝到分配内存:

    第一帧数据如下,info->rasterBits = 0x6FDE75C580

    在经过double-free掉0xa8大小内存后,第二次解析中,首先创建一个大小为0xa8的info结构体,之后将会调用DDGifSlurp解码gif,并为gif分配0xa8大小的内存,因为android的两次释放会导致两次分配同一大小内存指向同一地址特殊性,所以x0和x19都指向了0x6FDE75C580,x0是gif数据,x19是info结构体:

    此时结构体指向0x6FDE75C580

    之后经过DGifGetLine拷贝数据后,我们gif的第一帧数据将会覆盖掉0x6FDE75C580,最后运行到函数末尾,调用info->rewindFunction(info)

    此时运行到了info->rewindFunction(info),x19寄存器保存着我们覆盖了的info指针,

    此时x8寄存器指向了我们需要的指令,在libhwui中:

    此时我们来分析下如何构造的数据,在我的本机上泄露了俩个地址,0x707d540804和0x707f3f11d8,如上所示,运行到info->rewindFunction(info)后,x19存储了我们覆盖的数据大小为0xa8,汇编代码如下:

    所以我们需要泄露的第一个地址要放在X19+0X80处为0x707d540804,而0x707d540804的指令如下,所以以如下指令作为跳板执行我们的代码:

    所以刚好我们x19+0x18放的是执行libc的system函数的地址0x707f3f11d8,而x19+20是我们执行的代码所在位置:

    提供的测试小工具中,我们将会遍历lib库中的指令直到找到我们所需滑板指令的地址:

    还有libc中的system地址,将这两个地址写入gif

    跳转到libhwui后,此地址指令刚好和我们构造的数据吻合

    X8寄存器指向了libc的system调用

    X0寄存器指向我们将要运行的代码:

    0x05

    参考链接如下:

    [0] https://awakened1712.github.io/hacking/hacking-whatsapp-gif-rce
    [1] https://github.com/awakened1712/CVE-2019-11932
    [2] https://drive.google.com/file/d/1T-v5XG8yQuiPojeMpOAG6UGr2TYpocIj/view
    [3] https://github.com/koral--/android-gif-drawable/releases


    Paper

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

    作者:吴烦恼 | Categories:安全研究 | Tags:
  • 使用 Ghidra 分析 phpStudy 后门

    2019-10-22

    作者:lu4nx@知道创宇404积极防御实验室
    作者博客:《使用 Ghidra 分析 phpStudy 后门》

    这次事件已过去数日,该响应的也都响应了,虽然网上有很多厂商及组织发表了分析文章,但记载分析过程的不多,我只是想正儿八经用 Ghidra 从头到尾分析下。

    1 工具和平台

    主要工具:

    • Kali Linux
    • Ghidra 9.0.4
    • 010Editor 9.0.2

    样本环境:

    • Windows7
    • phpStudy 20180211

    2 分析过程

    先在 Windows 7 虚拟机中安装 PhpStudy 20180211,然后把安装完后的目录拷贝到 Kali Linux 中。

    根据网上公开的信息:后门存在于 php_xmlrpc.dll 文件中,里面存在“eval”关键字,文件 MD5 为 c339482fd2b233fb0a555b629c0ea5d5。

    因此,先去找到有后门的文件:

    将文件 ./PHPTutorial/php/php-5.4.45/ext/php_xmlrpc.dll 单独拷贝出来,再确认下是否存在后门:

    从上面的搜索结果可以看到文件中存在三个“eval”关键字,现在用 Ghidra 载入分析。

    在 Ghidra 中搜索下:菜单栏“Search” > “For Strings”,弹出的菜单按“Search”,然后在结果过滤窗口中过滤“eval”字符串,如图:

    从上方结果“Code”字段看的出这三个关键字都位于文件 Data 段中。随便选中一个(我选的“@eval(%s(‘%s’));”)并双击,跳转到地址中,然后查看哪些地方引用过这个字符串(右击,References > Show References to Address),操作如图:

    结果如下:

    可看到这段数据在 PUSH 指令中被使用,应该是函数调用,双击跳转到汇编指令处,然后 Ghidra 会自动把汇编代码转成较高级的伪代码并呈现在 Decompile 窗口中:

    如果没有看到 Decompile 窗口,在菜单Window > Decompile 中打开。

    在翻译后的函数 FUN_100031f0 中,我找到了前面搜索到的三个 eval 字符,说明这个函数中可能存在多个后门(当然经过完整分析后存在三个后门)。

    这里插一句,Ghidra 转换高级代码能力比 IDA 的 Hex-Rays Decompiler 插件要差一些,比如 Ghidra 转换的这段代码:

    在IDA中翻译得就很直观:

    还有对多个逻辑的判断,IDA 翻译出来是:

    Ghidra 翻译出来却是:

    而多层 if 嵌套阅读起来会经常迷路。总之 Ghidra 翻译的代码只有反复阅读后才知道是干嘛的,在理解这类代码上我花了好几个小时。

    2.1 第一个远程代码执行的后门

    第一个后门存在于这段代码:

    阅读起来非常复杂,大概逻辑就是通过 PHP 的 zend_hash_find 函数寻找 $_SERVER 变量,然后找到 Accept-Encoding 和 Accept-Charset 两个 HTTP 请求头,如果 Accept-Encoding 的值为 gzip,deflate,就调用 zend_eval_string 去执行 Accept-Encoding 的内容:

    这里 zend_eval_string 执行的是 local_10 变量的内容,local_10 是通过调用一个函数赋值的:

    函数 FUN_100040b0 最后分析出来是做 Base64 解码的。

    到这里,就知道该如何构造 Payload 了:

    朝虚拟机构造一个请求:

    结果如图:

    2.2 第二处后门

    沿着伪代码继续分析,看到这一段代码:

    重点在这段:

    变量 puVar8 是作为累计变量,这段代码像是拷贝地址 0x1000d66c 至 0x1000e5c4 之间的数据,于是选中切这行代码:

    双击 DAT_1000d66c,Ghidra 会自动跳转到该地址,然后在菜单选择 Window > Bytes 来打开十六进制窗口,现已处于地址 0x1000d66c,接下来要做的就是把 0x1000d66c~0x1000e5c4 之间的数据拷贝出来:

    1. 选择菜单 Select > Bytes;
    2. 弹出的窗口中勾选“To Address”,然后在右侧的“Ending Address”中填入 0x1000e5c4,如图:

    按回车后,这段数据已被选中,我把它们单独拷出来,点击右键,选择 Copy Special > Byte String (No Spaces),如图:

    然后打开 010Editor 编辑器:

    1. 新建文件:File > New > New Hex File;
    2. 粘贴拷贝的十六进制数据:Edit > Paste From > Paste from Hex Text

    然后,把“00”字节全部去掉,选择 Search > Replace,查找 00,Replace 那里不填,点“Replace All”,处理后如下:

    把处理后的文件保存为 p1。通过 file 命令得知文件 p1 为 Zlib 压缩后的数据:

    用 Python 的 zlib 库就可以解压,解压代码如下:

    执行结果如下:

    用 base64 命令把这段 Base64 代码解密,过程及结果如下:

    2.3 第三个后门

    第三个后门和第二个实现逻辑其实差不多,代码如下:

    重点在这段:

    后门代码在地址 0x1000d028~0x1000d66c 中,提取和处理方法与第二个后门的一样。找到并提出来,如下:

    把这段Base64代码解码:

    3 参考


    Paper

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

    作者:吴烦恼 | Categories:安全研究 | Tags:
  • CVE-2019-14287(Linux sudo 漏洞)分析

    2019-10-22

    作者:lu4nx@知道创宇404积极防御实验室
    作者博客:《CVE-2019-14287(Linux sudo 漏洞)分析》

    近日 sudo 被爆光一个漏洞,非授权的特权用户可以绕过限制获得特权。官方的修复公告请见:https://www.sudo.ws/alerts/minus_1_uid.html

    1. 漏洞复现

    实验环境:

    操作系统CentOS Linux release 7.5.1804
    内核3.10.0-862.14.4.el7.x86_64
    sudo 版本1.8.19p2

    首先添加一个系统帐号 test_sudo 作为实验所用:

    然后用 root 身份在 /etc/sudoers 中增加:

    表示允许 test_sudo 帐号以非 root 外的身份执行 /usr/bin/id,如果试图以 root 帐号运行 id 命令则会被拒绝:

    sudo -u 也可以通过指定 UID 的方式来代替用户,当指定的 UID 为 -1 或 4294967295(-1 的补码,其实内部是按无符号整数处理的) 时,因此可以触发漏洞,绕过上面的限制并以 root 身份执行命令:

    [test_sudo@localhost ~]

    $ sudo -u#4294967295 id uid=0(root) gid=1004(test_sudo) 组=1004(test_sudo) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

    2. 漏洞原理分析

    在官方代码仓库找到提交的修复代码:https://www.sudo.ws/repos/sudo/rev/83db8dba09e7

    从提交的代码来看,只修改了 lib/util/strtoid.c。strtoid.c 中定义的 sudo_strtoid_v1 函数负责解析参数中指定的 UID 字符串,补丁关键代码:

    llval 变量为解析后的值,不允许 llval 为 -1 和 UINT_MAX(4294967295)。

    也就是补丁只限制了取值而已,从漏洞行为来看,如果为 -1,最后得到的 UID 却是 0,为什么不能为 -1?当 UID 为 -1 的时候,发生了什么呢?继续深入分析一下。

    我们先用 strace 跟踪下系统调用看看:

    因为 strace -u 参数需要 root 身份才能使用,因此上面命令需要先切换到 root 帐号下,然后用 test_sudo 身份执行了 sudo -u#-1 id 命令。从输出的系统调用中,注意到:

    sudo 内部调用了 setresuid 来提升权限(虽然还调用了其他设置组之类的函数,但先不做分析),并且传入的参数都是 -1。

    因此,我们做一个简单的实验来调用 setresuid(-1, -1, -1) ,看看为什么执行后会是 root 身份,代码如下:

    注意,需要将编译后的二进制文件所属用户改为 root,并加上 s 位,当设置了 s 位后,其他帐号执行时就会以文件所属帐号的身份运行。

    为了方便,我直接在 root 帐号下编译,并加 s 位:

    [root@localhost tmp]

    # chmod +s a.out

    然后以 test_sudo 帐号执行 a.out:

    可见,运行后,当前身份变成了 root。

    其实 setresuid 函数只是系统调用 setresuid32 的简单封装,可以在 GLibc 的源码中看到它的实现:

    setresuid32 最后调用的是内核函数 sys_setresuid,它的实现如下:

    简单来说,内核在处理时,会调用 prepare_creds 函数创建一个新的凭证结构体,而传递给函数的 ruid、euid和suid 三个参数只有在不为 -1 的时候,才会将 ruid、euid 和 suid 赋值给新的凭证(见上面三个 if 逻辑),否则默认的 UID 就是 0。最后调用 commit_creds 使凭证生效。这就是为什么传递 -1 时,会拥有 root 权限的原因。

    我们也可以写一段 SystemTap 脚本来观察下从应用层调用 setresuid 并传递 -1 到内核中的状态:

    然后执行:

    接着运行前面我们编译的 a.out,看看 stap 捕获到的:


    Paper

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

    作者:吴烦恼 | Categories:安全研究 | Tags:
  • 从 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