RSS Feed
更好更安全的互联网
  • Linux HIDS agent 概要和用户态 HOOK(一)

    2019-12-24

    作者:u2400@知道创宇404实验室
    时间:2019年12月19日

    前言:最近在实现linux的HIDS agent, 搜索资料时发现虽然资料不少, 但是每一篇文章都各自有侧重点, 少有循序渐进, 讲的比较全面的中文文章, 在一步步学习中踩了不少坑, 在这里将以进程信息收集作为切入点就如何实现一个HIDS的agent做详细说明, 希望对各位师傅有所帮助.

    1. 什么是HIDS?

    主机入侵检测, 通常分为agent和server两个部分

    其中agent负责收集信息, 并将相关信息整理后发送给server.

    server通常作为信息中心, 部署由安全人员编写的规则(目前HIDS的规则还没有一个编写的规范),收集从各种安全组件获取的数据(这些数据也可能来自waf, NIDS等), 进行分析, 根据规则判断主机行为是否异常, 并对主机的异常行为进行告警和提示.

    HIDS存在的目的在于在管理员管理海量IDC时不会被安全事件弄的手忙脚乱, 可以通过信息中心对每一台主机的健康状态进行监视.

    相关的开源项目有OSSEC, OSquery等, OSSEC是一个已经构建完善的HIDS, 有agent端和server端, 有自带的规则, 基础的rootkit检测, 敏感文件修改提醒等功能, 并且被包含到了一个叫做wazuh的开源项目, OSquery是一个facebook研发的开源项目, 可以作为一个agent端对主机相关数据进行收集, 但是server和规则需要自己实现.

    每一个公司的HIDS agent都会根据自身需要定制, 或多或少的增加一些个性化的功能, 一个基础的HIDS agent一般需要实现的有:

    • 收集进程信息
    • 收集网络信息
    • 周期性的收集开放端口
    • 监控敏感文件修改

    下文将从实现一个agent入手, 围绕agent讨论如何实现一个HIDS agent的进程信息收集模块

    2. agent进程监控模块提要

    2.1进程监控的目的

    在Linxu操作系统中几乎所有的运维操作和入侵行为都会体现到执行的命令中, 而命令执行的本质就是启动进程, 所以对进程的监控就是对命令执行的监控, 这对运维操作升级和入侵行为分析都有极大的帮助

    2.2 进程监控模块应当获取的数据

    既然要获取信息那就先要明确需要什么, 如果不知道需要什么信息, 那实现便无从谈起, 即便硬着头皮先实现一个能获取pid等基础信息的HIDS, 后期也会因为缺少规划而频繁改动接口, 白白耗费人力, 这里参考《互联网企业安全高级指南》给出一个获取信息的基础列表, 在后面会补全这张表的的获取方式

    数据名称含义
    path可执行文件的路径
    ppath父进程可执行文件路径
    ENV环境变量
    cmdline进程启动命令
    pcmdline父进程启动命令
    pid进程id
    ppid父进程id
    pgid进程组id
    sid进程会话id
    uid启动进程用户的uid
    euid启动进程用户的euid
    gid启动进程用户的用户组id
    egid启动进程用户的egid
    mode可执行文件的权限
    owner_uid文件所有者的uid
    owner_gid文件所有者的gid
    create_time文件创建时间
    modify_time最近的文件修改时间
    pstart_time进程开始运行的时间
    prun_time父进程已经运行的时间
    sys_time当前系统时间
    fd文件描述符

    2.3 进程监控的方式

    进程监控, 通常使用hook技术, 而这些hook大概分为两类:

    应用级(工作在r3, 常见的就是劫持libc库, 通常简单但是可能被绕过 - 内核级(工作在r0或者r1, 内核级hook通常和系统调用VFS有关, 较为复杂, 且在不同的发行版, 不同的内核版本间均可能产生兼容性问题, hook出现严重的错误时可能导致kenrel panic, 相对的无法从原理上被绕过

    首先从简单的应用级hook说起

    3. HIDS 应用级hook

    3.1 劫持libc库

    库用于打包函数, 被打包过后的函数可以直接使用, 其中linux分为静态库和动态库, 其中动态库是在加载应用程序时才被加载, 而程序对于动态库有加载顺序, 可以通过修改 /etc/ld.so.preload 来手动优先加载一个动态链接库, 在这个动态链接库中可以在程序调用原函数之前就把原来的函数先换掉, 然后在自己的函数中执行了自己的逻辑之后再去调用原来的函数返回原来的函数应当返回的结果.

    想要详细了解的同学, 参考这篇文章

    劫持libc库有以下几个步骤:

    3.1.1 编译一个动态链接库

    一个简单的hook execve的动态链接库如下.
    逻辑非常简单

    1. 自定义一个函数命名为execve, 接受参数的类型要和原来的execve相同
    2. 执行自己的逻辑

    通过gcc编译为so文件.

    3.1.2 修改ld.so.preload

    ld.so.preload是LD_PRELOAD环境变量的配置文件, 通过修改该文件的内容为指定的动态链接库文件路径,

    注意只有root才可以修改ld.so.preload, 除非默认的权限被改动了

    自定义一个execve函数如下:

    image.png

    可以输出当前进程的Pid和所有的环境变量, 编译后修改ld.so.preload, 重启shell, 运行ls命令结果如下

    3.1.3 libc hook的优缺点

    优点: 性能较好, 比较稳定, 相对于LKM更加简单, 适配性也很高, 通常对抗web层面的入侵.

    缺点: 对于静态编译的程序束手无策, 存在一定被绕过的风险.

    3.1.4 hook与信息获取

    设立hook, 是为了建立监控点, 获取进程的相关信息, 但是如果hook的部分写的过大过多, 会导致影响正常的业务的运行效率, 这是业务所不能接受的, 在通常的HIDS中会将可以不在hook处获取的信息放在agent中获取, 这样信息获取和业务逻辑并发执行, 降低对业务的影响.

    4 信息补全与获取

    如果对信息的准确性要求不是很高, 同时希望尽一切可能的不影响部署在HIDS主机上的正常业务那么可以选择hook只获取PID和环境变量等必要的数据, 然后将这些东西交给agent, 由agent继续获取进程的其他相关信息, 也就是说获取进程其他信息的同时, 进程就已经继续运行了, 而不需要等待agent获取完整的信息表.

    /proc/[pid]/stat

    /proc是内核向用户态提供的一组fifo接口, 通过伪文件目录的形式调用接口

    每一个进程相关的信息, 会被放到以pid命名的文件夹当中, ps等命令也是通过遍历/proc目录来获取进程的相关信息的.

    一个stat文件内容如下所示, 下面self是/proc目录提供的一个快捷的查看自己进程信息的接口, 每一个进程访问/self时看到都是自己的信息.

    会发现这些数据杂乱无章, 使用空格作为每一个数据的边界, 没有地方说明这些数据各自表达什么意思.

    一般折腾找到了一篇文章里面给出了一个列表, 这个表里面说明了每一个数据的数据类型和其表达的含义, 见文章附录1

    最后整理出一个有52个数据项每个数据项类型各不相同的结构体, 获取起来还是有点麻烦, 网上没有找到轮子, 所以自己写了一个

    具体的结构体定义:

    从文件中读入并格式化为结构体:

    和我们需要获取的数据做了一下对比, 可以获取以下数据

    ppid父进程id
    pgid进程组id
    sid进程会话id
    start_time父进程开始运行的时间
    run_time父进程已经运行的时间

    /proc/[pid]/exe

    通过/proc/[pid]/exe获取可执行文件的路径, 这里/proc/[pid]/exe是指向可执行文件的软链接, 所以这里通过readlink函数获取软链接指向的地址.

    这里在读取时需要注意如果readlink读取的文件已经被删除, 读取的文件名后会多一个 (deleted), 但是agent也不能盲目删除文件结尾时的对应字符串, 所以在写server规则时需要注意这种情况

    /proc/[pid]/cmdline

    获取进程启动的是启动命令, 可以通过获取/proc/[pid]/cmdline的内容来获得, 这个获取里面有两个坑点

    1. 由于启动命令长度不定, 为了避免溢出, 需要先获取长度, 在用malloc申请堆空间, 然后再将数据读取进变量.
    2. /proc/self/cmdline文件里面所有的空格和回车都会变成 '\0'也不知道为啥, 所以需要手动换源回来, 而且若干个相连的空格也只会变成一个'\0'.

    这里获取长度的办法比较蠢, 但是用fseek直接将文件指针移到文件末尾的办法每次返回的都是0, 也不知道咋办了, 只能先这样

    获取cmdline的内容

    小结

    这里写的只是实现的一种最常见最简单的应用级hook的方法具体实现和代码已经放在了github上, 同时github上的代码会保持更新, 下次的文章会分享如何使用LKM修改sys_call_table来hook系统调用的方式来实现HIDS的hook.

    参考文章

    https://www.freebuf.com/articles/system/54263.htmlhttp://abcdefghijklmnopqrst.xyz/2018/07/30/Linux_INT80/https://cloud.tencent.com/developer/news/337625https://github.com/g0dA/linuxStack/blob/master/%E8%BF%9B%E7%A8%8B%E9%9A%90%E8%97%8F%E6%8A%80%E6%9C%AF%E7%9A%84%E6%94%BB%E4%B8%8E%E9%98%B2-%E6%94%BB%E7%AF%87.md

    附录1

    这里完整的说明了/proc目录下每一个文件具体的意义是什么.
    http://man7.org/linux/man-pages/man5/proc.5.html


    Paper

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

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • 认识 JavaAgent –获取目标进程已加载的所有类

    2019-12-24

    之前在一个应用中搜索到一个类,但是在反序列化测试的时出错,错误不是class notfound,是其他0xxx这样的错误,通过搜索这个错误大概是类没有被加载。最近刚好看到了JavaAgent,初步学习了下,能进行拦截,主要通过Instrument Agent来进行字节码增强,可以进行字节码插桩,bTrace,Arthas 等操作,结合ASM,javassist,cglib框架能实现更强大的功能。Java RASP也是基于JavaAgent实现的。趁热记录下JavaAgent基础概念,以及简单使用JavaAgent实现一个获取目标进程已加载的类的测试。

    JVMTI与Java Instrument

    Java平台调试器架构(Java Platform Debugger Architecture,JPDA)是一组用于调试Java代码的API(摘自维基百科):

    • Java调试器接口(Java Debugger Interface,JDI)——定义了一个高层次Java接口,开发人员可以利用JDI轻松编写远程调试工具
    • Java虚拟机工具接口(Java Virtual Machine Tools Interface,JVMTI)——定义了一个原生(native)接口,可以对运行在Java虚拟机的应用程序检查状态、控制运行
    • Java虚拟机调试接口(JVMDI)——JVMDI在J2SE 5中被JVMTI取代,并在Java SE 6中被移除
    • Java调试线协议(JDWP)——定义了调试对象(一个 Java 应用程序)和调试器进程之间的通信协议

    JVMTI 提供了一套"代理"程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。

    JVMTIAgent是一个利用JVMTI暴露出来的接口提供了代理启动时加载(agent on load)、代理通过attach形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。Instrument Agent可以理解为一类JVMTIAgent动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent),是专门为java语言编写的插桩服务提供支持的代理

    Instrumentation接口

    以下接口是Java SE 8 API文档中[1]提供的(不同版本可能接口有变化):

    redefineClasses与redefineClasses

    重新定义功能在Java SE 5中进行了介绍,重新转换功能在Java SE 6中进行了介绍,一种猜测是将重新转换作为更通用的功能引入,但是必须保留重新定义以实现向后兼容,并且重新转换操作也更加方便。

    Instrument Agent两种加载方式

    在官方API文档[1]中提到,有两种获取Instrumentation接口实例的方法 :

    1. JVM在指定代理的方式下启动,此时Instrumentation实例会传递到代理类的premain方法。
    2. JVM提供一种在启动之后的某个时刻启动代理的机制,此时Instrumentation实例会传递到代理类代码的agentmain方法。

    premain对应的就是VM启动时的Instrument Agent加载,即agent on load,agentmain对应的是VM运行时的Instrument Agent加载,即agent on attach。两种加载形式所加载的Instrument Agent都关注同一个JVMTI事件 – ClassFileLoadHook事件,这个事件是在读取字节码文件之后回调时用,也就是说premain和agentmain方式的回调时机都是类文件字节码读取之后(或者说是类加载之后),之后对字节码进行重定义或重转换,不过修改的字节码也需要满足一些要求,在最后的局限性有说明

    premain与agentmain的区别

    premainagentmain两种方式最终的目的都是为了回调Instrumentation实例并激活sun.instrument.InstrumentationImpl#transform()(InstrumentationImpl是Instrumentation的实现类)从而回调注册到Instrumentation中的ClassFileTransformer实现字节码修改,本质功能上没有很大区别。两者的非本质功能的区别如下:

    • premain方式是JDK1.5引入的,agentmain方式是JDK1.6引入的,JDK1.6之后可以自行选择使用premain或者agentmain
    • premain需要通过命令行使用外部代理jar包,即-javaagent:代理jar包路径agentmain则可以通过attach机制直接附着到目标VM中加载代理,也就是使用agentmain方式下,操作attach的程序和被代理的程序可以是完全不同的两个程序。
    • premain方式回调到ClassFileTransformer中的类是虚拟机加载的所有类,这个是由于代理加载的顺序比较靠前决定的,在开发者逻辑看来就是:所有类首次加载并且进入程序main()方法之前,premain方法会被激活,然后所有被加载的类都会执行ClassFileTransformer列表中的回调。
    • agentmain方式由于是采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调。
    • 通过premain方式的代理Jar包进行了更新的话,需要重启服务器,而agentmain方式的Jar包如果进行了更新的话,需要重新attach,但是agentmain重新attach还会导致重复的字节码插入问题,不过也有HotswapDCE VM方式来避免。

    通过下面的测试也能看到它们之间的一些区别。

    premain加载方式

    premain方式编写步骤简单如下:

    1.编写premain函数,包含下面两个方法的其中之一:

    java public static void premain(String agentArgs, Instrumentation inst); public static void premain(String agentArgs);

    如果两个方法都被实现了,那么带Instrumentation参数的优先级高一些,会被优先调用。agentArgspremain函数得到的程序参数,通过命令行参数传入

    2.定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项

    3.将 premain 的类和 MANIFEST.MF 文件打成 jar 包

    4.使用参数 -javaagent: jar包路径启动代理

    premain加载过程如下:

    1.创建并初始化 JPLISAgent 
    2.MANIFEST.MF 文件的参数,并根据这些参数来设置 JPLISAgent 里的一些内容 
    3.监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情: 
    (1)创建 InstrumentationImpl 对象 ; 
    (2)监听 ClassFileLoadHook 事件 ; 
    (3)调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 类的 premain 方法

    下面是一个简单的例子(在JDK1.8.0_181进行了测试):

    PreMainAgent

    MANIFEST.MF:

    Testmain

    将PreMainAgent打包为Jar包(可以直接用idea打包,也可以使用maven插件打包),在idea可以像下面这样启动:

    命令行的话可以用形如java -javaagent:PreMainAgent.jar路径 -jar TestMain/TestMain.jar启动

    结果如下:

    可以看到在PreMainAgent之前已经加载了一些必要的类,即PreMainAgent get loaded class:xxx部分,这些类没有经过transform。然后在main之前有一些类经过了transform,在main启动之后还有类经过transform,main结束之后也还有类经过transform,可以和agentmain的结果对比下。

    agentmain加载方式

    agentmain方式编写步骤简单如下:

    1.编写agentmain函数,包含下面两个方法的其中之一:

    如果两个方法都被实现了,那么带Instrumentation参数的优先级高一些,会被优先调用。agentArgspremain函数得到的程序参数,通过命令行参数传入

    2.定义一个 MANIFEST.MF 文件,必须包含 Agent-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项

    3.将 agentmain 的类和 MANIFEST.MF 文件打成 jar 包

    4.通过attach工具直接加载Agent,执行attach的程序和需要被代理的程序可以是两个完全不同的程序:

    agentmain方式加载过程类似:

    1.创建并初始化JPLISAgent 
    2.解析MANIFEST.MF 里的参数,并根据这些参数来设置 JPLISAgent 里的一些内容 
    3.监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情: 
    (1)创建 InstrumentationImpl 对象 ; 
    (2)监听 ClassFileLoadHook 事件 ; 
    (3)调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法。

    下面是一个简单的例子(在JDK 1.8.0_181上进行了测试):

    SufMainAgent

    MANIFEST.MF

    TestSufMainAgent

    Testmain

    将SufMainAgent和TestSufMainAgent打包为Jar包(可以直接用idea打包,也可以使用maven插件打包),首先启动Testmain,然后先列下当前有哪些Java程序:

    attach SufMainAgent到Testmain:

    在Testmain中的结果如下:

    和前面premain对比下就能看出,在agentmain中直接getloadedclasses的类数目比在premain直接getloadedclasses的数量多,而且premain getloadedclasses的类+premain transform的类和agentmain getloadedclasses基本吻合(只针对这个测试,如果程序中间还有其他通信,可能会不一样)。也就是说某个类之前没有加载过,那么都会通过两者设置的transform,这可以从最后的java/lang/Shutdown看出来。

    测试Weblogic的某个类是否被加载

    这里使用weblogic进行测试,代理方式使用agentmain方式(在jdk1.6.0_29上进行了测试):

    WeblogicSufMainAgent

    WeblogicTestSufMainAgent:

    列出正在运行的Java应用程序:

    进行attach:

    Weblogic输出:

    假如在进行Weblogic t3反序列化利用时,如果某个类之前没有被加载,但是能够被Weblogic找到,那么利用时对应的类会通过Agent的transform,但是有些类虽然在Weblogic目录下的某些Jar包中,但是weblogic不会去加载,需要一些特殊的配置Weblogic才会去寻找并加载。

    Instrumentation局限性

    大多数情况下,使用Instrumentation都是使用其字节码插桩的功能,笼统说是类重转换的功能,但是有以下的局限性:

    1. premain和agentmain两种方式修改字节码的时机都是类文件加载之后,就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。这里需要注意的就是上面提到过的重新定义,刚才这里说的不能重新定义是指不能重新换一个类名,字节码内容依然能重新定义和修改,不过字节码内容修改后也要满足第二点的要求。
    2. 类转换其实最终都回归到类重定义Instrumentation#retransformClasses()方法,此方法有以下限制: 
      1.新类和老类的父类必须相同; 
      2.新类和老类实现的接口数也要相同,并且是相同的接口; 
      3.新类和老类访问符必须一致。 新类和老类字段数和字段名要一致; 
      4.新类和老类新增或删除的方法必须是private static/final修饰的; 
      5.可以删除修改方法体。

    实际中遇到的限制可能不止这些,遇到了再去解决吧。如果想要重新定义一全新类(类名在已加载类中不存在),可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过只能通过反射调用该全新类的局限性。

    小结

    • 文中只是描述了JavaAgent相关的一些基础的概念,目的只是知道有这个东西,然后验证下之前遇到的一个问题。写的时候也借鉴了其他大佬写的几篇文章[4]&[5]
    • 在写文章的过程中看了一些如一类PHP-RASP实现的漏洞检测的思路[6],利用了污点跟踪、hook、语法树分析等技术,也看了几篇大佬们整理的Java RASP相关文章[2]&[3],如果自己要写基于RASP的漏洞检测/利用工具的话也可以借鉴到这些思路

    代码放到了github上,有兴趣的可以去测试下,注意pom.xml文件中的jdk版本,在切换JDK测试如果出现错误,记得修改pom.xml里面的JDK版本。

    参考

    1.https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html 
    2.https://paper.seebug.org/513/#0x01-rasp 
    3.https://paper.seebug.org/1041/#31-java-agent 
    4.http://www.throwable.club/2019/06/29/java-understand-instrument-first/#Instrumentation%E6%8E%A5%E5%8F%A3%E8%AF%A6%E8%A7%A3 
    5.https://www.cnblogs.com/rickiyang/p/11368932.html 
    6.https://c0d3p1ut0s.github.io/%E4%B8%80%E7%B1%BBPHP-RASP%E7%9A%84%E5%AE%9E%E7%8E%B0/


    Paper

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

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • 从 0 开始入门 Chrome Ext 安全(一) — 了解一个 Chrome Ext

    2019-11-29

    作者:LoRexxar'@知道创宇404实验室
    时间:2019年11月21日

    在2019年初,微软正式选择了Chromium作为默认浏览器,并放弃edge的发展。并在19年4月8日,Edge正式放出了基于Chromium开发的Edge Dev浏览器,并提供了兼容Chrome Ext的配套插件管理。再加上国内的大小国产浏览器大多都是基于Chromium开发的,Chrome的插件体系越来越影响着广大的人群。

    在这种背景下,Chrome Ext的安全问题也应该受到应有的关注,《从0开始入门Chrome Ext安全》就会从最基础的插件开发开始,逐步研究插件本身的恶意安全问题,恶意网页如何利用插件漏洞攻击浏览器等各种视角下的安全问题。

    第一部分我们就主要来聊聊关于Chrome Ext的一些基础。

    获取一个插件的代码

    Chrome Ext的存在模式类似于在浏览器层新加了一层解释器,在我们访问网页的时候,插件会加载相应的html、js、css,并解释执行。

    所以Chrome Ext的代码也就是html、js、css这类,那我们如何获取插件的代码呢?

    当我们访问扩展程序的页面可以获得相应的插件id

    然后我们可以在https://chrome-extension-downloader.com/中下载相应的crx包。

    把crx改名成zip之后解压缩就可以了

    manifest.json

    在插件的代码中,有一个重要的文件是manifest.json,在manifest.json中包含了整个插件的各种配置,在配置文件中,我们可以找到一个插件最重要的部分。

    首先是比较重要的几个字段

    • browser_action
      • 这个字段主要负责扩展图标点击后的弹出内容,一般为popup.html
    • content_scripts
      • matches 代表scripts插入的时机,默认为document_idle,代表页面空闲时
      • js 代表插入的scripts文件路径
      • run_at 定义了哪些页面需要插入scripts
    • permissions
      • 这个字段定义了插件的权限,其中包括从浏览器tab、历史纪录、cookie、页面数据等多个维度的权限定义
    • content_security_policy
      • 这个字段定义了插件页面的CSP
      • 但这个字段不影响content_scripts里的脚本
    • background
      • 这个字段定义插件的后台页面,这个页面在默认设置下是在后台持续运行的,只随浏览器的开启和关闭
      • persistent 定义了后台页面对应的路径
      • page 定义了后台的html页面
      • scripts 当值为false时,background的页面不会在后台一直运行

    在开始Chrome插件的研究之前,除了manifest.json的配置以外,我们还需要了解一下围绕chrome建立的插件结构。

    Chrome Ext的主要展现方式

    browserAction - 浏览器右上角

    浏览器的右上角点击触发的就是mainfest.json中的browser_action

    其中页面内容来自popup.html

    pageAction

    pageAction和browserAction类似,只不过其中的区别是,pageAction是在满足一定的条件下才会触发的插件,在不触发的情况下会始终保持灰色。

    contextMenus 右键菜单

    通过在chrome中调用chrome.contextMenus这个API,我们可以定义在浏览器中的右键菜单。

    当然,要控制这个api首先你必须申请控制contextMenus的权限。

    一般来说,这个api会在background中被定义,因为background会一直在后台加载。

    https://developer.chrome.com/extensions/contextMenus

    override - 覆盖页面

    chrome提供了override用来覆盖chrome的一些特定页面。其中包括历史记录、新标签页、书签等...

    比如Toby for Chrome就是一个覆盖新标签页的插件

    devtools - 开发者工具

    chrome允许插件重构开发者工具,并且相应的操作。

    插件中关于devtools的生命周期和F12打开的窗口时一致的,当F12关闭时,插件也会自动结束。

    而在devtools页面中,插件有权访问一组特殊的API,这组API只有devtools页面中可以访问。

    https://developer.chrome.com/extensions/devtools

    option - 选项

    option代表着插件的设置页面,当选中图标之后右键选项可以进入这个页面。

    omnibox - 搜索建议

    在chrome中,如果你在地址栏输入非url时,会将内容自动传到google搜索上。

    omnibox就是提供了对于这个功能的魔改,我们可以通过设置关键字触发插件,然后就可以在插件的帮助下完成搜索了。

    这个功能通过chrome.omnibox这个api来定义。

    notifications - 提醒

    notifications代表右下角弹出的提示框

    权限体系和api

    在了解了各类型的插件的形式之后,还有一个比较重要的就是Chrome插件相关的权限体系和api。

    Chrome发展到这个时代,其相关的权限体系划分已经算是非常细致了,具体的细节可以翻阅文档。

    https://developer.chrome.com/extensions/declare_permissions

    抛开Chrome插件的多种表现形式不谈,插件的功能主要集中在js的代码里,而js的部分主要可以划分为5种injected script、content-script、popup js、background js和devtools js.

    • injected script 是直接插入到页面中的js,和普通的js一致,不能访问任何扩展API.
    • content-script 只能访问extension、runtime等几个有限的API,也可以访问dom.
    • popup js 可以访问大部分API,除了devtools,支持跨域访问
    • background js 可以访问大部分API,除了devtools,支持跨域访问
    • devtools js 只能访问devtools、extension、runtime等部分API,可以访问dom
    JS是否能访问DOM是否能访问JS是否可以跨域
    injected script可以访问可以访问不可以
    content script可以访问不可以不可以
    popup js不可直接访问不可以可以
    background js不可直接访问不可以可以
    devtools js可以访问可以访问不可以

    同样的,针对这多种js,我们也需要特殊的方式进行调试

    • injected script: 直接F12就可以调试
    • content-script:在F12中console选择相应的域
    • popup js: 在插件右键的列表中有审查弹出内容
    • background js: 需要在插件管理页面点击背景页然后调试

    通信方式

    在前面介绍过各类js之后,我们提到一个重要的问题就是,在大部分的js中,都没有给与访问js的权限,包括其中比较关键的content script.

    那么插件怎么和浏览器前台以及相互之间进行通信呢?

    -injected-scriptcontent-scriptpopup-jsbackground-js
    injected-script-window.postMessage--
    content-scriptwindow.postMessage-chrome.runtime.sendMessage chrome.runtime.connectchrome.runtime.sendMessage chrome.runtime.connect
    popup-js-chrome.tabs.sendMessage chrome.tabs.connect-chrome.extension. getBackgroundPage()
    background-js-chrome.tabs.sendMessage chrome.tabs.connectchrome.extension.getViews-
    devtools-jschrome.devtools.inspectedWindow.eval-chrome.runtime.sendMessagechrome.runtime.sendMessage

    popup和background两个域互相直接可以调用js并且访问页面的dom。

    popup可以直接用chrome.extension.getBackgroundPage()获取background页面的对象,而background可以直接用chrome.extension.getViews({type:'popup'})获取popup页面的对象。

    popup\background 和 content js

    popup\background 和 content js之间沟通的方式主要依赖chrome.tabs.sendMessagechrome.runtime.onMessage.addListener这种有关事件监听的交流方式。

    发送方使用chrome.tabs.sendMessage,接收方使用chrome.runtime.onMessage.addListener监听事件。

    接收方

    injected script 和 content-script

    由于injected script就相当于页面内执行的js,所以它没权限访问chrome对象,所以他们直接的沟通方式主要是利用window.postMessage或者通过DOM事件来实现。

    injected-script中:

    content script中:

    popup\background 动态注入js

    popup\background没办法直接访问页面DOM,但是可以通过chrome.tabs.executeScript来执行脚本,从而实现对页面DOM的操作。

    要注意这种操作要求必须有页面权限

    js

    chrome.storage

    chrome 插件还有专门的储存位置,其中包括chrome.storage和chrome.storage.sync两种,其中的区别是:

    • chrome.storage 针对插件全局,在插件各个位置保存的数据都会同步。
    • chrome.storage.sync 根据账户自动同步,不同的电脑登陆同一个账户都会同步。

    插件想访问这个api需要提前声明storage权限。

    总结

    这篇文章主要描述了关于Chrome ext插件相关的许多入门知识,在谈及Chrome ext的安全问题之前,我们可能需要先了解一些关于Chrome ext开发的问题。

    在下一篇文章中,我们将会围绕Chrome ext多个维度的安全问题进行探讨,在现代浏览器体系中,Chrome ext到底可能会带来什么样的安全问题。

    re


    Paper

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

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • 代码分析引擎 CodeQL 初体验

    2019-11-19

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

    QL是一种查询语言,支持对C++,C#,Java,JavaScript,Python,go等多种语言进行分析,可用于分析代码,查找代码中控制流等信息。

    之前笔者有简单的研究通过JavaScript语义分析来查找XSS,所以对于这款引擎有浓厚的研究兴趣 。

    安装

    1.下载分析程序:https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip

    分析程序支持主流的操作系统,Windows,Mac,Linux

    2.下载相关库文件:https://github.com/Semmle/ql

    库文件是开源的,我们要做的是根据这些库文件来编写QL脚本。

    3.下载最新版的VScode,安装CodeQL扩展程序:https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-codeql

    • 用vscode的扩展可以方便我们看代码
    • 然后到扩展中心配置相关参数
    image-20191116223514188
    image-20191116223649659

    4.

    • cli填写下载的分析程序路径就行了,windows可以填写codeql.cmd
    • 其他地方默认就行

    建立数据库

    以JavaScript为例,建立分析数据库,建立数据库其实就是用分析程序来分析源码。到要分析源码的根目录,执行codeql database create jstest --language=javascript

    image-20191117111305487

    接下来会在该目录下生成一个jstest的文件夹,就是数据库的文件夹了。

    接着用vscode打开之前下载的ql库文件,在ql选择夹中添加刚才的数据库文件,并设置为当前数据库。

    image-20191117111940680

    接着在QL/javascript/ql/src目录下新建一个test.ql,用来编写我们的ql脚本。为什么要在这个目录下建立文件呢,因为在其他地方测试的时候import javascript导入不进来,在这个目录下,有个javascript.qll就是基础类库,就可以直接引入import javascript,当然可能也有其他的方法。

    看它的库文件,它基本把JavaScript中用到的库,或者其他语言的定义语法都支持了。

    image-20191117113240934

    输出一段hello world试试?

    image-20191118130324959

    语义分析查找的原理

    刚开始接触ql语法的时候可能会感到它的语法有些奇怪,它为什么要这样设计?我先说说自己之前研究基于JavaScript语义分析查找dom-xss是怎样做的。

    首先一段类似这样的javascript代码

    常规的思路是,我们先找到document.write函数,由这个函数的第一个参数回溯寻找,如果发现它最后是location.hash.split("#")[1];,就寻找成功了。我们可以称document.writesink,称location.hash.splitsource。基于语义分析就是由sink找到source的过程(当然反过来找也是可以的)。

    而基于这个目标,就需要我们设计一款理解代码上下文的工具,传统的正则搜索已经无法完成了。

    第一步要将JavaScript的代码转换为语法树,通过pyjsparser可以进行转换

    最终就得到了如下一个树结构

    image-20191118131714042

    这些树结构的一些定义可以参考:https://esprima.readthedocs.io/en/3.1/syntax-tree-format.html

    大概意思可以这样理解:变量param是一个Identifier类型,它的初始化定义的是一个MemberExpression表达式,该表达式其实也是一个CallExpression表达式,CallExpression表达式的参数是一个Literal类型,而它具体的定义又是一个MemberExpression表达式。

    第二步,我们需要设计一个递归来找到每个表达式,每一个Identifier,每个Literal类型等等。我们要将之前的document.write转换为语法树的形式

    location.hash也是同理

    在找到了这些sinksource后,再进行正向或反向的回溯分析。回溯分析也会遇到不少问题,如何处理对象的传递,参数的传递等等很多问题。之前也基于这些设计写了一个在线基于语义分析的demo

    QL语法

    QL语法虽然隐藏了语法树的细节,但其实它提供了很多类似,函数的概念来帮助我们查找相关'语法'。

    依旧是这段代码为例子

    上文我们已经建立好了查询的数据库,现在我们分别来看如何查找sink,source,以及怎样将它们关联起来。

    我也是看它的文档:https://help.semmle.com/QL/learn-ql/javascript/introduce-libraries-js.html 学习的,它提供了很多方便的函数,我没有仔细看。我的查询语句都是基于语法树的查询思想,可能官方已经给出了更好的查询方式,所以看看就行了,反正也能用。

    查询 document.write

    这段语句的意思是查找document.write,并输出它的第一个参数

    image-20191118134431944

    查找 location.hash.split

    查找location.hash.split并输出

    image-20191118134554200

    数据流分析

    接着从sink来找到source,将上面语句组合下,按照官方的文档来就行

    image-20191118134945286

    将source和sink输出,就能找到它们具体的定义。

    我们找到查询到的样本

    image-20191118135549113

    可以发现它的回溯是会根据变量,函数的返回值一起走的。

    当然从source到sink也不可能是一马平川的,中间肯定也会有阻挡的条件,ql官方有给出解决方案。总之就是要求我们更加细化完善ql查询代码。

    接下来放出几个查询还不精确的样本,大家可以自己尝试如何进行查询变得精确。

    最后

    CodeQL将语法树抽离出来,提供了一种用代码查询代码的方案,更增强了基于数据分析的灵活度。唯一的遗憾是它并没有提供很多查询漏洞的规则,它让我们自己写。这也不由得让我想起另一款强大的基于语义的代码审计工具fortify,它的规则库是公开的,将这两者结合一下说不定会有不一样的火花。

    Github公告说将用它来搜索开源项目中的问题,而作为安全研究员的我们来说,也可以用它来做类似的事情?


    Paper

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

    作者:吴烦恼 | 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:
  • BlueKeep 漏洞利用分析

    2019-09-20

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

    0x00 信道的创建、连接与释放

    通道的数据包定义在MCS Connect Inittial PDU with GCC Conference Create Request中,在rdp连接过程如下图所示:

    信道创建数据包格式如下:

    在MCS Connect Inittial中属于Client Network Data数据段,MS_T120将会在连接一开始的时候通过函数termdd!_IcaRegisterVcBin创建一个虚拟通道id是0x1f大小为0x18的结构体,之后就调用termdd!IcaCreateChannel开始创建大小为0x8c的信道结构体之后将会与虚拟通道id是0x1f绑定,也就是这个结构体将会被我们利用

    信道的定义字段主要是名字加上配置,配置主要包括了优先级等

    在server对MCS Connect Inittial应答包,将会依次给出对应虚拟通道的id值:

    在rdp内核中依次注册的值对应应该是0、1、2、3, MS_T120信道将会通过我们发送的用户虚拟id为3的值再一次绑定,首先通过termdd!_IcaFindVcBind找到了刚开始注册的虚拟通道id是0x1f,如下所示:

    但是在termdd!_IcaBindChannel时,却将我们自定义的id值为3与信道结构体再一次绑定在一起了,此信道结构体就是MS_T120

    同时我们自己的用户id将内部绑定的0x1f给覆盖了

    我们往信道MS_T120发送数据主动释放其分配的结构体,其传入虚拟通道id值为3通过函数termdd!IcaFindChannel在channeltable中查找返回对应的信道结构体:

    下图为返回的MS_T120信道结构体,其中0xf77b4300为此信道可调用的函数指针数组:

    在这个函数指针数组中主要存放了三个函数,其中对应了termdd!IcaCloseChanneltermdd!IcaReadChanneltermdd!IcaWriteChannel

    我们传入释放MS_T120信道的数据如下,字节大小为0x12,主要数据对应了0x02

    之后将会进入nt! IofCompleteRequest函数,通过apc注入后,将会通过nt! IopCompleteRequestnt!IopAbortRequest进行数据请求的响应,最终在termdd!IcaDispatch完成我们发送数据的的请求,_BYTE v2就是我们发送的数据,所以我们发送的数据0x02将会最终调用到IcaClose函数进入IcaCloseChannel函数,最后主动释放掉了MS_T120信道结构体

    0x01 通过RDPDR信道进行数据占位

    我们先来了解下rdpdr信道,首先rdpdr信道是文件系统虚拟通道扩展,该扩展在名为rdpdr的静态虚拟通道上运行。目的是将访问从服务器重定向到客户端文件系统,其数据头部将会主要是两种标识和PacketId字段组成:

    在这里我们刚好利用到了rdpde客户端name响应的数据来进行池内存的占位

    在完全建立连接后,将会创建rdpdr信道的结构体

    在window7中,在建立完成后接收到server的rdpdr请求后,通过发送客户端name响应数据,将会调用到termdd! IcaChannelInputInternal中的ExAllocatePoolWithTag分配非分页池内存,并且其长度是我们可以控制的,基本满足了UAF利用的需求:

    可是在windowsxp中,直接发送client name request将会导致内存分配失败,直接进入termdd! _IcaCopyDataToUserBuffer,并且在Tao Yan and Jin Chen[1]一文中也提到了通过发送client name request在触发一定的条件后将会绕过termdd!_IcaCopyDataToUserBuffer而进入ExAllocatePoolWithTag分配我们想要的非分页内存,而打破条件如下:

    我们先来看看最开始信道结构体的创建,我们可以发现从一开始创建信道结构体的时候,将会出现两个标志,而这两个标志是按照地址顺序排列的,而在上面需要打破的条件中,只要channelstruct +0x108的地址存放的是同一个地址,循环就会被break

    我们发送一个正常的rdpdr的name request数据包,头部标识是0x7244和0x4e43

    经过termdd!_IcaCopyDataToUserBuffer之后,将会进入nt!IofCompleteRequest,在响应请求后进入rdpdr!DrSession::ReadCompletion,此函数处理逻辑如下,其将会遍历一个链表,从链表中取出对应的vftable函数数组

    遍历第一次取出第一张函数数组

    传入我们发送的数据后,通过函数数组调用rdpdr!DrSession::RecognizePacket进行读取

    判断头部标志是否为(RDPDR_CTYP_CORE)0x7244

    接着将会读取函数vftable第二个地址,进行转发

    如下图可以看到rdpdr的数据包处理逻辑

    rdpdr经过一系列数据包处理后最终进入了我们关心的地方,将会传入channelstruct通过调用termdd! _IcaQueueReadChannelRequest进行标志位的处理

    最初rdpdr的channelstruct的标志位如下

    经过函数termdd! _IcaQueueReadChannelRequest对此标志的处理后变成如下,所以下一个数据依然会进入termdd!_IcaCopyDataToUserBuffer,导致我们进行池喷射的失败

    回到rdpdr头部处理函数rdpdr!DrSession::RecognizePacket,我们发现在链表遍历失败后将会进行跳转,最后将会进入读取失败处理函数rdpdr!DrSession::ChannelIoFailed,然后直接return了

    我们构造一个头部异常的数据包发送,头部标志我们构造的是0x7240,将会导致rdpdr!DrSession::RecognizePacket判断失败,之后将会继续遍历链表依次再取出两张函数数组

    最后两个函数数组依次调用rdpdr!DrExchangeManager::RecognizePacketrdpdr!DrDeviceManager::RecognizePacket,都会判断错误的头部标志0x7240,最后导致链表遍历完后进行错误跳转,直接绕过了termdd! _IcaQueueReadChannelRequest对标志位的修改,将会打破循环

    最后我们连续构造多个错误的数据包后将会进入ExAllocatePoolWithTag,分配到我们需要的非分页内存!

    0x02 win7 EXP 池喷射简要分析

    首先被释放的MS_T120池大小包括是0x170,池的标志是TSic

    分析Win7 exp 可以知道数据占位是用的rdpsnd信道,作者没有采用rdpdr信道,应该也和喷射的稳定性有关,rdpsnd喷射是再建立完了rdpdr初始化后开始的,在free掉MS_T120结构体前,发送了1044个数据包去申请0x170大小的池内存,这样做可以说应该是为了防止之后被free掉的内存被其他程序占用了,提高free后内存被我们占用的生存几率

    占位被free的实际数据大小为0x128,利用的中转地址是0xfffffa80ec000948

    之后开始池喷射,将payload喷射到可以call [rax] == 0xfffffa80ec000948的地方,喷射的payload大小基本是0x400,总共喷射了200mb的数据大小,我们先来看下喷射前带标志TSic总共占用池内存大小是58kib左右

    喷射完后带TSic标志池内存大小大约就是201mb,池内存喷射基本是成功的,我的win7是sp1,总共内存大小是1GB,再喷射过程中也没有其他干扰的,所以喷射很顺利

    图中可以发现基本已经很稳定的0x400大小的池喷射payload,地址越高0x400大小的内存基本就很稳定了

    最后断开连接时候,被free的内存已经被我们喷射的0x128大小的数据给占用了

    执行call指令后稳定跳转到了我们的payload,成功执行!

    参考链接:
    [0] https://github.com/rapid7/metasploit-framework/pull/12283
    [1] https://unit42.paloaltonetworks.com/exploitation-of-windows-cve-2019-0708-bluekeep-three-ways-to-write-data-into-the-kernel-with-rdp-pdu/
    [2] https://wooyun.js.org/drops/%E7%BE%8A%E5%B9%B4%E5%86%85%E6%A0%B8%E5%A0%86%E9%A3%8E%E6%B0%B4%EF%BC%9A%20%E2%80%9CBig%20Kids%E2%80%99%20Pool%E2%80%9D%E4%B8%AD%E7%9A%84%E5%A0%86%E5%96%B7%E6%8A%80%E6%9C%AF.html

    作者:吴烦恼 | Categories:安全研究技术分享 | Tags:
  • Java 反序列化工具 gadgetinspector 初窥

    2019-09-17

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

    起因

    一开始是听@Badcode师傅说的这个工具,在Black Hat 2018的一个议题提出来的。这是一个基于字节码静态分析的、利用已知技巧自动查找从source到sink的反序列化利用链工具。看了几遍作者在Black Hat上的演讲视频PPT,想从作者的演讲与PPT中获取更多关于这个工具的原理性的东西,可是有些地方真的很费解。不过作者开源了这个工具,但没有给出详细的说明文档,对这个工具的分析文章也很少,看到一篇平安集团对这个工具的分析,从文中描述来看,他们对这个工具应该有一定的认识并做了一些改进,但是在文章中对某些细节没有做过多的阐释。后面尝试了调试这个工具,大致理清了这个工具的工作原理,下面是对这个工具的分析过程,以及对未来工作与改进的设想。

    关于这个工具

    • 这个工具不是用来寻找漏洞,而是利用已知的source->...->sink链或其相似特征发现分支利用链或新的利用链。
    • 这个工具是在整个应用的classpath中寻找利用链。
    • 这个工具进行了一些合理的预估风险判断(污点判断、污点传递等)。
    • 这个工具会产生误报不是漏报(其实这里还是会漏报,这是作者使用的策略决定的,在后面的分析中可以看到)。
    • 这个工具是基于字节码分析的,对于Java应用来说,很多时候我们并没有源码,而只有War包、Jar包或class文件。
    • 这个工具不会生成能直接利用的Payload,具体的利用构造还需要人工参与。

    序列化与反序列化

    序列化(Serialization)是将对象的状态信息转化为可以存储或者传输形式的过程,转化后的信息可以存储在磁盘上,在网络传输过程中,可以是字节、XML、JSON等格式;而将字节、XML、JSON等格式的信息还原成对象这个相反的过程称为反序列化。

    在JAVA中,对象的序列化和反序列化被广泛的应用到RMI(远程方法调用)及网络传输中。

    Java中的序列化与反序列化库

    • JDK(ObjectInputStream)
    • XStream(XML,JSON)
    • Jackson(XML,JSON)
    • Genson(JSON)
    • JSON-IO(JSON)
    • FlexSON(JSON)
    • Fastjson(JSON)
    • ...

    不同的反序列化库在反序列化不同的类时有不同的行为、被反序列化类的不同"魔术方法"会被自动调用,这些被自动调用的方法就能够作为反序列化的入口点(source)。如果这些被自动调用的方法又调用了其他子方法,那么在调用链中某一个子方法也可以作为source,就相当于已知了调用链的前部分,从某个子方法开始寻找不同的分支。通过方法的层层调用,可能到达某些危险的方法(sink)。

    • ObjectInputStream

    例如某个类实现了Serializable接口,ObjectInputStream.readobject在反序列化类得到其对象时会自动查找这个类的readObject、readResolve等方法并调用。

    例如某个类实现了Externalizable接口,ObjectInputStream.readobject在反序列化类得到其对象时会自动查找这个类的readExternal等方法并调用。

    • Jackson

    ObjectMapper.readValue在反序列化类得到其对象时,会自动查找反序列化类的无参构造方法、包含一个基础类型参数的构造方法、属性的setter、属性的getter等方法并调用。

    • ...

    在后面的分析中,都使用JDK自带的ObjectInputStream作为样例。

    控制数据类型=>控制代码

    作者说,在反序列化漏洞中,如果控制了数据类型,我们就控制了代码。这是什么意思呢?按我的理解,写了下面的一个例子:

    为了方便我把所有类写在一个类中进行测试。在Person类中,有一个Animal类的属性pet,它是Cat和Dog的接口。在序列化时,我们能够控制Person的pet具体是Cat对象或者Dog对象,因此在反序列化时,在readObject中pet.eat()具体的走向就不一样了。如果是pet是Cat类对象,就不会走到执行有害代码Runtime.getRuntime().exec("calc");这一步,但是如果pet是Dog类的对象,就会走到有害代码。

    即使有时候类属性在声明时已经为它赋值了某个具体的对象,但是在Java中通过反射等方式依然能修改。如下:

    在Person类中,不能通过构造器或setter方法或其他方式对pet赋值,属性在声明时已经被定义为Cat类的对象,但是通过反射能将pet修改为Dog类的对象,因此在反序列化时依然会走到有害代码处。

    这只是我自己对作者"控制了数据类型,就控制了代码"的理解,在Java反序列化漏洞中,很多时候是利用到了Java的多态特性来控制代码走向最后达到恶意执行目的。

    魔术方法

    在上面的例子中,能看到在反序列化时没有调用Person的readobject方法,它是ObjectInputStream在反序列化对象时自动调用的。作者将在反序列化中会自动调用的方法称为"魔术方法"。

    使用ObjectInputStream反序列化时几个常见的魔术方法:

    • Object.readObject()
    • Object.readResolve()
    • Object.finalize()
    • ...

    一些可序列化的JDK类实现了上面这些方法并且还自动调用了其他方法(可以作为已知的入口点):

    • HashMap
    • Object.hashCode()
    • Object.equals()
    • PriorityQueue
    • Comparator.compare()
    • Comparable.CompareTo()
    • ...

    一些sink:

    • Runtime.exec(),这种最为简单直接,即直接在目标环境中执行命令
    • Method.invoke(),这种需要适当地选择方法和参数,通过反射执行Java方法
    • RMI/JNDI/JRMP等,通过引用远程对象,间接实现任意代码执行的效果
    • ...

    作者给出了一个从Magic Methods(source)->Gadget Chains->Runtime.exec(sink)的例子:

    上面的HashMap实现了readObject这个"魔术方法",并且调用了hashCode方法。某些类为了比较对象之间是否相等会实现equals方法(一般是equals和hashCode方法同时实现)。从图中可以看到AbstractTableModel$ff19274a正好实现了hashCode方法,其中又调用了f.invoke方法,f是IFn对象,并且f能通过属性__clojureFnMap获取到。IFn是一个接口,上面说到,如果控制了数据类型,就控制了代码走向。所以如果我们在序列化时,在__clojureFnMap放置IFn接口的实现类FnCompose的一个对象,那么就能控制f.invokeFnCompose.invoke方法,接着控制FnCompose.invoke中的f1、f2为FnConstant就能到达FnEval.invoke了(关于AbstractTableModel$ff19274a.hashcode中的f.invoke具体选择IFn的哪个实现类,根据后面对这个工具的测试以及对决策原理的分析,广度优先会选择短的路径,也就是选择了FnEval.invoke,所以这也是为什么要人为参与,在后面的样例分析中也可以看到)。

    有了这条链,只需要找到触发这个链的漏洞点就行了。Payload使用JSON格式表示如下:

    gadgetinspector工作流程

    如作者所说,正好使用了五个步骤:

    Step1 枚举全部类以及每个类的所有方法

    要进行调用链的搜索,首先得有所有类及所有类方法的相关信息:

    来看下classes.dat、methods.dat分别长什么样子:

    • classes.dat

    找了两个比较有特征的

    第一个类com/sun/deploy/jardiff/JarDiffPatcher:

    和上面的表格信息对应一下,是吻合的

    • 类名:com/sun/deploy/jardiff/JarDiffPatcher
    • 父类: java/lang/Object,如果一类没有显式继承其他类,默认隐式继承java/lang/Object,并且java中不允许多继承,所以每个类只有一个父类
    • 所有接口:com/sun/deploy/jardiff/JarDiffConstants、com/sun/deploy/jardiff/Patcher
    • 是否是接口:false
    • 成员:newBytes!2![B,newBytes成员,Byte类型。为什么没有将static/final类型的成员加进去呢?这里还没有研究如何操作字节码,所以作者这里的判断实现部分暂且跳过。不过猜测应该是这种类型的变量并不能成为污点所以忽略了

    第二个类com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl:

    和上面的表格信息对应一下,也是吻合的

    • 类名:com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl,是一个内部类
    • 父类: com/sun/corba/se/spi/orbutil/proxy/CompositeInvocationHandlerImpl
    • 所有接口:com/sun/corba/se/spi/orbutil/proxy/LinkedInvocationHandler,java/io/Serializable
    • 是否是接口:false
    • 成员:stub!130!com/sun/corba/se/spi/presentation/rmi/DynamicStub!this$0!4112!com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl,!*!这里可以暂时理解为分割符,有一个成员stub,类型com/sun/corba/se/spi/presentation/rmi/DynamicStub。因为是内部类,所以多了个this成员,这个this指向的是外部类
    • methods.dat

    同样找几个比较有特征的

    sun/nio/cs/ext/Big5#newEncoder:

    • 类名:sun/nio/cs/ext/Big5
    • 方法名: newEncoder
    • 方法描述信息: ()Ljava/nio/charset/CharsetEncoder; 无参,返回java/nio/charset/CharsetEncoder对象
    • 是否是静态方法:false

    sun/nio/cs/ext/Big5_HKSCS$Decoder#\<init>:

    • 类名:sun/nio/cs/ext/Big5_HKSCS$Decoder
    • 方法名:\<init>
    • 方法描述信息: (Ljava/nio/charset/Charset;Lsun/nio/cs/ext/Big5_HKSCS1;)V参数1是java/nio/charset/Charset类型,参数2是sun/nio/cs/ext/Big5HKSCS1;)V参数1是java/nio/charset/Charset类型,参数2是sun/nio/cs/ext/Big5HKSCS1类型,返回值void
    • 是否是静态方法:false

    继承关系的生成:

    继承关系在后面用来判断一个类是否能被某个库序列化、以及搜索子类方法实现等会用到。