Ring0行为争夺战 -- OpenRASP(Runtime application self-protection)技术初探
1. 背景
0x1:RASP简介
OASES(Open AI System Security Alliance)智能终端安全生态联盟是国内首个致力于提升智能终端生态安全的联合组织,由百度、华为、中国信通院联合发起成立,成员包括安全厂商、行业安全专家、设备厂商、⾼校和各行业安全专家等。
OASES 目前包含 KARMA、OASP、OpenRASP、MesaLink、MesaLock Linux 等几个子项目。其中,OpenRASP 主要应⽤于云端应用安全防护,以及上线前的应用安全测试。Gartner 在2014年提出了「 运行时应用自我保护」技术的概念,即对应⽤服务的保护,不应该依赖于外部系统;应⽤应该具备自我保护的能⼒。OpenRASP 是该技术的开源实现,它改变了防⽕墙依赖请求特征来拦截攻击的模式。
以注入类的漏洞举例,Openrasp 可以识别用户输入的部分,并检查程序逻辑是否被修改。由于不依赖请求特征,确保每条告警都是有效的。
0x2:RASP和WAF的区别
和RASP对应的一种Web应用入侵检测技术栈就是WAF(Web Application Firewall),WAF 的实现方式属于「门卫模式」。WAF 先确定访问者的身份并根据他的身份特征和特征库进行对比,如果访问者身份特征符合攻击者的特征,便将其拦截在外;如果访问者的身份特征符合正常用户的特征或特征库中不存在此访问者的特征,便放行。
基于 WAF 的实现方式,导致了 WAF 无法摆脱的两个弊端:
- 1. 必须根据攻击者的特征进行防御。
- 2. 误报率高。倘若攻击者对身份特征再次进行伪装,WAF 必须更新它的特征库,否则将无法有效的阻止攻击者。
OpenRASP 的实现方式属于「管家模式」。与「门卫模式」不同的是,OpenRASP 并不对访问者进行阻拦,无论是普通访问者还是攻击者都可直接进入。但在应用内部的每一个功能上,OpenRASP 都为其添加了一道「门槛」。
比如根据访问者的操作,需要执行某函数。如果以正确的姿势访问这个函数,OpenRASP 不会阻拦,若是监测到给这个函数的参数有异常「即攻击动作」,则将其拦截,并在后台展示。
攻击者访问程序肯定是要做「坏事」,比如 SQL 注入,攻击者就是要把数据库给拿下来,在攻击者执行「拿数据库」操作之前,做了什么正常操作,OpenRASP 并不关心,一旦检测到攻击者有「攻击动作」则立即拦截。
从这点来看,RASP的防御思维和Ring0防御是类似的,即不在上层显性态层和攻击者博弈,而是下沉到底层行为态层进行统一管控,底层管控最大的优势在于维度低,因为不管上层显性态层如果变化,一旦到了底层,变化的维度都会剧烈降低,这对防御者来意味着可以用更少的策略和规则来描绘更多的恶意行为。
总的来说,
- WAF 是监视了程序的入口
- OpenRASP 监视了程序所有步骤的上下文联系
可以举个例子:
如果一个人带着炸弹进办公楼,门卫把他拦住了,说他有大规模杀伤性武器,不能进,这就很正常。但如果这个人拿着个锤子要进来,由于有可能这个人是过来修天花板的;或者这个人天生喜欢锤子,就喜欢拿着自己欣赏,不会对人员设备进行危害;再或者这人就是过来砸门的,门卫不清楚他的目的,就不会妄加拦截;
还是有个人带着锤子进办公楼,没有门卫,但这个人进来之后,二话不说想砸门,这时候有人把他拦下一脚踹出去了,这就是 OpenRASP 的监视上下文联系。
之所以门卫不敢拦下这个拿锤子的人,就是因为他不掌握这个人接下来的行动信息,而 OpenRASP 之所以敢直接把这个人踹出去,是因为他有充足的把握确定这个人接下来的动作就是搞破坏。
Relevant Link:
http://blog.nsfocus.net/openrasp-tech/ https://www.cnblogs.com/jinqi520/p/10271337.html https://blog.csdn.net/qq_34307082/article/details/106001517 https://www.freebuf.com/articles/web/195300.html https://blog.csdn.net/sacredbook/article/details/105342185
2. 应用场景
0x1:Web攻击检测
- sql注入
- 命令注入
- xss
- 敏感文件下载
- 任意文件读取
- xxe
- 反序列化(fastjson等)
- Struts OGNL 代码执行
- ssrf
- 敏感日志打印
- CVE检测
0x2:服务器安全基线检查
- 关键 Cookie 是否开启 HttpOnly(Tomcat)
- 进程启动账号检查
- 后台密强度检查(Tomcat)
- 不安全的默认应用检查(Tomcat)
- Directory Listing 检查(Tomcat)
- 数据库连接账号审计 (jdbc)
0x3:应用加固
当应用收到请求,通过输出响应头的方式,实现对应用的加固。
0x4:基于openrasp-iast 灰盒扫描更好完成SDL审计
SDL大致有这么几个阶段,
- 需求评审
- 开发
- 测试
- 发布
- 上线
每个阶段对应的重要的安全活动有,
- 威胁建模
- 代码扫描
- 自动化安全测试
- CICD发布拦截
- 上线依赖漏洞识别
- 威胁情报收集
openrasp-iast 作为 openrasp 的一个插件集成到 openrasp 中,采用被动扫描的方式,java 探针在请求结束后会将本次请求参数、hook 点信息提交给服务端进行分析并选择性的 fuzz 目标。openrasp 会根据 hook 点信息精确检测漏洞。
IAST交互式安全测试工具是全新一代“灰盒”代码审计。是近年来兴起的一项新技术,被Gartner公司列为信息安全领域的Top10技术之一。融合了SAST和DAST技术的优点,无需源码,支持对字节码的检测。IAST极大的提高了安全测试的效率和准确率,良好的适用于敏捷开发和DevOps,可以在软件的开发和测试阶段无缝集成现有开发流程,让开发人员和测试人员在执行功能测试的同时,无感知的完成安全测试,解决了现有应用安全测试技术面临的挑战。
openrasp-iast 的定位是DevSecOps工具,通常部署在预上线环境(UAT),在QA完成功能测试后发起漏洞扫描。与黑盒扫描器不同之处在于,它既不需要强大的爬虫,也不需要依赖于页面响应来检测漏洞,而是根据应用内部的行为信息来判断。
举个例子,若要检测目录遍历漏洞,扫描器通常会替换请求参数为 ../../../../etc/passwd,并检测页面是否包含 root:x:0:0:root:/root:/bin/bash
关键字来判断是否存在漏洞。
对于 openrasp-iast 而言,在替换完参数后,只需要检查应用是否真的读取了 /etc/passwd 就可以做出判断。
若要评估扫描器的检测效果,通常从如下几个维度入手:
- 扫描效率。在避免业务扫挂的情况下,能否在最短的时间,检出最多的漏洞?
- 爬虫能力。能覆盖多少接口?如果需要用户交互、用户登录如何处理?
- 检测能力。对于没有回显,或者难以判断的漏洞(比如文件上传),如何处理?
下面我们来看下 openrasp-iast 是如何处理上述问题的。首先是扫描效率。黑盒扫描器通常无法得知应用内部行为,因此需要发送大量的请求才能确定漏洞类型:
/article.php?id=1'+and+1=2 /article.php?id=1/../../etc/passwd /article.php?id=1;curl+xx.ceye.com; ...
确定漏洞类型后,若要给出PoC,还需要根据操作系统、服务版本等信息,发出更多的请求才能确定。比如SQLMap按照默认的BEUSTQ顺序,会依次尝试盲注、报错、UNION、堆叠、时间差以及内联注入,直到成功为止。
openrasp-iast 改变了这种模式。通过在管理后台下发IAST专用的信息采集插件,OpenRASP Agent 可以直接获取应用内部的执行信息,比如读取了 ../../../../etc/passwd,访问了 10.10.32.173.xip.io 等等:
之后,IAST专用插件会调用 RASP.request 接口将行为信息、请求信息发送给 openrasp-iast Fuzz 服务器进行分析:
当我们把扫描器的payload,与应用内部的行为关联起来之后,就可以很快的分析出应用存在何种漏洞。无论攻击参数出现在哪里,文件名、SQL语句、执行的命令等等,openrasp-iast 都可以在同一个请求内确认。
比如,用户输入的 id 参数会触发SQL查询操作,且会修改程序的逻辑,因此 article.php 的 id 参数存在SQL注入漏洞。
因此,对于单个请求参数,openrasp-iast 只需要发出几个Fuzz请求,就可以定位漏洞类型和利用方式,而且几乎不会误报。
其次是爬虫能力。对于黑盒扫描器,或者其他自动化渗透工具而言,由于是主动针对目标发起扫描,因此爬虫的能力至关重要。若接口覆盖不全,只能用浏览器插件、旁路嗅探、模拟事件等方式来补足。
openrasp-iast 的定位是半自动化DevSecOps测试工具,长期部署在测试环境。通过强大的OpenRASP Java/PHP插桩扩展,可以在功能测试阶段自动的采集请求信息,以及请求对应的应用行为信息。因此,openrasp-iast 无需爬虫,只要研发、QA能够覆盖到的接口,它就可以提取到并进行扫描。同时,openrasp-iast 还可以将接口导出给第三方扫描器,进行补充检测。
最后是检测能力。对于SSRF、文件上传漏洞,如果页面没有给出回显,扫描器将难以进行判断。这个问题不仅影响扫描器,对于检测Web流量的IDS系统也是一个难题。
以基于 Java Commons FileUpload 组件的文件上传漏洞为例,openrasp-iast 通过关联 fileUpload 和 writeFile 两个hook点的信息,可以准确的得知上传的文件是否被解析,是否被写入硬盘。
相比于黑盒扫描器,openrasp-iast 对安全人员更加友好。当漏洞检出,黑盒扫描器只会给出PoC,而openrasp-iast不但会给出PoC,还会给出漏洞堆栈,代码行号等关键信息,帮助你与研发人员沟通。
0x5:数据泄露检测
对于企业而言,API 接口是数据泄露的常见来源。随着公司业务的扩大,无论是梳理敏感数据的类型,还是梳理数据展示接口,都将成为一个难题。
Relevant Link:
https://www.freebuf.com/news/216185.html https://blog.csdn.net/sacredbook/article/details/106584599 https://mp.weixin.qq.com/s/geGO8klz_a6TNLNNtW9dyg https://mp.weixin.qq.com/s/q5mcXdAwLa_PQSSF4hcESg
3. RASP 技术原理
0x1:Java技术栈
在 Java 技术栈下,RASP 引擎以 javaagent 的形式实现,并运⾏在 JVM 之上。
在应⽤服务器启动的时候,RASP 引擎借助 JVM 自身提供的 instrumentation 技术,通过替换字节码的方式对关键类⽅法进行挂钩。RASP 的技术原理与 APM 是一样的,只是挂钩的函数要少很多。以 OpenRASP 为例,按照不同的防御目的,挂钩点如下:
攻击检测
其中,SQL注入检测点为 execute/executeUpdate/executeQuery/executeBatch/executeBatchInternal/addBatch 方法之一,
检查内容 | 方法名称 |
---|---|
文件上传 | org.apache.commons.fileupload.disk.DiskFileItem.setHeaders() |
org.apache.commons.fileupload.FileUploadBase.parseRequest() | |
文件读取 | java.io.FileInputStream(File file) |
文件写入 | java.io.FileOutputStream() |
java.io.FileOutputStream(String name, boolean append) | |
文件重命名 | java.io.File.renameTo() |
文件遍历 | java.io.File.list() |
SSRF | org.apache.commons.httpclient.URI.parseUriReference() |
org.apache.http.client.methods.HttpRequestBase.setURI() | |
com.squareup.okhttp3.HttpUrl.parse(String) | |
com.squareup.okhttp.HttpUrl.parse(String) | |
sun.net.www.protocol.http.HttpURLConnection.connect() | |
反序列化 | java.io.ObjectInputStream.resolveClass |
命令执行 | java.lang.UNIXProcess.<init> |
java.lang.ProcessImpl.<init> | |
OGNL 表达式执行 | ognl.OgnlParser.topLevelExpression() |
XXE | com.sun.org.apache.xerces.internal.util.XMLResourceIdentifierImpl() |
org.apache.xerces.util.XMLResourceIdentifierImpl的setValues() | |
JSTL import | org.apache.taglibs.standard.tag.common.core.ImportSupport.targetUrl() |
DubboRPC | com.alibaba.dubbo.rpc.filter.ContextFilter.invoke() |
com.alibaba.dubbo.rpc.filter.GenericFilter.invoke() | |
SQL 注入 | com.mysql.jdbc.StatementImpl |
com.mysql.jdbc.PreparedStatement | |
com.mysql.cj.jdbc.PreparedStatement | |
org.sqlite.Stmt | |
org.sqlite.PrepStmt | |
org.sqlite.jdbc3.JDBC3Statement | |
org.sqlite.jdbc3.JDBC3PreparedStatement | |
oracle.jdbc.driver.OracleStatement | |
oracle.jdbc.driver.OraclePreparedStatement | |
com.microsoft.sqlserver.jdbc.SQLServerStatement | |
com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement | |
org.postgresql.jdbc.PgStatement | |
org.postgresql.jdbc1.AbstractJdbc1Statement | |
org.postgresql.jdbc2.AbstractJdbc2Statement | |
org.postgresql.jdbc3.AbstractJdbc3Statement | |
org.postgresql.jdbc3g.AbstractJdbc3gStatement | |
org.postgresql.jdbc4.AbstractJdbc4Statement | |
com.ibm.db2.jcc.am | |
com.ibm.db2.jcc.am.Connection |
基线检查
检查内容 | 方法名称 |
---|---|
数据库连接账号 | com.mysql.jdbc.NonRegisteringDriver.connect() |
com.mysql.cj.jdbc.NonRegisteringDriver.connect() | |
org.sqlite.JDBC.connect() | |
com.microsoft.sqlserver.jdbc.SQLServerDriver.connect() | |
org.postgresql.Driver.connect() | |
oracle.jdbc.driver.OracleDriver.connect() | |
com.ibm.db2.jcc.DB2Driver.connect() |
服务器启动检测点
用途 | 方法名称 |
---|---|
Tomcat | org.apache.catalina.startup.Catalina.start() |
Jetty | org.eclipse.jetty.server.Server.doStart() |
JBoss 4 | org.jboss.system.server.ServerImpl.start() |
JBoss 5 | org.jboss.bootstrap.AbstractServerImpl.start() |
JBoss 6 | org.jboss.bootstrap.impl.base.server.AbstractServer.start() |
Resin | com.caucho.server.resin.Resin.initMain() |
WebSphere | org.eclipse.core.launcher.Main.run() |
Tomcat/Jetty/JBoss/Resin/WebSphere 通用
用途 | 方法名称 |
---|---|
preRequest | apache.catalina.connector.CoyoteAdapter.service() |
request | apache.catalina.core.ApplicationFilterChain.doFilter() |
缓存body | org.apache.catalina.connector.InputBuffer.readByte(int) |
org.apache.catalina.connector.InputBuffer.read(byte[],int ,int) | |
HTML 注入 | org.apache.catalina.connector.OutputBuffer.close() |
Resin 服务器
用途 | 方法名称 |
---|---|
preRequest | com.caucho.server.http.HttpRequest.handleRequest() |
com.caucho.server.http.HttpRequest.handleResume() | |
request | com.caucho.server.dispatch.ServletInvocation.service() |
com.caucho.server.dispatch.ServletInvocation.doResume() | |
缓存body | com.caucho.server.connection.ServletInputStreamImpl.read(int) |
com.caucho.server.connection.ServletInputStreamImpl.read(byte[],int ,int) | |
com.caucho.server.http.ServletInputStreamImpl.read(int) | |
com.caucho.server.http.ServletInputStreamImpl.read(byte[],int ,int) | |
HTML注入 | com.caucho.server.connection.AbstractHttpResponse.finish() |
com.caucho.server.connection.AbstractHttpResponse.finishInvocation() | |
com.caucho.server.http.AbstractHttpResponse.finish() | |
com.caucho.server.http.AbstractHttpResponse.finishInvocation() |
Jetty 服务器
用途 | 方法名称 |
---|---|
preRequest | org.eclipse.jetty.server.Server.handle() |
request | org.eclipse.jetty.server.handler.HandlerWrapper.handle() |
缓存body | org.eclipse.jetty.server.HttpInput.read(int) |
org.eclipse.jetty.server.HttpInput.read(byte[],int ,int) | |
HTML注入 | org.eclipse.jetty.server.HttpOutput.close() |
WebSphere 服务器
用途 | 方法名称 |
---|---|
preRequest | com.ibm.ws.webcontainer.WebContainer.handleRequest() |
request | com.ibm.ws.webcontainer.filter.WebAppFilterManager.invokeFilters() |
缓存body | com.ibm.ws.webcontainer.srt.http.HttpInputStream.read(int) |
com.ibm.ws.webcontainer.srt.http.HttpInputStream.read(byte[],int ,int) | |
HTML注入 | com.ibm.ws.webcontainer.srt.SRTServletResponse.finish() |
0x2:PHP技术栈
攻击检测
检查内容 | 方法名称 |
---|---|
webshell callable | array_walk |
array_map | |
array_filter | |
ReflectionFunction::__construct | |
命令执行 | passthru |
system | |
exec | |
shell_exec | |
proc_open | |
popen | |
pcntl_exec | |
命令执行(webshell) | passthru |
system | |
exec | |
shell_exec | |
proc_open | |
popen | |
pcntl_exec | |
目录遍历 | dir |
scandir | |
opendir | |
XSS(echo) | echo |
文件读取 | file |
readfile | |
file_get_contents | |
fopen | |
SplFileObject::__construct | |
文件写入 | file_put_contents |
fopen | |
SplFileObject::__construct | |
文件写入(webshell) | file_put_contents |
文件拷贝 | copy |
文件移动 | rename |
文件上传 | move_uploaded_file |
文件包含 | include |
文件运行(webshell) | eval |
assert(仅PHP5) | |
SQL注入 | mysql_query(仅PHP5) |
mysqli_query | |
mysqli::query | |
mysqli_real_query | |
mysqli::real_query | |
mysqli_prepare | |
mysqli::prepare | |
PDO::query | |
PDO::exec | |
PDO::prepare | |
pg_query | |
pg_send_query | |
pg_prepare | |
SQLite3::query | |
SQLite3::exec | |
SQLite3::querySingle | |
SSRF | curl_exec |
基线检查
检查内容 | 方法名称 |
---|---|
数据库连接账号 | mysql_connect (仅PHP5) |
mysql_pconnect (仅PHP5) | |
mysqli::__construct | |
mysqli::connect | |
mysqli_connect | |
mysqli::real_connect | |
mysqli_real_connect | |
PDO::__construct | |
pg_connect | |
pg_pconnect |
PHP RASP的底层原理是基于PHP Zend扩展实现的,以 cli SAPI 为例,其单个请求生命周期如下图所示:
OpenRASP 核心原理为:在 MINIT 阶段,替换全局compiler_globals
的function_table
与class_table
中特定 PHP_FUNCTION 对应的函数指针(封装原有handler,增加前置、后置处理),由此实现对敏感函数的挂钩。通过敏感函数参数结合请求信息判断是否存在攻击行为,进而采取拦截或者放行操作。
Relevant Link:
https://rasp.baidu.com/doc/hacking/architect/php.html https://blog.csdn.net/u012313382/article/details/86524807 https://rasp.baidu.com/doc/usage/web.html#overview
4. RASP技术思想
0x1:零规则SQL注入检测算法
这个算法通过识别查询语句里的用户输入来实现,当用户的输入改变了查询语句的逻辑,OpenRASP就会判定为注入。
举个例子,当攻击者发起这样一个请求时,
?id=abc--')/*!UNION/**/ALL*//*!SELECT*//*!USER*/(),/*!user*//*!FROM*/users#
OpenRASP因为插桩在应用的底层,因此会看到完整的SQL查询语句,e.g
SELECT * FROM users WHERE id = 'abc--')/*!UNION/**/ALL*//*!SELECT*//*!USER*/(),/*!user*//*!FROM*/users#
此时,OpenRASP便可以关联数据库的查询语句和Web请求参数,实现零规则SQL注入检测。
首先,检测插件会调用RASP.sql_tokenize这个API,对语句进行token解析
['SELECT', '*', 'FROM', 'users', 'WHERE', 'id', '=', 'abc', '--', '\'', ']/*!', 'UNION', 'ALL', '*//*!', 'SELECT', '*//*!', 'USER', '*/()', ',', '/*!', 'user', '*//*!', 'FROM', '*/', 'users' ]
然后,在语句里去掉匹配到的用户输入,
SELECT * FROM users WHERE id =
并再次进行token解析,
[ 'SELECT','*', 'FROM', 'users', 'WHERE', 'id', '=' ]
当token个数相差超过2,即表明当前的查询语句逻辑发生了改变,也就意味着这里就存在SQL注入漏洞。
对于传统WAF的来说,一方面拿不到数据库的查询语句,只能在请求里寻找SQL注入特征;另一方面也无法识别用户输入,所以存在绕过的可能。而 OpenRASP能够结合上下文和语义引擎,实现更加灵活的检测算法。
笔者思考:
本质上说,这是一种通过输入流差分评估输出结果差分度的方法论,在其他领域也同样可以应用。例如对于webshell文本检测来说,业内常常会使用沙箱这种技术进行动态行为模拟,但这种方案最大的问题就在于旁路模拟环境下,样本的运行行为相对于真实黑客的利用环境,被降维了,这导致对于沙箱视角来说,往往看不到真正存在危险行为的行为路径。这种情况下,我们可以利用这种差分攻击思想,通过改变输入参数或者污点标记参数,来认为地构造出不同的输入,通过观察输出结果的差异性,来实现升维,增加一个可以区分正常文件和异常文件的判断维度。
0x2:零规则脱裤检测算法
通常,黑客会先尝试上传WebShell,一旦成功,就可以利用数据库管理功能,进行拖库操作。当单个SELECT语句读取行数超过预先定义的阈值,OpenRASP就会产生报警,提示管理员检查应用是否异常。这个条数默认是500,可以在 rasp.properties 里配置。
0x3:堆栈异常检测算法
当应用进行了敏感操作,比如执行命令、查看目录结构等等,我们可能无法直接判断这个行为是否存在异常。但如果这些敏感函数的调用来自 eval/create_function/preg_replace/.. 等动态执行的代码,我们就有理由怀疑这里存在代码执行漏洞,或者webshell一句话后门。
以最近爆出的 CVE-2018-7600 – Drupalgeddon远程代码执行漏洞为例,这个漏洞通过 form API,控制了 call_user_func 的参数,最终实现任意代码执行。当我们利用这个漏洞执行命令时,它的堆栈是这样的:
我们清楚的看到,system函数的调用来自call_user_func。OpenRASP 根据堆栈识别出这个命令执行是动态触发的,故而拦截。
0x4:识别常用渗透命令(探针)
实战场景中见到最多的就是冰蝎动态后门了,其中JSP版本通过自定义ClassLoader + defineClass方法来实现eval特性。
new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
因为流量是AES双向加密的,对绕过WAF和IDS会比较有效。但部署在应用内部的OpenRASP,还是能够看到后门操作(安装 999-event-logger 插件即可看到日志):
[event-logger] Listing directory content: / [event-logger] Execute command: whoami
同样,我们依然可以通过命令执行的堆栈识别冰蝎:
java.lang.ProcessBuilder.start
...
net.rebeyond.behinder.payload.java.Cmd.RunCMD
net.rebeyond.behinder.payload.java.Cmd.equals
因此,无论服务端的JSP如何变形,只要客户端代码不变,我们通过校验堆栈就可以检测冰蝎动态后门。
相比于传统的WAF,OpenRASP填补了IDC内部横向渗透防护能力缺失的不足。当黑客突破防火墙上传了后门,当EDR缺少相应的文件规则时,OpenRASP依然可以根据应用行为进行检测。目前OpenRASP官方插件提供了 算法3 - 识别常用渗透命令(探针),允许用户审计命令执行(例如whoami、id、wget、cur等)并识别服务器上的未知WebShell。
Relevant Link:
https://mp.weixin.qq.com/s/50AFAVp8dSwuc1ACY5kRKw? https://github.com/baidu/openrasp/blob/master/plugins/official/plugin.js https://mp.weixin.qq.com/s/Ur5tlRr7yN91nphDg8V_jw?